指针的5层境界,从入门到劝退:C语言指针进阶全解析

指针的5层境界,从入门到劝退:C语言指针进阶全解析

学C语言绕不开指针。指针入门的时候,你知道了*&,知道了指针就是存地址的变量。

但这只是开始。

当你遇到函数指针、指针数组、二级指针、回调函数......才真正体会到什么叫"从入门到劝退"。

今天从低到高,拆解指针的5层境界。每一层都附代码和图解,看懂了,指针这块你就通关了。


第1层:指针数组 vs 数组指针------名字差不多,东西完全不同

这是最容易搞混的一对,面试必考。

名字看起来差不多,但本质完全不同。区分方法就一个:看谁先和变量名结合

c 复制代码
int *p[5];     // 指针数组
int (*p)[5];   // 数组指针

怎么区分?

[] 的优先级高于 *,所以:

  • int *p[5]:p 先和 [5] 结合 → p 是数组 ,数组里每个元素是 int*指针数组
  • int (*p)[5]:p 先和 * 结合 → p 是指针 ,指向一个有 5 个 int 的数组 → 数组指针

指针数组用来干嘛?

存放多个地址,最常见的用法是管理一组字符串:

c 复制代码
// 指针数组:每个元素是一个 char*,指向一个字符串
char *fruits[3] = {"apple", "banana", "orange"};

for (int i = 0; i < 3; i++) {
    printf("%s\n", fruits[i]);
}

💡 记忆口诀 :先结合 [] 就是数组(指针数组),先结合 * 就是指针(数组指针)。


第2层:二级指针------什么时候需要"指向指针的指针"

一级指针能改数据,二级指针能改指向。

先看一个问题:

c 复制代码
// ❌ 错误:想在函数里给指针分配内存
void allocate(int *p) {
    p = (int *)malloc(sizeof(int));  // 改的是副本,外面感知不到
}

int main() {
    int *ptr = NULL;
    allocate(ptr);
    // ptr 还是 NULL!因为函数里改的是副本
}

问题出在哪?C 语言是值传递,函数拿到的是指针的副本。你改副本的指向,原指针纹丝不动。

正确写法:传指针的地址,也就是二级指针

c 复制代码
// ✅ 正确:用二级指针修改外部指针的指向
void allocate(int **pp) {
    *pp = (int *)malloc(sizeof(int));  // 通过 *pp 修改的是原始指针
    **pp = 42;
}

int main() {
    int *ptr = NULL;
    allocate(&ptr);  // 传 ptr 的地址
    printf("%d\n", *ptr);  // 输出 42
    free(ptr);
}

💡 核心逻辑

  • 想改 int a → 传 int *
  • 想改 int *p → 传 int **
  • 规则永远不变:想改谁,就传谁的地址

二级指针的实际场景

最典型的就是链表头节点操作:

c 复制代码
// 在链表头部插入节点,需要修改 head 指针
void insert_head(Node **head, int data) {
    Node *new_node = (Node *)malloc(sizeof(Node));
    new_node->data = data;
    new_node->next = *head;   // 新节点指向原头节点
    *head = new_node;         // 修改 head 指向新节点
}

// 调用
Node *head = NULL;
insert_head(&head, 10);
insert_head(&head, 20);
// 链表:20 -> 10 -> NULL

💡 记住:凡是需要在函数里"改变一个指针的指向",就用二级指针。


第3层:函数指针------函数也有地址

函数名就是地址,就像数组名一样。

c 复制代码
int add(int a, int b) {
    return a + b;
}

add 这个函数名,本身就是函数在内存中的入口地址。所以你可以用指针来存它:

c 复制代码
// 定义一个函数指针,指向"接收两个 int,返回 int"的函数
int (*fp)(int, int) = add;

// 通过指针调用函数
int result = fp(3, 5);    // 等价于 add(3, 5)
printf("%d\n", result);   // 输出 8

函数指针的写法怎么看?

拆解 int (*fp)(int, int)

  1. (*fp) → fp 是指针
  2. 指针指向什么?看右边 (int, int) → 一个函数,参数是两个 int
  3. 再看左边 int → 这个函数返回 int

💡 用 typedef 简化,不然太长了:

c 复制代码
// 给函数指针类型起个别名
typedef int (*CalcFunc)(int, int);

// 用起来就清爽了
CalcFunc fp = add;
printf("%d\n", fp(3, 5));

第4层:回调函数------函数指针作为参数

这才是函数指针真正厉害的地方:让函数变得"可插拔"。

标准库里的 qsort 就是一个经典例子。它不关心你怎么比较大小,你告诉它就行:

c 复制代码
// 你的比较函数
int compare_int(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    int n = 6;

    // qsort 不关心比较逻辑,你传什么函数,它就怎么排
    qsort(arr, n, sizeof(int), compare_int);

    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出:1 2 3 5 8 9
    }
}

