C语言【第七篇】 ——— 指针的核心概念与高级应用

目录

指针+-整数

指针的类型决定了地址偏移的 “步长”

通过指针对数组元素进行赋值和打印

指针-指针

指针解引用

指针解引用时访问的内存空间大小

二级指针

指针数组

字符指针

常量字符串

数组指针

数组指针的使用

函数指针

函数指针的使用

函数指针数组

函数指针数组的使用

回调函数


指针+-整数

指针的类型决定了地址偏移的 “步长”

代码演示:

int a = 0x11223344;

int* pa = &a;
char* pc = &a;

printf("pa = %p\n", pa);
printf("pc = %p\n\n", pc);

printf("pa+1 = %p\n", pa + 1);
printf("pc+1 = %p\n", pc + 1);

1. 初始变量与指针的基础关联

首先定义 int a = 0x112233440x 是 16 进制数的标识,而 16 进制中每两个数字对应 1 个字节(如 0x11 是 1 字节,0x22 是另一字节),0x11223344 共 4 个 “两数字组”,恰好填满 int 类型通常占用的4 字节空间(这也是选择该值的原因)。

随后定义两个不同类型的指针:int* pa = &apa 是指向 int 类型的指针,存储 a 的起始地址)和 char* pc = &apc 是指向 char 类型的指针,同样存储 a 的起始地址)。由于二者都指向变量 a 的起始内存地址,因此首次打印 pa 和 pc 时,输出的地址值完全相同 —— 指针的 “指向位置” 一致,但 “类型属性” 不同,这为后续加 1 操作的差异埋下伏笔。

2. 指针加 1 的关键差异:步长由类型决定

当对指针执行 “+1” 操作时,并非简单地让地址值加 1,而是按指针指向的数据类型的大小,调整地址偏移量—— 这个偏移量就是 “步长”,而步长的大小由指针类型直接决定:

  • 对于 int* paint 类型占 4 字节,因此 pa + 1 会让地址值增加 4(即跳过 1 个完整的 int 类型所占的内存空间);
  • 对于 char* pcchar 类型占 1 字节,因此 pc + 1 只会让地址值增加 1(即跳过 1 个 char 类型所占的内存空间)。

这就是为什么 “初始地址相同的两个指针,加 1 后地址不同”:指针类型不同 → 步长不同 → 地址偏移量不同,这是指针 ± 整数操作最核心的规律。

3. 核心结论:指针类型需与指向数据类型匹配

代码隐含的关键原则是:什么类型的变量,就该用对应类型的指针接收其地址。若随意混用(如用 char* 接收 int 变量的地址),虽不会导致初始地址错误,但后续执行 ± 整数、解引用等操作时,会因步长错误或访问范围错误,引发不可预期的问题(例如用 pc 遍历 int 变量 a 时,每次只能访问 1 字节,无法完整获取 a 的 4 字节数据)。

代码验证:

通过指针对数组元素进行赋值和打印

代码演示:

int arr[10] = { 0 };
int* p = arr;

int sz = sizeof(arr) / sizeof(arr[0]);

for (int i = 0; i < sz; i++)
{
	*(p + i) = i;
}

for (int i = 0; i < sz; i++)
{
	printf("%d ", *(p + i));
}

第一个for循环:通过指针给数组元素赋值

循环体为 *(p + i) = i;,核心逻辑是:

  • p 是指向数组 arr 首元素(arr[0])的 int* 类型指针(数组名 arr 本质是首元素地址,因此 p = arr 表示 p 指向 arr[0])。
  • p + i 是指针的偏移运算:由于 p 是 int* 类型,p + i 会指向数组的第 i 个元素(arr[i])—— 偏移步长由指针类型决定(int 占 4 字节,p + i 实际地址比 p 大 i * 4 字节,恰好跳过前 i 个 int 元素)。
  • *(p + i) 是对偏移后指针的解引用:表示访问 p + i 所指向的内存空间,即数组的第 i 个元素 arr[i]

因此,*(p + i) = i 等价于 arr[i] = i,循环执行后,数组 arr 的元素被依次赋值为 0, 1, 2, ..., 9

第二个for循环:通过指针打印数组元素

