4. 字符串与指针:字符串常量与字符数组、字符指针操作、常见字符串函数指针实现

字符串在C/C++里,说穿了就是字符数组加一个'\0'结尾。但就是这个看似简单的东西,坑了我好几年。我记得刚入行那会儿,有个同事因为字符串拷贝越界,把堆上的malloc元信息给覆盖了,查了整整三天才定位到。嗯,咱们今天就把这块彻底讲透。

4.1 字符串常量 vs 字符数组

先看一个最基础的区别。字符串常量存在只读区,你改不了它。字符数组存在栈上,你可以随便改。

// 字符串常量 - 存在只读区
char *p = "hello";
p[0] = 'H';  // 崩溃!段错误

// 字符数组 - 存在栈上
char arr[] = "hello";
arr[0] = 'H';  // 没问题,变成"Hello"

我个人习惯:只要需要修改字符串内容,一律用字符数组。用指针指向字符串常量然后试图修改,这是新手最常见的越界操作之一。

⚠️ 警告: 字符串常量是 const char* 类型,虽然C语言允许你用 char* 指向它,但修改行为是未定义的。C++里直接编译报错,C语言里运行时崩溃。

4.2 字符指针操作:你以为的赋值,其实是浅拷贝

字符指针的赋值,只是拷贝地址,不是拷贝内容。这个坑我踩过不止一次。

char str1[] = "hello";
char *str2 = str1;  // str2指向str1的同一块内存

str2[0] = 'H';      // str1也变成了"Hello"
printf("%s\n", str1); // 输出"Hello"

为什么会这样?因为指针赋值只拷贝了地址值。如果你想要一份独立的副本,必须用 strcpy 或者自己写循环拷贝。

💡 技巧: 判断两个字符串是否相等,千万别用 if (str1 == str2)。这是在比较地址,不是比较内容。要用 strcmp。

4.3 常见字符串函数的指针实现

咱们自己动手实现几个常用函数,你就能深刻理解指针操作了。我曾经在面试时让候选人手写 strcpy,十个人里有八个写不对边界条件。

4.3.1 实现 strlen - 计算字符串长度

size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') {
        p++;
    }
    return p - s;  // 指针相减得到元素个数
}

注意这里用了 const char*,表示我不会修改字符串内容。指针相减得到的是两个地址之间的元素个数,不是字节数。嗯,这个细节很多人会忽略。

4.3.2 实现 strcpy - 字符串拷贝

char *my_strcpy(char *dest, const char *src) {
    char *ret = dest;  // 保存起始地址用于返回
    while ((*dest++ = *src++) != '\0') {
        // 空循环体,赋值操作在条件判断里完成
    }
    return ret;
}

这个实现很经典。赋值表达式 *dest++ = *src++ 的值就是被赋的字符,当遇到'\0'时循环结束。但这里有个大坑:dest 的空间必须足够大,否则就越界了。

🔑 关键点: strcpy 不检查目标缓冲区大小。如果你用 strcpy 把长字符串拷贝到短缓冲区里,就是缓冲区溢出漏洞。安全做法是用 strncpy 或 snprintf。

4.3.3 实现 strcat - 字符串拼接

char *my_strcat(char *dest, const char *src) {
    char *ret = dest;
    // 先找到dest的结尾
    while (*dest != '\0') {
        dest++;
    }
    // 然后拷贝src
    while ((*dest++ = *src++) != '\0') {
        // 空循环体
    }
    return ret;
}

这个函数的问题和 strcpy 一样:不检查缓冲区大小。我曾经在项目里见过有人用 strcat 拼接用户输入,结果导致堆栈被破坏,程序随机崩溃。查了三天才找到原因。

4.3.4 实现 strcmp - 字符串比较

int my_strcmp(const char *s1, const char *s2) {
    while (*s1 != '\0' && *s1 == *s2) {
        s1++;
        s2++;
    }
    return (unsigned char)*s1 - (unsigned char)*s2;
}

注意这里用 unsigned char 做比较,而不是 char。因为 char 可能是 signed 类型,直接相减可能得到负数。这个细节,嗯,很多教科书都没提。

4.4 字符串指针越界的典型场景

我总结了几种最常见的越界情况,你对照着检查自己的代码:

场景 错误代码 正确做法
忘记留'\0'空间 char buf[5]; strcpy(buf, "hello"); char buf[6]; strcpy(buf, "hello");
指针未初始化 char *p; strcpy(p, "hello"); char buf[10]; char *p = buf; strcpy(p, "hello");
越界访问字符串常量 char *p = "hello"; p[5] = '!'; char arr[] = "hello"; arr[5] = '!';
strcat目标空间不足 char buf[10] = "hello"; strcat(buf, " world!!!"); char buf[20] = "hello"; strcat(buf, " world!!!");
⚠️ 我曾经 在嵌入式项目里遇到过一个bug:用 sprintf 拼接字符串时,目标缓冲区只分配了64字节,但实际拼接了128字节。结果把紧挨着的任务控制块给覆盖了,系统直接死机。从那以后,我所有字符串操作都改用 snprintf 并检查返回值。

4.5 知识体系结构图

下面这张图把字符串与指针的核心知识点串起来了,你可以对照着梳理自己的知识体系:

字符串与指针知识体系 字符串常量 • 存储在只读数据段 • 不可修改 • 相同常量可能共享 • 修改导致段错误 字符数组 • 存储在栈/全局区 • 可修改内容 • 大小固定 • 注意'\0'空间 字符指针操作 • 指向字符串首地址 • 赋值是浅拷贝 • 指针运算遍历 • 未初始化是野指针 常见字符串函数指针实现 strlen strcpy strcat strcmp ⚠️ 越界风险:缓冲区溢出、段错误、堆栈破坏

4.6 避坑指南

最后,我把自己这些年踩过的坑总结成几条原则:

  1. 能用数组就别用指针 - 数组有固定大小,编译器能帮你检查越界。指针太灵活,容易出问题。
  2. 字符串操作前先算长度 - 用 strlen 算一下源字符串长度,确保目标缓冲区够大。
  3. 优先用安全版本 - strncpy 代替 strcpy,snprintf 代替 sprintf,strncat 代替 strcat。
  4. 指针初始化 - 要么赋值为 NULL,要么指向合法内存。不要声明了就用。
  5. const 保护 - 不修改的字符串参数用 const char*,让编译器帮你检查。
💡 我个人习惯: 每次写字符串操作代码,我都会在注释里标注缓冲区大小和预期最大长度。这样半年后回来看代码,一眼就能看出有没有越界风险。

字符串指针这块,说白了就是内存管理的问题。你只要记住:指针指向的内存,要么是只读的,要么大小是固定的,要么是动态分配的但你要负责释放。搞清楚这三类,大部分越界问题都能避免。

可可精品资料库