指针(二):指针的运算、指针与字符串、指针数组、数组指针、多级指针

指针这东西,很多人学到这儿就开始犯晕。说实话,我当年刚接触指针时也绕了好一阵子。但等你真正理解了,你会发现指针其实是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;
}

我在项目中用二级指针管理动态数组的扩容。当数组满了,就重新分配一块更大的内存,然后通过二级指针更新原来的指针变量。

注意:多级指针容易让人晕头转向。我曾经在代码里用了三级指针,结果自己都搞不清哪个星号对应哪一层。后来我重构了代码,用结构体代替了多级指针,代码清晰多了。所以,能用结构体就别用多级指针。

知识体系总览

下面这张图把指针的核心知识点串起来了,你可以对照着复习。

指针(二) 指针的运算 • 加减运算:步长 = sizeof(类型) • 指针相减:得到元素个数 指针与字符串 • 字符串以 '\0' 结尾 • 指针遍历字符串更灵活 指针数组 • 数组元素是指针 • 常用于管理多个字符串 数组指针 • 指向数组的指针 • 用于传递二维数组 多级指针 • 指向指针的指针 • 用于修改指针本身的值 核心:理解指针的类型决定了一切行为

指针这部分内容,说白了就是理解「类型决定行为」这个核心思想。指针的类型决定了加减运算的步长,决定了如何解引用,决定了能做什么操作。把这个搞明白了,指针就不再是拦路虎了。

我的建议:学指针最好的方法就是动手写代码。在调试器里一步步看指针的值怎么变化,地址怎么移动。我当年就是这么过来的,看着看着就通了。

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