目录
指针+-整数
指针的类型决定了地址偏移的 “步长”
代码演示:
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 = 0x11223344:0x 是 16 进制数的标识,而 16 进制中每两个数字对应 1 个字节(如 0x11 是 1 字节,0x22 是另一字节),0x11223344 共 4 个 “两数字组”,恰好填满 int 类型通常占用的4 字节空间(这也是选择该值的原因)。
随后定义两个不同类型的指针:int* pa = &a(pa 是指向 int 类型的指针,存储 a 的起始地址)和 char* pc = &a(pc 是指向 char 类型的指针,同样存储 a 的起始地址)。由于二者都指向变量 a 的起始内存地址,因此首次打印 pa 和 pc 时,输出的地址值完全相同 —— 指针的 “指向位置” 一致,但 “类型属性” 不同,这为后续加 1 操作的差异埋下伏笔。
2. 指针加 1 的关键差异:步长由类型决定
当对指针执行 “+1” 操作时,并非简单地让地址值加 1,而是按指针指向的数据类型的大小,调整地址偏移量—— 这个偏移量就是 “步长”,而步长的大小由指针类型直接决定:
- 对于
int* pa:int类型占 4 字节,因此pa + 1会让地址值增加 4(即跳过 1 个完整的int类型所占的内存空间); - 对于
char* pc:char类型占 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),此时减法运算的结果没有任何实际意义,甚至可能引发未定义行为(程序逻辑混乱或崩溃)。
代码结果解析
要理解输出结果,需结合数组的内存特性和指针减法规则:
- 数组的内存本质:
int arr[10]是一个包含 10 个 int 类型元素的数组,在内存中以连续方式存储—— 从arr[0]到arr[9],每个元素依次排列,且每个 int 元素占 4 字节(默认情况下)。 - 指针的指向:
&arr[0]是数组首元素(第 0 个元素)的地址,&arr[9]是数组最后一个元素(第 9 个元素)的地址,这两个指针明确指向同一块空间(arr 数组的内存区域),完全满足指针减法的前提条件。 - 运算过程与结果:指针减法会自动根据元素类型(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 个字节从低到高为 0x44、0x33、0x22、0x11)。执行 *pc = 0 时,仅将起始地址的第一个字节(0x44)改为 0,其余 3 个字节(0x33、0x22、0x11)保持不变。因此,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)。
指针数组
指针数组,核心是数组,但它的每个元素不是普通数据(如int、char),而是同类型的指针变量—— 简单说,这是一个 “专门用来存储指针的数组”。
代码演示:
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的指针),专门存储arr1、arr2、arr3这三个普通int数组的首元素地址(数组名本身就是首元素地址),这是parr与三个子数组产生联动的基础。
二、重点解析嵌套 for 循环:parr 与子数组的联动逻辑
循环的核心是 “通过指针数组parr,统一遍历arr1、arr2、arr3三个子数组”,外层循环管理 “指向哪个子数组”,内层循环管理 “访问子数组的哪个元素”,二者通过parr的指针特性紧密联动,具体拆解如下:
1. 外层循环(i从 0 到psz-1):通过parr[i]定位子数组
psz是指针数组parr的元素个数(sizeof(parr)/sizeof(parr[0]),结果为 3,对应arr1、arr2、arr3三个子数组)。
- 当
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对应arr1,sz[1]=6对应arr2,sz[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]指向的首地址开始,向右偏移j个int类型的步长(每个步长 4 字节),然后解引用访问这个位置的元素”,也就是第i个子数组的第j个元素。
比如:
- 当
i=0、j=2时,parr[0][2]=(parr[0])[2]=*(arr1 + 2)=arr1[2],值为 3; - 当
i=1、j=4时,parr[1][4]=arr2[4],值为 6; - 当
i=2、j=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]访问子数组元素,最终依次打印出arr1、arr2、arr3的所有元素,实现了 “用一套逻辑管理多组数据” 的效果。
字符指针
常量字符串
代码演示:
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=1、j=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=3、y=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 }中的每个元素(Add、Sub等)都是函数名,而函数名本身代表该函数的入口地址(即函数指针)。由于Add、Sub、Mul、Div这四个函数的返回值类型(均为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函数,通过冒泡排序的思想实现 “通用排序功能”—— 即能对任意类型的数组(如int、float、结构体等)进行排序,而这一通用性的实现,依赖于回调函数(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* p1和const void* p2:void*确保能接收任意类型元素的地址(这里实际是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)—— 元素地址计算与比较
这行代码是冒泡排序中 “判断相邻元素是否需要交换” 的核心,解决了 “任意类型元素的地址定位” 问题:
-
地址计算逻辑:
base是void*,不能直接参与地址运算(C 语言不允许void*加减整数),因此强制转换为char*——char*的步长是 1 字节,可精确计算任意类型元素的地址。- 第
j个元素的地址:(char*)base + j * size(每个元素占size字节,第j个元素相对于首地址偏移j*size字节); - 第
j+1个元素的地址:(char*)base + (j+1)*size。
-
调用比较函数:将计算出的两个相邻元素地址传给
cmp(即cmp_int),通过返回值判断是否需要交换 —— 若返回值 > 0,说明前一个元素大于后一个,需要交换(符合冒泡排序 “大元素后移” 的逻辑)。
四、关键函数:Swap((char*)base + j * size, (char*)base + (j + 1) * size, size)—— 通用元素交换
Swap函数负责交换两个元素,通过 “按字节交换” 实现对任意类型元素的通用交换:
- 参数
char* buf1和char* buf2:指向两个待交换元素的首地址(已通过上述地址计算得到); - 参数
size:每个元素的字节大小(确保交换完整的元素); - 逻辑:循环
size次,每次交换两个元素的一个字节(从低地址到高地址)。例如交换两个int(4 字节),会依次交换第 0、1、2、3 字节,最终实现完整交换。
这种 “按字节交换” 的方式,不依赖具体类型,无论是int(4 字节)、double(8 字节)还是自定义结构体(n 字节),只要传入正确的size,都能正确交换。

6797

被折叠的 条评论
为什么被折叠?



