news 2026/7/16 15:50:04

C语言基础回顾(2026/7/16)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言基础回顾(2026/7/16)

目录

第二阶段:C 基础

<一>:一维数组

1. 声明

2.初始化

3:遍历数组

4: 数组名 = 首元素地址

<二>:二维数组

1: 声明

2:初始化

3:遍历

4: 二维数组名

①:二维数组名matrix

②:matrix[0]

③:matrix[0][0]

总结:

<三>:字符串

1:字符串的本质(char 数组)

2:char 数组 vs 字符串字面量

①:char str[] = "Hello"

②:char *str = "Hello"

③:总结

④:字面量的应用

3:'\0' 结束符详解

①:忘了预留位置

②:手动构造字符串时忘了写 '\0'

③:strncpy 不自动加 '\0'(重点!)

<四>:常用字符串函数

①:strlen — 获取字符串长度

②:strcpy / strncpy/ strlcpy 复制字符串

1:strcpy 是干什么的?

2:strcpy 最大的问题是什么

3:为什么叫“写穿栈”?

4:strncpy改进strcpy

5:为什么 strncpy 这么设计?

6:为什么嵌入式推荐strncpy这个写法?

7:那 strlcpy 是什么?

③:strcmp / strncmp 比较字符串

1:strcmp 是干什么的?

2:strncmp 是什么?

3:为什么嵌入式特别喜欢 strncmp?

④:strcat 拼接字符串

1:它的作用

2:为什么危险?

3:嵌入式中推荐 snprintf?

4:strncpy + strlen 拼接代替strcat

⑤:sprintf — 格式化输出到字符串

⑥:snprintf — 嵌入式里的超级英雄

五:格式化输入:sscanf

1:基本用法

2:为什么嵌入式里 sscanf / snprintf 比 scanf / printf 更常用

3:sscanf 实战示例

4:sscanf 注意事项

六:综合总结


第二阶段:C 基础

<一>:一维数组

1. 声明

类型 数组名[元素个数];

int arr[10]; // 声明一个能放 10 个 int 数据的数组

这几句话合起来,其实是在教你如何在C语言(特别是嵌入式领域)安全地使用数组:

