嵌入式(C语言篇)Day10
嵌入式Day10
一、strcmp函数功能
- 比较两个字符串的内容是否一致。
- 比较字符串的大小关系,用于对字符串的数组进行排序操作,是重要且常用的字符串操作库函数。
二、函数声明
int strcmp(const char *str1, const char *str2);
三、返回值含义
将函数调用看成“str1 - str2”,返回值决定两字符串的大小及是否相等关系:
- 返回值 < 0:说明
str1 < str2
- 返回值 = 0:说明
str1 = str2
,即两个字符串内容一致 - 返回值 > 0:说明
str1 > str2
注意:返回值只关注符号性和是否为 0,不关注具体取值。strcmp 函数的返回值实际只有 0、-1、1,用于表示两字符串的大小关系,只要知道大于 0、小于 0 以及等于 0 即可,具体值不重要。
四、实现原理
- 逐一比较两个字符串对应位置的字符,直到找到一个不相同的字符,然后直接返回它的编码值之差。
- 如果两个字符串的内容完全一致,相当于返回空字符编码值之差,也就是 0。
- 字符串大小判断遵循字典顺序,即按“字符编码”从左到右逐个比较的排序方式,编码值越小的字符串越小,编码值越大的越大,strcmp 函数实现的就是字典顺序判断。
五、手动实现strcmp函数示例
int my_strcmp(const char *s1, const char *s2) {while (*s1 && *s2 && *s1 == *s2) { // 当两个字符都不为空且相等时,移动指针继续比较下一对元素s1++;s2++;}// 不管是哪种情况(找到不相等字符或其中一个指针指向空字符),返回它们的编码值差就是结果return *s1 - *s2;
}
六、字符串数组的两种实现方式
(一)二维字符数组实现
本质
- 一维字符数组的一维数组,即
char类型的二维数组
,本质是连续内存存储的字符矩阵。
示例代码
char week_days[][10] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
特点
- 内存结构:字符串在内存中连续存储,每个字符串以
'\0'
结尾。 - 字符串性质:由栈上的局部数组初始化,非字符串字面值,可修改单个字符(如
week_days[0][0] = 'm'
),但数组名不可整体赋值。
优缺点
优点 | 缺点 |
---|---|
1. 实现简单直接,逻辑易懂 2. 连续内存访问效率高 | 1. 列长需固定为最长字符串长度,空间浪费 2. 排序、删除等操作繁琐,适合只读场景 |
(二)字符指针数组实现
本质
char*类型的一维数组
,每个元素是字符串首元素指针,指向独立的字符数组(可位于栈、堆或只读数据段)。
示例代码
char *week_days2[] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
特点
- 内存结构:指针数组本身在栈上连续存储,但指向的字符串内存不连续(如字面值存于只读数据段)。
- 字符串性质:直接指向字符串字面值时,内容不可修改(如
week_days2[2][0] = 'w'
会报错),但指针指向可修改(如week_days2[0] = "星期一"
)。
优缺点
优点 | 缺点 |
---|---|
1. 无空间浪费,按需分配 2. 操作灵活,排序、删除等通过指针操作实现 | 1. 内存不连续,访问性能略低 2. 需理解指针指向规则,操作复杂度较高 |
七、两种实现方式对比
维度 | 二维字符数组 | 字符指针数组 |
---|---|---|
内存连续性 | 字符串连续存储 | 字符串存储不连续,指针数组连续 |
空间效率 | 固定列长,可能浪费空间 | 按需分配,无浪费 |
操作灵活性 | 仅支持简单访问,修改繁琐 | 支持指针操作,适合频繁修改场景 |
适用场景 | 固定只读字符串集合(如星期列表) | 动态修改场景(如命令行参数处理) |
典型示例 | char days[][10] = {...} | char *args[] = {"a", "b", "c"} |
八、命令行参数(Linux系统编程)
(一)基本概念
- 定义:操作系统启动程序时传递给
main函数
的参数,以空格分割的字符串数组。 - main函数声明:
int main(int argc, char *argv[]); // 常用声明 // argc:参数个数(至少为1),argv:字符串数组(首个元素为程序路径)
(二)参数解析示例
int main(int argc, char *argv[]) {printf("参数个数:%d\n", argc);for (int i = 0; i < argc; i++) {printf("参数%d:%s\n", i, argv[i]);}// 将第二个参数转为整数int num;sscanf(argv[1], "%d", &num); // 或使用atoi(argv[1])printf("转换后的整数:%d\n", num);return 0;
}
(三)关键规则
- 参数个数:
argc ≥ 1
,第一个参数argv[0]
为程序路径(如./a.out
)。 - 数据类型:参数均为字符串,需手动转换为其他类型(如
atoi
、sscanf
)。 - 应用场景:传递配置参数、文件路径等(如
cp source.txt dest.txt
中source.txt
和dest.txt
为参数)。
九、扩展:可修改指向与内容的字符串数组
(一)实现条件
- 指针数组:必须使用指针数组(二维数组无法修改指针指向)。
- 非只读存储:字符串需存于可写区域(栈区或堆区)。
(二)示例代码
// 栈上可修改的字符串
char name1[] = "Faker"; // 栈上数组,内容可修改
char name2[] = "Uzi";
char *names[] = {name1, name2}; // 指针指向栈上数组names[1][0] = 'u'; // 合法,修改栈上字符串内容
// names[0] = "Bin"; // 合法,但"Bin"是字面值,指向后内容不可修改
(三)注意事项
- 指向字面值时(如
names[0] = "Bin"
),内容不可修改;指向栈/堆区时,内容可修改。 - 堆区场景需手动管理内存(如
malloc
分配后释放)。
十、核心要点总结
- 字符串数组本质:存储多个字符串的容器,可通过二维数组或指针数组实现。
- 选择原则:
- 只读、固定长度场景 → 二维字符数组。
- 动态操作场景 → 字符指针数组。
- 命令行参数:通过
argc
和argv
获取,首个参数为程序路径,需手动转换数据类型。 - 指针操作核心:理解指针指向与内存区域的可写性(只读数据段 vs 栈/堆区)。
十一、结构体类型定义
(一)基础语法
typedef struct 结构体本名 {成员类型1 成员名1;成员类型2 成员名2;// ...
} 结构体别名;
示例:
typedef struct student {int stu_id; // 学号char name[25]; // 姓名(字符数组)char gender; // 性别int score; // 成绩
} Student; // 别名Student
(二)关键规则
- 成员要求:至少包含1个成员,不允许空结构体。
- 关键字使用:
- 仅用本名声明对象时需加
struct
:struct student stu;
。 - 用别名声明时可省略
struct
:Student stu;
(推荐使用别名)。
- 仅用本名声明对象时需加
- 自引用限制:
- 成员可以是自身结构体指针(用于链表等数据结构):
typedef struct node {int data;struct node *next; // 必须用本名struct node } Node;
- 禁止直接定义自身结构体对象成员:
struct A { struct A a; };
(非法)。
- 成员可以是自身结构体指针(用于链表等数据结构):
十二、结构体对象声明与初始化
(一)声明方式
场景 | 语法 | 示例 |
---|---|---|
仅用本名声明 | struct 结构体本名 对象名; | struct student stu; |
用别名声明 | 结构体别名 对象名; | Student stu; |
定义时初始化 | 结构体别名 对象名 = {值列表}; | Student s = {1, "Faker", 'm', 100}; |
(二)初始化规则
- 聚合类型特性:与数组类似,使用
{}
按顺序初始化成员,未赋值成员默认零值。 - 字符串初始化:字符数组成员可用字符串字面值初始化(如
"Faker"
是{'F','a'...'\0'}
的简写)。 - 全零初始化:
Student s2 = {0};
(所有成员初始化为0或'\0'
)。
十三、结构体成员访问
(一)运算符
对象类型 | 运算符 | 语法 | 示例 |
---|---|---|---|
结构体对象 | . | 对象名.成员名 | s.name = "Uzi"; |
结构体指针 | -> | 指针名->成员名 | p->score = 90; |
(二)底层等价性
指针->成员
等价于 (*指针).成员
,因.
优先级高于*
,需显式加括号:
(*p).score = 90; // 与 p->score = 90; 等价
十四、结构体数据传递
(一)值传递(对象作为参数)
- 特点:函数接收结构体副本,修改不影响原始对象(类似
int
值传递)。 - 示例:
void swap_struct(Student s1, Student s2) { // 值传递Student temp = s1;s1 = s2;s2 = temp; // 仅修改副本,原对象不变 }
(二)指针传递(指针作为参数)
- 特点:函数接收结构体地址,可通过指针修改原始对象(类似数组传递)。
- 示例:
void swap_struct_ptr(Student *p1, Student *p2) { // 指针传递Student temp = *p1;*p1 = *p2;*p2 = temp; // 修改原始对象 }
(三)作为返回值
- 对象返回:返回结构体副本(类似
int
返回,不涉及原内存)。Student create_stu() {return (Student){2, "Clearlove", 'm', 85}; // 返回副本 }
- 指针返回:禁止返回栈区结构体指针(会成为野指针),可返回堆区或静态区地址。
Student* create_stu_static() { // 合法(静态区)static Student s = {3, "Bin", 'm', 95};return &s; }
十五、结构体与数组的区别
特性 | 结构体 | 数组 |
---|---|---|
初始化方式 | 用{} 按成员赋值 | 用{} 按元素赋值 |
对象赋值 | 支持= (成员逐一拷贝) | 禁止= (需逐个元素复制) |
作为函数参数 | 值传递(副本)或指针传递 | 隐式转换为指针(传递首地址) |
返回值支持 | 允许(返回副本或指针) | 禁止(只能返回指针) |
内存连续性 | 成员在内存中连续存储 | 元素在内存中连续存储 |
十六、枚举类型基础
(一)定义语法
typedef enum 枚举本名 {枚举值1, // 默认为0枚举值2, // 默认为1枚举值3 = 5 // 显式赋值为5,后续值依次+1
} 枚举别名;
示例:
typedef enum color {RED, // 0GREEN, // 1BLUE = 3 // 3,下一个枚举值YELLOW为4
} Color;
(二)核心特性
- 本质:枚举值是编译时常量,底层为
int
类型,可直接使用整数赋值或传参。Color c = GREEN; // 等价于 c = 1 func(2); // 合法,传入枚举值BLUE的底层值
- 类型非安全:枚举类型与
int
可隐式转换,缺乏严格类型检查。 - 取值规则:
- 未显式赋值时,从0开始递增(如
RED=0
,GREEN=1
)。 - 显式赋值后,后续值依次加1(如
BLUE=3
,则YELLOW=4
)。
- 未显式赋值时,从0开始递增(如
十七、枚举类型优点
- 语义化管理:用命名值代替魔数(如
RED
代替0
),提高代码可读性。 - 调试友好:保留枚举值命名信息,方便调试时识别状态。
- 编译时常量:可用于数组长度、
switch
分支条件等场景:int arr[RED + 1]; // 合法,RED是编译时常量 switch (c) {case RED: ... // 合法 }
十八、结构体与枚举对比
维度 | 结构体 | 枚举 |
---|---|---|
本质 | 自定义复合数据类型(含多成员) | 给整数取别名的聚合类型 |
内存占用 | 各成员内存之和 | 等于底层整数类型大小(通常4字节) |
类型安全 | 强类型(需通过成员访问) | 弱类型(与int可隐式转换) |
典型用途 | 存储复杂数据(如学生信息) | 定义状态常量(如颜色、错误码) |
十九、关键要点总结
- 结构体设计原则:
- 用
typedef
定义别名,简化对象声明。 - 成员需避免直接包含自身结构体对象,允许包含自身指针(用于链表)。
- 用
- 数据传递效率:
- 大型结构体优先用指针传递(减少副本开销)。
- 函数返回结构体时,返回指针需确保指向有效内存(堆/静态区)。
- 枚举使用场景:
- 替代宏常量,用于定义有限状态集合(如错误码、方向值)。
- 利用编译时常量特性,简化
switch
分支或数组维度定义。
二十、C语言存储期限分类
(一)自动存储期限(栈区)
- 作用范围:函数内局部变量(包括形参)。
- 生命周期:函数调用时创建,结束时自动释放(与栈帧同生共死)。
- 特点:自动管理,无需手动释放;空间大小编译时确定,适合小数据。
(二)静态存储期限(数据段)
- 作用范围:全局变量、
static
修饰的局部变量、字符串字面值。 - 生命周期:程序启动时创建,结束时释放(贯穿整个程序)。
- 特点:数据持久化存储,可能导致数据安全隐患(如全局变量被意外修改)。
(三)动态存储期限(堆区)
- 作用范围:通过
malloc/calloc/realloc
申请的内存。 - 生命周期:手动调用
free
释放,否则程序结束时由系统回收。 - 特点:灵活管理,适合大数据或动态数据;需手动维护,易引发内存问题。
二十一、栈与堆内存对比
特性 | 栈内存 | 堆内存 |
---|---|---|
管理方式 | 自动(栈指针移动) | 手动(调用分配/释放函数) |
空间大小 | 小(通常几KB到MB级) | 大(受限于系统内存) |
动态性 | 编译时确定大小 | 运行时动态分配 |
访问速度 | 快(寄存器直接操作) | 慢(涉及系统调用和碎片化管理) |
典型用途 | 局部变量、函数调用栈帧 | 大数据结构、动态数组、跨函数数据 |
二十二、通用指针void*
(一)核心特性
- 类型无关性:可接收任意类型指针(如
int*
、char*
),无需显式转换(C语言允许隐式转换,C++需强转)。 - 解引用限制:不能直接解引用,需先转换为具体类型指针:
int num = 100; void *p = # int val = *(int*)p; // 强转后解引用
- 用途场景:
- 作为函数参数,接收任意类型指针(如
malloc
返回值)。 - 实现泛型数据结构(如链表存储不同类型数据)。
- 作为函数参数,接收任意类型指针(如
(二)注意事项
- 平台差异:GCC等严格编译器可能对隐式转换发出警告,建议显式强转。
- 空指针定义:
NULL
本质是(void*)0
,表示无效地址。
二十三、动态内存分配函数
(一)malloc
函数
void* malloc(size_t size); // 分配连续的`size`字节内存块
关键点:
- 参数:
size
为所需字节数,需手动计算(如malloc(sizeof(int)*n)
)。 - 返回值:成功返回内存块首地址(
void*
),失败返回NULL
。 - 初始化:分配的内存未初始化,包含随机值(MSVC平台显示为
0xCD
)。 - 错误处理:必须检查返回值,避免使用
NULL
指针:int *p = (int*)malloc(100); if (p == NULL) {perror("malloc failed");exit(1); }
(二)calloc
函数
void* calloc(size_t num, size_t size); // 分配`num`个元素,每个`size`字节的内存块
关键点:
- 初始化:自动将内存块清零(优于
malloc
的随机值)。 - 参数计算:总字节数为
num*size
,适合初始化数组:int *arr = (int*)calloc(5, sizeof(int)); // 等价于malloc(20)并清零
(三)realloc
函数
void* realloc(void* ptr, size_t new_size); // 调整已分配内存块的大小
关键点:
- 内存迁移:
- 若原内存块后有足够空间,直接扩展。
- 否则分配新块,复制数据并释放旧块(原指针可能失效)。
- 返回值:新内存块首地址,可能与原指针不同;若失败,返回
NULL
且原内存块保持不变。 - 使用场景:动态调整数组大小:
int *arr = (int*)malloc(5*sizeof(int)); arr = (int*)realloc(arr, 10*sizeof(int)); // 扩展至10个元素
(四)free
函数
void free(void *ptr); // 释放动态分配的内存块
关键点:
- 参数要求:必须传入分配函数返回的原始指针,否则导致未定义行为(如
free
中途移动的指针)。 - 野指针处理:释放后建议将指针置为
NULL
,避免二次释放或访问悬空指针:int *p = (int*)malloc(4); free(p); p = NULL; // 防止野指针
- 多次释放:重复调用
free
同一指针会导致程序崩溃(double free
错误)。
二十四、内存管理常见问题
(一)内存泄漏(Memory Leak)
- 定义:已分配的内存未释放且无法再访问,导致内存浪费。
- 场景:
- 循环内分配内存但未释放。
- 函数返回前未释放局部指针指向的堆内存。
- 后果:程序长期运行后内存不足,可能引发内存溢出。
(二)内存溢出(Memory Overflow)
- 定义:访问或分配超过内存块边界的空间,导致数据覆盖或系统崩溃。
- 场景:
- 数组越界写入(如
arr[10] = 100
,但arr
仅分配5个元素)。 - 分配内存时计算错误(如
malloc(n)
实际需要n+1
字节)。
- 数组越界写入(如
- 后果:程序崩溃、数据损坏或安全漏洞(如缓冲区溢出攻击)。
二十五、内存管理最佳实践
- 优先使用栈内存:小数据或确定大小的数据尽量用局部变量,避免堆分配开销。
- 及时释放内存:
- 遵循“谁分配,谁释放”原则,明确内存管理职责。
- 在函数出口前检查是否有未释放的堆内存(如使用
finally
风格的代码结构)。
- 避免野指针:
free
后立即将指针置为NULL
。- 操作指针前检查是否为
NULL
(如if (p != NULL) { ... }
)。
- 动态数组管理:
- 使用
realloc
扩展数组时,先保存原指针,避免分配失败导致数据丢失:int *old_ptr = arr; arr = (int*)realloc(arr, new_size); if (arr == NULL) { // 分配失败,恢复原指针arr = old_ptr;perror("realloc failed"); }
- 使用
- 调试工具:利用平台特性(如MSVC的
0xCD/0xDD
标记)或工具(如Valgrind)检测内存问题。
二十六、MSVC平台特殊优化
- 未初始化内存:
malloc
分配的内存填充为0xCD
(表示“未初始化”)。 - 已释放内存:
free
后的内存填充为0xDD
(表示“已释放”)。 - 未初始化栈变量:局部变量默认值为
0xCC
(防止使用随机值)。 - 作用:辅助开发者检测未初始化或非法访问内存的行为(非标准行为,仅MSVC有效)。
二十七、核心要点总结
- 存储期限选择:
- 短期小数据→栈区(自动变量)。
- 持久化数据→数据段(全局/静态变量)。
- 动态大数据→堆区(
malloc/calloc/realloc
)。
- 指针操作原则:
- 通用指针
void*
需强转后解引用。 - 堆指针释放后必须置
NULL
。
- 通用指针
- 内存管理铁律:
- 分配必检查(
if (ptr == NULL)
)。 - 释放必配对(每个
malloc
对应一个free
)。 - 操作必合法(避免越界、野指针、重复释放)。
- 分配必检查(
- 性能权衡:
- 栈内存优先,堆内存仅用于必要场景。
- 大块内存使用
calloc
(自动清零),小块内存使用malloc
(轻量高效)。