第1章:字符串与指针——字符串常量与字符数组、字符串拷贝越界、gets函数的危险

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 知识体系总览

下面这张图,是我个人梳理的本章知识脉络。你可以把它当作一个检查清单——写代码时对照着看,能避开大部分坑。

字符串与指针:核心知识体系 字符串常量 • 存储在只读数据段 • 不可修改 • 生命周期:程序全程 • sizeof = 指针大小 字符数组 • 存储在栈/全局区 • 可修改 • 生命周期:作用域内 • sizeof = 数组大小 危险操作 • strcpy 越界拷贝 • gets 无边界检查 • 修改字符串常量 • 忘记 '\0' 结尾 避坑指南(我的血泪经验) 1. 能用 fgets 就别用 gets,能用 strncpy 就别用 strcpy 2. 字符数组一定要预留 '\0' 的位置 3. 字符串常量当只读数据用,别手贱去改它

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;
}

嗯,说白了,字符串操作的核心就一句话:永远假设输入比你预期的要长,永远预留足够的空间,永远检查边界。 我这些年踩过的坑,十有八九都是忘了这条原则。

📌 个人习惯:我写代码时,只要涉及字符串操作,都会在注释里标注缓冲区大小和预期输入长度。这样半年后回来看代码,自己也不会掉坑里。

可可精品资料库