3. 指针核心原理

字符串在C语言里,说白了就是字符数组加个'\0'结尾。但指针和字符串搅在一起时,坑特别多。我做了十几年嵌入式,见过太多同事在这上面翻车——有人把字符串常量当数组改,有人被const修饰符骗得团团转。今天咱们就把这些事彻底理清楚。

3.1 字符串常量与指针

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

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

为什么会崩溃?因为"hello"这个字符串常量,编译器把它放在了只读数据区(.rodata段)。你试图通过指针去修改它,就是往只读内存里写数据——段错误没跑。

我在项目里遇到过这样的bug:一个同事写了个字符串处理函数,直接修改了传入的字符串常量,结果程序跑着跑着就挂了。查了两天才定位到问题。

警告:字符串常量是只读的,任何修改操作都会导致未定义行为。嵌入式系统中,这些常量可能被放在Flash或ROM中,写操作直接触发硬件异常。

3.2 字符数组与指针的区别

很多人觉得字符数组和字符指针差不多,其实差别大了去了。咱们用表格对比一下:

特性 字符数组 字符指针
内存位置 栈或全局区 指向常量区或堆
是否可修改 内容可改 指向的内容可能只读
sizeof结果 数组实际字节数 指针本身大小(4或8字节)
赋值操作 不能整体赋值 可以指向新地址
作为函数参数 退化为指针 本身就是指针

看个具体例子:

char arr[] = "hello";  // arr在栈上,占6字节
char *ptr = "hello";   // ptr在栈上,指向常量区

arr[0] = 'H';  // 合法,修改栈上的数据
ptr[0] = 'H';  // 非法,常量区不可写

printf("sizeof(arr) = %lu\n", sizeof(arr));  // 输出6
printf("sizeof(ptr) = %lu\n", sizeof(ptr));  // 输出8(64位系统)

嗯,这里要注意:sizeof(arr)返回的是整个数组的大小,包括结尾的'\0'。而sizeof(ptr)只是指针变量本身的大小。这个区别在嵌入式开发中特别重要——你想想看,如果你用sizeof去算字符串长度,用指针和用数组结果完全不同。

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

标准库里的字符串函数,底层全是指针操作。我建议你自己动手实现一遍,这样才能真正理解指针的用法。下面是我常用的几个实现:

3.3.1 实现strlen

size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') {
        p++;
    }
    return (size_t)(p - s);
}

这个实现用了指针减法——两个指针相减得到的是它们之间的元素个数。我在调试内存越界时经常用这个技巧来快速计算偏移量。

3.3.2 实现strcpy

char *my_strcpy(char *dest, const char *src) {
    char *ret = dest;
    while ((*dest++ = *src++) != '\0') {
        // 空循环体,赋值操作已经完成
    }
    return ret;
}

这个写法很经典:*dest++ = *src++,先赋值,再自增。注意这里src用const修饰,保证不会修改源字符串。我曾经见过有人把src和dest写反了,结果源字符串被覆盖,查了半天才发现。

3.3.3 实现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可能是有符号的,如果字符值大于127,直接相减会得到负数,导致比较结果错误。我在一个通信协议解析模块里就踩过这个坑——两个字符串明明一样,strcmp却返回非零值。

技巧:嵌入式系统中,如果内存紧张,可以用指针运算代替数组索引。比如while (*p)比while (p[i] != '\0')生成的代码更紧凑,执行效率也更高。

3.4 const修饰指针

const和指针的组合,是C语言里最容易让人晕的地方。我总结了一个规律:const修饰的是它右边的东西。

3.4.1 四种组合

写法 含义 可修改指针 可修改指向内容
const char *p 指向常量的指针 是 否
char const *p 同上(等价) 是 否
char * const p 常量指针 否 是
const char * const p 指向常量的常量指针 否 否

看代码更直观:

char str[] = "hello";
const char *p1 = str;   // p1可以指向别处,但不能通过p1修改内容
char * const p2 = str;  // p2不能指向别处,但可以通过p2修改内容
const char * const p3 = str;  // 都不能改

p1 = "world";  // 合法
p2 = "world";  // 编译错误
*p1 = 'H';     // 编译错误
*p2 = 'H';     // 合法

3.4.2 实际应用场景

在嵌入式开发中,const指针用得最多的是函数参数声明。我个人习惯:只要函数不修改传入的字符串,参数就用const char *。这样做有两个好处:

  • 编译器能帮你检查,防止意外修改
  • 调用者知道这个函数不会改数据,更安全

举个例子:

// 好的做法
void print_message(const char *msg) {
    printf("%s\n", msg);
}

// 不好的做法
void print_message(char *msg) {
    printf("%s\n", msg);
    // 调用者会担心msg被修改
}
核心要点:const修饰指针时,记住"右结合"原则——const修饰它右边的符号。const char *p中,const修饰char,所以指向的内容是常量;char * const p中,const修饰p,所以指针本身是常量。

3.5 本章知识结构图

下面这张图总结了指针与字符串的核心关系:

指针与字符串知识结构图 指针与字符串核心知识结构 字符串操作 字符串常量 字符数组 const修饰指针 只读数据区 不可修改 栈或全局区 sizeof不同 指向常量 常量指针 核心原则:字符串常量只读,字符数组可改,const修饰符遵循"右结合"规则 字符串操作函数底层都是指针运算,理解指针才能写出高效代码

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 不要用sizeof求指针指向的字符串长度——它返回的是指针大小,不是字符串长度。用strlen。
  • 字符串常量作为函数参数时,形参一定要用const char *——否则编译器不会报错,但运行时修改会崩溃。
  • 实现字符串函数时,注意返回值类型——比如strcmp返回int,不是char。我曾经见过有人用char接收返回值,结果比较结果永远不对。
  • 指针自增自减的优先级——*p++等价于*(p++),先取当前值,再移动指针。这个顺序搞反了,程序逻辑全乱。

嗯,字符串和指针这块,说白了就是多练。你写个自己的字符串库,把这些函数都实现一遍,遇到问题再回来翻翻这章的内容,慢慢就熟了。