C 指针(重点)

指针是 C 最强大也最吓人的特性,但它本质就一句话:指针是存"内存地址"的变量。理解了指针,你就理解了 C;理解了 C,你就理解了所有语言里的"引用"、"传地址"、"对象句柄"。这是 C 区别于 Java/Python(把指针藏起来)的核心特征,也是学习 C 的最大门槛。本篇用大量示例带你彻底搞懂指针。

一、内存与地址:先理解比喻

把内存想象成一排带编号的抽屉,每个抽屉能放一个字节,编号就是地址。一个 int n = 42; 在内存里占了 4 个连续抽屉(假设 int 是 4 字节),这 4 个抽屉的起始编号就是 n 的地址。指针就是一张小纸条,上面写着"某号抽屉"的编号

二、指针基础

三个核心符号要分清:

#include <stdio.h>

int main(void) {
    int n = 42;

    // & 取地址:获得变量 n 在内存中的地址
    // * 声明指针:int* 表示"指向 int 的指针"
    int *p = &n;

    printf("n 的值:    %d\n", n);       // 42
    printf("n 的地址:  %p\n", (void*)&n); // 0x7ffee...
    printf("p 的值:    %p\n", (void*)p);  // 等于 &n
    printf("p 指向的值: %d\n", *p);     // 42(解引用)

    // 通过指针修改原变量
    *p = 100;
    printf("现在 n = %d\n", n);         // 100
    return 0;
}

注意 %p 是打印指针地址的占位符,默认输出十六进制。指针本身也有地址——指针也是一个变量,它存在某个内存位置。指向指针的指针(二级指针)就是存"指针地址"的变量。

三、指针与数组(关键关系)

数组名在 C 里会退化为首元素指针。这意味着 arr&arr[0] 是等价的。指针可以像数组一样用下标,数组也可以像指针一样做加减运算:

#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *p = arr;        // 数组名 = 首元素地址 = &arr[0]

    // 指针 + 整数:跳过 n 个元素
    printf("%d\n", *p);         // 10
    printf("%d\n", *(p + 1));   // 20
    printf("%d\n", *(p + 2));   // 30

    // p[i] 就是 *(p + i) 的语法糖
    printf("%d\n", p[3]);       // 40

    // 指针自增:每次移动一个元素的距离
    int *q = arr;
    printf("%d ", *q++);   // 10,然后 q 移到下一个
    printf("%d ", *q++);   // 20
    printf("%d\n", *q);    // 30

    // 指针 - 指针:得到两地址间的元素个数
    int *start = &arr[0];
    int *end = &arr[4];
    printf("元素个数: %ld\n", end - start);   // 4

    // 注意:指针 +1 实际加的是 sizeof(int)=4 字节
    // 编译器自动按类型大小缩放,不用手算
    return 0;
}

理解这条等式非常重要:arr[i] 永远等价于 *(arr + i)。这就是为什么数组传给函数后 sizeof 失效——它退化成了指针。也是为什么 arr[i]i[arr] 居然都能编译通过(虽然后者是恶搞写法)。

四、指针运算

指针只能做有限的几种运算:

不能做的运算:指针 + 指针(无意义)、指针 * 整数、指针除以整数。

五、NULL 指针、野指针、悬垂指针(危险!)

这三类"问题指针"是 C 程序崩溃的头号原因:

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

int main(void) {
    // 1. NULL:明确的"空指针",不指向任何地址
    int *p = NULL;
    if (p == NULL) {
        printf("指针是空的,不能解引用!\n");
    }
    // *p = 10;   // 错!解引用 NULL → 段错误崩溃

    // 2. 野指针:未初始化,值随机
    int *q;       // 危险!未初始化
    // *q = 10;   // 极可能崩溃,或破坏数据

    // 3. 悬垂指针:指向已释放的内存
    int *r = malloc(sizeof(int));
    *r = 42;
    free(r);
    // *r = 99;   // 错!r 已被释放,行为未定义
    r = NULL;     // 好习惯:free 后立即置 NULL

    // 安全使用指针的三原则:
    // 1. 声明时初始化(没指向就赋 NULL)
    // 2. 用前检查是否为 NULL
    // 3. free 后立即置 NULL

    return 0;
}

三条铁律:(1) 声明指针就初始化(没指向就赋 NULL);(2) 用前检查是否为 NULL;(3) free 后立即置 NULL。养成这三个习惯,能避免 90% 的指针 bug。

六、指针的实战:为什么 swap 必须用指针

这是 C 指针最经典的入门案例。理解了它,你就理解了"为什么 C 没有 reference 就必须用指针":

