当前位置: 首页 > news >正文

嵌入式(C语言篇)Day10

嵌入式Day10

一、strcmp函数功能

  • 比较两个字符串的内容是否一致。
  • 比较字符串的大小关系,用于对字符串的数组进行排序操作,是重要且常用的字符串操作库函数。

二、函数声明

int strcmp(const char *str1, const char *str2);

三、返回值含义

将函数调用看成“str1 - str2”,返回值决定两字符串的大小及是否相等关系:

  1. 返回值 < 0:说明 str1 < str2
  2. 返回值 = 0:说明 str1 = str2,即两个字符串内容一致
  3. 返回值 > 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;
}

(三)关键规则

  1. 参数个数argc ≥ 1,第一个参数argv[0]为程序路径(如./a.out)。
  2. 数据类型:参数均为字符串,需手动转换为其他类型(如atoisscanf)。
  3. 应用场景:传递配置参数、文件路径等(如cp source.txt dest.txtsource.txtdest.txt为参数)。

九、扩展:可修改指向与内容的字符串数组

(一)实现条件

  • 指针数组:必须使用指针数组(二维数组无法修改指针指向)。
  • 非只读存储:字符串需存于可写区域(栈区或堆区)。

(二)示例代码

// 栈上可修改的字符串
char name1[] = "Faker"; // 栈上数组,内容可修改
char name2[] = "Uzi";
char *names[] = {name1, name2}; // 指针指向栈上数组names[1][0] = 'u'; // 合法,修改栈上字符串内容
// names[0] = "Bin"; // 合法,但"Bin"是字面值,指向后内容不可修改

(三)注意事项

  • 指向字面值时(如names[0] = "Bin"),内容不可修改;指向栈/堆区时,内容可修改。
  • 堆区场景需手动管理内存(如malloc分配后释放)。

十、核心要点总结

  1. 字符串数组本质:存储多个字符串的容器,可通过二维数组或指针数组实现。
  2. 选择原则
    • 只读、固定长度场景 → 二维字符数组。
    • 动态操作场景 → 字符指针数组。
  3. 命令行参数:通过argcargv获取,首个参数为程序路径,需手动转换数据类型。
  4. 指针操作核心:理解指针指向与内存区域的可写性(只读数据段 vs 栈/堆区)。

十一、结构体类型定义

(一)基础语法

typedef struct 结构体本名 {成员类型1 成员名1;成员类型2 成员名2;// ...
} 结构体别名;

示例

typedef struct student {int stu_id;       // 学号char name[25];    // 姓名(字符数组)char gender;      // 性别int score;        // 成绩
} Student; // 别名Student

(二)关键规则

  1. 成员要求:至少包含1个成员,不允许空结构体。
  2. 关键字使用
    • 仅用本名声明对象时需加structstruct student stu;
    • 用别名声明时可省略structStudent stu;(推荐使用别名)。
  3. 自引用限制
    • 成员可以是自身结构体指针(用于链表等数据结构):
      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;

(二)核心特性

  1. 本质:枚举值是编译时常量,底层为int类型,可直接使用整数赋值或传参。
    Color c = GREEN; // 等价于 c = 1
    func(2); // 合法,传入枚举值BLUE的底层值
    
  2. 类型非安全:枚举类型与int可隐式转换,缺乏严格类型检查。
  3. 取值规则
    • 未显式赋值时,从0开始递增(如RED=0, GREEN=1)。
    • 显式赋值后,后续值依次加1(如BLUE=3,则YELLOW=4)。

十七、枚举类型优点

  1. 语义化管理:用命名值代替魔数(如RED代替0),提高代码可读性。
  2. 调试友好:保留枚举值命名信息,方便调试时识别状态。
  3. 编译时常量:可用于数组长度、switch分支条件等场景:
    int arr[RED + 1]; // 合法,RED是编译时常量
    switch (c) {case RED: ... // 合法
    }
    

十八、结构体与枚举对比

维度结构体枚举
本质自定义复合数据类型(含多成员)给整数取别名的聚合类型
内存占用各成员内存之和等于底层整数类型大小(通常4字节)
类型安全强类型(需通过成员访问)弱类型(与int可隐式转换)
典型用途存储复杂数据(如学生信息)定义状态常量(如颜色、错误码)