循环体为 printf("%d ", *(p + i));,核心逻辑与赋值循环一致:

  • 同样通过 p + i 定位到数组的第 i 个元素 arr[i],再通过解引用 *(p + i) 获取该元素的值。
  • 循环依次打印 arr[0] 到 arr[9] 的值,最终输出 0 1 2 3 4 5 6 7 8 9 

核心结论

代码通过 *(p + i) 的形式,将指针的偏移运算与解引用结合,实现了对数组元素的访问 —— 这本质上是数组下标访问(arr[i])的底层实现逻辑:数组下标 i 本质是指针从首元素开始的偏移量arr[i] 等价于 *(arr + i),也等价于 *(p + i)(因为 p = arr)。这种指针操作更直观地体现了数组在内存中连续存储的特性,也展示了指针在数组操作中的灵活性。


指针-指针

代码演示:

int arr[10] = { 0 };

printf("%d\n", &arr[9] - &arr[0]);

指针减法运算的规则与前提

指针之间的减法运算并非简单的 “地址数值相减”,而是有明确的含义和严格限制:

  • 运算结果的含义:两个指针相减,最终得到的是它们所指向位置之间的 “元素个数”(而非地址值相减的绝对值)。具体来说,结果数值 = (后指针地址值 - 前指针地址值) ÷ 指针指向元素的类型大小,本质是统计两个指针中间能容纳多少个目标类型的元素。
  • 严格前提条件:执行指针减法的两个指针,必须指向同一块连续的内存空间(例如同一个数组、同一个结构体的连续成员区域等)。若两个指针指向无关的内存(比如一个指向数组 A,一个指向单独的 int 变量 B),此时减法运算的结果没有任何实际意义,甚至可能引发未定义行为(程序逻辑混乱或崩溃)。

代码结果解析

要理解输出结果,需结合数组的内存特性和指针减法规则:

  1. 数组的内存本质int arr[10] 是一个包含 10 个 int 类型元素的数组,在内存中以连续方式存储—— 从 arr[0] 到 arr[9],每个元素依次排列,且每个 int 元素占 4 字节(默认情况下)。
  2. 指针的指向&arr[0] 是数组首元素(第 0 个元素)的地址,&arr[9] 是数组最后一个元素(第 9 个元素)的地址,这两个指针明确指向同一块空间(arr 数组的内存区域),完全满足指针减法的前提条件。
  3. 运算过程与结果:指针减法会自动根据元素类型(int,4 字节)计算 “元素个数”:
    • 地址差:&arr[9] 的地址值 - &arr[0] 的地址值 = 9 × 4 字节(因为从第 0 个到第 9 个元素,中间间隔 9 个 int 元素,每个占 4 字节);
    • 元素个数:地址差 ÷ 元素类型大小 = (9×4)÷4 = 9。

因此,printf 最终输出的结果是 9,即 &arr[9] 与 &arr[0] 之间有 9 个 int 类型的元素(从 arr[1] 到 arr[8],加上首尾本身,共 10 个元素,间隔 9 个)。


指针解引用

指针解引用时访问的内存空间大小

代码演示:

int a = 0x11223344;
int b = 0x11223344;

int* pa = &a;
char* pc = &b;

*pa = 0;
*pc = 0;

printf("a = %x\n", a);
printf("b = %x\n", b);

1. *pa 访问的空间:4 字节(int 类型大小)

pa 是 int* 类型指针(指向 int 变量 a)。解引用操作 *pa 时,会按照 int 类型的大小(通常 4 字节)访问内存—— 即从 a 的起始地址开始,连续操作 4 个字节的空间。

当执行 *pa = 0 时,会将 a 所占的全部 4 字节都赋值为 0(16 进制下为 00 00 00 00)。因此,a 的值被完整覆盖为 0,以 %x 打印时结果为 0

2. *pc 访问的空间:1 字节(char 类型大小)

pc 是 char* 类型指针(指向 int 变量 b)。解引用操作 *pc 时,只会按照 char 类型的大小(1 字节)访问内存—— 即仅操作 b 起始地址处的 1 个字节,其他 3 个字节不受影响。

