前面几篇文章我们从指针一路聊到数组、二级指针、命令行参数很多地方都出现了字符串的影子——printf用%s输出argv是一堆char*。但 C 语言里并没有一个叫string的类型。那字符串到底是个什么东西答案其实很纯粹字符串就是以空字符\0结尾的字符数组。今天我们就来彻底搞清楚 C 语言字符串的里里外外。你会学到字符串在内存里怎么存、char[]和char*的真正区别、怎么安全地读写字符串以及标准库提供的那些字符串处理函数。学完这篇你就再也不会被strcpy和strcmp搞迷糊也能写出不怕缓冲区溢出的健壮代码。一、C 风格字符串的本质\0是灵魂在 C 语言里字符串就是一段连续内存中、以空字符\0ASCII 码 0结尾的字符序列。比如字符串hello在内存里实际占用 6 个字节地址: 100 101 102 103 104 105 内容: h e l l o \0所有的字符串操作不管是printf、strlen、strcmp都是从起始地址开始往后逐字节扫描直到遇到\0为止。这个终止符就是字符串的灵魂——没有它字符串函数就无法知道该在哪里停下结果要么读到垃圾值要么直接崩溃。所以一个字符数组如果没有\0就只是一堆字符不是合法的 C 字符串。二、字符数组 vs 字符指针再辨析第十六篇我们已经初步对比过这里再从字符串角度强化一下。方式一字符数组可修改charstr[]hello;编译器在栈上分配一个 6 字节的数组把hello的内容包括\0复制进去。这块内存是你的随便改str[0]H;// 合法变成 Hellosizeof(str)得到的是整个数组的大小6 字节因为str是数组名。方式二字符指针指向只读字面量char*ptrhello;这里ptr只是一个指针指向存储在只读数据区的字符串字面量hello。试图修改会导致未定义行为通常崩溃ptr[0]H;// 危险不要这样做sizeof(ptr)得到的是指针的大小4 或 8 字节而不是字符串长度。方式三const char *最诚实如果你只是想“借来看一看”一个字符串而不修改它用const char *最能表达意图而且编译器会帮你拦下误改的代码constchar*msghello;// msg[0] H; // 编译错误完美一张表总结声明内存位置可否修改内容sizeof 结果char str[] hi栈可以数组大小3char *ptr hi指针在栈字面量在只读区不可以指针大小4/8const char *ptr hi同上不可修改编译器检查指针大小三、字符串的输入输出printf与%s%s从给定地址开始逐个输出字符直到遇到\0不包括它。charstr[]hello;printf(%s\n,str);// 输出 hello可以指定最小宽度和精度.5s表示最多输出前 5 个字符这在我们讲第六篇时提过复习一下。scanf与%s极度危险charname[10];scanf(%s,name);%s会在遇到第一个空白字符空格、换行、Tab时停止读取。它不会做越界检查如果用户输入 20 个字符scanf照样往name里写直接覆盖后面的栈空间这就是典型的缓冲区溢出。解决方案给%s加宽度限制比如%9s表示最多读 9 个字符留一个位置给\0charname[10];scanf(%9s,name);// 最多读 9 个字符安全但%s仍然不能读取带空格的字符串比如 “John Doe”。fgets更安全的输入charbuffer[100];if(fgets(buffer,sizeof(buffer),stdin)!NULL){// 读到的内容可能带换行符}fgets会一直读直到遇到换行符、文件结束或读满sizeof(buffer) - 1个字符然后在末尾加\0。换行符会被保留在字符串里如果空间够你不想要的话得手动去掉。去换行的小技巧buffer[strcspn(buffer,\n)]\0;strcspn返回第一个匹配字符的索引把那个位置的字符替换成\0就行。四、常用字符串处理函数C 标准库string.h提供了一组操作字符串的函数。我们用例子一个一个说明。1.strlen(s)求字符串长度不包括\0#includestring.h#includestdio.hintmain(void){charstr[]hello;printf(长度: %zu\n,strlen(str));// 5return0;}它的内部实现就像我们上回写的size_tstrlen(constchar*s){constchar*ps;while(*p)p;returnp-s;}2.strcpy(dest, src)拷贝字符串把src包括\0复制到dest指向的空间。charsrc[]world;chardest[20];strcpy(dest,src);// dest 现在是 world危险dest必须足够大否则溢出。更安全的是用strncpy。3.strncpy(dest, src, n)限制长度的拷贝chardest[5];strncpy(dest,hello, world,sizeof(dest)-1);dest[sizeof(dest)-1]\0;// 重要手动加 \0strncpy最多拷贝n个字符但如果src的长度 n它不会自动在dest末尾加\0。这是个大坑你必须自己手动补上。4.strcat(dest, src)拼接字符串把src追加到dest末尾从dest的\0处开始覆盖。chardest[20]hello ;strcat(dest,world);// dest 变成 hello world同样有溢出风险推荐strncat。5.strncat(dest, src, n)限制长度的拼接chardest[10]hello ;strncat(dest,world!!!,sizeof(dest)-strlen(dest)-1);strncat最多追加n个字符然后自动加上\0。它比strncpy安全得多。6.strcmp(s1, s2)比较两个字符串按字典序比较返回0相等负数s1小于s2正数s1大于s2if(strcmp(apple,banana)0){printf(apple 在 banana 前面\n);}陷阱不能用比较两个字符串的内容if (s1 s2)比较的是两个指针的地址而不是字符串内容。7.strncmp(s1, s2, n)前 n 个字符比较if(strncmp(hello,helicopter,3)0){printf(前三个字符相同\n);}五、snprintf格式化到字符串更安全的利器很多时候你不需要手动拼接snprintf能把格式化的结果安全地写入缓冲区并保证不溢出、有\0结尾C99 起。charbuffer[50];intyear2026;snprintf(buffer,sizeof(buffer),今年是 %d 年,year);printf(%s\n,buffer);它让格式化输出到字符串变得既安全又灵活强烈推荐。六、自己动手写深入理解实现自己的字符串函数是检验理解的绝佳方式。这里写两个my_strcpy和my_strcmp。#includestddef.hchar*my_strcpy(char*dest,constchar*src){char*originaldest;while(*src!\0){*dest*src;dest;src;}*dest\0;// 记得加终止符returnoriginal;}intmy_strcmp(constchar*s1,constchar*s2){while(*s1!\0*s2!\0){if(*s1!*s2){return(unsignedchar)*s1-(unsignedchar)*s2;}s1;s2;}// 谁先到 \0 谁更短return(unsignedchar)*s1-(unsignedchar)*s2;}观察循环里指针的移动你会发现它们和数组遍历完全一致。自己写一遍就再也不会忘记\0的意义。七、常见错误与陷阱1. 用比较字符串内容chars1[]abc;chars2[]abc;if(s1s2)// 永远为假比较的是地址应该用strcmp。2. 忘记留\0的位置charbuf[5];strncpy(buf,hello,5);// buf 没有 \0printf 会越界永远保证缓冲区比最大字符数多 1。3. 使用gets已从 C11 标准移除但老代码中可能有charbuf[100];gets(buf);// 无论输入多长都往里写极度危险用fgets替代。4. 返回局部字符数组的地址char*get_message(void){charmsg[]hello;returnmsg;// 返回栈上数组地址无效}应返回字面量指针、静态数组地址或动态分配的内存。八、小结今天我们真正理解了 C 语言字符串的本质一个以\0结尾的字符数组。你学到了\0是所有字符串函数工作的基础。字符数组和字符指针的区别决定了你可不可以修改字符串。scanf(%s)危险fgets安全宽度限制不能忘。strlen、strcpy、strcat、strcmp的用法以及它们的安全版本strncpy、strncat、strncmp。snprintf是一个更安全的格式化工具。字符串是 C 语言中最常用的“非标量”类型后续我们写文件操作、网络通信、用户交互几乎都离不开它。现在你已经有了安全使用它的基础。下一篇我们要迈入另一个重量级话题函数与指针的强强联合——如何让函数接收函数作为参数回调函数以及那个让很多人头疼的“函数指针”到底怎么用。课后小练习写一个函数size_t my_strlen(const char *s)不使用任何库函数实现strlen的功能。写一个函数void safe_concat(char *dest, const char *src, size_t dest_size)安全地把src拼接到dest末尾保证不溢出并以\0结尾。用fgets读取用户输入的一行文字可能含空格去掉末尾换行符然后统计其中的字母、数字和空格的个数。陷阱题下面的代码有问题吗如果有指出问题并修正char*p;strcpy(p,hello);实战实现一个简单的grep过滤器从标准输入逐行读取字符串如果该行包含用户指定的关键词通过argv[1]传入就打印这一行。提示使用fgets和strstrstrstr(haystack, needle)返回指向 needle 首次出现位置的指针找不到返回 NULL。我们下期见获取本系列示例代码请访问 GitCode 仓库。