十九、关键要点总结

  1. 结构体设计原则
    • typedef定义别名,简化对象声明。
    • 成员需避免直接包含自身结构体对象,允许包含自身指针(用于链表)。
  2. 数据传递效率
    • 大型结构体优先用指针传递(减少副本开销)。
    • 函数返回结构体时,返回指针需确保指向有效内存(堆/静态区)。
  3. 枚举使用场景
    • 替代宏常量,用于定义有限状态集合(如错误码、方向值)。
    • 利用编译时常量特性,简化switch分支或数组维度定义。

二十、C语言存储期限分类

(一)自动存储期限(栈区)

  • 作用范围:函数内局部变量(包括形参)。
  • 生命周期:函数调用时创建,结束时自动释放(与栈帧同生共死)。
  • 特点:自动管理,无需手动释放;空间大小编译时确定,适合小数据。

(二)静态存储期限(数据段)

  • 作用范围:全局变量、static修饰的局部变量、字符串字面值。
  • 生命周期:程序启动时创建,结束时释放(贯穿整个程序)。
  • 特点:数据持久化存储,可能导致数据安全隐患(如全局变量被意外修改)。

(三)动态存储期限(堆区)

  • 作用范围:通过malloc/calloc/realloc申请的内存。
  • 生命周期:手动调用free释放,否则程序结束时由系统回收。
  • 特点:灵活管理,适合大数据或动态数据;需手动维护,易引发内存问题。

二十一、栈与堆内存对比

特性栈内存堆内存
管理方式自动(栈指针移动)手动(调用分配/释放函数)
空间大小小(通常几KB到MB级)大(受限于系统内存)
动态性编译时确定大小运行时动态分配
访问速度快(寄存器直接操作)慢(涉及系统调用和碎片化管理)
典型用途局部变量、函数调用栈帧大数据结构、动态数组、跨函数数据

二十二、通用指针void*

(一)核心特性

  1. 类型无关性:可接收任意类型指针(如int*char*),无需显式转换(C语言允许隐式转换,C++需强转)。
  2. 解引用限制:不能直接解引用,需先转换为具体类型指针:
    int num = 100;
    void *p = &num;
    int val = *(int*)p; // 强转后解引用
    
  3. 用途场景
    • 作为函数参数,接收任意类型指针(如malloc返回值)。
    • 实现泛型数据结构(如链表存储不同类型数据)。

(二)注意事项

  • 平台差异:GCC等严格编译器可能对隐式转换发出警告,建议显式强转。
  • 空指针定义NULL本质是(void*)0,表示无效地址。

二十三、动态内存分配函数

(一)malloc函数