初始时 b = 0x11223344(假设内存按 “小端存储”,低地址存储低字节,即 4 个字节从低到高为 0x440x330x220x11)。执行 *pc = 0 时,仅将起始地址的第一个字节(0x44)改为 0,其余 3 个字节(0x330x220x11)保持不变。因此,b 的值变为 0x11223300,以 %x 打印时结果为 11223300

核心结论

指针的类型决定了解引用时访问的内存空间大小int* 解引用访问 4 字节,char* 解引用仅访问 1 字节。这种差异导致同样执行 “赋值 0” 操作,a 被完整清零,b 仅部分字节被修改,最终 16 进制打印结果不同。这也体现了 “指针类型不仅决定步长,还决定内存操作范围” 的核心特性。


二级指针

代码演示:

int a = 10;

int* p = &a;
int** pp = &p;

1. 一级指针变量 p:指向普通变量的地址

p 是一级指针变量,其类型为 int*(读作 “指向 int 的指针”)。它的核心作用是存储普通变量的内存地址—— 这里通过 &a(取 a 的地址)将 a 的内存地址赋值给 p,意味着 p 指向了变量 a。简单来说,一级指针是 “连接代码与普通数据” 的桥梁:通过 p 我们能找到 a 的地址,再通过解引用 *p 就能直接操作 a 的值(比如 *p = 20 会将 a 的值改为 20)。

2. 关键前提:指针本身也是变量,有自己的地址

很多人容易忽略一个核心事实:指针变量本质上也是 “变量”。和 a 一样,p 作为一级指针变量,在内存中同样会占据一块存储空间(例如在 32 位系统中,所有指针变量都占 4 字节,64 位系统占 8 字节),因此 p 自身也拥有一个独立的内存地址。正是因为指针变量有自己的地址,才为 “二级指针” 的存在提供了基础 —— 二级指针的作用,就是存储这个 “指针变量的地址”。

3. 二级指针变量 pp:指向一级指针变量的地址

pp 是二级指针变量,其类型为 int**(读作 “指向 int 指针的指针”)。它的核心作用与一级指针不同:不再存储普通变量的地址,而是专门存储一级指针变量的内存地址—— 这里通过 &p(取 p 的地址)将 p 的内存地址赋值给 pp,意味着 pp 指向了一级指针变量 p。可以理解为,二级指针是 “连接代码与一级指针” 的桥梁:通过 pp 能找到 p 的地址,再通过一次解引用 *pp 能得到 p 的值(即 a 的地址),若再解引用一次 **pp,就能最终操作 a 的值(比如 **pp = 30 会将 a 的值改为 30)。


指针数组

指针数组,核心是数组,但它的每个元素不是普通数据(如intchar),而是同类型的指针变量—— 简单说,这是一个 “专门用来存储指针的数组”。

代码演示:

int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6,7 };
int arr3[] = { 3,4,5,6,7,8,9 };

int sz[] = { sizeof(arr1) / sizeof(arr1[0]),sizeof(arr2) / sizeof(arr2[0]),sizeof(arr3) / sizeof(arr3[0]) };

int* parr[] = { arr1,arr2,arr3 };

int psz = (int)(sizeof(parr) / sizeof(parr[0]));

for (int i = 0; i < psz; i++)
{
	for (int j = 0; j < sz[i]; j++)
	{
		// printf("%d ", *(*(parr + i) + j));

		printf("%d ", parr[i][j]);
	}
	
	printf("\n");
}

一、先明确:指针数组的本质

代码中的int* parr[]就是典型的指针数组:数组parr的每个元素都是int*类型(指向int的指针),专门存储arr1arr2arr3这三个普通int数组的首元素地址(数组名本身就是首元素地址),这是parr与三个子数组产生联动的基础。

二、重点解析嵌套 for 循环:parr 与子数组的联动逻辑

循环的核心是 “通过指针数组parr,统一遍历arr1arr2arr3三个子数组”,外层循环管理 “指向哪个子数组”,内层循环管理 “访问子数组的哪个元素”,二者通过parr的指针特性紧密联动,具体拆解如下:

1. 外层循环(i从 0 到psz-1):通过parr[i]定位子数组

