第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。

咱们用一张图来总结一下字符串的存储本质:

字符串存储本质对比图 字符数组 char str[] = "Hi" 'H' 0x1000 'i' 0x1001 '\0' 0x1002 栈上连续分配,可修改 指针 char *p = "Hi" 0x3000 栈上指针变量 'H' 0x3000 'H' '\0' 只读区存储,不可修改 关键总结 1. 字符数组:在栈上分配连续空间,内容可修改,数组名是首地址常量。 2. 字符串常量指针:指针在栈上,字符串在只读区,内容不可修改。 3. 两者都自动包含 '\0' 结束符,但指针方式容易误写导致段错误。 4. 避坑:需要修改字符串内容时,永远用字符数组!

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