void* malloc(size_t size); // 分配连续的`size`字节内存块
关键点:
  1. 参数size为所需字节数,需手动计算(如malloc(sizeof(int)*n))。
  2. 返回值:成功返回内存块首地址(void*),失败返回NULL
  3. 初始化:分配的内存未初始化,包含随机值(MSVC平台显示为0xCD)。
  4. 错误处理:必须检查返回值,避免使用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`字节的内存块
关键点:
  1. 初始化:自动将内存块清零(优于malloc的随机值)。
  2. 参数计算:总字节数为num*size,适合初始化数组:
    int *arr = (int*)calloc(5, sizeof(int)); // 等价于malloc(20)并清零
    

(三)realloc函数

void* realloc(void* ptr, size_t new_size); // 调整已分配内存块的大小
关键点:
  1. 内存迁移
    • 若原内存块后有足够空间,直接扩展。
    • 否则分配新块,复制数据并释放旧块(原指针可能失效)。
  2. 返回值:新内存块首地址,可能与原指针不同;若失败,返回NULL且原内存块保持不变。
  3. 使用场景:动态调整数组大小:
    int *arr = (int*)malloc(5*sizeof(int));
    arr = (int*)realloc(arr, 10*sizeof(int)); // 扩展至10个元素
    

(四)free函数

void free(void *ptr); // 释放动态分配的内存块
关键点:
  1. 参数要求:必须传入分配函数返回的原始指针,否则导致未定义行为(如free中途移动的指针)。
  2. 野指针处理:释放后建议将指针置为NULL,避免二次释放或访问悬空指针:
    int *p = (int*)malloc(4);
    free(p);
    p = NULL; // 防止野指针
    
  3. 多次释放:重复调用free同一指针会导致程序崩溃(double free错误)。

二十四、内存管理常见问题

(一)内存泄漏(Memory Leak)

  • 定义:已分配的内存未释放且无法再访问,导致内存浪费。
  • 场景
    • 循环内分配内存但未释放。
    • 函数返回前未释放局部指针指向的堆内存。
  • 后果:程序长期运行后内存不足,可能引发内存溢出。

(二)内存溢出(Memory Overflow)

  • 定义:访问或分配超过内存块边界的空间,导致数据覆盖或系统崩溃。
  • 场景
    • 数组越界写入(如arr[10] = 100,但arr仅分配5个元素)。
    • 分配内存时计算错误(如malloc(n)实际需要n+1字节)。
  • 后果:程序崩溃、数据损坏或安全漏洞(如缓冲区溢出攻击)。

二十五、内存管理最佳实践

  1. 优先使用栈内存:小数据或确定大小的数据尽量用局部变量,避免堆分配开销。
  2. 及时释放内存
    • 遵循“谁分配,谁释放”原则,明确内存管理职责。
    • 在函数出口前检查是否有未释放的堆内存(如使用finally风格的代码结构)。
  3. 避免野指针
    • free后立即将指针置为NULL
    • 操作指针前检查是否为NULL(如if (p != NULL) { ... })。
  4. 动态数组管理
    • 使用realloc扩展数组时,先保存原指针,避免分配失败导致数据丢失:
      int *old_ptr = arr;
      arr = (int*)realloc(arr, new_size);
      if (arr == NULL) { // 分配失败,恢复原指针arr = old_ptr;perror("realloc failed");
      }
      
  5. 调试工具:利用平台特性(如MSVC的0xCD/0xDD标记)或工具(如Valgrind)检测内存问题。

二十六、MSVC平台特殊优化

  • 未初始化内存malloc分配的内存填充为0xCD(表示“未初始化”)。
  • 已释放内存free后的内存填充为0xDD(表示“已释放”)。
  • 未初始化栈变量:局部变量默认值为0xCC(防止使用随机值)。
  • 作用:辅助开发者检测未初始化或非法访问内存的行为(非标准行为,仅MSVC有效)。

二十七、核心要点总结

  1. 存储期限选择
    • 短期小数据→栈区(自动变量)。
    • 持久化数据→数据段(全局/静态变量)。
    • 动态大数据→堆区(malloc/calloc/realloc)。
  2. 指针操作原则
    • 通用指针void*需强转后解引用。
    • 堆指针释放后必须置NULL
  3. 内存管理铁律
    • 分配必检查(if (ptr == NULL))。
    • 释放必配对(每个malloc对应一个free)。
    • 操作必合法(避免越界、野指针、重复释放)。
  4. 性能权衡
    • 栈内存优先,堆内存仅用于必要场景。
    • 大块内存使用calloc(自动清零),小块内存使用malloc(轻量高效)。

相关文章:

  • DAPO:用于指令微调的直接偏好优化解读
  • 让数据驱动增长更简单! ClkLog用户行为分析系统正式入驻GitCode
  • 人工智能重塑医疗健康:从辅助诊断到个性化治疗的全方位变革
  • 物联网安全技术的最新进展与挑战
  • 深入理解仿函数(Functors):从概念到实践
  • java.io.IOException: Broken pipe \ 你的主机中的软件中止了一个已建立的连接
  • 【Python训练营打卡】day30 @浙大疏锦行
  • HarmonyOS:使用PickerController将编辑后的图片替换原图
  • GloVe 模型讲解与实战
  • 自动化测试框架搭建步骤
  • 分组背包问题:如何最大化背包价值?
  • NC105NC106美光固态颗粒NC108NC109
  • FD+Mysql的Insert时的字段赋值乱码问题
  • 论坛系统(中-2)
  • Java转Go日记(三十九):Gorm查询
  • Python Day26 学习
  • sherpa-ncnn:音频处理跟不上采集速度 -- 语音转文本大模型
  • 前缀和——和为K的子数组
  • 【Unity网络编程知识】Unity的 WWW相关类学习
  • 基础深度补全模型DepthLab: From Partial to Complete
  • 中疾控专家:新冠感染的临床严重性未发生显著变化
  • 中方对美俄领导人就俄乌冲突进行通话有何评论?外交部回应
  • 美国前驻华大使携美大学生拜访中联部、外交部
  • 复旦建校120周年|迎来复旦大学艺术馆开馆
  • 专家:炎症性肠病发病率上升,需加强疾病早期诊断
  • 抖音开展“AI起号”专项治理,整治利用AI生成低俗猎奇视频等