4. 字符串与指针
1.1 字符串常量 vs 字符数组:看似一样,实则天差地别
先问大家一个问题:下面这两行代码,有什么区别?
char *str1 = "Hello";
char str2[] = "Hello";
嗯,很多初学者觉得差不多。但在我眼里,这俩完全是两码事。
字符串常量(比如 "Hello")是存储在只读数据段的。你只能读它,不能改它。我见过不少新手直接写 str1[0] = 'h';,然后程序就崩了——段错误,没商量。
字符数组则不同。它在栈上分配了6个字节(别忘了结尾的'\0'),你可以随意修改里面的内容。
核心区别一览:
| 特性 | 字符串常量 (char *) | 字符数组 (char []) |
|---|---|---|
| 存储位置 | 只读数据段 | 栈(或全局区) |
| 能否修改 | 不能(未定义行为) | 能 |
| 生命周期 | 程序整个运行期 | 作用域内 |
| sizeof 结果 | 指针大小(4或8字节) | 数组实际大小 |
⚠️ 警告:千万不要试图修改字符串常量!我曾经在项目中看到有人这么写:
char *p = "fixed_string";
p[0] = 'F'; // 运行时崩溃!
这种错误在嵌入式开发中尤其隐蔽,因为有些编译器不会报错,但运行到特定条件下才会崩溃。
1.2 字符串拷贝越界:最经典的缓冲区溢出
说到字符串拷贝,strcpy 绝对是头号危险分子。我调试过的崩溃问题里,至少有三成跟它有关。
char dest[10];
char *src = "This is a very long string that will overflow!";
strcpy(dest, src); // 灾难!
为什么会这样?strcpy 不会检查目标缓冲区的大小。它只管把源字符串一个字节一个字节地复制过去,直到遇到 '\0'。如果源字符串比目标缓冲区大,那就直接越界写入了。
越界的后果是什么?
- 覆盖了栈上其他局部变量
- 覆盖了函数的返回地址(黑客最爱)
- 程序崩溃,或者更糟——静默地产生错误结果
💡 我的建议:永远使用 strncpy 代替 strcpy。但要注意,strncpy 也有坑——如果源字符串长度大于等于 n,它不会自动添加 '\0'。所以正确的写法是:
char dest[10] = {0};
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0'; // 手动保证结尾
1.3 gets 函数:为什么它被彻底废弃了?
如果你还在用 gets,我建议你立刻停下来。这个函数在 C11 标准中已经被正式删除了。为什么?因为它根本不做任何边界检查。
char buf[16];
gets(buf); // 用户输入超过15个字符,直接崩
我曾经接手过一个遗留项目,里面大量使用了 gets。测试环境一切正常,一上线就随机崩溃。查了两天才发现是某个用户输入了超长字符串,把栈上的返回地址给覆盖了。从那以后,我对 gets 深恶痛绝。
⚠️ 替代方案:
fgets(buf, sizeof(buf), stdin)—— 安全,会限制读取长度getline(&buf, &size, stdin)—— POSIX 标准,动态分配缓冲区
记住:fgets 会保留换行符,记得手动去掉:
if (fgets(buf, sizeof(buf), stdin)) {
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行
}
1.4 知识体系总览
下面这张图,是我个人梳理的本章知识脉络。你可以把它当作一个检查清单——写代码时对照着看,能避开大部分坑。
1.5 实战:一个完整的错误示例
下面这段代码,集中了本章提到的所有问题。你能找出几处?
#include <stdio.h>
#include <string.h>
int main() {
char *name = "Alice"; // 问题1:字符串常量
char buffer[10];
printf("请输入名字:");
gets(buffer); // 问题2:gets 无边界检查
strcpy(buffer, name); // 问题3:strcpy 可能越界
name[0] = 'B'; // 问题4:修改字符串常量
printf("Hello, %s\n", buffer);
return 0;
}
✅ 修正后的版本:
#include <stdio.h>
#include <string.h>
int main() {
char name[] = "Alice"; // 改为字符数组
char buffer[10] = {0};
printf("请输入名字:");
if (fgets(buffer, sizeof(buffer), stdin)) {
buffer[strcspn(buffer, "\n")] = '\0'; // 去掉换行
}
strncpy(buffer, name, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
name[0] = 'B'; // 现在可以安全修改了
printf("Hello, %s\n", buffer);
return 0;
}
嗯,说白了,字符串操作的核心就一句话:永远假设输入比你预期的要长,永远预留足够的空间,永远检查边界。 我这些年踩过的坑,十有八九都是忘了这条原则。
📌 个人习惯:我写代码时,只要涉及字符串操作,都会在注释里标注缓冲区大小和预期输入长度。这样半年后回来看代码,自己也不会掉坑里。