嵌入式Day08
一、指针类型作为函数的返回值
1. 基本原则
函数返回的指针所指向的内存区域在函数返回后应仍然有效、有定义,不能是已释放或未定义的区域。
2. 不允许作为返回值的指针
函数返回的指针不能指向函数自身栈区,否则返回后会成为悬空指针(野指针的一种特殊情况),这是非常危险的,会导致未定义行为,可能使程序崩溃。
3. 允许作为返回值的指针
- 指向数据段的指针:数据段中的数据生命周期为静态存储期限,整个程序运行期间都生效。但由于全局变量使用较少,这种操作也不常见。
- 指向堆区的指针:这是最常见的情况。堆区内存由程序员手动管理,只要合理管理,返回堆区指针是可行的。但如果在返回后使用前释放了该内存,返回值指针会变为悬空指针。
- 函数参数指针:函数可直接返回传入的参数指针。若传入的是非法指针(野指针),函数调用出现问题的责任在调用者,与函数本身无关。
二、概念辨析
1. 空指针
空指针是指针变量的特殊取值,表示未指向任何内存区域(或指向地址值 0),是指针不可用的标志。解引用空指针是未定义行为,但现代编译平台大多会让程序崩溃并给出空指针异常提示,相对安全。
2. 野指针
野指针指向随机未知非法内存区域的指针,操作野指针会带来未定义行为,可能导致程序崩溃或出现错误执行,非常不安全。
3. 悬空指针
悬空指针是野指针的特殊情况,专指曾经指向有效内存区域,但因内存被释放销毁而未改变指向,从而指向非法内存区域的指针。
三、函数返回值为指针类型时的内存管理
1. 指向数据段
程序员无需、也无法手动管理其内存,因为具有静态存储期限的数据生命周期不由程序员控制。
2. 指向堆区
程序员通常需要考虑返回值指针指向内存块的生命周期管理,具体操作需查看函数使用手册。
3. 函数传参传入的指针
- 局部变量内存块:无需考虑内存块生命周期管理,栈区会自动管理。
- 堆区内存块:需要程序员手动管理(申请和释放)。
- 数据段内存块:不需要进行内存管理。
四、函数形参列表规律
1. 形参类型分类
- 基本数据类型、结构体类型、枚举类型:采用值传递,函数内部得到的是拷贝,对外部无影响。
- 指针类型、数组类型:值传递本质不变,但拷贝指针存储的地址与原始指针相同,指向相同内容,传参指针有修改原始数据的可能性。
2. 指针类型形参分类
- 可修改指向内容:指针形参类型未用 const 修饰,目的是通过指针修改指向内容,相当于函数返回值,传入的实际是内存区域,函数内部会操作修改该区域数据。
- 不可修改指向内容:指针形参类型用 const 修饰,只能访问内存区域,不能修改。
五、传入参数和传入传出参数
1. 传入参数
单纯将数据传递给函数内部使用,函数内部修改不影响实参变量取值,包括基本数据类型、结构体和枚举类型传参、const 修饰的指针类型传参。
2. 传入传出参数
将原始数据的地址传递给函数内部,函数内部不能修改实参指针指向,但可通过指针修改指向内容,只能是未用 const 修饰的指针类型传参。
六、总结
1. 查看新函数
重点关注指针类型形参。指针类型形参看似需要传入指针,实际需要的是指针指向的内存块。通过有无 const 修饰判断函数对内存块的操作,无 const 修饰可能读写,有 const 修饰只会读。
2. 设计函数
若指针类型形参不需要修改指向内容,用 const 修饰;需要修改则不用 const 修饰。
3. 函数传参和返回值
函数传参和返回值为指针类型时,传参和返回的不仅是指针变量,更重要的是内存块。传参时不能传入野指针和空指针,返回值指针不能指向当前栈区。
七、C 语言字符串本质
在 C 语言中,任何以空字符(编码值为 0,即 '\0'
)结尾的字符数组,都可视为字符串。从字符数组首元素开始,到空字符(不包含)为止的部分,是字符串的内容。C 语言没有专属的字符串类型,用特殊字符数组表示字符串。
八、字符串与字符数组概念辨析
1. 字符串长度
从首元素到空字符(不包含)的字符数量。例如 char str[] = {'w', 'o', 'r', 'l', 'd', '\0', 'A', 'B', 'C'};
,字符串 “world” 的长度是 5。
2. 字符数组长度
字符数组中元素的个数。上述数组长度是 9。能表示字符串的字符数组,其长度至少比字符串长度大 1,也可大更多。
3. 两者关系
C 语言的字符串一定是字符数组,但并非所有字符数组都是字符串,只有以空字符结尾的字符数组才能表示字符串。数组长度最小是 1,而字符串长度可以是 0,此时是长度为 1 且仅有一个空元素的字符数组 。
九、字符串与字符数组示例
#include <stdio.h>
int main(void) {
// 字符串内容是"hello",长度是5,字符数组长度是6
char str[] = {'h', 'e', 'l', 'l', 'o', '\0'};
// 此字符数组不能表示字符串,因为没有空字符
char str3[] = {'7', '7', '7'};
// 由于str3不是字符串,用%s打印会越界访问,是未定义行为
printf("%s\n", str3);
// 字符数组长度是5,可表示字符串,内容是"666",长度是3
char str2[] = {'6', '6', '6', '\0', '7'};
// 扩展:转换说明"[num]s",num为正整数,表示输出打印字符的数量
printf("%.3s\n", str3);
// 使用printf函数,以转换说明"%s"打印字符串,遇到空字符停止
printf("%s\n", str);
int str_len = 3;
printf("%s\n", str2);
// 扩展:转换说明"%.*s",*是占位符,可传参整数取代固定num
printf("%. *s\n", str_len, str3);
return 0;
}
十、字符串字面值
1. 存储位置
字符串字面值本质是以空字符结尾的字符数组,存储在数据段的只读数据段中。数据段分为静态数据段(存储具有静态存储期限的变量,可读可写)和只读数据段(存储常量,字符串字面值的字符数组存储在此,生命周期为静态存储期限 )。
2. 特点总结
- 本质上是存储在只读数据段中以空字符结尾的字符数组,包含字符串内容和一个空字符,无多余内容。
- 生命周期是静态存储期限,程序运行期间都有效。
- 字符数组只能访问,不能修改,修改会引发未定义行为,通常导致程序崩溃。
- 多数情况下,代码中可直接视为数据段中字符串字符数组首元素指针。例如
char *p = "hello";
,“hello” 可视为'h'
元素指针,p
指向数据段中'h'
元素,p
指向区域不可修改,但p
本身可改变指向。 - 取地址或
sizeof
运算符连接字符串字面值时,字符串字面值代表整个字符数组,而非首元素指针。
3. 函数参数
若函数需使用字符串字面值作为参数传递,其形参基本是 const char*
类型。看到函数形参是 const char*
类型,说明该形参需传入字符串字面值。
十一、字符串字面值操作示例
#include <stdio.h>
#include <stddef.h>
int main(void) {
// 用指针操作字符串字面值
// "hello"是字符串字面值,p指向该字符数组首元素
char *p = "hello";
printf("%c\n", *p);
printf("%c\n", *(p + 2));
// 指针指向的数据只读,修改会导致未定义行为,如程序崩溃
//*p = 'H';
p = "world"; // 可修改指针指向的内容
// 为体现字符串字面值只读特点,可如下定义指针变量
const char *p2 = "hello";
//*p2 = 'H';
// 直接操作字符串字面值
// "hello"在代码中等价于数据段中只读字符数组的数组名
size_t arr_len = sizeof("hello");// 求字符数组的长度
printf("arr_len = %zu\n", arr_len);
// 得到一个指向字符数组的指针,即数组指针
char (*arr_ptr)[6] = &("hello");
// "hello"视为只读字符数组的数组名,即首元素指针
printf("%c\n", "hello"[0]);
printf("%c\n", "hello"[1]);
printf("%c\n", "hello"[2]);
// "hello"[0] = 'H';
// "hello" = "world";
return 0;
}
十二、多行字符串字面值语法
C 语言多行字符串主要用于宏函数定义,如定义交换两个元素取值的宏:
#define SWAP_ELEMENT(a, b) {int temp = (a); (a) = (b); (b) = temp; }
#define SWAP_ELEMENT2(a, b){\
int temp = (a); \
(a) = (b);\
(b) = temp;\
}
使用 printf
打印多行字符串时,推荐如下格式:
#include <stdio.h>
int main(void) {
printf("When you come to a fork in the road, take it. \n"
"--Yogi Berra\n");
printf(
"*\n"
"* *\n"
"*\n"
"*\n"
"*\n"
"*\n"
"*\n");
return 0;
}
注意两个字符串字面值中间无需逗号、分号等符号。
十三、C 语言字符串变量相关知识总结
1. C 语言字符串设计
- 设计方式:C 语言没有独立的字符串类型,用空字符结尾的字符数组表示字符串。
- 优点:简洁统一,符合 C 语言设计哲学;降低复杂性,使用者无需学习专门的字符串类型;可提升内存利用率,减少内存空间占用。
- 缺点:判断字符数组是否为字符串、获取字符串长度都需遍历数组,耗费性能;字符串不能包含空字符;设计字符串处理函数时,需对空字符特殊处理,易出现数组越界操作。
2. 其他语言的 string 类型
- 设计原理:如 C++、Java 中的 string 类型,包含字符指针
str
(指向存储字符串数据的字符数组)、表示字符串长度的str_len
和存储字符串的字符数组真实长度capacity
。 - 优点:获取字符串长度更方便;确定字符串类型更简单直接;字符串可包含任意字符。
- 缺点:复杂性高,可能影响性能;学习难度增加;空间占用增多。
3. 字符串变量的声明
- 声明字符数组:
char str[10];
,在函数内部声明时,数组在栈上分配空间,为局部变量字符数组。适用于已知字符串长度和内容且希望修改字符串内容的情况 。 - 声明字符指针:
char *str2;
,在函数内部声明时,指针在栈上分配空间,为局部变量字符指针。若不手动初始化则为野指针。适用于动态内存分配堆上数组,或明确知道指向字符串字符数组的情况。操作字符串时应使用char*
指针,避免误用int*
。
4. 字符串变量的初始化
- 字符数组初始化:
char str[10] = "hello";
,在栈上创建字符数组并用字符串字面值初始化,此时“hello”是{'h', 'e', 'l', 'l', 'o', '\0'}
的语法糖。数组内容可修改,但数组名视为指针时,不允许用=
改变指向。 - 字符指针初始化:
char *str2 = "hello";
,在栈上创建字符指针并用字符串字面值初始化,字符串数据存储在只读数据段。指针指向的字符串内容只读,不可修改,但指针可改变指向。
5. 字符串变量输出到屏幕
- 使用
printf
:使用转换说明%s
输出字符串。 - 使用
puts
函数:int puts(const char *str);
,将字符串输出到stdout
标准输出缓冲区,并自动在末尾加换行符。它是printf
输出字符串并换行的特化版本,性能更好。使用时需传入能表示字符串的字符数组,否则会产生未定义行为。 - 指针遍历输出:C 语言中操作字符串的函数无需额外传入数组长度参数,因为字符串以空字符作为结束标志。可通过指针遍历字符串,如:
#include <stdio.h>
int main(void) {
char str3[] = "ZhangJiaYuan";
char *p = str3;
while (*p != '\0') {
printf("%c ", *p);
p++;
}
// 简化写法
p = str3;
while (*p) {
printf("%c", *p++);
}
return 0;
}
6. 键盘录入字符串
scanf
录入:使用转换说明%s
从stdin
标准输入缓冲区读取字符串,自动跳过前面的空白字符,从第一个非空白字符开始录入,碰到空白字符结束,并自动在录入数据后加空字符。存在越界风险,可使用%[num]s
限制录入字符数量,num
一般取sizeof(str) - 1
。gets
录入:char *gets( char *str );
,从键盘录入一整行字符串并存储到str
指向的字符数组中,返回str
指针。录入时从开头任意字符开始,碰到换行符结束,会自动在末尾加空字符,但该函数不安全,无法限制最大录入数量,易出现越界修改。fgets
录入:char *fgets( char *str, int num, FILE *stream);
,从文件流中读取一行字符串信息并限制读取数量。用于键盘录入时,stream
传stdin
,num
表示一次最多读取num - 1
个字符,会留一个位置给空字符,比gets
更安全。
十四、C 语言字符串标准库函数 - strlen 函数
1. strlen 函数介绍
C 语言提供了一系列用于字符串处理的标准库函数,strlen
函数是其中之一。它用于计算字符串的长度。使用 strlen
函数时,必须确保传入的参数是一个字符串(即以空字符 '\0'
结尾的字符数组的首元素指针),否则函数会越界访问,直至找到一个空字符,这属于未定义行为。
2. strlen 与 sizeof 的区别
sizeof
运算符:当sizeof
连接数组名时,如果数组名没有退化为指针,它获取的是整个数组在内存中所占的字节数。例如对于字符数组,其结果一般是strlen(str)+1
,多出来的 1 个字节用于存储字符串结束标志'\0'
。但如果数组名已经传参退化为指针,此时sizeof
求的是指针变量本身的长度。strlen
函数:要求传参必须是能表示字符串的字符数组的首元素指针,只要传参正确,它就能准确求出字符串的长度(不包含字符串结束标志'\0'
)。
3. 手动实现 strlen 函数思路
手动实现 strlen
函数,核心思路是从字符串的首字符开始遍历,直到遇到空字符 '\0'
,统计遍历过的字符个数,这个个数就是字符串的长度。在遍历过程中,通过一个计数器变量记录字符数量,每次访问一个字符,计数器就加 1 ,直到遇到空字符停止遍历并返回计数器的值。