C语言:指针进阶-->核心知识点全解析

前言:

本篇系统梳理 C 语言指针进阶全部核心知识点,从易混淆概念辨析、底层内存模型到回调函数设计思想、面试高频考点全覆盖,搭配经典代码示例与易错坑点总结,是 C 语言从入门到进阶的核心分水岭,适合零基础进阶、知识点复盘与校招社招面试突击复习。


一、前置核心:指针的本质是「类型 + 指向」

指针的核心永远是两点:指向什么对象自身是什么类型

  • 基础指针:指向普通变量,如 int* p 指向 int 变量,+1 跳过 4 字节
  • 进阶指针:指向数组、函数、其他指针,步长和解析规则由指向的对象决定

所有复杂指针的拆解,都遵循「优先级从高到低,从内到外」的语法规则,搞懂本质就不会混淆。


二、最易混淆:指针数组 vs 数组指针

这是面试必考题,90% 的初学者都分不清,核心区别在于:到底是数组还是指针

1. 指针数组:是数组,每个元素都是指针

复制代码
// 指针数组:[]优先级更高,arr先和[]结合成数组,元素类型是int*
int* arr[3];
  • 本质:数组,数组长度为 3,每个元素都是一个 int 类型的指针
  • sizeof 结果:3 × 指针大小(32 位 4 字节 / 64 位 8 字节)
  • 典型用途:管理多个字符串、批量管理指针、命令行参数

代码示例:管理多个字符串

复制代码
int main() {
    // 指针数组,每个元素指向一个字符串常量
    const char* names[3] = {"张三", "李四", "王五"};
    for (int i = 0; i < 3; i++) {
        printf("%s\n", names[i]);
    }
    return 0;
}

2. 数组指针:是指针,指向一整个数组

复制代码
// 数组指针:()提升优先级,p先和*结合成指针,指向一个大小为5的int数组
int (*p)[5];
  • 本质:指针,指向一个长度为 5 的 int 数组,步长是整个数组的大小(5×4=20 字节)
  • sizeof 结果:就是指针本身的大小(4/8 字节)
  • 典型用途:接收二维数组、操作连续的一维数组块

代码示例:& 数组名的类型就是数组指针

复制代码
int main() {
    int arr[5] = {1,2,3,4,5};
    int (*p)[5] = &arr; // &arr是整个数组的地址,类型为数组指针
    
    printf("%p\n", p);
    printf("%p\n", p+1); // 地址跳过20字节,直接跳到数组末尾
    return 0;
}

3. 一秒区分法

看变量名先和谁结合:

  • 先和 [] 结合 → 数组 → 指针数组
  • 先和 * 结合 → 指针 → 数组指针

口诀:括号改优先级,右左法则从内往外读。

4. 核心对比表

对比维度 指针数组 int* arr[5] 数组指针 int (*p)[5]
本质 数组 指针
存储内容 5 个指针变量 1 个数组的地址
sizeof 结果 5 × 指针大小 单个指针大小
+1 步长 跳过 1 个指针(4/8 字节) 跳过一整个数组(20 字节)
典型场景 管理多个字符串、指针集合 二维数组传参、数组块操作

三、二级指针深度解析

二级指针也叫指向指针的指针,核心是:指针变量本身也是变量,也有自己的内存地址

1. 内存模型与基础用法

复制代码
int main() {
    int a = 10;
    int* p = &a;    // 一级指针,存变量a的地址
    int** pp = &p;  // 二级指针,存一级指针p的地址

    printf("%d\n", **pp); // 两次解引用,最终拿到a的值10
    return 0;
}

内存关系:pppa,每多一级 * 就多一次解引用。

2. 经典面试场景:函数内修改指针的指向

这是二级指针最核心的用途,也是笔试改错题高频考点。 错误写法:传一级指针,无法修改实参

复制代码
void getMemory(int* p) {
    p = (int*)malloc(sizeof(int)*10);
    // p是形参,函数内修改的只是副本,出函数就销毁,实参不受影响
}

int main() {
    int* ptr = NULL;
    getMemory(ptr);
    // ptr仍然是NULL,malloc的内存泄漏,无法访问
    return 0;
}