psz是指针数组parr的元素个数(sizeof(parr)/sizeof(parr[0]),结果为 3,对应arr1arr2arr3三个子数组)。

  • i=0时,parr[i]parr[0],存储的是arr1的首元素地址(parr[0] = arr1),此时parr[0]就相当于arr1的 “别名指针”,通过它能找到arr1的内存起始位置;
  • i=1时,parr[1]存储arr2的首元素地址,通过它定位arr2
  • i=2时,parr[2]存储arr3的首元素地址,通过它定位arr3

这一步是联动的关键:parr通过 “元素存储子数组首地址”,将三个独立的子数组 “串联” 起来,外层循环只需改变i,就能切换到不同的子数组,无需单独处理每个子数组的地址。

2. 内层循环(j从 0 到sz[i]-1):通过parr[i][j]访问子数组元素

sz[i]存储的是第i个子数组的元素个数(sz[0]=5对应arr1sz[1]=6对应arr2sz[2]=7对应arr3),内层循环负责遍历当前子数组的所有元素,核心是parr[i][j]这个表达式:

  • parr[i][j]本质是 “指针数组的下标访问 + 子数组的下标访问” 的结合,可拆解为(parr[i])[j]:① 先看parr[i]:它是parr数组的第i个元素,本质是一个int*类型的指针(指向第i个子数组的首地址);② 再看(parr[i])[j]:对 “parr[i]这个指针” 进行下标访问 —— 由于parr[i]指向int数组,(parr[i])[j]等价于*(parr[i] + j),意思是 “从parr[i]指向的首地址开始,向右偏移jint类型的步长(每个步长 4 字节),然后解引用访问这个位置的元素”,也就是第i个子数组的第j个元素。

比如:

  • i=0j=2时,parr[0][2] = (parr[0])[2] = *(arr1 + 2) = arr1[2],值为 3;
  • i=1j=4时,parr[1][4] = arr2[4],值为 6;
  • i=2j=5时,parr[2][5] = arr3[5],值为 8。

而注释中的*(*(parr + i) + j),是parr[i][j]的底层指针写法,二者完全等价(数组下标arr[k]本质是*(arr + k)),只是前者更直观,后者更体现 “指针操作” 的本质。

3. 联动性的最终表现:统一遍历多个子数组

通过parr的 “指针数组特性”,原本三个独立、长度不同的子数组(arr15 个元素、arr26 个、arr37 个),被纳入同一个嵌套循环中处理:外层循环通过parr[i]切换子数组,内层循环通过parr[i][j]访问子数组元素,最终依次打印出arr1arr2arr3的所有元素,实现了 “用一套逻辑管理多组数据” 的效果。


字符指针

常量字符串

代码演示:

char arr[] = "abcdef";

const char* ptr = "abcdef";

