嵌入式(C语言篇)Day10

嵌入式Day10

一、strcmp函数功能

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

二、函数声明

c 复制代码
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函数示例

c 复制代码
int my_strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2 && *s1 == *s2) { // 当两个字符都不为空且相等时,移动指针继续比较下一对元素
        s1++;
        s2++;
    }
    // 不管是哪种情况(找到不相等字符或其中一个指针指向空字符),返回它们的编码值差就是结果
    return *s1 - *s2;
}

六、字符串数组的两种实现方式

(一)二维字符数组实现

本质
  • 一维字符数组的一维数组,即char类型的二维数组,本质是连续内存存储的字符矩阵
示例代码
c 复制代码
char week_days[][10] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
特点
  • 内存结构 :字符串在内存中连续存储,每个字符串以'\0'结尾。
  • 字符串性质 :由栈上的局部数组初始化,非字符串字面值,可修改单个字符(如week_days[0][0] = 'm'),但数组名不可整体赋值。
优缺点
优点 缺点
1. 实现简单直接,逻辑易懂 2. 连续内存访问效率高 1. 列长需固定为最长字符串长度,空间浪费 2. 排序、删除等操作繁琐,适合只读场景

(二)字符指针数组实现

本质
  • char*类型的一维数组,每个元素是字符串首元素指针,指向独立的字符数组(可位于栈、堆或只读数据段)。
示例代码
c 复制代码
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函数声明

    c 复制代码
    int main(int argc, char *argv[]); // 常用声明
    // argc:参数个数(至少为1),argv:字符串数组(首个元素为程序路径)

(二)参数解析示例

c 复制代码
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为参数)。

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

(一)实现条件

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

(二)示例代码

c 复制代码
// 栈上可修改的字符串
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 栈/堆区)。

十一、结构体类型定义

(一)基础语法

c 复制代码
typedef struct 结构体本名 {
    成员类型1 成员名1;
    成员类型2 成员名2;
    // ...
} 结构体别名;

示例

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

(二)关键规则

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

      c 复制代码
      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;

(二)底层等价性

指针->成员 等价于 (*指针).成员,因.优先级高于*,需显式加括号:

c 复制代码
(*p).score = 90; // 与 p->score = 90; 等价

十四、结构体数据传递

(一)值传递(对象作为参数)

  • 特点 :函数接收结构体副本,修改不影响原始对象(类似int值传递)。

  • 示例

    c 复制代码
    void swap_struct(Student s1, Student s2) { // 值传递
        Student temp = s1;
        s1 = s2;
        s2 = temp; // 仅修改副本,原对象不变
    }

(二)指针传递(指针作为参数)

  • 特点:函数接收结构体地址,可通过指针修改原始对象(类似数组传递)。

  • 示例

    c 复制代码
    void swap_struct_ptr(Student *p1, Student *p2) { // 指针传递
        Student temp = *p1;
        *p1 = *p2;
        *p2 = temp; // 修改原始对象
    }

(三)作为返回值

  • 对象返回 :返回结构体副本(类似int返回,不涉及原内存)。

    c 复制代码
    Student create_stu() {
        return (Student){2, "Clearlove", 'm', 85}; // 返回副本
    }
  • 指针返回 :禁止返回栈区结构体指针(会成为野指针),可返回堆区或静态区地址。

    c 复制代码
    Student* create_stu_static() { // 合法(静态区)
        static Student s = {3, "Bin", 'm', 95};
        return &s;
    }

十五、结构体与数组的区别

特性 结构体 数组
初始化方式 {}按成员赋值 {}按元素赋值
对象赋值 支持=(成员逐一拷贝) 禁止=(需逐个元素复制)
作为函数参数 值传递(副本)或指针传递 隐式转换为指针(传递首地址)
返回值支持 允许(返回副本或指针) 禁止(只能返回指针)
内存连续性 成员在内存中连续存储 元素在内存中连续存储

十六、枚举类型基础

(一)定义语法

c 复制代码
typedef enum 枚举本名 {
    枚举值1, // 默认为0
    枚举值2, // 默认为1
    枚举值3 = 5 // 显式赋值为5,后续值依次+1
} 枚举别名;