错误本质:指针传参本质还是传值,传递的是指针的副本,修改副本不影响原指针。

正确写法:传二级指针,修改原指针本身

复制代码
void getMemory(int** pp) {
    *pp = (int*)malloc(sizeof(int)*10);
    // *pp就是外面的ptr本身,直接修改ptr的指向
}

int main() {
    int* ptr = NULL;
    getMemory(&ptr); // 传入指针本身的地址
    ptr[0] = 10; // 可以正常使用
    free(ptr);
    return 0;
}

3. 二级指针与指针数组的关系

指针数组传参时,会退化成指向首元素的指针;首元素是一级指针,所以退化成二级指针。

复制代码
// 以下三种函数形参写法完全等价
void printStr(char** arr, int len);
void printStr(char* arr[], int len);
void printStr(char* arr[3], int len);

本质:数组传参都会退化成首元素指针,指针数组的首元素是 char*,所以退化成 char**。


四、函数指针与回调函数(面试核心重点)

函数指针是 C 语言实现「通用逻辑、回调机制」的核心手段,也是进阶开发者的必备技能。

1. 函数指针基础

函数名本身就是函数的入口地址,函数指针就是用来存储函数地址的指针。

复制代码
// 函数指针p:指向 参数为int、返回值为void 的函数
void (*p)(int);

注意:括号不能省,写成 void *p(int) 就变成了「返回值为 void * 的函数声明」。

代码示例:基础使用

复制代码
void printNum(int n) {
    printf("%d\n", n);
}

int main() {
    void (*p)(int) = printNum; // 函数名就是地址
    p(100); // 通过函数指针调用,等价于printNum(100)
    return 0;
}

2. 函数指针数组:转移表

把多个同类型的函数指针放在数组里,用来替代大量 switch-case 分支,代码更简洁易扩展。

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

int main() {
    // 函数指针数组
    int (*op[2])(int, int) = {add, sub};
    printf("%d\n", op[0](10, 20)); // 调用add
    printf("%d\n", op[1](10, 20)); // 调用sub
    return 0;
}

适用场景:菜单选择、指令解析、状态机,新增功能只需要加函数和数组元素,无需改分支逻辑。

3. 回调函数:设计思想核心

什么是回调函数

把一个函数的指针作为参数传给另一个函数,由接收方在合适的时机反向调用这个函数,这个被传入的函数就叫回调函数。

核心价值:把「算法框架」和「业务逻辑」解耦,同一个算法可以适配任意类型、任意比较规则,是 C 语言实现通用能力的核心方式。

案例 1:标准库 qsort 的使用

qsort 是 C 标准库的快速排序函数,通过回调函数支持任意类型数据的排序。

复制代码
#include <stdlib.h>

