指针与字符串:字符串常量与字符指针、字符串指针数组、常用字符串操作函数的指针实现

字符串在C语言里,其实是个挺“拧巴”的存在。你想想看,语言本身并没有专门的字符串类型,但我们天天都在用。怎么做到的?靠的就是指针和字符数组的配合。这一章,我就带你把这些弯弯绕绕彻底理清楚。

字符串常量与字符指针

先看一个最基础的问题:字符串常量到底存在哪?

char *p = "hello";
char arr[] = "hello";

这两行代码,看着差不多,但内存布局完全不同。我个人习惯把前者叫做“指针指向常量区”,后者是“数组在栈上拷贝了一份”。

"hello" 这个字符串常量,是存储在只读数据段(.rodata)里的。你通过指针 p 去访问它,只能读,不能改。我曾经在项目里看到有人这么写:

char *p = "hello";
p[0] = 'H';  // 运行时崩溃!

嗯,这种错误在嵌入式开发里尤其隐蔽。因为有些MCU的Flash是统一编址的,写操作可能不会立刻报错,但后续行为完全不可预测。所以我的建议是:凡是用指针指向字符串常量,一律加上 const 修饰

const char *p = "hello";  // 这样编译器会帮你拦住修改操作
注意: 字符串常量是静态存储期,程序启动时就分配好了,直到程序结束才释放。不要试图用 free() 去释放它,那会出大问题。

字符串指针数组

实际项目中,我们经常要管理一组字符串。比如一个菜单系统,或者一串错误信息。这时候字符串指针数组就派上用场了。

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]?我在项目中遇到过这种情况:用二维数组,每个字符串长度都得按最长的来分配,内存浪费严重。而且如果后期要修改字符串内容,数组大小还得重新调。用指针数组,每个指针指向的字符串长度是自由的,内存利用率高得多。

小技巧: 字符串指针数组经常和枚举配合使用,实现“枚举值到字符串”的映射。比如错误码转错误描述,这种写法比 switch-case 清爽多了。

常用字符串操作函数的指针实现

面试的时候,经常有人被问到“手写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' 时,赋值完成,循环结束。注意这里 destsrc 都自增了,但 ret 保存了原始地址用于返回。这是C语言里“返回目标指针”的惯例,方便链式调用。

避坑指南: 我曾经在通信协议栈里遇到过一个bug,就是因为 destsrc 指向了重叠的内存区域。标准 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;
}

比较的逻辑很简单:逐字符比较,直到遇到不相等或者某个字符串结束。返回值是 s1s2 当前字符的差值。这里用 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 之后直接缓冲区溢出,把栈上的返回地址给覆盖了。嗯,那调试起来真是欲哭无泪。

核心要点: 所有字符串操作函数,本质上都是对指针的移动和比较。理解指针的算术运算(++、--、减法)和间接访问(*),你就能自己写出任何字符串函数。

知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个快速索引,遇到问题回来对照着看。

指针与字符串 · 知识结构 字符串常量 存储在 .rodata 段 字符指针 vs 字符数组 可修改性 / 内存位置不同 字符串指针数组 管理一组字符串的利器 常用字符串操作函数的指针实现 strlen 指针减法求长度 strcpy 赋值即判断 strcmp 逐字符差值 strcat 先找末尾再拷贝 核心思想 指针的算术运算(++、--、减法) + 间接访问(*) = 字符串操作的一切 注意:const 保护、缓冲区大小、重叠内存 —— 这三个坑绕过去就稳了

好了,这一章的内容就这些。字符串和指针的结合,说白了就是“用指针去操控内存里的字符序列”。你只要把指针的移动、比较、赋值这几个动作练熟了,什么字符串处理都不在话下。下次写代码的时候,多想想“这个指针指向哪?那个内存能不能写?”——养成这个习惯,很多bug在写出来的那一刻就被你掐死了。


公众号:蓝海资料掘金营,微信deep3321