指针与字符串:字符串常量与字符指针、字符串指针数组、常用字符串操作函数的指针实现
字符串在C语言里,其实是个挺“拧巴”的存在。你想想看,语言本身并没有专门的字符串类型,但我们天天都在用。怎么做到的?靠的就是指针和字符数组的配合。这一章,我就带你把这些弯弯绕绕彻底理清楚。
字符串常量与字符指针
先看一个最基础的问题:字符串常量到底存在哪?
char *p = "hello";
char arr[] = "hello";
这两行代码,看着差不多,但内存布局完全不同。我个人习惯把前者叫做“指针指向常量区”,后者是“数组在栈上拷贝了一份”。
"hello" 这个字符串常量,是存储在只读数据段(.rodata)里的。你通过指针 p 去访问它,只能读,不能改。我曾经在项目里看到有人这么写:
char *p = "hello";
p[0] = 'H'; // 运行时崩溃!
嗯,这种错误在嵌入式开发里尤其隐蔽。因为有些MCU的Flash是统一编址的,写操作可能不会立刻报错,但后续行为完全不可预测。所以我的建议是:凡是用指针指向字符串常量,一律加上 const 修饰。
const char *p = "hello"; // 这样编译器会帮你拦住修改操作
字符串指针数组
实际项目中,我们经常要管理一组字符串。比如一个菜单系统,或者一串错误信息。这时候字符串指针数组就派上用场了。
const char *menu[] = {
"打开文件",
"保存文件",
"退出程序",
NULL // 用NULL做结束标记,这是C语言里的常见手法
};
你看,这个数组里每个元素都是一个 const char * 指针,指向一个字符串常量。遍历起来非常方便:
for (int i = 0; menu[i] != NULL; i++) {
printf("%s\n", menu[i]);
}
为什么不用二维字符数组?比如 char menu[4][20]?我在项目中遇到过这种情况:用二维数组,每个字符串长度都得按最长的来分配,内存浪费严重。而且如果后期要修改字符串内容,数组大小还得重新调。用指针数组,每个指针指向的字符串长度是自由的,内存利用率高得多。
常用字符串操作函数的指针实现
面试的时候,经常有人被问到“手写strcpy”。其实这背后考察的就是对指针操作的理解深度。下面我挑几个最常用的函数,用指针的方式重新实现一遍。你仔细看,每个实现里都藏着指针的精髓。
strlen:计算字符串长度
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return (size_t)(p - s);
}
这里的关键是 指针减法。两个指向同一数组的指针相减,结果就是它们之间元素的个数。这个技巧在很多底层代码里都会用到。
strcpy:字符串拷贝
char *my_strcpy(char *dest, const char *src) {
char *ret = dest;
while ((*dest++ = *src++) != '\0') {
// 空循环体,赋值操作已经完成了
}
return ret;
}
这段代码很经典。赋值表达式 *dest++ = *src++ 的值就是被赋的那个字符。当遇到 '\0' 时,赋值完成,循环结束。注意这里 dest 和 src 都自增了,但 ret 保存了原始地址用于返回。这是C语言里“返回目标指针”的惯例,方便链式调用。
dest 和 src 指向了重叠的内存区域。标准 strcpy 不处理重叠问题,这种情况得用 memmove。所以拷贝之前,先确认一下源和目标有没有重叠。
strcmp:字符串比较
int my_strcmp(const char *s1, const char *s2) {
while (*s1 != '\0' && *s1 == *s2) {
s1++;
s2++;
}
return (unsigned char)*s1 - (unsigned char)*s2;
}
比较的逻辑很简单:逐字符比较,直到遇到不相等或者某个字符串结束。返回值是 s1 和 s2 当前字符的差值。这里用 unsigned char 转换,是为了避免 char 的符号扩展问题。你想想看,如果 char 是有符号的,EOF 和普通字符比较时就会出乱子。
strcat:字符串拼接
char *my_strcat(char *dest, const char *src) {
char *ret = dest;
while (*dest != '\0') {
dest++;
}
while ((*dest++ = *src++) != '\0') {
// 拼接
}
return ret;
}
说白了就是先找到 dest 的末尾,然后从那里开始拷贝 src。这个函数有个隐含风险:dest 必须有足够的空间。我在嵌入式项目里见过有人 strcat 之后直接缓冲区溢出,把栈上的返回地址给覆盖了。嗯,那调试起来真是欲哭无泪。
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个快速索引,遇到问题回来对照着看。
好了,这一章的内容就这些。字符串和指针的结合,说白了就是“用指针去操控内存里的字符序列”。你只要把指针的移动、比较、赋值这几个动作练熟了,什么字符串处理都不在话下。下次写代码的时候,多想想“这个指针指向哪?那个内存能不能写?”——养成这个习惯,很多bug在写出来的那一刻就被你掐死了。
公众号:蓝海资料掘金营,微信deep3321