1. char arr[] = "abcdef":数组存储完整的字符串(含结束符\0

arr 是一个char 类型的数组,当用字符串常量 “abcdef” 初始化时,编译器会自动将字符串的所有字符(包括 C 语言字符串必需的结束标志'\0')依次存入数组的连续内存空间中。也就是说,arr 数组的实际存储内容是 [a, b, c, d, e, f, \0],共 7 个元素(“abcdef” 6 个字符 + 1 个'\0')。这里的关键是:数组arr存储的是 “字符串的实体”—— 每个字符都实实在在存放在arr占用的内存中,且由于数组未被const修饰,其存储的字符是可修改的(例如arr[0] = 'A'可将第一个字符改为 'A')。

2. const char* ptr = "abcdef":指针指向常量字符串的首字符地址

ptr 是一个指向 const char 类型的指针,它的核心作用是 “存储一个地址”,而非 “存储字符串本身”。这里的 “abcdef” 是字符串常量—— 这类字符串在程序运行时会被存储在内存的 “只读数据区”(如.rodata段),其属性是 “不可修改”;而字符串常量的 “值”,本质上就是它的首字符('a')的内存地址。因此,ptr = "abcdef" 的实际操作是 “将常量字符串首字符 'a' 的地址赋值给指针ptr”,ptr 最终指向的是只读数据区中 “abcdef” 的起始位置。

而代码中用const修饰*ptr(即const char*),正是为了匹配常量字符串的 “不可修改” 属性:const限制的是 “ptr 指向的内容(*ptr)不能被修改”(例如*ptr = 'A'会编译报错),但ptr本身的指向可以改变(例如ptr = "xyz"可让ptr转而指向另一个常量字符串 “xyz”)。这一步const的修饰至关重要,若省略const,可能因误修改只读数据区的内容导致程序崩溃(未定义行为)。


数组指针

数组指针,顾名思义是指向数组的指针,存放的是数组的地址的指针变量

代码演示:

int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

int(*p)[10] = &arr;

1. 关键前提:arr 与 &arr 的本质区别

数组名 arr 有一个特殊的地址属性:在大多数场景下(如赋值给普通指针、参与运算),arr 表示数组首元素的地址(即 &arr[0],类型为 int*);但当用 & 取数组名的地址时,&arr 表示的是整个数组的地址(而非首元素地址),其类型是 “指向包含 10 个 int 元素的数组的指针”,即 int(*)[10]

简单来说:

  • arr → 指向单个 int 元素(首元素),类型 int*
  • &arr → 指向 “10 个 int 的数组” 这个整体,类型 int(*)[10]

2. int(*p)[10] = &arr:用数组指针存储 “整个数组的地址”

由于 &arr 的类型是 int(*)[10](指向 10 个 int 的数组的指针),普通指针(如 int* p)无法匹配这个类型(类型不兼容会导致编译错误或后续操作异常),因此必须用数组指针来接收 &arr

  • int(*p)[10] 是数组指针的定义语法:(*p) 表明 p 是指针,[10] 表明该指针指向的是 “包含 10 个元素的数组”,int 表明数组元素的类型是 int;
  • p = &arr 即把 “整个数组 arr 的地址” 赋值给数组指针 p,此时 p 指向的是数组 arr 所占内存的 “整体起始位置”,而非单个元素。

数组指针的使用

代码演示:

void Print(int(*parr)[4], int row, int col)
{
	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
		{
			printf("%d ", *(*(parr + i) + j));
		}

		printf("\n");
	}
}

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

	int row = sizeof(arr) / sizeof(arr[0]);
	int col = sizeof(arr[0]) / sizeof(arr[0][0]);

	Print(arr, row, col);

	return 0;
}

一、核心逻辑:二维数组名作为实参传递时的 “退化” 与数组指针的匹配

代码中 Print(arr, row, col) 的核心是:二维数组名 arr 作为实参传递时,会自动 “退化” 为指向其首行的数组指针,而形参 int(*parr)[4] 正是与之匹配的 “指向包含 4 个 int 元素的数组的指针” 类型,二者类型完全兼容,确保了参数传递的合法性。

二、分步解析:实参 arr 的传递与形参 parr 的接收

1. 实参 arr 的本质:从 “二维数组名” 退化为 “行指针”

main 中定义的 int arr[3][4] 是一个 3 行 4 列的二维数组,其内存布局是 “连续的一维空间”,但逻辑上可看作 “由 3 个一维数组(每行 arr[0]arr[1]arr[2])组成”,每个行数组的类型是 int[4](包含 4 个 int 的数组)。

当二维数组名 arr 作为函数参数传递时(如 Print(arr, ...)),它不会代表整个二维数组的地址,而是会 “退化” 为指向第一行数组(arr[0])的指针—— 这个指针的类型正是 int(*)[4](读作 “指向包含 4 个 int 的数组的指针”),与形参 parr 的类型 int(*parr)[4] 完全一致,因此参数传递合法且无类型冲突。

2. 形参 parr 的作用:通过数组指针定位 “行” 与 “元素”

