5、指针与字符串:字符串常量、字符指针与字符数组、字符串指针的遍历、字符串指针作为函数参数

字符串在C语言里,其实是个挺特殊的存在。你想想看,C语言本身并没有真正的字符串类型,我们平时用的字符串,本质上就是一个以'\0'结尾的字符数组。嗯,这里要注意,这个'\0'就是字符串的终结者,没有它,你的字符串就不知道什么时候该停下来。

我个人习惯把字符串和指针的关系,看作是C语言指针应用中最贴近实际的一环。因为你在嵌入式开发中,几乎每天都在跟字符串打交道——串口打印、协议解析、LCD显示,哪样离得开它?

核心要点:字符串的本质是字符数组 + '\0' 结尾。指针操作字符串,就是操作这块连续的内存。

5.1 字符串常量:藏在代码段里的"钉子户"

先说说字符串常量。比如你写一句:

char *p = "Hello, World!";

这里的"Hello, World!"就是一个字符串常量。它被存放在代码段(.rodata区),是只读的。你想想看,如果你试图去修改它:

p[0] = 'h';  // 运行时崩溃!

我在项目中遇到过这种问题。有一次调试一个串口命令解析模块,发现只要对某个字符串常量做修改,程序就莫名其妙地跑飞。查了半天,才发现是有人把字符串常量当普通数组用了。嗯,从那以后,我只要看到字符串常量,就会下意识地加上const修饰:

const char *p = "Hello, World!";  // 这样更安全
警告:字符串常量不可修改!试图修改它会导致未定义行为,在嵌入式平台上通常表现为硬fault。

5.2 字符指针 vs 字符数组:看似一样,实则不同

很多初学者会搞混这两个东西。我直接给你看代码:

char str1[] = "Hello";  // 字符数组,在栈上分配
char *str2 = "Hello";   // 字符指针,指向代码段

这两者有什么区别?说白了,str1是一个数组,它有自己的内存空间,你可以修改里面的内容。而str2只是一个指针,它指向的是字符串常量,不能修改。

我建议你记住这张表:

特性 字符数组 char str[] 字符指针 char *str
内存位置 栈或全局区 指向代码段或堆
是否可修改 可以修改内容 指向常量时不可修改
sizeof结果 数组实际大小(含'\0') 指针本身大小(4或8字节)
赋值方式 不能整体赋值 可以指向新地址

你想想看,如果你用sizeof(str1),得到的是6("Hello"加上'\0')。而sizeof(str2),在32位系统上得到的是4。这就是数组和指针最直观的区别。

5.3 字符串指针的遍历:三种姿势

遍历一个字符串,说白了就是从头走到尾,直到碰到'\0'。我常用的有三种方式:

方式一:下标遍历

char str[] = "Embedded";
for (int i = 0; str[i] != '\0'; i++) {
    putchar(str[i]);
}

方式二:指针移动遍历

char *p = "Embedded";
while (*p != '\0') {
    putchar(*p);
    p++;
}

方式三:指针直接判断(我最喜欢的写法)

char *p = "Embedded";
while (*p) {  // '\0' 就是 0,可以直接判断
    putchar(*p++);
}

我个人习惯用第三种。为什么?因为代码简洁,而且效率高。你想想看,*p++这个表达式,先取当前字符,然后指针自增,一步到位。我在嵌入式项目中写串口打印函数时,经常这么用。

小技巧:遍历字符串时,用 while (*p) 代替 while (*p != '\0'),代码更清爽,编译器生成的汇编代码也几乎一样。

5.4 字符串指针作为函数参数

把字符串指针传给函数,这是C语言中最常见的操作之一。说白了,就是传地址,不传整个数组。这样效率高,而且函数内部可以直接修改原字符串(前提是它可修改)。

我给你看一个典型的例子——自己实现一个strlen

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

注意这里用了const char *。我建议你,只要函数不需要修改字符串内容,就加上const。这样做有两个好处:一是告诉调用者"我不会改你的数据",二是防止自己不小心写错。

再来看一个修改字符串的例子——把字符串中的小写字母转大写:

void to_upper(char *str) {
    while (*str) {
        if (*str >= 'a' && *str <= 'z') {
            *str -= 32;  // ASCII码差值
        }
        str++;
    }
}

我曾经在写一个AT指令解析器时,就用到了这种传参方式。把接收到的命令字符串传进去,解析出命令字和参数。嗯,这里要注意,如果你传进去的是字符串常量,那to_upper就会崩溃。所以调用前一定要确认字符串是可修改的。

避坑指南:我曾经在项目里犯过一个低级错误——把字符串常量传给了一个需要修改字符串的函数。结果程序跑起来就死机。从那以后,我养成了一个习惯:凡是可能修改字符串的函数,参数类型用 char *;只读的用 const char *。这样编译器就能帮你检查出来。

5.5 知识结构图

下面这张图,帮你理清本章的核心逻辑:

指针与字符串 字符串常量 存放在代码段 只读,不可修改 字符指针 vs 字符数组 指针:4/8字节,可指向 数组:连续内存,可修改 字符串遍历 下标 / 指针移动 while(*p) 最常用 作为函数参数 传地址,效率高 const char* 保护只读

这张图把本章的四个核心知识点串起来了。你从中心出发,往四个方向走,每个方向都是一个独立但又相互关联的知识点。我个人建议你先把"字符指针 vs 字符数组"这块吃透,因为它是后面所有操作的基础。

我的经验:在嵌入式开发中,字符串操作最常见的坑就是缓冲区溢出和常量修改。记住一句话:指针指向谁,心里要有数;内存能不能改,代码要写清。

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