5、指针与字符串:字符串常量、字符指针与字符数组、字符串指针的遍历、字符串指针作为函数参数
字符串在C语言里,其实是个挺特殊的存在。你想想看,C语言本身并没有真正的字符串类型,我们平时用的字符串,本质上就是一个以'\0'结尾的字符数组。嗯,这里要注意,这个'\0'就是字符串的终结者,没有它,你的字符串就不知道什么时候该停下来。
我个人习惯把字符串和指针的关系,看作是C语言指针应用中最贴近实际的一环。因为你在嵌入式开发中,几乎每天都在跟字符串打交道——串口打印、协议解析、LCD显示,哪样离得开它?
5.1 字符串常量:藏在代码段里的"钉子户"
先说说字符串常量。比如你写一句:
char *p = "Hello, World!";
这里的"Hello, World!"就是一个字符串常量。它被存放在代码段(.rodata区),是只读的。你想想看,如果你试图去修改它:
p[0] = 'h'; // 运行时崩溃!
我在项目中遇到过这种问题。有一次调试一个串口命令解析模块,发现只要对某个字符串常量做修改,程序就莫名其妙地跑飞。查了半天,才发现是有人把字符串常量当普通数组用了。嗯,从那以后,我只要看到字符串常量,就会下意识地加上const修饰:
const char *p = "Hello, World!"; // 这样更安全
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 字符数组"这块吃透,因为它是后面所有操作的基础。
公众号:蓝海资料掘金营,微信deep3321