第1章:C语言字符串基础——字符数组与字符串常量、字符串的存储本质、'\0'结束符的重要性
各位同学,咱们今天聊聊C语言里最基础、也最容易踩坑的东西——字符串。
说实话,我刚开始学C语言那会儿,觉得字符串不就是一堆字符嘛,有啥好讲的?直到我在一个嵌入式项目里,因为少写了一个'\0',导致整个通信协议解析全乱套,排查了整整两天……嗯,从那以后,我再也不敢小看这个小小的结束符了。
1.1 字符数组与字符串常量:两个长得像,脾气不一样
先问大家一个问题:下面这两行代码,有什么区别?
char str1[] = "Hello";
char *str2 = "Hello";
看起来差不多,对吧?但它们的本质完全不同。
str1 是一个字符数组。它在栈上分配了6个字节的空间(5个字符 + 1个'\0')。你可以修改里面的内容,比如 str1[0] = 'h'; 是合法的。
str2 是一个指针,指向一个字符串常量。"Hello"这个字符串常量存储在代码段(只读区)里。你试试 str2[0] = 'h'; —— 大概率会段错误。
核心区别一句话: 数组是“自己的房子”,可以随便装修;指针是“别人家的房子”,只能看不能动。
我在一个老项目中见过这样的代码:
char *p = "Hello";
p[0] = 'M'; // 试图改成 "Mello"
printf("%s\n", p);
这段代码在某些编译器上能跑,在某些编译器上直接崩溃。为什么?因为字符串常量是否可修改,是C标准里定义的“未定义行为”。说白了,就是编译器想怎么干就怎么干。你猜我后来怎么修的?改成字符数组就完事了。
1.2 字符串的存储本质:内存里到底长啥样?
咱们把内存想象成一排带编号的格子。每个格子能放一个字节的数据。
对于 char str[] = "Hi";,内存布局是这样的:
地址: 0x1000 0x1001 0x1002
数据: 'H' 'i' '\0'
对于 char *p = "Hi";,情况就复杂一些:
指针p本身(在栈上): 0x2000 0x2001 0x2002 0x2003 (存储着地址值)
指向 ↓
字符串常量区: 0x3000 0x3001 0x3002
'H' 'i' '\0'
你看,指针变量p自己占4个字节(32位系统),里面存的是0x3000这个地址。而字符串本身在另一个地方。
我的个人习惯: 只要字符串内容需要修改,一律用字符数组。只有确定只读访问时,才用指针指向字符串常量。这样能避免很多莫名其妙的bug。
咱们用一张图来总结一下字符串的存储本质:
1.3 '\0'结束符的重要性:没有它,字符串就是个野孩子
'\0' 是ASCII码为0的字符,也叫空字符。它不是数字'0',也不是空格。它的唯一作用就是告诉C语言:字符串到这里结束了。
你想想看,C语言怎么知道一个字符串有多长?它没有像Java那样的 length() 方法。它只能从起始地址开始,一个字节一个字节地往后读,直到碰到'\0'为止。
这就是为什么下面这段代码会出问题:
char buf[5] = {'H', 'e', 'l', 'l', 'o'};
printf("%s\n", buf); // 危险!没有'\0'结束符
printf会一直往后读内存,直到在某个地方碰巧遇到一个0字节。你可能会看到 "Hello" 后面跟着一堆乱码,甚至直接崩溃。
我曾经踩过的坑: 在一个串口通信项目中,我定义了一个接收缓冲区 char rx_buf[64];,然后从串口读数据填进去。我忘了在末尾加'\0',就直接用 strlen() 计算长度。结果 strlen() 返回了127,因为缓冲区后面正好有个'\0'在很远的地方。那次排查花了我整整一个下午。
正确的做法是:
char buf[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 显式加结束符
// 或者更简单:
char buf[] = "Hello"; // 编译器自动加'\0'
1.4 常见陷阱与避坑指南
我总结了几个新手(甚至老手)容易犯的错误,你对照看看中过几个:
| 陷阱类型 | 错误示例 | 正确做法 |
|---|---|---|
| 忘记分配'\0'空间 | char s[5] = "Hello"; |
char s[6] = "Hello"; 或 char s[] = "Hello"; |
| 误用sizeof计算字符串长度 | char *p = "Hi"; sizeof(p) 返回4或8 |
用 strlen(p) 获取实际字符数 |
| 修改字符串常量 | char *p = "Hi"; p[0] = 'h'; |
用字符数组 char p[] = "Hi"; |
| strcpy目标缓冲区太小 | char dst[5]; strcpy(dst, "Hello"); |
确保目标缓冲区足够大,或用 strncpy |
我的一个小技巧: 定义字符数组时,如果确定要存N个字符,就分配N+1个字节。多出来的1个字节留给'\0'。这个习惯帮我避免了很多缓冲区溢出问题。
1.5 字符串初始化:几种方式对比
咱们来看看几种常见的初始化方式,以及它们的区别:
// 方式1:完全初始化
char s1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// 方式2:部分初始化,剩余自动填0
char s2[10] = {'H', 'e', 'l', 'l', 'o'}; // s2[5]到s2[9]都是'\0'
// 方式3:字符串常量初始化(最常用)
char s3[] = "Hello"; // 自动分配6字节
// 方式4:指针指向字符串常量
char *s4 = "Hello"; // 只读,不可修改
// 方式5:全部初始化为0
char s5[10] = {0}; // 所有字节都是'\0'
我个人最喜欢方式3和方式5。方式3简洁明了,方式5适合用作缓冲区,因为一开始就是干净的。
你可能会问:方式2中只给了5个字符,但数组有10个字节,剩下的怎么办?C标准规定:未显式初始化的元素,自动初始化为0。而'\0'的ASCII码就是0。所以剩下的5个字节自动变成了'\0'。
1.6 字符串的本质:字符数组 + '\0'
说了这么多,其实就一句话:C语言的字符串,本质上就是一个以'\0'结尾的字符数组。
没有'\0',它就不是一个合法的字符串,只是一堆字符的集合。所有C标准库的字符串函数(strlen、strcpy、strcmp等)都依赖'\0'来工作。你给它们一个没有'\0'的数组,它们就会像脱缰的野马一样,在内存里乱跑。
我记得有一次帮同事review代码,他写了一个自定义的字符串拼接函数,里面用 while (*p != '\0') 来定位末尾。结果他忘了在拼接后加'\0',导致后续的 printf 打印出了奇怪的内容。我给他加了一行 *p = '\0';,问题就解决了。
记住这个原则: 任何时候你手动构造或修改字符串,都要确保在正确的位置放一个'\0'。这是C语言程序员的“职业素养”。
1.7 本章小结
咱们这一章讲了三个核心概念:
- 字符数组 vs 字符串常量: 数组可修改,常量不可修改。需要修改时用数组,只读时可以用指针。
- 存储本质: 数组在栈上连续存储,指针指向只读区。理解内存布局能帮你避免很多低级错误。
- '\0'结束符: 它是字符串的“句号”。没有它,所有字符串操作都会出问题。永远记得为'\0'预留空间。
下一章咱们会深入讲字符串的输入输出,以及那些容易让人头疼的缓冲区问题。到时候我会分享一个我当年在Linux下写网络协议解析器时遇到的真实案例——保证让你印象深刻。
公众号:蓝海资料掘金营,微信deep3321