欢迎来到第八章在上一章中我们学习了指针。从这一章开始我们将把指针和数组结合起来深入学习 C 语言中最常用的数据之一 ——字符串。字符串在编程中无处不在用户输入、文件内容、网络数据、界面文字…… 几乎所有和 文字 相关的东西本质上都是字符串。但 C 语言比较特别 —— 它没有专门的 字符串类型而是用字符数组 结束符来表示字符串。这种设计既灵活又容易出错是新手踩坑的重灾区。本章我们将系统学习字符串的本质是什么\0到底是干嘛的字符串怎么输入、怎么输出常用的字符串函数有哪些怎么用字符数组和字符指针有什么区别字符串操作有哪些常见的坑学完本章你就能熟练处理各种字符串操作了准备好了吗让我们开始一、字符串的本质1.1 什么是字符串C 语言中字符串就是以 \0 结尾的字符数组。两个关键点它是一个字符数组—— 每个元素是一个 char末尾必须有一个结束标志 \0\0是一个特殊字符叫空字符它的 ASCII 码值是 0。它不算字符串的内容只是用来标记 字符串到这里结束了。比如字符串Hello看起来是 5 个字符实际在内存中占 6 个字节H e l l o \0 ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ 72 │ 101 │ 108 │ 108 │ 111 │ 0 │ └─────┴─────┴─────┴─────┴─────┴─────┘⚠️重要提醒\0是必须的没有它就不是合法的字符串。很多字符串相关的 bug都是因为少了这个结束符导致的。1.2 字符串的两种写法写法一逐个字符初始化不推荐char str[6] {H, e, l, l, o, \0};要手动加\0麻烦容易忘。写法二用双引号推荐char str[] Hello;用双引号括起来的叫字符串字面量。编译器会自动在末尾加上\0不用你手动写。注意用双引号初始化时数组大小要记得给结束符留位置。比如 Hello 有 5 个字符数组至少要 6 个字节。写成char str[5] Hello是不对的会越界。1.3 字符串字面量用双引号括起来的Hello就是一个字符串字面量。它有几个特点存储在只读内存区不能修改类型是const char[]直接写Hello代表的是这个字符串首字符的地址char *p Hello; // p指向字符串字面量的首地址⚠️注意字符指针指向字符串字面量时不要尝试修改内容否则会崩溃char *p Hello; p[0] h; // ❌ 错误字符串字面量是只读的修改会崩溃但如果是字符数组就可以改char arr[] Hello; arr[0] h; // ✅ 没问题数组在栈上可以修改这是字符数组和字符指针的一个重要区别后面还会详细讲。二、字符串的输入输出2.1 输出字符串printf % s用printf的%s格式符可以输出字符串。#include stdio.h int main() { char str[] Hello World; printf(%s\n, str); // 输出Hello World return 0; }%s的工作原理从你给的地址开始一个字符一个字符地输出直到遇到\0为止。 所以如果你的字符串没有\0%s就会一直往后打打出一堆乱码直到碰巧遇到一个 0 为止 —— 这就是越界了。2.2 输入字符串scanf % s用scanf的%s可以读入字符串。#include stdio.h int main() { char name[20]; printf(请输入你的名字); scanf(%s, name); // 注意数组名前面不用加 printf(你好%s\n, name); return 0; }⚠️scanf 的 % s 有两个大坑坑 1遇到空格就停止输入Zhang San 输出你好Zhangscanf(%s)遇到空格、回车、Tab 就会停止所以它读不了带空格的句子。坑 2不检查数组长度容易越界如果你的数组只有 20 个字节用户输入了 100 个字符scanf照样往里写直接越界 —— 这就是著名的缓冲区溢出问题非常危险。2.3 gets 和 puts了解即可puts输出一行字符串char str[] Hello; puts(str); // 输出字符串自动加换行和printf(%s\n)差不多更简单但只能输出字符串。gets读入一行字符串不推荐char str[100]; gets(str); // 读一整行包括空格gets可以读一整行遇到回车才停止但它完全不检查缓冲区长度比scanf还危险已经被 C 标准废弃了。安全建议实际开发中更推荐用fgets来读字符串可以指定最大读多少更安全。初学阶段先用scanf没问题知道有这个坑就行。三、字符数组 vs 字符指针很多新手搞不清这两个的区别char arr[] Hello; // 字符数组 char *p Hello; // 字符指针它们看起来都能当字符串用但区别很大对比项char arr[] Hellochar *p Hello存储位置栈上局部变量时只读数据区内容可修改吗✅ 可以修改❌ 不能修改arr/p 是什么常量指针地址不能改指针变量可以改指向大小整个数组的大小6 字节指针本身的大小4 或 8 字节3.1 修改内容char arr[] Hello; arr[0] h; // ✅ 没问题数组内容可以改 char *p Hello; p[0] h; // ❌ 崩溃字符串字面量是只读的3.2 修改指向char arr[] Hello; arr World; // ❌ 错误数组名是常量不能改 char *p Hello; p World; // ✅ 没问题p可以指向别的地方3.3 简单总结想修改字符串内容→ 用字符数组只是读字符串→ 两种都可以需要改变指向不同的字符串→ 用字符指针新手建议初学阶段如果你不确定用哪个就用字符数组更安全不容易出问题。四、常用字符串函数C 语言标准库提供了很多处理字符串的函数都在string.h头文件里。下面我们一个个来讲。4.1 strlen求字符串长度size_t strlen(const char *s);作用计算字符串的长度不包括末尾的\0。#include stdio.h #include string.h int main() { char str[] Hello; printf(长度%zu\n, strlen(str)); // 输出 5 return 0; }注意返回值是size_t类型无符号整数用%zu格式化输出长度不包括\0Hello 长度是 5 不是 64.2 strcpy字符串复制char *strcpy(char *dest, const char *src);作用把src字符串复制到dest中包括\0。#include stdio.h #include string.h int main() { char src[] Hello; char dest[20]; strcpy(dest, src); // 把src复制到dest printf(%s\n, dest); // 输出Hello return 0; }⚠️注意dest必须有足够的空间否则会越界会覆盖dest原来的内容4.3 strcat字符串拼接char *strcat(char *dest, const char *src);作用把src拼接到dest的末尾。#include stdio.h #include string.h int main() { char dest[20] Hello ; char src[] World; strcat(dest, src); // 把src拼到dest后面 printf(%s\n, dest); // 输出Hello World return 0; }原理先找到dest末尾的\0把src从那里开始覆盖过去最后加\0。⚠️ 同样要注意dest空间要足够大。4.4 strcmp字符串比较int strcmp(const char *s1, const char *s2);作用比较两个字符串的大小按 ASCII 码逐位比较。返回值s1 s2 → 返回0s1 s2 → 返回正数s1 s2 → 返回负数#include stdio.h #include string.h int main() { printf(%d\n, strcmp(abc, abc)); // 0相等 printf(%d\n, strcmp(abc, abd)); // 负数abc abd printf(%d\n, strcmp(abd, abc)); // 正数abd abc return 0; }⚠️新手常错比较字符串不能用if (s1 s2) { ... } // ❌ 这比的是地址不是内容 if (strcmp(s1, s2) 0) { ... } // ✅ 这样才是比内容比的是两个指针的值地址而不是字符串内容。比较内容一定要用strcmp。4.5 带 n 的安全版本上面的strcpy、strcat、strcmp都有一个 安全版本—— 可以指定最多操作多少个字符防止越界。普通版安全版区别strcpystrncpy最多复制 n 个字符strcatstrncat最多拼接 n 个字符strcmpstrncmp最多比较前 n 个字符比如strncpychar dest[10]; strncpy(dest, Hello World, 9); // 最多复制9个字符防止越界 dest[9] \0; // 注意strncpy不一定自动加\0最好手动补虽然用起来麻烦一点但更安全。实际开发中推荐优先用带 n 的版本。4.6 strchr查找字符char *strchr(const char *s, int c);作用在字符串s中查找字符c第一次出现的位置。找到了 → 返回该位置的指针没找到 → 返回 NULL#include stdio.h #include string.h int main() { char str[] Hello World; char *p strchr(str, o); if (p ! NULL) { printf(找到了位置%td\n, p - str); // 4第5个字符 printf(从这里开始%s\n, p); // o World } return 0; }4.7 strstr查找子串char *strstr(const char *haystack, const char *needle);作用在字符串haystack中查找子串needle第一次出现的位置。找到了 → 返回子串起始位置的指针没找到 → 返回 NULL#include stdio.h #include string.h int main() { char str[] Hello World; char *p strstr(str, World); if (p ! NULL) { printf(找到了%s\n, p); // World } return 0; }五、自己实现字符串函数光会用还不够最好能理解它们是怎么实现的。我们来手写几个加深理解。5.1 实现 strlen思路从开头往后数直到遇到\0。// 自己实现strlen size_t my_strlen(const char *s) { size_t len 0; while (*s ! \0) // 没到结束符就继续 { len; s; } return len; }更简洁的版本size_t my_strlen(const char *s) { const char *p s; while (*p) p; // 一直走到\0 return p - s; // 两个指针相减就是长度 }5.2 实现 strcpy思路一个字符一个字符地复制直到把\0也复制过去。// 自己实现strcpy char *my_strcpy(char *dest, const char *src) { char *p dest; while ((*dest *src)) ; // 空循环体赋值表达式的值就是复制的字符遇到\0就结束 return p; }这个写法很经典 ——*dest *src一步完成了取值、赋值、两个指针后移循环条件就是赋值的结果遇到\0值为 0循环就结束。5.3 实现 strcmp思路逐位比较遇到不同的或者结束符就停下来。// 自己实现strcmp int my_strcmp(const char *s1, const char *s2) { while (*s1 *s2 *s1 *s2) { s1; s2; } return *s1 - *s2; }建议自己动手实现一遍这些函数你对字符串和指针的理解会上一个台阶。这也是面试常考题哦六、字符串常见的坑字符串是新手踩坑的重灾区我们来盘点一下最常见的几个。坑 1忘记 \0char str[5] {H, e, l, l, o}; // 没有\0 printf(%s\n, str); // ❌ 会输出乱码直到遇到0才停解决用双引号初始化或者手动加\0。坑 2数组空间不够char str[5] Hello; // ❌ Hello要6个字节含\05个不够解决数组大小要留够或者直接写char str[] Hello让编译器自己算。坑 3修改字符串字面量char *p Hello; p[0] h; // ❌ 崩溃字面量是只读的解决要修改就用字符数组不要用指针指向字面量然后改。坑 4用 比较字符串char s1[] abc; char s2[] abc; if (s1 s2) { ... } // ❌ 比的是地址不是内容解决用strcmp比较内容。坑 5越界写入char str[5]; strcpy(str, Hello World); // ❌ 严重越界解决确保目标数组足够大或者用strncpy限制长度。七、综合示例示例 1统计字符出现次数#include stdio.h #include string.h int main() { char str[100]; char ch; int count 0; printf(请输入一个字符串); scanf(%s, str); getchar(); // 吃掉回车 printf(请输入要统计的字符); scanf(%c, ch); for (int i 0; str[i] ! \0; i) { if (str[i] ch) { count; } } printf(字符%c出现了%d次\n, ch, count); return 0; }示例 2字符串反转#include stdio.h #include string.h int main() { char str[100]; printf(请输入字符串); scanf(%s, str); int len strlen(str); int left 0; int right len - 1; while (left right) { // 交换左右两个字符 char temp str[left]; str[left] str[right]; str[right] temp; left; right--; } printf(反转后%s\n, str); return 0; }八、本章小结本章我们深入学习了 C 语言中的字符串1. 字符串的本质字符串 字符数组 末尾的\0\0是结束标志不算内容但占一个字节用双引号初始化时编译器自动加\02. 字符数组 vs 字符指针字符数组内容可修改地址不可改字符指针指向字面量内容不可修改指针可以改指向需要修改内容就用数组只读可以用指针3. 常用字符串函数都在 string.h函数作用strlen求字符串长度不含 \0strcpy/strncpy复制字符串strcat/strncat拼接字符串strcmp/strncmp比较字符串strchr查找字符strstr查找子串4. 常见坑点忘记加\0数组空间不够修改字符串字面量用比较字符串内容越界写入学习建议字符串是编程的基本功一定要多练。建议你把常用的字符串函数都自己实现一遍既能加深对指针的理解又能记住函数的用法 —— 一举两得。九、写在最后Talk is cheap. Show me the code.——Linus Torvalds林纳斯・托瓦兹Linux 之父翻译成中文就是废话少说放码过来。这句话是 Linux 之父林纳斯的名言也成为了程序员圈的经典梗。道理很简单说得再多不如直接看代码。学字符串也是一样。看十遍讲解不如自己动手写一遍。写代码、调 bug、踩坑、爬坑 —— 这个过程才是真正的学习。⚠️ 关于本文由于笔者水平有限文中难免存在疏漏、错误或表述不够准确的地方。如果你在阅读过程中发现了任何问题 —— 无论是概念错误、代码 bug、还是表述不清 —— 都欢迎在评论区留言指正我会及时修正。技术之路道阻且长行则将至。我们互相学习共同进步