形参 parr 接收的是 “指向二维数组首行的指针”,在 Print 函数的嵌套循环中,它通过 “指针偏移 + 解引用” 的组合,实现对二维数组任意行、任意列元素的访问,核心是 *(*(parr + i) + j) 这个表达式的拆解:

  • 第一步:parr + i → 定位到第 i 行由于 parr 是 int(*)[4] 类型的数组指针,其 “偏移步长” 是 “一行的大小”(即 4 个 int 的字节数:4×4=16 字节)。因此 parr + i 会从 “首行地址” 偏移 i 个 “行大小”,最终指向二维数组的第 i 行(等价于指向 arr[i] 这个行数组)。

  • 第二步:*(parr + i) → 得到第 i 行的 “行数组名”对 parr + i(指向第 i 行的数组指针)解引用,得到的是第 i 行对应的一维数组本身(即 arr[i])。而一维数组名 arr[i] 又会进一步 “退化” 为指向该行首元素(arr[i][0])的普通指针(类型为 int*)。

  • 第三步:*(parr + i) + j → 定位到第 i 行第 j 列元素由于 *(parr + i) 是 int* 类型的指针(指向行首元素),其偏移步长是 “1 个 int 的大小”(4 字节)。因此 *(parr + i) + j 会从 “第 i 行首元素地址” 偏移 j 个 “int 大小”,最终指向 第 i 行第 j 列的元素(等价于 &arr[i][j])。

  • 第四步:*(*(parr + i) + j) → 取出目标元素的值对 “指向第 i 行第 j 列元素的指针” 解引用,即可得到该元素的具体值,完全等价于 arr[i][j]

例如:当 i=1j=2 时,*(*(parr + 1) + 2) 等价于 arr[1][2],值为 4。


函数指针

函数指针,顾名思义就是存储函数的指针

代码演示:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*p)(int, int) = &Add;

	return 0;
}

1. 先明确目标函数 Add 的 “类型”

Add 是一个具体的函数,其 “函数类型” 由返回值类型形参列表类型共同决定:

  • 返回值类型:int(函数最终返回两数之和,为 int 类型);
  • 形参列表类型:(int, int)(接收两个 int 类型的参数 x 和 y);因此,Add 的完整函数类型是 int(int, int)(读作 “接收两个 int 参数、返回 int 的函数”)。

2. 函数指针 p 的定义:int(*p)(int, int)

int(*p)(int, int) 是函数指针的标准定义语法,每个部分都对应 “匹配函数类型” 的需求,拆解如下:

  • (*p):括号的优先级高于 *,强制表明 p 是一个指针变量(这一点与数组指针 int(*p)[10] 逻辑一致 —— 数组指针用 (*p)[10] 表明 p 是指向数组的指针,函数指针用 (*p) 表明 p 是指向函数的指针);
  • (int, int):紧跟在 (*p) 后,指定 p 所指向函数的形参列表类型—— 必须与目标函数 Add 的形参(两个 int)完全一致;
  • 最前面的 int:指定 p 所指向函数的返回值类型—— 必须与目标函数 Add 的返回值(int)完全一致;

综上,函数指针 p 的完整类型是 int(*)(int, int)(读作 “指向‘接收两个 int 参数、返回 int 的函数’的指针”),恰好与 Add 的函数类型 int(int, int) 匹配,因此才能通过 &Add 为 p 赋值。

3. &Add 的含义:取函数的入口地址

函数在内存中会占据一块连续空间,其入口地址(即函数第一条指令的地址)是函数的 “标识”—— 函数名 Add 本身就代表这个入口地址,因此 &Add(显式取函数地址)与 Add(隐式代表函数地址)在数值上完全等价。这意味着代码中 int(*p)(int, int) = Add; 也是合法的,但 &Add 更直观地体现了 “将函数的地址赋值给函数指针” 的操作逻辑。

函数指针的使用

代码演示:

int Add(int x, int y)
{
	return x + y;
}

int main()
{
	int(*p)(int, int) = &Add;

	int ret = (*p)(3, 5);

	printf("ret = %d\n", ret);

	return 0;
}

关键代码 int ret = (*p)(3, 5); 实现了通过函数指针调用函数的核心操作:

  • (*p) 是对函数指针的 “解引用”,但在函数指针语境下,这一操作的本质是 “通过指针找到它所指向的函数”(即 Add 函数);
  • 紧跟的 (3, 5) 是传递给函数的实参(对应 Add 函数的形参 x=3y=5),与直接调用 Add(3, 5) 时传递参数的逻辑完全一致;
  • 因此,(*p)(3, 5) 等价于直接调用 Add(3, 5),最终会执行 Add 函数的逻辑(计算 3 + 5),并将结果 8 赋值给 ret

函数指针数组

函数指针数组,顾名思义是存放函数指针的数组

