第1章:字符数组与字符串——从底层理解C语言的灵魂

大家好,我是你们的老朋友。今天咱们来聊聊C语言里一个特别容易踩坑、但又特别基础的话题——字符数组与字符串。

说实话,我刚开始学C语言那会儿,觉得字符串不就是用双引号括起来的一串字符吗?有什么好讲的?直到我在一个嵌入式项目里,因为字符串拷贝没处理好,导致整个系统跑飞了……嗯,从那以后,我再也不敢小看这玩意儿了。

1.1 字符数组的初始化:你以为你懂了,其实未必

字符数组,说白了就是存放字符的数组。但它的初始化方式,比普通数组要灵活得多。

// 方式一:逐个字符初始化
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

// 方式二:字符串常量初始化
char str2[6] = "Hello";

// 方式三:省略数组大小
char str3[] = "Hello";  // 编译器自动计算为6个元素

我个人习惯用方式二或方式三。为什么?因为省事,而且可读性强。但要注意一个细节:字符串常量末尾自动带一个'\0'(空字符),所以数组长度至少要比字符个数多1。

⚠️ 常见陷阱: 如果你写成 char str[5] = "Hello";,编译器不会报错,但数组只有5个元素,放不下结尾的'\0'。这就埋下了隐患——后面用字符串函数时,它会一直往后读内存,直到碰上一个'\0'为止。你想想看,这多危险?

我在项目中遇到过类似的问题。有一次调试一个串口通信程序,发现接收到的数据总是多出几个乱码。查了半天,原来就是字符数组没留够'\0'的位置。从那以后,我每次定义字符数组都会多算一个位置。

1.2 字符串的存储方式:内存里到底长什么样?

咱们来看看字符串在内存中是怎么存的。我用一个简单的例子来说明:

char str[] = "C语言";
printf("数组大小: %lu\n", sizeof(str));  // 输出 7(每个汉字占2字节,加上结尾'\0')
printf("字符串长度: %lu\n", strlen(str)); // 输出 6

这里有个关键区别:sizeof 计算的是数组占用的总字节数,包括结尾的'\0';而 strlen 只计算有效字符个数,不包含'\0'。

为什么会这样?因为C语言中字符串的结束标志就是'\0'。所有字符串处理函数,比如 strcpystrcatstrlen,都是靠检测'\0'来判断字符串在哪结束的。

核心要点: 没有'\0',就不是一个合法的C字符串。它只是一个普通的字符数组。

我画了一张图,帮你理解字符串在内存中的布局:

字符串 "Hello" 在内存中的存储布局 H e l l o \0 0x1000 0x1001 0x1002 0x1003 0x1004 0x1005 结束标志 数组大小: 6字节 | 字符串长度: 5字符

1.3 字符串输入输出的陷阱:那些年我们踩过的坑

字符串的输入输出,看起来简单,实际上处处是坑。我挑几个最常见的说说。

陷阱一:scanf 读取字符串时的缓冲区溢出

char name[10];
scanf("%s", name);  // 如果用户输入超过9个字符,直接溢出!

我曾经在一个学生项目中看到这样的代码。用户输入了一个很长的名字,程序直接崩溃了。为什么?因为 scanf("%s") 不会检查缓冲区大小,它会一直读,直到遇到空白字符为止。超出数组的部分,就会覆盖相邻的内存区域。

💡 我的建议: 使用 scanf("%9s", name) 来限制读取长度,或者干脆用 fgets() 代替。

陷阱二:gets 函数——C语言史上最危险的函数之一

char buf[100];
gets(buf);  // 危险!没有长度限制

这个函数在C11标准中已经被正式移除了。但很多老代码里还能看到。它没有任何缓冲区保护,输入多长它就读多长。黑客经常利用这个漏洞进行缓冲区溢出攻击。

嗯,这里要注意:永远不要在生产代码中使用 gets()。用 fgets() 替代它。

陷阱三:printf 输出时的格式匹配

char str[] = "Hello";
printf("%s\n", str);   // 正确
printf("%s\n", &str);  // 错误!但编译器可能不报错

这里 str 本身就是数组首地址,所以 %s 直接传 str 就行。传 &str 取的是整个数组的地址,类型不匹配,但有些编译器只会给个警告。

1.4 字符串安全函数:别再拿生产环境开玩笑

传统的字符串函数,比如 strcpystrcatsprintf,都有一个共同的问题:不检查目标缓冲区的大小。这在嵌入式系统里尤其致命——一个缓冲区溢出就可能让整个系统崩溃。

好在C11标准引入了一批安全函数,它们以 _s 结尾,比如:

传统函数 安全函数 说明
strcpy strcpy_s 需要指定目标缓冲区大小
strcat strcat_s 拼接时检查剩余空间
sprintf snprintf 限制输出长度(非_s系列,但更常用)
gets gets_s 限制读取长度

来看看 strncpy 的正确用法:

char dest[10];
const char* src = "Hello, World!";

// 错误用法:strcpy 会溢出
// strcpy(dest, src);

// 正确用法:strncpy 限制拷贝长度
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';  // 手动添加结束符

printf("dest = %s\n", dest);  // 输出 "Hello, Wo"

⚠️ 注意: strncpy 有一个坑——如果源字符串长度大于等于指定的拷贝长度,它不会自动添加 '\0'。所以一定要手动在末尾加 '\0',就像上面代码做的那样。

我个人更推荐使用 snprintf,它用起来更直观:

char dest[10];
const char* src = "Hello, World!";

snprintf(dest, sizeof(dest), "%s", src);
// snprintf 会自动在末尾加 '\0',并且不会溢出

我曾经在一个物联网项目里,用 snprintf 替代了所有的 sprintf。从那以后,再也没有出现过因为字符串格式化导致的崩溃问题。你想想看,一个简单的函数替换,就能避免那么多潜在风险,何乐而不为呢?

小结

这一章我们聊了字符数组的初始化、字符串的存储方式、输入输出的陷阱,以及安全函数的使用。说白了,核心就一句话:永远不要假设用户输入是安全的,永远给缓冲区留够空间,永远记得加 '\0'

这些看似基础的知识,恰恰是很多严重bug的根源。我在职业生涯中见过太多因为字符串处理不当导致的问题——从程序崩溃到安全漏洞,再到数据损坏。希望你能从这一章开始,养成好的编程习惯。

📌 本章核心要点:

  • 字符数组初始化时,记得留一个位置给 '\0'
  • sizeof 和 strlen 的区别:sizeof 算 '\0',strlen 不算
  • 永远不要用 gets(),少用 scanf("%s")
  • 优先使用 strncpy、snprintf 等安全函数
  • 使用 strncpy 后,手动添加 '\0'
公众号:蓝海资料掘金营,微信 deep3321