1:元素个数必须是编译期常量(C89/C90),C99 起支持变长数组(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(i=0;i<5;i++) { printf("%d",a[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) = 10

arr[1]=*(arr + 1) = 20

arr[2]=*(arr + 2) = 30

arr[3]=*(arr + 3) = 40

arr[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 没有专门的字符串类型。字符串就是一个以'\0'(NUL,数值为 0) 结尾的char数组

字符: 'H' 'e' 'l' 'l' 'o' '\0' 下标: 0 1 2 3 4 5

'\0'的重要性怎么强调都不过分:

所有标准库字符串函数(strlen,strcpy,strcmp等)都靠'\0'来判断字符串结束。忘了它会导致:

  • strlen一直数下去,读到数组外
  • strcpy疯狂拷贝,把栈写穿
  • 嵌入式设备死机、看门狗复位、Flash 被意外改写

这是嵌入式 C 开发排第一的 bug 来源。

2:char 数组 vs 字符串字面量
①:char str[] = "Hello"
// 方式一:char 数组(可修改) char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动逐字符 char str2[] = "Hello"; // 等价,编译器自动加 '\0' char str3[20] = "Hello"; // 剩余 14 字节自动填 0

char 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 *a="Hello"; char *b="Hello"; char *c="Hello"; 他只需要保存一份然后abc指针指向同一个地址接口即可

三个人共同指向这一份。所以不能改。否则,a改了,b变了,c也变了,程序不合理

因此干脆放到只读区谁都不能改。

为什么嵌入式特别强调这一点?

③:总结

④:字面量的应用

3:'\0'结束符详解

这是所有字符串 bug 的根源,值得多讲几句。

①:忘了预留位置
char buf[5] = "Hello"; // ❌ 数组太小!"Hello" = 5个字符 + 1个'\0' = 6字节 // 这里没有空间放 '\0',strlen(buf) 会越界
②:手动构造字符串时忘了写 '\0'
char 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复制字符串
1:strcpy 是干什么的?

char *strcpy(char *dest, const char *src); 指的是 把 src 指向的字符串,完整复制到 dest,完整复制包括最后的\0

例如:

dest[6] ~ dest[19]没有被修改。后面14个字节内容不动,14字节保持原来的状态。

2:strcpy 最大的问题是什么

比如:
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 ,返回地址 上面

所以叫:写穿栈,严重情况下甚至可以控制程序流程。

4:strncpy改进strcpy

复制了5个字符hello 到 dest 中,没有空间就不会加上\0了,所以它不是字符串。

5:为什么 strncpy 这么设计?

因为它原本不是为了字符串,它是为了:固定长度字段

6:为什么嵌入式推荐strncpy这个写法?

例如:

src:ABCDEFGHIJKLM

strncpy(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比较字符串
1:strcmp 是干什么的?

比较两个字符串是否相同

完全一样时,返回0 ,不相同时,返回非0值 (有的小于0 有的大于0)

2:strncmp 是什么?

比如:

strncmp("Hello","Help",3);

返回0 ,后面的: 没有比较。

3:为什么嵌入式特别喜欢 strncmp?

因为很多通信不是完整字符串,比如串口接收AT+RST,这是 ESP8266/ESP32 常见 AT 命令

用于接收比较,但是实际接收AT+RST\r\n,那么此时strcmp的比较返回值就对不上,

可以实现较好的比较功能来实习通信的协议。

④:strcat 拼接字符串
1:它的作用

把一个字符串追加到另一个字符串的末尾,

2:为什么危险?

但是它的危险点也和strcpy()一样:它完全不知道目标数组有多大。

写到了别人的内存。

3:嵌入式中推荐 snprintf?

strcpy和 strcat是以前常用的函数,但它们有个大毛病:它们不知道你的内容有多大。

为了解决这个“不知道蛋糕有多大”的问题,C语言标准后来引入了snprintf​ 函数。它的特点是:自带保险,绝不溢出

snprintf(buf, sizeof(buf), "Speed=%d", 100);

snprintf是怎么干的?

  • 拿到图纸:看到你要拼成 "Speed=100"。

  • 测量盒子:看到你告诉它盒子只有 20 个格子大。

  • 小心翼翼地装:它开始往盒子里放字符 'S', 'p', 'e', 'e', 'd', '=', '1', '0', '0'...

  • 绝不让溢出:如果在装的过程中发现剩下的格子不够放了,它会自动停止,绝不会硬塞导致溢出。

如果数组长度不够,那么最后一个字符一定是放\0,其余字符就截断了。

4:strncpy + 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— 嵌入式里的超级英雄

它在嵌入式里面非常常用,因为它同时解决两个问题:

  1. 格式化数据
  2. 防止缓冲区溢出

sizeof(buf)=10,所以最多写9个字符 + 1个\0,不会导致写入的数据越界。

和 sprintf 的唯一区别就是多了一个size参数,保证不会写超出size-1个字符,且一定会以'\0'结尾

返回值:可以用来判断缓冲区是否够用

五:格式化输入:sscanf

1:基本用法
int sscanf(const char *str, const char *format, ...);

从字符串中按格式读取数据,返回值是成功匹配并赋值的变量个数。

2:为什么嵌入式里 sscanf / snprintf 比 scanf / printf 更常用

sscanf()这一部分其实比前面的strlen/strcpy更接近嵌入式实际开发,因为它解决的是:

把收到的一串文本,按照规则拆成变量。

你之前学 UART 数据包时,其实已经接近这个思想:

串口收到:"AT+CSQ: 17,99"

3:sscanf 实战示例

int sscanf( const char *str, const char *format, ... );

第一个参数:要解析的字符串,比如"123 456"

第二个参数:解析规则,比如"%d %d"

第三个参数:保存结果的变量地址,&a&b

例1:GPS模块输出$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

这样子就把时间给解析到 hour=12,min=35,sec=19

例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 跳过一整个字符串(到空白为止)
4:sscanf 注意事项
// ❌ 错误:%d 对应 int*,不能传 char 或 short 的地址 char c; sscanf("65", "%d", &c); // 把 4 字节 int 写到 1 字节 char 里,栈破坏! // ✅ 正确:用正确的格式符 int val; sscanf("65", "%d", &val); c = (char)val;

六:综合总结

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 15:49:33

2026年7月16日关注简报:AI、Windows与硬件

&#x1f525; 个人主页&#xff1a; 杨利杰YJlio ❄️ 个人专栏&#xff1a; 《Windows 疑难杂症与工单复盘案例库》 《Sysinternals实战教程》 《WINDOWS教程》 《Windows PowerShell 实战》 《IOS插件分析测试》 《超简单&#xff1a;用Python让Excel飞起来》…

作者头像 李华
网站建设 2026/7/16 15:49:32

【限时解密】WPS AI表格私有化部署中的AI算力调度算法(附企业级API调用性能压测报告:TPS提升3.8倍)

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;WPS AI 表格私有化部署全景概览 WPS AI 表格私有化部署是企业级数据智能应用的关键基础设施&#xff0c;旨在将AI驱动的表格理解、公式生成、数据洞察与自动化分析能力完全运行于客户自有环境中&#x…

作者头像 李华
网站建设 2026/7/16 15:49:11

【告别云盘依赖】利用IPv6+Windows共享,打造你的专属高速私有云

1. 为什么你需要私有云替代传统网盘 每次上传大文件到网盘时&#xff0c;那种进度条龟速爬行的焦虑感&#xff0c;相信大家都深有体会。我最近整理家庭相册时&#xff0c;想把50GB的4K视频备份到网盘&#xff0c;结果上传整整耗了一天一夜。更糟的是&#xff0c;当我急需在外调…

作者头像 李华
网站建设 2026/7/16 15:48:16

终极系统稳定性测试:380+种压力测试方法的stress-ng完全指南

终极系统稳定性测试&#xff1a;380种压力测试方法的stress-ng完全指南 【免费下载链接】stress-ng This is the stress-ng upstream project git repository. stress-ng will stress test a computer system in various selectable ways. It was designed to exercise various…

作者头像 李华
网站建设 2026/7/16 15:47:47

SPA部署本质:静态托管、路由回退与基础路径三大核心

1. 这不是“找平台”而是“选基建”&#xff1a;单页面程序部署的本质逻辑 单页面程序&#xff08;SPA&#xff09;部署这件事&#xff0c;很多人一上来就搜“免费平台有哪些”&#xff0c;结果在 Vercel、Cloudflare Pages、Netlify 之间反复横跳&#xff0c;最后发现&#xf…

作者头像 李华