第三问:指针的本质——地址就是一切

指针,说白了就是地址。这个道理我当年学C语言时听了不下十遍,但真正理解它,是在一次调试内存泄漏的深夜。嗯,今天我们就来把指针这层窗户纸彻底捅破。

一、指针与数组:名字不同,本质相通

数组名是什么?是首元素的地址。指针是什么?是存放地址的变量。它们俩在底层其实是一回事。

我见过不少新手纠结于「数组名是不是指针」这个问题。我的答案是:在表达式中,数组名会被隐式转换为指向首元素的指针,但数组名本身不是变量,你不能给它赋值。

核心理解:

  • arr[i] 等价于 *(arr + i)
  • &arr[i] 等价于 arr + i
  • 数组名 arr 是常量地址,不能做 arr++
#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;  // p指向数组首元素

    // 两种访问方式完全等价
    printf("arr[2] = %d\n", arr[2]);   // 30
    printf("*(p+2) = %d\n", *(p+2));   // 30

    // 指针可以移动,数组名不行
    p++;  // 合法,p现在指向arr[1]
    // arr++;  // 编译错误

    printf("现在p指向: %d\n", *p);  // 20

    return 0;
}

我的习惯:在需要遍历数组时,我更喜欢用指针而不是下标。因为指针自增比下标计算更直接,编译器优化后效率也更高。但要注意边界,别越界了。

二、指针与函数:传值还是传址?

C语言函数参数默认是传值的。你想在函数里修改外面的变量?必须传地址。这就是指针在函数中的核心作用。

我曾经在项目中写过一个交换函数,传了两个int进去,结果发现外面根本没变。排查了半天,才意识到自己忘了传指针。嗯,从那以后,凡是需要修改实参的函数,我第一反应就是加星号。

#include <stdio.h>

// 错误示范:传值
void swap_wrong(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    // 这里的修改不影响外面
}

// 正确示范:传地址
void swap_right(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;

    swap_wrong(x, y);
    printf("swap_wrong后: x=%d, y=%d\n", x, y);  // 5, 10 没变

    swap_right(&x, &y);
    printf("swap_right后: x=%d, y=%d\n", x, y);  // 10, 5 交换成功

    return 0;
}

避坑指南:我曾经在嵌入式驱动中,把一个结构体直接传进函数,结果栈空间爆了。结构体太大时,一定要传指针,而不是传值。传指针只占4或8字节,传值可能几百字节。

三、多级指针:指向指针的指针

多级指针听起来吓人,其实逻辑很简单:一级指针存普通变量的地址,二级指针存一级指针的地址,以此类推。

你想想看,什么时候需要二级指针?当你想在函数里修改传入的指针本身时。比如动态分配内存,你想让函数内部malloc后,把地址带回给外面的指针变量。

#include <stdio.h>
#include <stdlib.h>

// 用二级指针修改指针本身
void alloc_array(int **p, int size) {
    *p = (int *)malloc(size * sizeof(int));
    if (*p == NULL) {
        printf("内存分配失败\n");
        return;
    }
    // 初始化
    for (int i = 0; i < size; i++) {
        (*p)[i] = i * 10;
    }
}

int main() {
    int *arr = NULL;

    // 传入 &arr,即二级指针
    alloc_array(&arr, 5);

    if (arr != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("arr[%d] = %d\n", i, arr[i]);
        }
        free(arr);
    }

    return 0;
}

多级指针的典型场景:

  • 二维数组的动态分配与访问
  • 函数需要修改指针参数本身(如上面例子)
  • 链表、树等数据结构的节点操作
  • 命令行参数 char **argv

四、知识体系总览

下面这张图,是我梳理的指针知识脉络。你看完应该能明白,指针、数组、函数这三者是怎么串起来的。

指针本质 = 地址 指针与数组 指针与函数 多级指针 arr[i] = *(arr+i) 数组名是常量地址 传值 vs 传地址 函数指针做回调 int **p 动态二维数组 一句话总结 指针就是地址,理解了这个,一切迎刃而解

五、实战中的指针思维

指针不是语法糖,它是C语言与硬件对话的桥梁。我做了这么多年嵌入式,总结出三条指针心法:

  1. 明确指向:每次声明指针,问自己「这个指针指向什么类型?」
  2. 检查空指针:解引用前,先判断是否为NULL。我曾经因为没检查,在STM32上直接HardFault。
  3. 注意生命周期:不要返回局部变量的地址。函数结束后,栈上的东西就没了。

我的调试技巧:遇到指针相关bug,我习惯先打印地址值。用 printf("%p", ptr) 看看指针到底指向哪里。很多时候,地址一出来,问题就清楚了一半。

指针这东西,说白了就是地址。数组名是地址,函数参数传地址,多级指针存地址。你只要抓住「地址」这两个字,指针就不再神秘。


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