// 回调函数:比较两个int,规则由我们自定义
int cmpInt(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int main() {
    int arr[5] = {3,1,4,2,5};
    // 传入数组、元素数、元素大小、比较函数指针
    qsort(arr, 5, sizeof(int), cmpInt);
    return 0;
}

qsort 本身只负责排序算法逻辑,不关心数据类型和比较规则,全部通过回调函数交给调用者实现。

案例 2:手撕通用冒泡排序(复刻 qsort 思想)
复制代码
#include <string.h>

// 通用冒泡排序:支持任意类型
void bubbleSort(void* base, size_t count, size_t size, 
                int (*cmp)(const void*, const void*)) {
    for (size_t i = 0; i < count-1; i++) {
        for (size_t j = 0; j < count-1-i; j++) {
            // 按字节计算第j和j+1个元素的地址
            char* p1 = (char*)base + j * size;
            char* p2 = (char*)base + (j+1) * size;
            if (cmp(p1, p2) > 0) {
                // 逐字节交换两个元素
                for (size_t k = 0; k < size; k++) {
                    char tmp = p1[k];
                    p1[k] = p2[k];
                    p2[k] = tmp;
                }
            }
        }
    }
}

设计思想:用 void * 接收任意数据,用 size 控制元素宽度,用回调函数实现比较逻辑,完全和数据类型解耦。


五、数组传参与指针退化

数组传参是最经典的坑点,本质是「数组名退化成首元素指针」。

1. 一维数组传参

复制代码
// 以下三种形参写法完全等价,最终都是int*指针
void test(int arr[10]);
void test(int arr[]);
void test(int* arr);

经典坑点:函数内无法用 sizeof 求数组总大小

复制代码
void test(int arr[]) {
    printf("%zu\n", sizeof(arr));
    // 结果是4/8字节(指针大小),不是数组总大小
}

原因:传参时数组退化成了指针,sizeof 计算的是指针的大小。

2. 二维数组传参

二维数组传参,退化成「指向第一行的数组指针」,不能用二级指针接收

复制代码
// 正确写法:数组指针接收
void test(int arr[3][4]);
void test(int arr[][4]);
void test(int (*arr)[4]);

// 错误写法:二级指针类型不匹配
void test(int** arr);

原因:二维数组的每一行是连续的,首元素是一整个一维数组,所以退化成数组指针;二级指针指向的是离散的指针集合,内存模型完全不同。

3. 数组名的两个特殊例外

绝大多数情况下,数组名都是首元素的地址,只有两个例外代表整个数组:

  1. sizeof(数组名):计算整个数组的总字节数
  2. &数组名:取整个数组的地址,类型为数组指针,+1 跳过整个数组

六、指针与数组的本质区别

对比维度 数组 指针
内存分配 连续空间,存放数据本身 只存一个地址,指向其他数据
sizeof 结果 总元素大小之和 指针本身大小(4/8 字节)
访问方式 直接访问数据 先取地址再间接访问
可修改性 数组名是常量,不能修改指向 指针是变量,可以随时修改指向
传参行为 退化成首元素指针 直接传递地址值
字符串赋值 初始化时赋值,后续不能整体赋值 可以随时指向新的字符串

七、面试高频考点与易错坑点

1. 经典面试问答

Q1:指针数组和数组指针有什么区别?

答:

  1. 本质不同:指针数组是数组,每个元素是指针;数组指针是指针,指向一整个数组
  2. 语法不同:int* arr[5] 是指针数组,int (*p)[5] 是数组指针
  3. 步长不同:指针数组 + 1 跳过一个指针;数组指针 + 1 跳过一整个数组
  4. 用途不同:指针数组用来管理多个指针;数组指针用来接收二维数组

Q2:什么时候需要用到二级指针?

答:两个核心场景:

  1. 需要在函数内部修改外部指针的指向时,必须传指针的地址(二级指针),传一级指针只是传值,无法修改原指针
  2. 管理指针数组、接收指针数组传参时,本质就是二级指针

Q3:什么是回调函数?有什么优势?

答:把函数指针作为参数传入另一个函数,由对方在合适时机调用的函数就是回调函数。 优势是解耦:把通用算法框架和具体业务逻辑分开,同一个函数可以适配不同数据类型、不同规则,提升代码通用性和可扩展性,典型代表是 qsort。

Q4:数组名和 & 数组名有什么区别?

答:

  1. 数值上地址值相同,但类型完全不同
  2. 数组名是首元素地址,类型是元素指针,+1 跳过一个元素
  3. & 数组名是整个数组的地址,类型是数组指针,+1 跳过一整个数组

Q5:二维数组传参可以用二级指针接收吗?为什么?

答:不可以。 二维数组是连续内存,传参退化成指向一维数组的数组指针;二级指针指向的是离散的指针数组,内存模型和步长都不一样,类型不匹配,强行使用会访问错误内存。

2. 常见易错坑点

  1. 定义漏括号:数组指针、函数指针漏写括号,变成指针数组、指针函数
  2. 一级指针改指向:函数内想修改外部指针指向,只传一级指针,导致内存泄漏
  3. 二维数组传参错用二级指针:类型不匹配,引发越界访问
  4. 函数内 sizeof 求数组长度:数组退化成指针,得到的是指针大小
  5. 混淆数组名与 & 数组名:误以为二者完全等价,忽略类型和步长差异

以上就是 C 语言指针进阶的全部核心内容,是 C 语言从入门到进阶的关键分水岭,也是数据结构、工程化开发的必备基础。


制作不易,如果对你有用,希望能点赞收藏支持一下。