代码演示:

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{	
	return x / y;
}

int main()
{
	int(*ptarr[])(int, int) = { Add,Sub,Mul,Div };

	return 0;
}

定义的 ptarr 是一个函数指针数组—— 它的本质是数组,但数组中的每个元素都不是普通数据,而是函数指针(即指向函数的指针变量)。

具体来说:

  • ptarr 后面的 [] 明确表明它是一个数组;
  • 数组的元素类型是 int(*)(int, int)—— 这是一种函数指针类型,专门指向 “返回值为 int、接收两个 int 类型参数” 的函数;
  • 初始化列表 { Add, Sub, Mul, Div } 中的每个元素(AddSub 等)都是函数名,而函数名本身代表该函数的入口地址(即函数指针)。由于 AddSubMulDiv 这四个函数的返回值类型(均为 int)和形参类型(均为 int, int)完全一致,它们的函数指针类型都是 int(*)(int, int),因此能被正确存入 ptarr 数组中。

简言之,ptarr 作为函数指针数组,像 “容器” 一样集中存储了四个同类型函数的指针,后续可通过数组下标访问这些函数指针,进而间接调用对应的函数(例如 ptarr[0](3,5) 等价于调用 Add(3,5))。这种设计的核心价值是将同类型函数 “批量管理”,便于通过下标动态选择执行不同函数,提升代码的灵活性。

函数指针数组的使用

代码演示:

int Add(int x, int y)
{
	return x + y;
}
int Sub(int x, int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{	
	return x / y;
}

void menu()
{
	printf("*****************************\n");
	printf("***  1.add         2.sub  ***\n");
	printf("***  3.mul         4.div  ***\n");
	printf("***  0.exit               ***\n");
	printf("*****************************\n");
}

int main()
{
	int(*ptarr[])(int, int) = { NULL,Add,Sub,Mul,Div };
	int sz = sizeof(ptarr) / sizeof(ptarr[0]);
	int input = 0;
	int x = 0;
	int y = 0;

	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);

		if (input >= 1 && input < sz)
		{
			printf("请输入两个整型操作数:");
			scanf("%d %d", &x, &y);

			printf("%d\n", ptarr[input](x, y));
		}
		else if(input == 0)
		{
			printf("退出计算机\n");
		}
		else
		{
			printf("输入错误,请重新输入\n");
		}

	} while (input);

	return 0;
}

1. 函数指针数组 ptarr 的定义与初始化:关联运算函数

int(*ptarr[])(int, int) = { NULL,Add,Sub,Mul,Div }; 定义了一个函数指针数组,其核心设计是让数组下标与用户操作选项直接对应

  • 数组元素类型是 int(*)(int, int)(指向 “接收两个 int 参数、返回 int 的函数” 的指针),与 Add(加)、Sub(减)、Mul(乘)、Div(除)四个函数的类型完全匹配,因此这四个函数的指针(函数名本身即指针)可直接存入数组;
  • 初始化时第一个元素设为 NULL,目的是让数组下标 1 对应 Add、2 对应 Sub、3 对应 Mul、4 对应 Div(与菜单中 “1.add、2.sub...” 的选项编号完全一致),方便后续通过用户输入的选项直接定位到对应的函数。

2. 用户选择与函数调用的联动:通过数组下标直接调用对应函数

在 do-while 循环中,用户通过菜单选择操作(1-4 对应加减乘除,0 退出),核心联动逻辑体现在:

  • 当用户输入 input(如 1)时,代码先判断 input 是否在有效范围(1 到数组长度 sz 之间);
  • 若有效,获取两个操作数 x 和 y 后,直接通过 ptarr[input](x, y) 调用对应函数:
    • 例如 input=1 时,ptarr[1] 是 Add 函数的指针,ptarr[1](x,y) 等价于 Add(x,y),执行加法;
    • input=2 时,ptarr[2](x,y) 等价于 Sub(x,y),执行减法,以此类推。

回调函数

回调函数是一个通过函数指针调用的函数,把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其他所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或者条件发生时由另外的一方调用的,用于对该事件或条件进行响应

代码演示:

int cmp_int(const void* p1, const void* p2)
{
	return *((int*)p1) - *((int*)p2);
}

