目录第二阶段C 基础一一维数组1. 声明2.初始化3遍历数组4 数组名 首元素地址二二维数组1: 声明2:初始化3遍历4 二维数组名①二维数组名matrix②matrix[0]③matrix[0][0]总结三字符串1字符串的本质char 数组2char 数组 vs 字符串字面量①char str[] Hello②char *str Hello③总结④字面量的应用3\0 结束符详解①忘了预留位置②手动构造字符串时忘了写 \0③strncpy 不自动加 \0重点四常用字符串函数①strlen — 获取字符串长度②strcpy / strncpy/ strlcpy 复制字符串1strcpy 是干什么的2strcpy 最大的问题是什么3为什么叫“写穿栈”4strncpy改进strcpy5为什么 strncpy 这么设计6为什么嵌入式推荐strncpy这个写法7那 strlcpy 是什么③strcmp / strncmp 比较字符串1strcmp 是干什么的2strncmp 是什么3为什么嵌入式特别喜欢 strncmp④strcat 拼接字符串1它的作用2为什么危险3嵌入式中推荐 snprintf4strncpy strlen 拼接代替strcat⑤sprintf — 格式化输出到字符串⑥snprintf — 嵌入式里的超级英雄五格式化输入sscanf1基本用法2为什么嵌入式里 sscanf / snprintf 比 scanf / printf 更常用3sscanf 实战示例4sscanf 注意事项六综合总结第二阶段C 基础一一维数组1. 声明类型 数组名[元素个数];int arr[10]; // 声明一个能放 10 个 int 数据的数组这几句话合起来其实是在教你如何在C语言特别是嵌入式领域安全地使用数组1元素个数必须是编译期常量C89/C90C99 起支持变长数组VLA但嵌入式开发中几2几乎不用 VLA——栈空间宝贵不确定性是致命的。比如 int arr[n]3数组在栈上分配如果声明在函数内占用 sizeof(类型) * 元素个数 字节。4声明后不会自动初始化为 0和全局变量不同里面是垃圾值int arr[10];假设 sizeof(int)是 4 字节。那么占用的内存就是4 * 10 40字节。这40字节紧紧挨在一起构成了数组。2.初始化int a[5] {1, 2, 3, 4, 5}; // 完整初始化 int b[5] {1, 2}; // 部分初始化 → {1, 2, 0, 0, 0} int c[5] {0}; // 全部初始化为 0 int d[] {1, 2, 3}; // 自动推导长度 → d 长度为 3 // 嵌入式常见写法 uint8_t buffer[256] {0}; // 清零整个缓冲区陷阱int e[5] {0}这只能把第一个元素设为 0但因为花括号初始化器的规则未指定的元素会被隐式清零——整个数组确实是 0但原理要搞清楚。不能在声明之外用arr {1,2,3}整体赋值这是语法错误。int arr[3];arr {1, 2, 3};❌ 错误数组名是常量地址不能作为左值被赋值3遍历数组int a[5]; for(i0;i5;i) { printf(%da[i]); }编译器分配了连续的 5 个int空间。a[0] ~a[4]没有a[5]试图访问第 6 个元素。就像一排房子门牌号是 1-5你去敲 6 号门里面虽然可能有人住但肯定不是这排房子的合法住户。可能会读到脏数据逻辑错误 / 破坏其他变量逻辑混乱 / HardFault系统崩溃4 数组名 首元素地址这是 C 语言最核心的概念之一int arr[5]; printf(%p\n, arr); // arr[0] printf(%p\n, arr[0]); // 和上面完全一样arr表达式的值 数组首元素的地址类型是int*。arr[0]同样是首元素地址类型也是int*。arr和上面两个的数值一样但类型不同——它是int (*)[5]指向整个数组的指针arr 1会跳过整个数组跳过 5 个 int。为指针铺垫的核心思想arr[i]本质上就是*(arr i)编译器把下标操作翻译成指针算术。后面学指针时会反复用到这个等价关系解释arr数组的首地址第 0 个元素的地址。i偏移量第几个元素。arr i这不是简单的数学加法。这是指针算术。意思是“从arr开始向后跳过i个‘格子’”。* 解引用。意思是“把那个地址里存的数给我取出来”。arr[i]在编译器的眼里根本就不是什么“数组操作”它就是“去那个计算出来的地址拿数据”。int arr[5] {10, 20, 30, 40, 50};arr[0]*(arr 0) 10arr[1]*(arr 1) 20arr[2]*(arr 2) 30arr[3]*(arr 3) 40arr[4]*(arr 4) 50二二维数组1: 声明int matrix[3][4]; // 3 行 4 列内存布局是行优先row-major同一行的 4 个元素在内存中连续存放然后是下一行。matrix[0][0], matrix[0][1], ..., matrix[2][2], matrix[2][3]依次排列。2:初始化int a[2][3] { {1,2,3}, {4,5,6} }; // 逐行初始化 int b[2][3] { 1,2,3,4,5,6 }; // 等价按行优先填充 int c[2][3] { {1}, {4} }; // 每行其余补 0 int d[][3] { {1,2,3}, {4,5,6} }; // 行数可省列数不能省关键规则只能省略第一维的大小第二维必须明确——编译器需要知道每一行是多少个元素才能正确计算偏移3遍历int matrix[3][4] for (int i 0; i 3; i) { for (int j 0; j 4; j) { printf(%d , matrix[i][j]); } printf(\n); }4 二维数组名int matrix[3][4]核心思想C语言没有真正的“二维数组”二维数组不是里面全是int而是里面放着三个一维数组。①二维数组名matrix当数组名参与表达式时会退化(decay)成指针。指向一个包含4个int数组的指针即int (*)[4]②matrix[0]第一行第一个元素③matrix[0][0]二位数组第一个数字总结三字符串1字符串的本质char数组C 没有专门的字符串类型。字符串就是一个以\0NUL数值为 0 结尾的char数组字符: H e l l o \0 下标: 0 1 2 3 4 5\0的重要性怎么强调都不过分所有标准库字符串函数strlen,strcpy,strcmp等都靠\0来判断字符串结束。忘了它会导致strlen一直数下去读到数组外strcpy疯狂拷贝把栈写穿嵌入式设备死机、看门狗复位、Flash 被意外改写这是嵌入式 C 开发排第一的 bug 来源。2char 数组 vs 字符串字面量①char str[] Hello// 方式一char 数组可修改 char str1[6] {H, e, l, l, o, \0}; // 手动逐字符 char str2[] Hello; // 等价编译器自动加 \0 char str3[20] Hello; // 剩余 14 字节自动填 0char str[] Hello是拷贝了一份字符串编译器实际干了两件事申请6个字节地址0x100~ 0x105然后把 Hello\ 0 全部复制进去修改时令str[0]A;②char *str Hello很多人以为编译器也是内存然后创建数组其实不是。编译器通常会先在程序里面放一份固定的数据然后我们创建的字符指针变量str里面存放地址0x5000指向该字符串的地址printf(%s,str);得到0x5000为什么不能修改如果写代码str[0]A; 所代表的意义就是把地址0x5000中首元素H改为A但是0x5000属于程序只读区(.rodata)CPU通常不给写报错HardFault为什么要放只读区因为程序里面可能写很多次Hello printf(Hello);char *aHello; char *bHello; char *cHello; 他只需要保存一份然后abc指针指向同一个地址接口即可三个人共同指向这一份。所以不能改。否则a改了b变了c也变了程序不合理因此干脆放到只读区谁都不能改。为什么嵌入式特别强调这一点③总结④字面量的应用3\0结束符详解这是所有字符串 bug 的根源值得多讲几句。①忘了预留位置char buf[5] Hello; // ❌ 数组太小Hello 5个字符 1个\0 6字节 // 这里没有空间放 \0strlen(buf) 会越界②手动构造字符串时忘了写 \0char buf[10]; buf[0] O; buf[1] K; // ❌ 没有 buf[2] \0; printf(%s, buf); // 打印 OK 后又打印一堆垃圾直到碰巧遇到一个 0③strncpy 不自动加 \0重点char dst[5]; strncpy(dst, Hello World, 5); // dst 现在是 {H,e,l,l,o} — 没有 \0 // strlen(dst) → 越界灾难strncpy用于复制固定长度字符数组工业协议用字符串判断的话就是通信协议出错了程序错乱不符合对应响应了。四常用字符串函数①strlen— 获取字符串长度从s开始逐字节扫描遇到\0停止。返回的是字符个数不含 \0。时间复杂度 O(n)不要在循环条件里频繁调用不推荐的写法是for (int i 0; i strlen(s); i) { ...因为每一次循环他都会 重新扫描整个字符串后再判断循环条件是否满足随后进入循环写成size_t len strlen(s); for (int i 0; i len; i) { ... }②strcpy/strncpy/strlcpy复制字符串1strcpy 是干什么的char *strcpy(char *dest, const char *src); 指的是 把 src 指向的字符串完整复制到 dest完整复制包括最后的\0。例如dest[6] ~ dest[19]没有被修改。后面14个字节内容不动14字节保持原来的状态。2strcpy 最大的问题是什么比如char dest[5];strcpy(dest,Hello);Hello需要6字节但是dest字符数组只有5个位置可能写到了栈上的其他变量这就叫缓冲区溢出buffer overflow。3为什么叫“写穿栈”name太小H e l l o W o r l d一直写可能覆盖 flag 返回地址 上面所以叫写穿栈严重情况下甚至可以控制程序流程。4strncpy改进strcpy复制了5个字符hello 到 dest 中没有空间就不会加上\0了所以它不是字符串。5为什么 strncpy 这么设计因为它原本不是为了字符串它是为了固定长度字段6为什么嵌入式推荐strncpy这个写法例如srcABCDEFGHIJKLMstrncpy(dest,src,9); 复制了A B C D E F G H I 随后dest[9]\0;强行封口A B C D E F G H I \0 永远合法。7那 strlcpy 是什么strncpy 是反人类涉及的一个函数strlcpy(dest,src,size);得到了H e l l \0 自动截断后补\0。③strcmp/strncmp比较字符串1strcmp 是干什么的比较两个字符串是否相同完全一样时返回0 不相同时返回非0值 有的小于0 有的大于02strncmp 是什么比如strncmp(Hello,Help,3);返回0 后面的 没有比较。3为什么嵌入式特别喜欢 strncmp因为很多通信不是完整字符串比如串口接收ATRST这是 ESP8266/ESP32 常见 AT 命令用于接收比较但是实际接收ATRST\r\n那么此时strcmp的比较返回值就对不上可以实现较好的比较功能来实习通信的协议。④strcat 拼接字符串1它的作用把一个字符串追加到另一个字符串的末尾2为什么危险但是它的危险点也和strcpy()一样它完全不知道目标数组有多大。写到了别人的内存。3嵌入式中推荐 snprintfstrcpy和 strcat是以前常用的函数但它们有个大毛病它们不知道你的内容有多大。为了解决这个“不知道蛋糕有多大”的问题C语言标准后来引入了snprintf​ 函数。它的特点是自带保险绝不溢出snprintf(buf, sizeof(buf), Speed%d, 100);snprintf是怎么干的拿到图纸看到你要拼成 Speed100。测量盒子看到你告诉它盒子只有 20 个格子大。小心翼翼地装它开始往盒子里放字符 S, p, e, e, d, , 1, 0, 0...绝不让溢出如果在装的过程中发现剩下的格子不够放了它会自动停止绝不会硬塞导致溢出。如果数组长度不够那么最后一个字符一定是放\0其余字符就截断了。4strncpy strlen 拼接代替strcat⑤sprintf— 格式化输出到字符串把格式化内容写入字符数组char buf[50]; sprintf(buf, ADC: %d mV, Temp: %d.%d C, adc_val, temp / 10, temp % 10);问题同 strcpy不检查边界char buf[10]; sprintf(buf, Hello World from sensor); // ❌ 写穿 10 字节嵌入式里基本不用sprintf⑥snprintf— 嵌入式里的超级英雄它在嵌入式里面非常常用因为它同时解决两个问题格式化数据防止缓冲区溢出sizeof(buf)10所以最多写9个字符 1个\0不会导致写入的数据越界。和 sprintf 的唯一区别就是多了一个size参数保证不会写超出size-1个字符且一定会以\0结尾。返回值可以用来判断缓冲区是否够用五格式化输入sscanf1基本用法int sscanf(const char *str, const char *format, ...);从字符串中按格式读取数据返回值是成功匹配并赋值的变量个数。2为什么嵌入式里 sscanf / snprintf 比 scanf / printf 更常用sscanf()这一部分其实比前面的strlen/strcpy更接近嵌入式实际开发因为它解决的是把收到的一串文本按照规则拆成变量。你之前学 UART 数据包时其实已经接近这个思想串口收到ATCSQ: 17,993sscanf 实战示例int sscanf( const char *str, const char *format, ... );第一个参数要解析的字符串比如123 456第二个参数解析规则比如%d %d第三个参数保存结果的变量地址ab例1GPS模块输出$GPGGA,123519,4807.038,N,01131.000,E,...这是 GPS 的标准格式。含义123519→12点35分19秒4807.038→纬度N→北纬01131.000→经度E→东经sscanf( nmea, $GPGGA,%2d%2d%2d,%f,%*c,%f,%*c, hour, min, sec, lat, lon );$GPGGA匹配 $GPGGA之后%2d%2d%2d继续接收123519这样子就把时间给解析到 hour12min35sec19例2// 3. 解析 AT 命令响应 // CSQ: 17,99 int rssi, ber; int matched sscanf(response, CSQ: %d,%d, rssi, ber); if (matched 2) { // RSSI 17, BER 99 (99 表示不可检测) }例3// 4. 解析十六进制 uint32_t addr; sscanf(0x08002000, %x, addr); // addr 0x08002000 // 5. 跳过不需要的内容 sscanf(Sensor ID:42 Temp:250, Sensor ID:%d %*s %*s, id); // %*s 跳过一整个字符串到空白为止4sscanf 注意事项// ❌ 错误%d 对应 int*不能传 char 或 short 的地址 char c; sscanf(65, %d, c); // 把 4 字节 int 写到 1 字节 char 里栈破坏 // ✅ 正确用正确的格式符 int val; sscanf(65, %d, val); c (char)val;六综合总结