嵌入式(C语言篇)Day08

发布于:2025-05-19 ⋅ 阅读:(15) ⋅ 点赞:(0)

嵌入式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 录入:使用转换说明 %sstdin 标准输入缓冲区读取字符串,自动跳过前面的空白字符,从第一个非空白字符开始录入,碰到空白字符结束,并自动在录入数据后加空字符。存在越界风险,可使用 %[num]s 限制录入字符数量,num 一般取 sizeof(str) - 1
  • gets 录入char *gets( char *str );,从键盘录入一整行字符串并存储到 str 指向的字符数组中,返回 str 指针。录入时从开头任意字符开始,碰到换行符结束,会自动在末尾加空字符,但该函数不安全,无法限制最大录入数量,易出现越界修改。
  • fgets 录入char *fgets( char *str, int num, FILE *stream);,从文件流中读取一行字符串信息并限制读取数量。用于键盘录入时,streamstdinnum 表示一次最多读取 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 ,直到遇到空字符停止遍历并返回计数器的值。


网站公告

今日签到

点亮在社区的每一天
去签到