#include <stdio.h>

// 经典:为什么 C 必须用指针才能交换两个变量
void bad_swap(int a, int b) {       // 值传递,只交换了拷贝
    int tmp = a; a = b; b = tmp;
}

void good_swap(int *a, int *b) {    // 传指针,交换原变量
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int x = 10, y = 20;

    bad_swap(x, y);
    printf("x=%d, y=%d\n", x, y);   // 10, 20(没变)

    good_swap(&x, &y);
    printf("x=%d, y=%d\n", x, y);   // 20, 10(交换了)

    return 0;
}

bad_swap 失败的原因:参数是值传递的拷贝,函数内交换的是拷贝,原变量不受影响。good_swap 成功的原因:传的是地址,函数内通过解引用 *a 修改的就是原变量本身。这是 C 没有"引用"机制带来的设计模式——所有"要在函数内修改外部变量"的场景都要传指针。

七、函数指针(进阶)

指针不仅能指向数据,还能指向函数。函数名本质上就是函数代码的内存地址。函数指针是 C 实现回调、多态、事件分派的核心机制:

#include <stdio.h>

// 普通函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

// 函数指针类型:返回 (*名)(参数)
// 用法类似"把函数当变量传来传去"

// 参数 op 是一个函数指针
int calculate(int x, int y, int (*op)(int, int)) {
    return op(x, y);     // 通过指针调用函数
}

int main(void) {
    // 声明函数指针
    int (*f)(int, int);

    f = add;
    printf("%d\n", f(3, 5));        // 8

    f = sub;
    printf("%d\n", f(10, 3));       // 7

    // 作为参数传递(回调函数模式)
    printf("%d\n", calculate(4, 5, add));   // 9
    printf("%d\n", calculate(4, 5, mul));   // 20

    // 函数指针数组:实现"命令分派"
    int (*ops[])(int, int) = {add, sub, mul};
    printf("%d\n", ops[0](10, 3));   // 13 (add)
    printf("%d\n", ops[1](10, 3));   // 7  (sub)
    return 0;
}

函数指针语法比较丑(int (*op)(int, int)),用 typedef 起别名可以更清晰:typedef int (*BinOp)(int, int);。函数指针的经典用途:

八、二级指针

指针本身也是一个变量,它也有地址。指向指针的指针叫二级指针(int **pp):

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

// 二级指针:指向指针的指针
// 主要用于"在函数内动态分配指针"

void alloc_string(char **str, const char *content) {
    *str = malloc(100);          // 修改外面的 char*
    // 把内容写进去...
}

int main(void) {
    int n = 10;
    int *p = &n;          // 一级指针:指向 int
    int **pp = &p;        // 二级指针:指向 int*

    printf("%d\n", **pp);   // 10(两次解引用)

    // 经典场景:函数内 malloc 一个指针
    char *s = NULL;
    alloc_string(&s, "hello");   // 传 s 的地址(二级指针)
    free(s);

    // 指针数组 + 二级指针(命令行参数就是这个结构)
    char *names[] = {"Alice", "Bob", "Carol"};
    char **np = names;
    printf("%s\n", np[0]);    // Alice
    printf("%s\n", *np);      // Alice
    return 0;
}

二级指针最常见的用途:在函数内动态分配一个指针。比如 alloc_string(&s, "hello")——要让函数能修改外面的 char *s,必须传 s 的地址(也就是二级指针 char **)。另一个常见场景是命令行参数 int main(int argc, char **argv)——argv 是一个指向字符串数组的指针,本质是二级指针。

九、const 与指针(易混淆)

const 和指针组合有三种情况,记忆口诀"const 在 * 左边修饰值,在 * 右边修饰指针":

API 设计中常把只读参数声明为 const T *,既清晰表达意图,又让编译器帮你检查误改。

十、常见段错误原因

"段错误"(segmentation fault)是 C 程序员最常遇到的崩溃,几乎都是指针误用:

调试段错误的工具:gdb(运行到崩溃点看栈)、valgrind(查内存错误)、AddressSanitizer(编译时加 -fsanitize=address,运行时实时检查)。

小结

指针 = 存地址的变量。三个符号:& 取地址、* 解引用、声明时 T *。数组名会退化为首元素指针,arr[i] == *(arr+i)。NULL/野/悬垂指针是崩溃主因,养成初始化、检查、置 NULL 的习惯。函数指针实现回调,二级指针用于动态分配指针。下一篇看 C 的字符串——字符数组的进阶用法。

← 上一篇 C 数组

下一篇 C 字符串

✈️💬