自己写一个回调,更好理解

c 复制代码
typedef int (*MathOp)(int, int);

// 这个函数不关心具体做加减乘除,你传什么就做什么
int calculate(int a, int b, MathOp op) {
    return op(a, b);
}

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; }

int main() {
    printf("%d\n", calculate(10, 3, add));  // 13
    printf("%d\n", calculate(10, 3, sub));  // 7
    printf("%d\n", calculate(10, 3, mul));  // 30
}

💡 回调的本质:把"做什么"交给调用者决定,框架代码只关心"怎么做"。这是 C 语言实现解耦的核心手段。


第5层:函数指针数组------转移表实战

把函数指针放进数组,用下标代替 switch,代码瞬间清爽。

还是那个计算器例子,先看普通写法:

c 复制代码
// ❌ 普通写法:一堆 switch-case
switch (op) {
    case 1: result = add(a, b); break;
    case 2: result = sub(a, b); break;
    case 3: result = mul(a, b); break;
    case 4: result = divide(a, b); break;
}

用函数指针数组(转移表):

c 复制代码
// ✅ 转移表写法:把函数地址存进数组,用下标直接调用
typedef int (*CalcFunc)(int, int);

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; }
int divide(int a, int b) { return b ? a / b : 0; }

// 函数指针数组:下标 0~3 对应 4 个函数
CalcFunc ops[] = { NULL, add, sub, mul };
//                                    ops[1] = add
//                                    ops[2] = sub
//                                    ops[3] = mul

int main() {
    int a = 10, b = 3;

    // 不用 switch,直接用下标调用
    printf("10 + 3 = %d\n", ops[1](a, b));   // 13
    printf("10 - 3 = %d\n", ops[2](a, b));   // 7
    printf("10 * 3 = %d\n", ops[3](a, b));   // 30
}

💡 转移表的优势

  • 消除 switch-case 的冗余代码
  • 新增操作只需往数组里加一个函数,不用改逻辑
  • 用下标 O(1) 查找,比 switch 更高效

附加:字符指针与常量字符串的隐藏陷阱

一个看起来没问题的代码,可能藏着段错误。

c 复制代码
// ❌ 危险:修改常量字符串
char *str = "hello";
str[0] = 'H';  // 段错误!"hello"是常量字符串,只读

"hello" 存储在程序的只读数据段str 只是指向它的指针。试图修改只读内存,程序直接崩。

正确写法:要修改就拷贝一份

c 复制代码
// ✅ 用数组:内容存在栈上,可以修改
char str[] = "hello";
str[0] = 'H';  // 没问题,现在 str 是 "Hello"

// ✅ 或者动态拷贝
char *str = strdup("hello");  // malloc + strcpy
str[0] = 'H';
free(str);

💡 记住char *str = "hello" 里的 "hello" 是常量,不能改。char str[] = "hello" 里的 "hello" 会被拷贝到栈上,可以改。


写在最后

指针的5层境界,从简单到复杂:

  1. 指针数组 vs 数组指针:看谁先和变量名结合
  2. 二级指针:想改指针的指向,就传它的地址
  3. 函数指针:函数名就是地址,指针可以存它
  4. 回调函数:把"做什么"交给调用者
  5. 函数指针数组:转移表消除 switch-case

它们的关系是一层套一层的:

指针 → 指针数组 → 指向指针的指针 → 指向函数的指针 → 函数指针数组

每一层都是在前一层的基础上多绕一个弯。弯多了容易晕,但只要把每一步拆开看,其实就那么回事。

C语言的指针不会让你失望的,前提是你别跳过它。

相关推荐
C++ 老炮儿的技术栈2 小时前
基于MFC+原生GDI手动自绘仪表盘控件
c语言·c++·visual studio·控件·工业控制·gdi·自绘
小肝一下2 小时前
3. 单链表
c语言·数据结构·c++·算法·leetcode·链表·dijkstra
炸膛坦客11 小时前
单片机/C/C++八股:(二十六)IIC 专题(I²C)---- 上集
c语言·c++·单片机
程序员Rock19 小时前
上位机开发-MODBUS面试常见问题
c语言·c++·面试·职场和发展·上位机
blevoice1 天前
抖抖燃脂机单 AC6966B 主控调试踩坑:Type-C 整机供电喇叭无声排障
c语言·开发语言·单片机
小小晓.1 天前
C++小白记:C风格字符串和数组用法
c语言·开发语言·c++
深念Y1 天前
开发者如何清理和迁移C盘堆积的垃圾
c语言·开发语言
十月的皮皮1 天前
STM32从零到量产开发:四路继电器工业控制模块开发实战笔记(小白视角)
c语言·笔记·stm32·stm32cubemx·hal库
不正经学生1 天前
C 语言函数深入剖析(基础篇)—— 从零理解函数的每一个细节
c语言·开发语言·数据结构·算法·c#