示例

c 复制代码
typedef enum color {
    RED,     // 0
    GREEN,   // 1
    BLUE = 3 // 3,下一个枚举值YELLOW为4
} Color;

(二)核心特性

  1. 本质 :枚举值是编译时常量,底层为int类型,可直接使用整数赋值或传参。

    c 复制代码
    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分支条件等场景:

    c 复制代码
    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. 解引用限制 :不能直接解引用,需先转换为具体类型指针:

    c 复制代码
    int num = 100;
    void *p = &num;
    int val = *(int*)p; // 强转后解引用
  3. 用途场景

    • 作为函数参数,接收任意类型指针(如malloc返回值)。
    • 实现泛型数据结构(如链表存储不同类型数据)。

(二)注意事项

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

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

(一)malloc函数

c 复制代码
void* malloc(size_t size); // 分配连续的`size`字节内存块
关键点:
  1. 参数size为所需字节数,需手动计算(如malloc(sizeof(int)*n))。

  2. 返回值 :成功返回内存块首地址(void*),失败返回NULL

  3. 初始化 :分配的内存未初始化,包含随机值(MSVC平台显示为0xCD)。

  4. 错误处理 :必须检查返回值,避免使用NULL指针:

    c 复制代码
    int *p = (int*)malloc(100);
    if (p == NULL) {
        perror("malloc failed");
        exit(1);
    }

(二)calloc函数

c 复制代码
void* calloc(size_t num, size_t size); // 分配`num`个元素,每个`size`字节的内存块
关键点:
  1. 初始化 :自动将内存块清零(优于malloc的随机值)。

  2. 参数计算 :总字节数为num*size,适合初始化数组:

    c 复制代码
    int *arr = (int*)calloc(5, sizeof(int)); // 等价于malloc(20)并清零

(三)realloc函数

c 复制代码
void* realloc(void* ptr, size_t new_size); // 调整已分配内存块的大小
关键点:
  1. 内存迁移

    • 若原内存块后有足够空间,直接扩展。
    • 否则分配新块,复制数据并释放旧块(原指针可能失效)。
  2. 返回值 :新内存块首地址,可能与原指针不同;若失败,返回NULL且原内存块保持不变。

  3. 使用场景 :动态调整数组大小:

    c 复制代码
    int *arr = (int*)malloc(5*sizeof(int));
    arr = (int*)realloc(arr, 10*sizeof(int)); // 扩展至10个元素

(四)free函数

c 复制代码
void free(void *ptr); // 释放动态分配的内存块
关键点:
  1. 参数要求 :必须传入分配函数返回的原始指针,否则导致未定义行为(如free中途移动的指针)。

  2. 野指针处理 :释放后建议将指针置为NULL,避免二次释放或访问悬空指针:

    c 复制代码
    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扩展数组时,先保存原指针,避免分配失败导致数据丢失:

      c 复制代码
      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(轻量高效)。
相关推荐
C++ 老炮儿的技术栈1 小时前
VSCode -配置为中文界面
大数据·c语言·c++·ide·vscode·算法·编辑器
刃神太酷啦1 小时前
聚焦 string:C++ 文本处理的核心利器--《Hello C++ Wrold!》(10)--(C/C++)
java·c语言·c++·qt·算法·leetcode·github
物联网嵌入式小冉学长2 小时前
10.C S编程错误分析
c语言·stm32·单片机·算法·嵌入式
不过四级不改名6771 天前
用c语言实现简易c语言扫雷游戏
c语言·算法·游戏
我命由我123451 天前
嵌入式 STM32 开发问题:烧录 STM32CubeMX 创建的 Keil 程序没有反应
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
C++ 老炮儿的技术栈1 天前
手动实现strcpy
c语言·开发语言·c++·算法·visual studio
xtmatao1 天前
正整数的正向分解
c语言
whoarethenext1 天前
使用C/C++的OpenCV 构建人脸识别并自动抓拍系统
c语言·c++·opencv
Navigator_Z1 天前
LeetCode //C - 757. Set Intersection Size At Least Two
c语言·算法·leetcode
几道之旅1 天前
零基础RT-thread第二节:按键控制
c语言·stm32