11、字符数组与字符串:字符数组与字符串的关系、字符串的初始化、字符串的输入输出、字符串处理函数(strlen/strcpy/strcat/strcmp)
说实话,字符数组和字符串这块,是很多初学者容易搞混的地方。我记得刚学C语言那会儿,总觉得它们就是同一个东西——毕竟打印出来都一样嘛。后来在项目里踩了坑才明白,这俩虽然长得像,但性格完全不同。
今天我就带你彻底理清楚它们的关系,顺便把字符串的初始化、输入输出,还有那几个最常用的处理函数(strlen、strcpy、strcat、strcmp)一次性讲透。
字符数组与字符串:到底有什么区别?
先给个结论:字符串是字符数组的一种特殊形式。说白了,所有字符串都是字符数组,但不是所有字符数组都是字符串。
区别在哪?字符串末尾必须有一个 '\0'(空字符),作为结束标志。而普通字符数组可以没有这个结束符。
核心要点:
- 字符数组:存放字符的数组,末尾不一定有
'\0' - 字符串:字符数组 + 末尾
'\0'结束符 - C语言中所有字符串处理函数都依赖
'\0'来识别字符串的结尾
我在项目中遇到过这样一个bug:定义了一个字符数组用来存数据,结果用 printf("%s") 打印时,后面多了一堆乱码。原因就是数组末尾没有 '\0',printf一直读到内存里遇到第一个0才停。
// 普通字符数组(不是字符串)
char arr1[] = {'H', 'e', 'l', 'l', 'o'}; // 没有'\0'
// 字符串(字符数组 + '\0')
char arr2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 有'\0'
char arr3[] = "Hello"; // 编译器自动加'\0'
你想想看,"Hello" 这种双引号括起来的写法,编译器会在末尾偷偷加一个 '\0'。所以 arr3 实际占6个字节,不是5个。
字符串的初始化:几种常见方式
字符串初始化有好几种写法,我习惯根据场景选不同的方式。下面这几种都是对的,但细节上有区别:
| 初始化方式 | 示例 | 说明 |
|---|---|---|
| 字符数组 + 双引号 | char s1[] = "hello"; |
自动加'\0',大小由编译器计算 |
| 指定大小 + 双引号 | char s2[10] = "hello"; |
剩余位置自动补'\0' |
| 字符逐个赋值 | char s3[] = {'h','e','l','l','o','\0'}; |
必须手动加'\0' |
| 指针指向字符串常量 | char *s4 = "hello"; |
字符串常量,不可修改 |
注意: char *s4 = "hello"; 这种方式定义的字符串是存放在只读数据区的,不能通过 s4[0] = 'H'; 修改。我曾经在项目里这么写过,结果程序直接崩溃——嗯,血的教训。
字符串的输入输出
输入输出这块,常用的就几个函数:printf、puts、scanf、gets、fgets。我一个个说。
输出:printf 和 puts
char str[] = "Hello C";
printf("%s\n", str); // 输出:Hello C
puts(str); // 输出:Hello C(自动换行)
puts 比 printf 简单,自动加换行。但 printf 更灵活,可以格式化输出多个变量。
输入:scanf、gets、fgets
char buf[100];
// scanf:遇到空格就停
scanf("%s", buf); // 输入 "hello world",只读到 "hello"
// gets:读取整行(危险!不检查缓冲区大小)
gets(buf); // 如果输入超过99个字符,会溢出
// fgets:安全读取整行
fgets(buf, sizeof(buf), stdin); // 最多读99个字符,留一个给'\0'
千万不要用 gets! 这个函数已经被C11标准废弃了。它不检查缓冲区大小,输入过长时直接溢出,是安全漏洞的常见来源。我在早期项目里用过一次,后来被安全审查要求全部改成 fgets。
我个人习惯用 fgets,虽然它会保留换行符,但处理起来也就多一行代码的事:
fgets(buf, sizeof(buf), stdin);
// 去掉末尾的换行符
buf[strcspn(buf, "\n")] = '\0';
字符串处理函数:四大金刚
这四个函数是C语言字符串操作的核心,面试也经常考。我一个个讲清楚。
strlen:求字符串长度
char s[] = "hello";
int len = strlen(s); // len = 5,不包含'\0'
strlen 从字符串开头一直数到 '\0' 为止,不包含 '\0' 本身。如果字符串没有 '\0',它会一直数下去,直到内存里碰到一个0——这就是我之前说的乱码问题的根源。
strcpy:字符串拷贝
char dest[20];
char src[] = "hello";
strcpy(dest, src); // 把src拷贝到dest
这里要注意:dest 的空间必须足够大,能放下 src 的内容(包括 '\0')。否则就是缓冲区溢出。
安全替代: 很多编译器推荐用 strcpy_s 或 strncpy,它们可以指定最大拷贝长度,防止溢出。
strcat:字符串拼接
char dest[20] = "hello";
char src[] = " world";
strcat(dest, src); // dest 变成 "hello world"
strcat 会找到 dest 的 '\0' 位置,然后把 src 的内容从那里开始粘贴上去。同样,dest 的空间要足够大。
strcmp:字符串比较
char s1[] = "hello";
char s2[] = "hello";
char s3[] = "world";
int r1 = strcmp(s1, s2); // r1 = 0,相等
int r2 = strcmp(s1, s3); // r2 < 0,s1 < s3(按字典序)
返回值规则:相等返回0,s1小于s2返回负数,s1大于s2返回正数。比较的是字符的ASCII码值,不是长度。
我见过有人直接用 if (s1 == s2) 来比较字符串——这是错的!这样比较的是两个指针的地址,不是字符串内容。一定要用 strcmp。
知识体系总览
下面这张图把本章的核心内容串起来了,你可以对照着复习:
避坑指南
最后总结几个我踩过的坑,你遇到了可以少走弯路:
- 忘记留'\0'的位置:定义数组时,长度要至少比字符串长度多1。比如存"hello"需要6个字节。
- 用 == 比较字符串:比较的是地址,不是内容。用
strcmp。 - gets 缓冲区溢出:永远用
fgets替代。 - strcpy/strcat 目标空间不足:确保目标数组足够大,或者用
strncpy/strncat。 - 修改字符串常量:
char *p = "hello"; p[0] = 'H';会导致未定义行为。
嗯,字符数组和字符串这块,说白了就是记住一个核心:'\0' 是字符串的灵魂。没有它,所有字符串处理函数都会乱套。你写代码的时候,脑子里时刻想着这个结束符,基本就不会出大问题。