void Swap(char* buf1, char* buf2, size_t sz)
{
	for (int i = 0; i < sz; i++)
	{
		char tmp = *(buf1 + i);
		*(buf1 + i) = *(buf2 + i);
		*(buf2 + i) = tmp;
	}
}

void bubble_sort(void* base, size_t num, size_t size, int(*cmp)(const void*, const void*))
{
	for (int i = 0; i < num - 1; i++)
	{
		int flag = 1;

		for (int j = 0; j < num - i - 1; j++)
		{
			if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)
			{
				Swap((char*)base + j * size, (char*)base + (j + 1) * size, size);

				flag = 0;
			}
		}

		if (flag)
			return;
	}
}

void Print(int arr[], int sz)
{
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}

	printf("\n");
}

int main()
{
	int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);

	Print(arr, sz);

	return 0;
}

以上代码实现了模拟标准库中的qsort函数,通过冒泡排序的思想实现 “通用排序功能”—— 即能对任意类型的数组(如intfloat、结构体等)进行排序,而这一通用性的实现,依赖于回调函数(cmp_int 和基于字节的地址计算与交换,具体解析如下:

一、整体逻辑:通用冒泡排序的设计思路

标准qsort的核心是 “不依赖具体数据类型,通过用户提供的比较函数决定排序逻辑”。这段代码中的bubble_sort函数正是模仿这一思路:

  • void* base接收任意类型数组的首地址(void*无类型限制,可指向任何类型数据);
  • size_t num表示数组元素个数,size_t size表示每个元素的字节大小(解决不同类型元素大小不同的问题);
  • 用函数指针int(*cmp)(const void*, const void*)接收 “比较函数”(由用户提供,负责比较两个元素的大小,即 “回调函数”)。

二、关键组件 1:比较函数cmp_int—— 实现回调逻辑

cmp_int是针对int类型的比较函数,也是 “回调” 的核心载体:

  • 参数为const void* p1const void* p2void*确保能接收任意类型元素的地址(这里实际是int元素的地址),const保证不修改元素值;
  • 功能:将void*强制转换为int*(因为知道实际排序的是int数组),解引用后相减 —— 返回值 > 0 表示p1指向的元素大于p2;=0 表示相等;<0 表示p1小于p2
  • 作用:bubble_sort函数通过调用cmp(即cmp_int),无需知道具体类型,就能判断两个元素的大小关系,实现 “通用比较”。

三、关键代码 1:if (cmp((char*)base + j * size, (char*)base + (j + 1) * size) > 0)—— 元素地址计算与比较

这行代码是冒泡排序中 “判断相邻元素是否需要交换” 的核心,解决了 “任意类型元素的地址定位” 问题:

  1. 地址计算逻辑

    • basevoid*,不能直接参与地址运算(C 语言不允许void*加减整数),因此强制转换为char*——char*的步长是 1 字节,可精确计算任意类型元素的地址。
    • j个元素的地址:(char*)base + j * size(每个元素占size字节,第j个元素相对于首地址偏移j*size字节);
    • j+1个元素的地址:(char*)base + (j+1)*size
  2. 调用比较函数:将计算出的两个相邻元素地址传给cmp(即cmp_int),通过返回值判断是否需要交换 —— 若返回值 > 0,说明前一个元素大于后一个,需要交换(符合冒泡排序 “大元素后移” 的逻辑)。

四、关键函数:Swap((char*)base + j * size, (char*)base + (j + 1) * size, size)—— 通用元素交换

Swap函数负责交换两个元素,通过 “按字节交换” 实现对任意类型元素的通用交换:

  • 参数char* buf1char* buf2:指向两个待交换元素的首地址(已通过上述地址计算得到);
  • 参数size:每个元素的字节大小(确保交换完整的元素);
  • 逻辑:循环size次,每次交换两个元素的一个字节(从低地址到高地址)。例如交换两个int(4 字节),会依次交换第 0、1、2、3 字节,最终实现完整交换。

这种 “按字节交换” 的方式,不依赖具体类型,无论是int(4 字节)、double(8 字节)还是自定义结构体(n 字节),只要传入正确的size,都能正确交换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值