指针(二):指针的运算、指针与字符串、指针数组、数组指针、多级指针
指针这东西,很多人学到这儿就开始犯晕。说实话,我当年刚接触指针时也绕了好一阵子。但等你真正理解了,你会发现指针其实是C语言最强大的工具之一。今天咱们就把指针的运算、指针与字符串、指针数组、数组指针、多级指针这几个硬骨头啃下来。
指针的运算
指针本质上就是个地址,所以它能做加减法。但要注意,指针的加减不是简单的整数加减,而是以指向的数据类型大小为步长。
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p指向arr[0]
printf("p = %p, *p = %d\n", p, *p); // 输出arr[0]的地址和值
p++; // 指针加1,实际地址增加了sizeof(int)个字节
printf("p = %p, *p = %d\n", p, *p); // 输出arr[1]的地址和值
p += 2; // 指针加2,实际地址增加了2*sizeof(int)个字节
printf("p = %p, *p = %d\n", p, *p); // 输出arr[3]的地址和值
我在项目中遇到过一个问题:有人用指针遍历数组时,不小心把指针类型写错了,结果步长不对,访问到了错误的内存位置。嗯,这种bug特别难查,因为程序可能不会立刻崩溃,而是产生一些莫名其妙的值。
避坑指南:我曾经因为指针越界访问,把一个嵌入式设备的系统栈给踩坏了,设备运行几个小时后就死机。排查了整整两天才发现是数组遍历时指针多走了一步。所以,指针运算一定要确保在合法范围内。
指针还可以做减法。两个同类型的指针相减,得到的是它们之间相差的元素个数,不是字节数。
int *p1 = &arr[0];
int *p2 = &arr[4];
int diff = p2 - p1; // diff = 4,表示相差4个元素
printf("diff = %d\n", diff); // 输出4
指针的比较也很有用。你可以用 ==、!=、<、> 等运算符比较两个指针。这在判断指针是否指向同一数组范围内特别实用。
指针与字符串
C语言里没有专门的字符串类型。字符串其实就是以 '\0' 结尾的字符数组。而指针操作字符串,比用数组下标灵活得多。
char str[] = "Hello";
char *p = str; // p指向字符串首字符
// 用指针遍历字符串
while (*p != '\0') {
putchar(*p);
p++;
}
// 输出: Hello
你想想看,用指针遍历字符串,代码是不是简洁多了?我个人习惯用指针操作字符串,尤其是在做字符串拷贝、拼接这类操作时。
// 自己实现字符串拷贝
void my_strcpy(char *dest, const char *src) {
while (*dest++ = *src++)
;
}
这里有个细节:const char *src 表示 src 指向的内容不能被修改。这是好习惯,能防止不小心把源字符串改坏了。
小技巧:字符串常量如 "Hello" 存储在只读数据区,不能修改。而字符数组 char str[] = "Hello" 存储在栈上,可以修改。我见过有人试图修改字符串常量,结果程序直接崩溃。
指针数组
指针数组,说白了就是一个数组,里面每个元素都是指针。最常见的用法是处理多个字符串。
const char *colors[] = {
"Red",
"Green",
"Blue",
"Yellow",
NULL // 用NULL作为结束标志
};
// 遍历指针数组
for (int i = 0; colors[i] != NULL; i++) {
printf("%s\n", colors[i]);
}
我在项目中用指针数组管理配置项的名字,这样增删改查都很方便。每个元素指向一个字符串常量,不占用额外空间。
| 特性 | 指针数组 | 二维字符数组 |
|---|---|---|
| 内存布局 | 每个指针指向独立的内存 | 连续的内存块 |
| 修改字符串 | 可以改变指针指向 | 可以修改字符内容 |
| 内存效率 | 高(字符串可共享) | 低(每行固定长度) |
数组指针
数组指针和指针数组,名字很像,但完全是两码事。数组指针是指向数组的指针。
int arr[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*p)[4] = arr; // p是指向包含4个int的数组的指针
// 用数组指针遍历二维数组
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", p[i][j]);
}
printf("\n");
}
为什么要有数组指针?说白了,当你需要把二维数组传给函数时,数组指针就派上用场了。
void print_matrix(int (*matrix)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
重点:int (*p)[4] 和 int *p[4] 的区别——前者是数组指针,后者是指针数组。记住这个口诀:括号优先,先看括号里的东西。
多级指针
多级指针,就是指向指针的指针。最常用的是二级指针,三级以上在实际项目中很少见。
int value = 42;
int *p1 = &value; // 一级指针
int **p2 = &p1; // 二级指针
printf("value = %d\n", **p2); // 输出42
二级指针最常见的用途是修改指针本身的值。比如在函数里分配内存,然后通过二级指针把地址传回给调用者。
void allocate_memory(int **ptr, int size) {
*ptr = (int *)malloc(size * sizeof(int));
if (*ptr == NULL) {
// 处理内存分配失败
}
}
int main() {
int *arr = NULL;
allocate_memory(&arr, 10);
// 现在arr指向了分配的内存
free(arr);
return 0;
}
我在项目中用二级指针管理动态数组的扩容。当数组满了,就重新分配一块更大的内存,然后通过二级指针更新原来的指针变量。
注意:多级指针容易让人晕头转向。我曾经在代码里用了三级指针,结果自己都搞不清哪个星号对应哪一层。后来我重构了代码,用结构体代替了多级指针,代码清晰多了。所以,能用结构体就别用多级指针。
知识体系总览
下面这张图把指针的核心知识点串起来了,你可以对照着复习。
指针这部分内容,说白了就是理解「类型决定行为」这个核心思想。指针的类型决定了加减运算的步长,决定了如何解引用,决定了能做什么操作。把这个搞明白了,指针就不再是拦路虎了。
我的建议:学指针最好的方法就是动手写代码。在调试器里一步步看指针的值怎么变化,地址怎么移动。我当年就是这么过来的,看着看着就通了。
公众号:蓝海资料掘金营,微信deep3321