4. 字符串与指针:字符串常量与字符数组、字符指针操作、常见字符串函数指针实现
字符串在C/C++里,说穿了就是字符数组加一个'\0'结尾。但就是这个看似简单的东西,坑了我好几年。我记得刚入行那会儿,有个同事因为字符串拷贝越界,把堆上的malloc元信息给覆盖了,查了整整三天才定位到。嗯,咱们今天就把这块彻底讲透。
4.1 字符串常量 vs 字符数组
先看一个最基础的区别。字符串常量存在只读区,你改不了它。字符数组存在栈上,你可以随便改。
// 字符串常量 - 存在只读区
char *p = "hello";
p[0] = 'H'; // 崩溃!段错误
// 字符数组 - 存在栈上
char arr[] = "hello";
arr[0] = 'H'; // 没问题,变成"Hello"
我个人习惯:只要需要修改字符串内容,一律用字符数组。用指针指向字符串常量然后试图修改,这是新手最常见的越界操作之一。
4.2 字符指针操作:你以为的赋值,其实是浅拷贝
字符指针的赋值,只是拷贝地址,不是拷贝内容。这个坑我踩过不止一次。
char str1[] = "hello";
char *str2 = str1; // str2指向str1的同一块内存
str2[0] = 'H'; // str1也变成了"Hello"
printf("%s\n", str1); // 输出"Hello"
为什么会这样?因为指针赋值只拷贝了地址值。如果你想要一份独立的副本,必须用 strcpy 或者自己写循环拷贝。
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 的空间必须足够大,否则就越界了。
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!!!"); |
4.5 知识体系结构图
下面这张图把字符串与指针的核心知识点串起来了,你可以对照着梳理自己的知识体系:
4.6 避坑指南
最后,我把自己这些年踩过的坑总结成几条原则:
- 能用数组就别用指针 - 数组有固定大小,编译器能帮你检查越界。指针太灵活,容易出问题。
- 字符串操作前先算长度 - 用 strlen 算一下源字符串长度,确保目标缓冲区够大。
- 优先用安全版本 - strncpy 代替 strcpy,snprintf 代替 sprintf,strncat 代替 strcat。
- 指针初始化 - 要么赋值为 NULL,要么指向合法内存。不要声明了就用。
- const 保护 - 不修改的字符串参数用 const char*,让编译器帮你检查。
字符串指针这块,说白了就是内存管理的问题。你只要记住:指针指向的内存,要么是只读的,要么大小是固定的,要么是动态分配的但你要负责释放。搞清楚这三类,大部分越界问题都能避免。
可可精品资料库