C语言指针详解:从数组、指针基础到多级指针与函数回调
数组是一段存放数据的连续内存空间。数组名表示第一个数组元素的地址内存偏移量取决于数据类型。对数组名取地址数组名时表示整个数组此时内存偏移量为整个数组的大小sizeof(数组名)。数组传参会退化为了效率。C 语言不会在传参时拷贝整个数组那样太慢且费栈空间而是只传一个地址。指针是地址内存访问偏移量为4个字节(32位处理器)/8字节64位处理器无论指向的数据类型如何都以地址类型进行解析。数组名不是指针它是表示整个数组的左值。只是在大多数表达式中会隐式转换为指向首元素的指针。指针是变量地址定义写法术语本质例子int *p[3]指针数组它是数组装的是指针char *menu[] {File, Edit}int (*p)[3]数组指针它是指针指向一个数组指向二维数组的一行void (*f)(int)函数指针它是指针指向一个函数回调函数、信号处理int *f(int)指针函数它是函数返回一个地址malloc 返回值数组的地址数组的几个地址—— a、a、a[0]当我们定义一个数组 a 时编译器根据指定的元素个数和元素的类型分配确定大小元素类型大小*元素个数的一块内存并把这块内存的名字命名为 a。名字 a 一旦与这块内存匹配就不能被改变。数组名a数组名a作为地址使用时表示数组首个元素的地址(指针)那它所指向的类型就是数组元素类型加减一个整数就相当于加减(整数*sizeof(元素类型))。数组名a可以看成一个指针常量它的值不能被修改不能单独作为左值使用。aa表示对编译器分配好内存的变量a取地址得到的是整个数组的地址(指针)其所指向的类型是数组的类型加减一个整数就相当于加减(整数*sizeof(数组的类型))。a[0]a[0]是数组的首个元素a[0]是对数组首个元素取地址得到的是数组首个元素的地址(指针)那它所指向的类型就是数组元素类型加减一个整数就相当于加减(整数*sizeof(元素类型))。表达式物理数值指向的对象指向对象的类型1 操作增加的字节数a数组首地址数组第一个元素 a[0]int1 * sizeof(int) 4a[0]数组首地址数组第一个元素 a[0]int1 * sizeof(int) 4a数组首地址整个数组int [5] (数组类型)1 * sizeof(a) 20野指针‼️野指针是指针指向的位置是不可知的随机的、不正确的、没有明确限制的。三大来源未初始化的指针指针指向已释放的内存指针超出变量作用域避免方法创建指针时对其设置一个初始值或NULL指针用完后释放内存并记得将其赋值为NULL避免指针访问越界避免返回函数中变量的指针指针的核心作用间接修改通过地址修改函数外部的变量文件指针指针传参动态内存堆上分配的内存必须用指针操作高效传递传指针避免结构体拷贝开销泛型编程void*实现通用数据结构如qsort字符串、数组、结构体指针数组 数组指针指针数组数组中的元素都是指针数组指针指向数组的指针指针数组int *p[5]是一个数组数组中的每个元素都是指向 int 的指针而数组指针int (*p)[5]是一个指针它指向一个包含 5 个 int 的数组。二者本质不同一个是“数组”一个是“指针”因此在内存布局和指针运算上也完全不同。函数指针 指针函数函数指针指向函数的指针#include stdio.h int Add(int x, int y) { return x y; } int main() { // 函数指针变量pf保存了Add函数的地址变量类型为int (*)(int, int) int (*pf)(int x, int y) Add; /* int (*pf)(int x, int y) Add; // AddAdd int (*pf)(int, int) Add; // 形参可以省略 int (*pf)(int, int) Add; // AddAdd形参可以省略 */ int sum (*pf)(3, 5); /* int sum pf(3, 5); // pf(3, 5) (*pf)(3, 5) int sum Add(3, 5); */ printf(%d\n, sum); // 8 return 0; }⚡ 回调函数回调函数通过函数指针调用的函数。你调用我提供的函数我在适当时机回调你的函数。也就是函数指针a作为参数在另一个函数b的参数列表中同时这个a函数还在另一个b中被调用#include stdio.h // 回调函数类型定义 typedef void (*Callback)(int); /* typedef 的作用它是在定义一个新类型别名而不是简单的“A 是 B 的别名” 这里定义的类型是 函数指针类型不是普通的数据类型 (*Callback)(int) 的解析 *Callback这是一个指针变量名称是 Callback (int)它指向的函数接受一个 int 参数 void函数的返回类型是 void 整体合起来Callback 是一个 指向函数的指针类型 */ // 接收回调函数作为参数的函数 void processNumbers(int arr[], int size, Callback callback) { for (int i 0; i size; i) { callback(arr[i]); // 调用回调函数 } } // 实际的回调函数实现 void printNumber(int num) { printf(Number: %d\n, num); } void squareNumber(int num) { printf(Square: %d\n, num * num); } int main() { int numbers[] {1, 2, 3, 4, 5}; int size sizeof(numbers) / sizeof(numbers[0]); printf(Printing numbers:\n); // 为什么不需要给函数传递参数?,其实 printNumber 这个地方只需要提供一个地址还没调用 // processNumbers(numbers, size, printNumber());是错误写法 processNumbers(numbers, size, printNumber); printf(\nPrinting squares:\n); processNumbers(numbers, size, squareNumber); return 0; }指针函数一个函数的返回值是一个指针的函数 --- int* a(void)指针常量和常量指const int *p; // 常量指针指向的值不可改指向可改 int *const p; // 指针常量指向不可改指向的值可改 const int *const p; // 都不可改悬空指针悬空指针是指向已释放或不存在内存的指针。产生悬空指针的情况释放后使用在内存被释放后继续使用指向该内存的指针。函数返回局部变量的地址从函数返回局部变量的地址后一旦函数退出局部变量的内存会被回收。避免悬空指针的方法及时重置指针释放内存后将指针设置为NULL。避免返回局部变量的地址使用动态分配的内存或全局/静态变量。野指针和悬空指针的区别野指针 (Wild Pointer)出生就带病。指针变量刚被创建时没有初始化它的值是随机的。悬空指针 (Dangling Pointer)后天致残。指针指向的内存被释放了但指针还指着那块地。void*指针void*被称为“万能指针”或“泛型指针”或“无类型指针”。它只表示“这是一个内存地址”但不说明该地址处存放的数据类型。 核心规则不能直接解引用因为它不知道要读取几个字节不知道类型。错误void *p; *p 10;正确*(int*)p 10;(必须先强转)不能直接进行算术运算在标准 C 中不能对void*进行或--因为不知道步长虽然 GCC 插件允许将其视为 1 字节但不建议这样做。 核心作用通用接口泛型malloc返回void*所以它可以分配任何类型内存。memset(void *s, int c, size_t n)可以清空任何类型的数组。回调函数中的上下文传递 在多线程如pthread_create或排序qsort中void*用于传递用户自定义的数据结构。多级指针指向指针的指针用于存储上一级指针的地址。如一级指针保存变量的地址。二级指针保存“一级指针变量”的地址。在函数内部修改外部指针的指向如果你传一级指针给函数函数内修改的是该指针的“副本”外部指针不会变。只有传二级指针才能真正改掉外部指针指向的内存地址。void allocateMemory(int **pp) { // 通过解引用二级指针修改外部的一级指针 *pp (int *)malloc(sizeof(int) * 5); } int main() { int *p NULL; allocateMemory(p); // 传入指针的地址二级指针 // 此时 p 不再是 NULL而是指向了堆内存 free(p); }二维数组/指针数组的动态处理char *argv[]命令行参数在传参时会退化为char **argv。argv指向第一个字符串指针的地址。*argv得到第一个字符串的首地址。**argv得到第一个字符串的第一个字符。对于int a[3][4];a是数组名指向第一行类型是int (*)[4]数组指针。a[0]是第一行的首元素地址类型是int *。((a i) j)等价于a[i][j]。#include stdlib.h int main() { int rows 3, cols 4; // 1. 申请行指针数组 (二级指针) int **matrix (int **)malloc(rows * sizeof(int *)); // 2. 为每一行申请空间 for (int i 0; i rows; i) { matrix[i] (int *)malloc(cols * sizeof(int)); } // --- 使用 matrix[i][j] --- // 3. 释放必须先释放每一行 for (int i 0; i rows; i) { free(matrix[i]); } // 4. 最后释放行指针数组本身 free(matrix); matrix NULL; // 避免悬空指针 return 0; }链表操作删除节点时需要修改头指针