欢迎来到第八章!
在上一章中,我们学习了指针。从这一章开始,我们将把指针和数组结合起来,深入学习 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 输出:你好,Zhang!scanf("%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[] = "Hello" | char *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格式化输出 - 长度不包括
'\0',"Hello" 长度是 5 不是 6
4.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 → 返回0
- s1 > 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都有一个 "安全版本"—— 可以指定最多操作多少个字符,防止越界。
| 普通版 | 安全版 | 区别 |
|---|---|---|
| strcpy | strncpy | 最多复制 n 个字符 |
| strcat | strncat | 最多拼接 n 个字符 |
| strcmp | strncmp | 最多比较前 n 个字符 |
比如strncpy:
char 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:忘记 '\0'
char str[5] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0' printf("%s\n", str); // ❌ 会输出乱码,直到遇到0才停解决:用双引号初始化,或者手动加'\0'。
坑 2:数组空间不够
char str[5] = "Hello"; // ❌ "Hello"要6个字节(含'\0'),5个不够解决:数组大小要留够,或者直接写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'是结束标志,不算内容,但占一个字节- 用双引号初始化时,编译器自动加
'\0'
2. 字符数组 vs 字符指针
- 字符数组:内容可修改,地址不可改
- 字符指针指向字面量:内容不可修改,指针可以改指向
- 需要修改内容就用数组,只读可以用指针
3. 常用字符串函数(都在 <string.h>)
| 函数 | 作用 |
|---|---|
strlen | 求字符串长度(不含 '\0') |
strcpy/strncpy | 复制字符串 |
strcat/strncat | 拼接字符串 |
strcmp/strncmp | 比较字符串 |
strchr | 查找字符 |
strstr | 查找子串 |
4. 常见坑点
- 忘记加
'\0' - 数组空间不够
- 修改字符串字面量
- 用
==比较字符串内容 - 越界写入
📌学习建议:字符串是编程的基本功,一定要多练。建议你把常用的字符串函数都自己实现一遍,既能加深对指针的理解,又能记住函数的用法 —— 一举两得。
九、写在最后
"Talk is cheap. Show me the code."
——Linus Torvalds(林纳斯・托瓦兹,Linux 之父)
翻译成中文就是:"废话少说,放码过来。"
这句话是 Linux 之父林纳斯的名言,也成为了程序员圈的经典梗。道理很简单:说得再多,不如直接看代码。
学字符串也是一样。看十遍讲解,不如自己动手写一遍。写代码、调 bug、踩坑、爬坑 —— 这个过程才是真正的学习。
⚠️ 关于本文
由于笔者水平有限,文中难免存在疏漏、错误或表述不够准确的地方。如果你在阅读过程中发现了任何问题 —— 无论是概念错误、代码 bug、还是表述不清 —— 都欢迎在评论区留言指正,我会及时修正。
技术之路,道阻且长,行则将至。我们互相学习,共同进步 💪