20、字符串与指针:字符串指针与字符数组的区别、字符串常量、指针操作字符串
字符串在C语言里,说简单也简单,说复杂也复杂。很多新手甚至工作一两年的朋友,都在这上面栽过跟头。我个人习惯把字符串看作C语言里的一堵墙——翻过去,指针这块基本就通了;翻不过去,后面写代码总是提心吊胆。
今天咱们就把这堵墙拆了。重点聊三个东西:字符串指针、字符数组,还有那个让人又爱又恨的字符串常量。
字符串指针 vs 字符数组:到底哪里不一样?
先看两行代码:
char str1[] = "Hello";
char *str2 = "Hello";
乍一看,好像差不多。都是存了个"Hello"嘛。但实际在内存里,完全是两码事。
字符数组:在栈上分配了一块连续内存,每个字符挨个放进去。你可以修改里面的内容。
字符串指针:指针本身在栈上,但它指向的是字符串常量区。那块内存是只读的,你敢改,程序就敢崩。
我在项目中遇到过一位同事,写了这样的代码:
char *p = "Hello";
p[0] = 'h'; // 运行时崩溃
他当时一脸懵:明明编译过了啊?嗯,编译能过,但运行时就给你个下马威。这就是典型的字符串常量不可修改的问题。
char *p = "xxx",第一反应就是:这玩意儿只能读,不能写。
咱们用个表格对比一下:
| 对比项 | 字符数组 char arr[] | 字符串指针 char *p |
|---|---|---|
| 内存位置 | 栈(或全局区) | 指针在栈,指向常量区 |
| 是否可修改 | 可以 | 不可以(常量区只读) |
| 大小 | sizeof(arr) 得到数组大小 | sizeof(p) 得到指针大小(4或8字节) |
| 赋值 | 不能整体赋值(如 arr = "abc" 不行) | 可以重新指向别的字符串 |
| 作为函数参数 | 退化为指针 | 本身就是指针 |
const char * 声明。这样编译器会帮你拦住修改操作,而不是等到运行时才崩溃。
字符串常量:藏在代码里的"钉子户"
字符串常量,比如 "Hello",在程序启动时就被加载到内存的只读段。你写多少次 "Hello",编译器可能只保留一份副本。这叫"字符串常量池"。
你想想看:
char *p1 = "Hello";
char *p2 = "Hello";
if (p1 == p2) {
printf("指向同一个地址\n");
}
在很多编译器上,这个判断是成立的。p1 和 p2 指向了同一块只读内存。但如果你用字符数组:
char arr1[] = "Hello";
char arr2[] = "Hello";
if (arr1 == arr2) {
printf("不会执行\n");
}
arr1 和 arr2 是两块不同的栈内存,地址当然不同。这里比较的是数组首地址,不是内容。
指针操作字符串:灵活但容易翻车
用指针操作字符串,说白了就是玩地址。我建议你先把指针的加减搞明白,再上手。
看个例子:
char str[] = "Hello";
char *p = str;
while (*p != '\0') {
putchar(*p);
p++; // 指针后移一个字符
}
这段代码逐个打印字符。p 一开始指向 'H',然后 p++ 指向 'e',直到遇到 '\0' 结束。
为什么会这样?因为字符串在内存里是连续存放的,最后一个字符后面跟着一个 '\0'。这是C语言的约定,也是字符串的"结束标志"。
我再给你看个常用的操作——字符串拷贝:
void my_strcpy(char *dest, const char *src) {
while (*dest++ = *src++)
; // 空循环体
}
这行代码看着有点绕,但效率很高。它做了三件事:赋值、判断、自增。当 src 指向 '\0' 时,赋值给 dest 后,表达式的值为 0,循环结束。
知识体系梳理
下面这张图,把本章的核心逻辑串起来了。建议你多看几遍,把每个节点的关系理清楚。
避坑指南
- 不要用
==比较字符串内容——它比较的是地址,不是字符。用strcmp()。 - 字符串指针初始化后,别想着改内容——除非你确定它指向的是可写内存。
- 字符数组作为函数参数时,会退化为指针——所以在函数里用
sizeof得到的是指针大小,不是数组大小。 - 字符串常量末尾自动带 '\0'——但如果你自己拼字符数组,别忘了手动加。
const 修饰,除非明确需要修改。这样一看代码就知道:哦,这个函数不会改我的字符串。代码的可读性和安全性都上去了。
好了,字符串与指针这块,说白了就是搞清楚"谁在栈上,谁在常量区,谁能改,谁不能改"。你把这个理清了,后面再遇到字符串相关的问题,心里就有底了。
公众号:蓝海资料掘金营,微信deep3321