第3章 指针与字符串:常量、数组与const修饰
字符串在C语言里,说白了就是字符数组加个'\0'结尾。但指针和字符串搅在一起时,坑特别多。我做了十几年嵌入式,见过太多同事在这上面翻车——有人把字符串常量当数组改,有人被const修饰符骗得团团转。今天咱们就把这些事彻底理清楚。
3.1 字符串常量与指针
先看一个最基础的问题:字符串常量存在哪?
char *p = "hello";
"hello"[0] = 'H'; // 运行时崩溃!
为什么会崩溃?因为"hello"这个字符串常量,编译器把它放在了只读数据区(.rodata段)。你试图通过指针去修改它,就是往只读内存里写数据——段错误没跑。
我在项目里遇到过这样的bug:一个同事写了个字符串处理函数,直接修改了传入的字符串常量,结果程序跑着跑着就挂了。查了两天才定位到问题。
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 char *p中,const修饰char,所以指向的内容是常量;char * const p中,const修饰p,所以指针本身是常量。
3.5 本章知识结构图
下面这张图总结了指针与字符串的核心关系:
3.6 避坑指南
最后,分享几个我踩过的坑:
- 不要用
sizeof求指针指向的字符串长度——它返回的是指针大小,不是字符串长度。用strlen。 - 字符串常量作为函数参数时,形参一定要用
const char *——否则编译器不会报错,但运行时修改会崩溃。 - 实现字符串函数时,注意返回值类型——比如
strcmp返回int,不是char。我曾经见过有人用char接收返回值,结果比较结果永远不对。 - 指针自增自减的优先级——
*p++等价于*(p++),先取当前值,再移动指针。这个顺序搞反了,程序逻辑全乱。
嗯,字符串和指针这块,说白了就是多练。你写个自己的字符串库,把这些函数都实现一遍,遇到问题再回来翻翻这章的内容,慢慢就熟了。