Appearance
指针下:函数指针、回调与 qsort
预计学习时长:约 3.5 小时(仅 L1 主线,建议分 2~3 次完成)· 运行环境:VS2026 / gcc / clang 均可
🎯 学习目标
学完本讲,你将能够:
- 区分数组指针和指针数组,正确声明
int (*p)[N],并用它接收二维数组 - 声明函数指针、通过它调用函数,用
typedef给复杂的指针类型起别名 - 看懂函数指针数组和转移表计算器的写法
- 说清什么是回调函数;能为
qsort编写比较函数,排序整型数组和结构体数组 - 说出
sizeof与strlen的本质区别,准确计算常见表达式的值
🔗 先修知识
- 第 12 讲:指针上:地址、解引用、
void*、指针数组是本讲的直接基础 - 第 8 讲:函数:函数名、形参实参
- 第 10 讲:字符串和字符串函数:
strcmp在结构体排序中出场
先修自检(三问)
int *arr[3];里的arr是数组还是指针?每个元素是什么类型?void*为什么不能直接解引用?要使用它得先做什么?strlen从哪里开始数、遇到什么停止?字符串没有\0会怎样?
答不上来先复习第 12 讲,再回来继续。
一、字符指针 char*
1.1 两种用法
第一种:指向单个字符,和普通指针没有区别。
📄 01_char_pointer.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
char ch = 'w';
char *pc = &ch;
*pc = 'x';
printf("%c\n", ch);
return 0;
}第二种更常见:让字符指针指向一个字符串字面量。注意容易误会的点——指针里不是整个字符串,而是字符串首字符的地址:
📄 02_string_literal.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
const char *pstr = "hello world"; // 存放首字符 'h' 的地址
printf("%s\n", pstr); // %s 从该地址打印到 \0
printf("%c\n", *pstr); // 只取首字符
return 0;
}运行结果:
text
hello world
h为什么写 const char *?因为字符串字面量通常放在内存的只读区域,试图通过指针修改它(pstr[0] = 'H';)会让程序崩溃,属于 🔴 未定义行为。加 const 让编译器在编译期就挡住这种写法。
1.2 经典笔试题:两个数组 vs 两个指针
📄 03_literal_or_array.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if (&str1[0] == &str2[0])
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}运行结果:
text
str1 and str2 are not same
str3 and str4 are samestr1、str2是两个独立数组:各自在栈上开辟空间,把同样的字符拷贝两份,地址当然不同str3、str4指向同一个字面量:相同的字面量在只读区只存一份,编译器让两个指针共用它(字符串池),地址相同
二、数组指针
2.1 是指针,不是数组
数组指针首先是指针,它指向一个数组。对照已学:整型指针指向整型,数组指针指向数组。
🧩 示意片段 · 仅说明语法,不能单独编译
c
int *p1[10]; // 指针数组:p1 先和 [] 结合 → 数组,元素是 int*
int (*p2)[10]; // 数组指针:() 让 p2 先和 * 结合 → 指针,指向 int[10]关键在于 [] 的优先级高于 *:想让变量先成为指针,就必须加括号 (*p)。
2.2 初始化与使用
数组指针存放整个数组的地址,即上一讲学过的 &数组名:
📄 04_array_pointer.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
int arr[5] = {10, 20, 30, 40, 50};
int (*p)[5] = &arr;
printf("arr 共 %zu 字节\n", sizeof(arr));
printf("第 3 个元素:%d\n", (*p)[2]); // 先 *p 拿到数组,再下标
printf("数组指针 +1 跨过 %td 字节\n",
(char *)(p + 1) - (char *)*p);
return 0;
}运行结果:
text
arr 共 20 字节
第 3 个元素:30
数组指针 +1 跨过 20 字节注意访问元素要写成 (*p)[2]:括号保证先解引用拿到数组,不能写成 *p[2],那会被理解成"先取第 2 个数组再解引用"。
2.3 二维数组传参的本质
二维数组可以看作"元素是一维数组的数组",所以它的首元素是第一行。二维数组名是"第一行的地址"——一个数组指针,类型为 int (*)[5](5 是列数)。
因此二维数组传参,形参可以写成二维数组形式,也可以直接写成数组指针:
📄 05_2d_param.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void print_matrix(int (*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *(*(p + i) + j)); // p+i 找第 i 行,再 +j 找第 j 列
}
printf("\n");
}
}
int main(void)
{
int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
print_matrix(arr, 3, 5);
return 0;
}运行结果:
text
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7规则提醒:形参写数组形式 int a[3][5] 时,第二维(列数)不能省,第一维可以省(int a[][5])——因为编译器要靠列数才能算出"跳过一行走多远"。
三、函数指针
3.1 函数也有地址
程序运行时,函数的指令同样存放在内存中,函数名就是函数的地址,&函数名 也一样:
📄 06_function_address.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
void test(void)
{
printf("hello\n");
}
int main(void)
{
printf("test = %p\n", (void *)test);
printf("&test = %p\n", (void *)&test);
return 0;
}3.2 函数指针变量与调用
📄 07_function_pointer.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main(void)
{
int (*pf)(int, int) = Add; // pf 存放 Add 的地址
printf("%d\n", (*pf)(2, 3)); // 显式解引用调用
printf("%d\n", pf(3, 5)); // 直接当函数名用,C 允许省略 *
return 0;
}运行结果:
text
5
8类型读法:int (*pf)(int, int) —— * 表示 pf 是指针,前面的 int 是所指函数的返回类型,后面的 (int, int) 是参数列表。函数指针类型必须与所指函数的返回类型、参数列表完全对应。
📘 提高(L2):两段"吓人"的声明(出自《C 陷阱和缺陷》)
第一段:
c
(*(void (*)())0)();读法:(void (*)()) 是一个函数指针类型(无参数、无返回值),把它套在 0 外面表示把地址 0 强转成函数指针;前面加 *、后面 () 表示"调用 0 地址处这个函数"。这只是语法演示,真的执行会崩溃。
第二段:
c
void (*signal(int, void(*)(int)))(int);这是一个函数声明:函数名叫 signal,参数两个(一个 int、一个函数指针 void(*)(int));signal 的返回值也是同类型的函数指针 void(*)(int)。最外层的 ... 被函数参数列表占了位置,所以读起来绕。
3.3 typedef:给复杂类型起别名
typedef 可以给复杂类型起一个短名字。函数指针重命名时,新名字要写在 * 的右边:
📄 08_typedef_demo.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
typedef int (*pfun_t)(int, int); // 给函数指针类型起名 pfun_t
int Add(int x, int y)
{
return x + y;
}
int main(void)
{
pfun_t pf = Add; // 比写 int (*pf)(int,int) 清爽
printf("%d\n", pf(4, 6));
return 0;
}上面那个吓人的 signal 声明,用 typedef 立刻就好读了:
c
typedef void (*pfun_t)(int);
pfun_t signal(int, pfun_t);3.4 函数指针数组与转移表
把同类型函数的地址放进一个数组,就是函数指针数组:int (*parr[5])(int, int);——parr 先和 [] 结合成为数组,每个元素是函数指针。
它的经典用途是转移表:计算器的菜单选项 1/2/3/4 正好作为数组下标,直接索引到对应函数,省掉 switch:
📄 09_transfer_table.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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 divi(int a, int b) { return a / b; }
int main(void)
{
int x = 0, y = 0, input = 1, ret = 0;
int (*p[5])(int, int) = {NULL, add, sub, mul, divi}; // 下标 1~4 对应四则
do
{
printf("1:add 2:sub 3:mul 4:div 0:exit,请选择:");
scanf("%d", &input);
if (input >= 1 && input <= 4)
{
printf("输入两个数:");
scanf("%d %d", &x, &y);
ret = p[input](x, y); // 直接用菜单值索引函数
printf("结果:%d\n", ret);
}
else if (input == 0)
{
printf("退出\n");
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}四、回调函数
4.1 什么是回调函数
回调函数就是"通过函数指针被调用"的函数:把 A 函数的地址作为参数传给 B 函数,B 在合适的时机通过这个指针调用 A,A 就叫回调函数。关键区别在于——A 不是被自己直接调用的,而是被 B"回头调用"的。
计算器 switch 版本里,每个 case 的输入、输出代码重复四遍,只有"调用哪个函数"不同。用回调把差异点变成参数:
📄 10_callback_calc.c · ✅ 完整程序(可直接复制编译)
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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 divi(int a, int b) { return a / b; }
void calc(int (*pf)(int, int))
{
int x = 0, y = 0;
printf("输入两个数:");
scanf("%d %d", &x, &y);
printf("结果:%d\n", pf(x, y)); // 回头调用传进来的函数
}
int main(void)
{
int input = 1;
do
{
printf("1:add 2:sub 3:mul 4:div 0:exit,请选择:");
scanf("%d", &input);
switch (input)
{
case 1: calc(add); break;
case 2: calc(sub); break;
case 3: calc(mul); break;
case 4: calc(divi); break;
case 0: printf("退出\n"); break;
default: printf("选择错误\n");
}
} while (input);
return 0;
}4.2 回调的意义
新增一种运算时,switch 版本要复制一整段输入输出;回调版本只需再写一个函数、加一个 case。把"不变的流程"留在主函数,把"变化的动作"作为函数指针传入——这是回调函数的通用思想,标准库的 qsort 就是这么设计的。
五、qsort:标准库的通用排序
5.1 函数原型
stdlib.h 里的 qsort 可以排序任意类型的数组,通用性靠两件事:void* 接收不带类型的地址 + 回调函数决定"两个元素怎么比较":
🧩 函数原型(示意片段)
c
void qsort(void *base, // 数组首地址
size_t num, // 元素个数
size_t size, // 每个元素的字节数
int (*compar)(const void *, const void *)); // 比较回调比较函数的契约(务必记住):
| 返回值 | 含义 |
|---|---|
| 负数(<0) | 第一个元素应排在前面(更小) |
| 0 | 两者相等 |
| 正数(>0) | 第一个元素应排在后面(更大) |
因为回调收到的是 void*,函数内部必须先转回具体元素的指针再比较。
5.2 排序整型数组
📄 11_qsort_int.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <stdlib.h>
int int_cmp(const void *p1, const void *p2)
{
return *(const int *)p1 - *(const int *)p2;
}
int main(void)
{
int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
int sz = (int)(sizeof(arr) / sizeof(arr[0]));
qsort(arr, (size_t)sz, sizeof(int), int_cmp);
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}运行结果:
text
0 1 2 3 4 5 6 7 8 95.3 排序结构体
📄 12_qsort_struct.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Stu
{
char name[20];
int age;
};
int cmp_by_age(const void *e1, const void *e2)
{
const struct Stu *s1 = (const struct Stu *)e1;
const struct Stu *s2 = (const struct Stu *)e2;
return s1->age - s2->age;
}
int cmp_by_name(const void *e1, const void *e2)
{
const struct Stu *s1 = (const struct Stu *)e1;
const struct Stu *s2 = (const struct Stu *)e2;
return strcmp(s1->name, s2->name);
}
int main(void)
{
struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
int sz = (int)(sizeof(s) / sizeof(s[0]));
qsort(s, (size_t)sz, sizeof(s[0]), cmp_by_age);
printf("按年龄:\n");
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
qsort(s, (size_t)sz, sizeof(s[0]), cmp_by_name);
printf("按名字:\n");
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
return 0;
}运行结果:
text
按年龄:
wangwu 15
zhangsan 20
lisi 30
按名字:
lisi 30
wangwu 15
zhangsan 205.4 模拟实现 qsort(冒泡版)
核心思路:外层逻辑就是冒泡排序;难点是"不知道元素类型"怎么比较和交换——比较交给回调,交换就逐字节搬运(上一讲内存函数的思路)。
📄 13_my_qsort.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <stdlib.h>
void byte_swap(char *p1, char *p2, size_t size)
{
for (size_t i = 0; i < size; i++)
{
char tmp = p1[i];
p1[i] = p2[i];
p2[i] = tmp;
}
}
void generic_bubble(void *base, size_t count, size_t size,
int (*cmp)(const void *, const void *))
{
for (size_t i = 0; i + 1 < count; i++)
{
for (size_t j = 0; j + 1 < count - i; j++)
{
char *cur = (char *)base + j * size;
char *nxt = cur + size;
if (cmp(cur, nxt) > 0)
{
byte_swap(cur, nxt, size);
}
}
}
}
int int_cmp(const void *p1, const void *p2)
{
return *(const int *)p1 - *(const int *)p2;
}
int main(void)
{
int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
size_t count = sizeof(arr) / sizeof(arr[0]);
generic_bubble(arr, count, sizeof(int), int_cmp);
for (size_t i = 0; i < count; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}运行结果:
text
0 1 2 3 4 5 6 7 8 9六、sizeof 和 strlen 的对比
| 对比项 | sizeof | strlen |
|---|---|---|
| 身份 | 操作符,编译期就能算 | 库函数,要 #include <string.h>,运行期才知道 |
| 算什么 | 操作数占用的字节数 | 从给定地址到第一个 \0 之间的字符个数 |
| 关心数据内容吗 | 不关心,只看类型大小 | 只关心 \0 在哪里 |
没有 \0 时 | 完全不受影响 | 会一直越界往后找,结果不确定(🔴 越界读) |
| 返回类型 | size_t | size_t |
📄 14_sizeof_strlen.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
#include <string.h>
int main(void)
{
char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f'}; // 无 \0
char arr2[] = "abcdef"; // 有 \0
const char *p = "abcdef";
printf("sizeof(arr1) = %zu, sizeof(arr2) = %zu\n",
sizeof(arr1), sizeof(arr2));
printf("sizeof(p) = %zu, strlen(p) = %zu\n",
sizeof(p), strlen(p));
printf("strlen(arr2) = %zu\n", strlen(arr2));
// 故意不调用 strlen(arr1):没有 \0,结果是越界读
return 0;
}64 位编译时的运行结果:
text
sizeof(arr1) = 6, sizeof(arr2) = 7
sizeof(p) = 8, strlen(p) = 6
strlen(arr2) = 6同样是"abcdef":无 \0 的数组 sizeof=6;有 \0 的字符串 sizeof=7;指针只占 8 字节,与字符串长度无关。
⚠️ 常见坑与报错(本讲汇总)
| 你看到的现象 | 原因 | 修法 |
|---|---|---|
运行 p[0]='H' 时崩溃 | 修改了字符串字面量(只读区) | 字面量指针写成 const char *;想改就用字符数组 |
| int (*p)[N] 写成 int *p[N] | 漏掉括号,变成指针数组 | 记住 [] 优先级高,先成指针必须加括号 |
| VS 报 C2087 / 二维数组列数缺失 | 形参 int a[][] 省了第二维 | 第二维必须写:int a[][5] |
| 函数指针赋值报类型不兼容 | 返回类型或参数列表对不上 | 按函数声明逐段对照 |
| qsort 排完顺序是乱的 | 比较函数返回值写反(正数当"更小") | 牢记"前减后,负数排前面" |
| qsort 排序结果不稳定/数据异常 | 比较 int 时两数相减溢出(极端数据) | 改用比较:return (*p1 > *p2) - (*p1 < *p2); |
| strlen 得到奇怪的大数 | 字符串没有 \0,越界查找 | 检查初始化;sizeof 不受影响但 strlen 会 |
| 给回调传参写成 calc(add()) | 多加括号变成"先调用 add" | 传函数地址:直接写函数名 add |
🛠️ 动手练习
规则:先独立做,卡住了再依次展开提示,不要一上来就看答案。
📊 本讲网页练习进度0 / 8(0%)
进度自动保存在本浏览器;编程题不计数,请在编辑器中完成。
练习 1 · ⭐ 基础 · 选择判断
知识点:数组指针 vs 指针数组
题干:下列哪个声明是数组指针(指向一个含 5 个 int 的数组)?
练习 2 · ⭐ 基础 · 选择判断
知识点:二维数组形参
题干:函数要接收一个 3 行 5 列的 int 二维数组,下列形参写法正确的是?
练习 3 · ⭐ 基础 · 程序填空
知识点:数组名两个例外的综合运用
题干:写出下面程序的输出(格式 x,y):
📄 ex3.c · ✅ 完整程序
c
#include <stdio.h>
int main(void)
{
int a[5] = {1, 2, 3, 4, 5};
int *ptr = (int *)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;
}空①
练习 4 · ⭐⭐ 提高 · 选择判断
知识点:qsort 比较函数的规则
题干:关于 qsort 的比较回调,下列哪些说法正确?(多选)
练习 5 · ⭐⭐ 提高 · 选择判断
知识点:字符串字面量的存放
题干:已有 const char *p = "hello";,下列说法正确的是?
练习 6 · ⭐⭐ 提高 · 写小程序
知识点:转移表扩展 + 错误处理
题干:在转移表版计算器中新增第 5 项取模运算 5:mod,并处理"对 0 取模"的情况。
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
在转移表版计算器中新增第 5 项取模运算 5:mod,并处理"对 0 取模"的情况。
✅ 过关标准:菜单变为 1~5、0 退出;选 5 输入 17 5 输出 2;第二数输入 0 时提示"不能对 0 取模",程序不崩溃、可继续使用。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
转移表是个数组,新增运算只需让数组多一个元素、下标 5 对应新函数。取模用 %;"模 0"在 C 里是 🔴 UB,必须在调用前判断第二个操作数是否为 0,把这次计算挡掉。
🧭 提示 2(步骤)
- 写
int mod(int a, int b):内部先判b == 0返回 0 并打印提示 - 数组扩为 6 个槽位:
{NULL, add, sub, mul, divi, mod} - 合法输入范围改成
1~5 - 更稳的做法:把"b 是否为 0"的判断放在 main 调用前,让运算函数保持纯粹
🔑 参考答案与解释
c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
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 divi(int a, int b) { return a / b; }
int mod(int a, int b) { return a % b; }
int main(void)
{
int x = 0, y = 0, input = 1, ret = 0;
int (*p[6])(int, int) = {NULL, add, sub, mul, divi, mod};
do
{
printf("1:add 2:sub 3:mul 4:div 5:mod 0:exit,请选择:");
if (scanf("%d", &input) != 1)
{
break;
}
if (input >= 1 && input <= 5)
{
printf("输入两个数:");
scanf("%d %d", &x, &y);
if ((input == 4 || input == 5) && y == 0)
{
printf("除数不能为 0\n");
continue;
}
ret = p[input](x, y);
printf("结果:%d\n", ret);
}
else if (input == 0)
{
printf("退出\n");
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}顺手加了 scanf 返回值检查:输入非数字时直接退出,避免 scanf 失败后 input 保留旧值造成死循环——这是处理菜单输入的常见防护。
练习 7 · ⭐⭐ 提高 · 写小程序
知识点:qsort 排序方向控制
题干:用 qsort 把学生结构体数组按年龄从大到小排列后输出。
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
用 qsort 把学生结构体数组按年龄从大到小排列后输出。
✅ 过关标准:数据 {"zhangsan",20} {"lisi",30} {"wangwu",15},输出年龄依次为 30、20、15;必须使用 qsort,比较函数符合回调签名。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
排序方向只由比较函数的正负号决定。升序写"前者年龄 − 后者年龄",降序反过来写"后者年龄 − 前者年龄",qsort 调用本身完全不用动。
🧭 提示 2(步骤)
- 定义
int cmp_age_desc(const void *e1, const void *e2) - 两个参数转为
const struct Stu* - 返回
s2->age - s1->age - 调用 qsort 后逐行输出
🔑 参考答案与解释
c
#include <stdio.h>
#include <stdlib.h>
struct Stu
{
char name[20];
int age;
};
int cmp_age_desc(const void *e1, const void *e2)
{
const struct Stu *s1 = (const struct Stu *)e1;
const struct Stu *s2 = (const struct Stu *)e2;
return s2->age - s1->age;
}
int main(void)
{
struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
size_t count = sizeof(s) / sizeof(s[0]);
qsort(s, count, sizeof(s[0]), cmp_age_desc);
for (size_t i = 0; i < count; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
return 0;
}想更严谨可以不写减法(避免年龄差理论上的溢出),改成 return (s2->age > s1->age) - (s2->age < s1->age);——比较运算的结果本身就是 0/1,组合起来正好得到 −1/0/1。
练习 8 · ⭐⭐⭐ 挑战 · 写小程序
知识点:qsort 模拟实现的完整复现
题干:独立实现一个通用冒泡排序(签名与 qsort 前三个参数一致),并用它排序整型数组。
🛠 动手编程题 · 请在 VS2026(或你的编辑器)中完成
独立实现一个通用冒泡排序(签名与 qsort 前三个参数一致),并用它排序整型数组。
✅ 过关标准:排序 {9, 1, 5, 7, 3} 后输出 1 3 5 7 9;比较通过回调完成,交换逐字节完成;换一组 double 数据也能复用同一个排序函数。
网页内无法练习写代码,亲手敲、亲手编译才能真正学会。下方按顺序展开 思路 → 步骤 → 答案。
💡 提示 1(思路)
把上一讲的冒泡排序里两处"int 特有"的东西换掉:比较 arr[j] > arr[j+1] 换成回调 cmp(元素j地址, 元素j+1地址) > 0;交换临时变量换成逐字节交换。元素地址 = (char*)base + j*size。
🧭 提示 2(步骤)
- 写 byte_swap 逐字节交换
- 写 generic_bubble,两层循环边界用 count
- cur、nxt 两个 char* 定位相邻元素
- main 里准备数据、比较函数并调用,验证结果
- 想验证通用性:再写一组 double 的 cmp 和数据测试同一个函数
🔑 参考答案与解释
c
#include <stdio.h>
#include <stdlib.h>
void byte_swap(char *p1, char *p2, size_t size)
{
for (size_t i = 0; i < size; i++)
{
char tmp = p1[i];
p1[i] = p2[i];
p2[i] = tmp;
}
}
void generic_bubble(void *base, size_t count, size_t size,
int (*cmp)(const void *, const void *))
{
for (size_t i = 0; i + 1 < count; i++)
{
for (size_t j = 0; j + 1 < count - i; j++)
{
char *cur = (char *)base + j * size;
char *nxt = cur + size;
if (cmp(cur, nxt) > 0)
{
byte_swap(cur, nxt, size);
}
}
}
}
int int_cmp(const void *p1, const void *p2)
{
int a = *(const int *)p1;
int b = *(const int *)p2;
return (a > b) - (a < b);
}
int main(void)
{
int arr[] = {9, 1, 5, 7, 3};
size_t count = sizeof(arr) / sizeof(arr[0]);
generic_bubble(arr, count, sizeof(int), int_cmp);
for (size_t i = 0; i < count; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}这个程序把本讲三件东西串到了一起:函数指针(回调)+ void(抹去类型)+ 逐字节操作(内存思维)*。能独立默写它,指针部分的主线就算过关了。
📝 小结与自测
本讲核心清单:
- 字符指针指向字面量时存的是首字符地址;字面量只读,要写 const;两个指针可指向同一个字面量
- 数组指针
int (*p)[N]是指向数组的指针;二维数组传参传的是"第一行的地址",列数不能省 - 函数名即地址;函数指针类型 = 返回类型 + 参数列表;typedef 让复杂类型变简单
- 函数指针数组可做转移表;回调函数 = 被别人通过函数指针"回头调用"的函数
- qsort 靠 void* + 回调排序任意类型;比较函数"负在前、正在后"
- sizeof 算占用空间(编译期),strlen 数到
\0(运行期);没有\0时 strlen 越界
自测三问(答完再进入 L3):
int *p[5]和int (*p)[5]各是什么?- 回调函数和"直接调用函数"写起来最大的区别是什么?
char a[] = "xy"; char *p = a;,64 位下sizeof(a)、sizeof(p)、strlen(a)各是多少?
🔑 点击核对答案
- 前者是指针数组(5 个 int*),后者是数组指针(指向 int[5])。
- 调用方拿到的不是函数调用结果而是函数地址(写函数名、不加括号);何时调用、用什么参数调用由接收方决定。
- 依次为 3、8、2:数组含 'x','y','\0';指针 8 字节;strlen 只数字符。
🔬 选学拓展(L3)
以下内容超出主线要求
L1/L2 过关即可进入第 14 讲。下面是原课程第 16 讲收录的两组经典笔试题集训,供找工作/想深入的同学钻研,全部配有实测。
集训 A:sizeof / strlen 表达式系列
判断口诀还是那两条:先看数组名是不是两个例外,再看子表达式有没有让它退化成指针。所有结果均在本机 64 位编译实测。
A1 · 一维数组
📄 drill_one_d.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
int a[] = {1, 2, 3, 4};
printf("%d\n", (int)sizeof(a)); // 1
printf("%d\n", (int)sizeof(a + 0)); // 2
printf("%d\n", (int)sizeof(*a)); // 3
printf("%d\n", (int)sizeof(a + 1)); // 4
printf("%d\n", (int)sizeof(a[1])); // 5
printf("%d\n", (int)sizeof(&a)); // 6
printf("%d\n", (int)sizeof(*&a)); // 7
printf("%d\n", (int)sizeof(&a + 1)); // 8
printf("%d\n", (int)sizeof(&a[0])); // 9
printf("%d\n", (int)sizeof(&a[0] + 1));// 10
return 0;
}答案表:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|
| 16 | 8 | 4 | 8 | 4 | 8 | 16 | 8 | 8 | 8 |
要点:1 中数组名在 sizeof 里代表整个数组;2、4 中 a+0、a+1 是指针运算,结果类型已是指针;7 中 *&a 先取数组地址再解引用,类型又回到数组(16)。
A2 · 字符数组(sizeof 三个变体)
📄 drill_char_size.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'}; // 无 \0
char str[] = "abcdef"; // 有 \0
const char *p = "abcdef"; // 指针
printf("%d %d %d\n",
(int)sizeof(arr), (int)sizeof(arr + 0), (int)sizeof(*arr));
printf("%d %d %d %d\n",
(int)sizeof(arr[1]), (int)sizeof(&arr),
(int)sizeof(&arr + 1), (int)sizeof(&arr[0] + 1));
printf("%d %d %d %d %d %d %d\n",
(int)sizeof(str), (int)sizeof(str + 0), (int)sizeof(*str),
(int)sizeof(str[1]), (int)sizeof(&str),
(int)sizeof(&str + 1), (int)sizeof(&str[0] + 1));
printf("%d %d %d %d %d %d %d\n",
(int)sizeof(p), (int)sizeof(p + 1), (int)sizeof(*p),
(int)sizeof(p[0]), (int)sizeof(&p),
(int)sizeof(&p + 1), (int)sizeof(&p[0] + 1));
return 0;
}答案表(每行对应一个变量的 7 个表达式):
| 变量(7 个表达式依次) | 值 |
|---|---|
arr(无 \0):sizeof(arr)、arr+0、*arr、arr[1]、&arr、&arr+1、&arr[0]+1 | 6, 8, 1, 1, 8, 8, 8 |
str(有 \0):同上 7 个 | 7, 8, 1, 1, 8, 8, 8 |
p(指针):sizeof(p)、p+1、*p、p[0]、&p、&p+1、&p[0]+1 | 8, 8, 1, 1, 8, 8, 8 |
三组里只要数组名被单独放进 sizeof,就是整个数组(6 或 7);一旦参与运算或被取地址变成指针类表达式,64 位下一律 8。
A3 · 字符数组(strlen 三个变体)
这三个变体中含有故意写错的调用和无 \0 的数组,为待分析片段,不建议运行:
🧩 试题片段 · 仅用于读代码求结果
c
#include <string.h>
/* 代码 2:char arr[] = {'a','b','c','d','e','f'}; 无 \0 */
strlen(arr); // ?
strlen(arr + 0); // ?
strlen(*arr); // ?
strlen(arr[1]); // ?
strlen(&arr); // ?
strlen(&arr + 1); // ?
strlen(&arr[0] + 1); // ?
/* 代码 4:char arr[] = "abcdef"; 有 \0 */
strlen(arr); // ?
strlen(arr + 0); // ?
strlen(*arr); // ?
strlen(arr[1]); // ?
strlen(&arr); // ?
strlen(&arr + 1); // ?
strlen(&arr[0] + 1); // ?
/* 代码 6:char *p = "abcdef"; */
strlen(p); // ?
strlen(p + 1); // ?
strlen(*p); // ?
strlen(p[0]); // ?
strlen(&p); // ?
strlen(&p + 1); // ?
strlen(&p[0] + 1); // ?答案表:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|
| 代码 2(无 \0) | 随机 | 随机 | 编译警告 | 编译警告 | 随机 | 随机 | 随机 |
| 代码 4(有 \0) | 6 | 6 | 编译警告 | 编译警告 | 6 | 随机 | 5 |
| 代码 6(指针) | 6 | 5 | 编译警告 | 编译警告 | 随机 | 随机 | 5 |
要点说明:
- "随机"不是玄学:起点之后有没有
\0、\0在哪里不受程序控制,属于越界读,每次运行可能不同 strlen(*arr)、strlen(arr[1])把**字符(小整数)**当地址传进去,gcc 警告passing argument 1 of 'strlen' makes pointer from integer without a cast,即便编译通过也不能得到有意义结果- 代码 4 第 6 个:
&arr+1从\0之后开始,内容未知;第 7 个从 'b' 开始 = 5 - 代码 6 第 5、6 个:
&p指向指针变量自身,栈上那几个字节里什么时候出现 0 完全看运气
A4 · 二维数组
📄 drill_two_d.c · ✅ 完整程序(可直接复制编译)
c
#include <stdio.h>
int main(void)
{
int a[3][4] = {0};
printf("%d\n", (int)sizeof(a)); // 1
printf("%d\n", (int)sizeof(a[0][0])); // 2
printf("%d\n", (int)sizeof(a[0])); // 3
printf("%d\n", (int)sizeof(a[0] + 1)); // 4
printf("%d\n", (int)sizeof(*(a[0] + 1))); // 5
printf("%d\n", (int)sizeof(a + 1)); // 6
printf("%d\n", (int)sizeof(*(a + 1))); // 7
printf("%d\n", (int)sizeof(&a[0] + 1)); // 8
printf("%d\n", (int)sizeof(*(&a[0] + 1))); // 9
printf("%d\n", (int)sizeof(*a)); // 10
printf("%d\n", (int)sizeof(a[3])); // 11
return 0;
}答案表:
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|
| 48 | 4 | 16 | 8 | 4 | 8 | 16 | 8 | 16 | 16 | 16 |
要点:3 中 a[0] 是第一行(数组),单独放 sizeof 得一整行 16;4 中 a[0]+1 已退化成指针;7、9 解引用后回到"一行"的类型 = 16;11 中 a[3] 看起来越界,但 sizeof 的操作数不会被真正执行,只求类型(一行的大小),所以不是 UB。
集训 B:指针运算七题(均实测)
B1:见练习 3,输出 2,5。
B2 · 结构体指针步进(注意不同的强转)
🧩 试题片段 · 仅用于读代码求结果
c
#include <stdio.h>
struct Test
{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
} *p = (struct Test *)0x100000;
int main(void)
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int *)p + 0x1);
return 0;
}按"结构体 20 字节"的原题假设(32 位环境),三行依次:0000000000100014、0000000000100001、0000000000100004。区别:第一行是结构体指针 +1,跳过 20 字节;第二行先转成整数(数值运算,只加 1);第三行是 unsigned int* +1,跳 4 字节。
64 位实测要小心:本片段直接在 64 位编译运行时,第一行实际输出 0000000000100020。因为结构体内的 char *pcName 在 64 位下占 8 字节,再算上对齐填充,结构体实际是 32 字节(不是 20),p+1 跳过 32 字节。这正说明"指针 +1 的步长 = 所指类型的真实大小"——换平台时这种题必须按实际布局重算,不能背 32 位答案。
B3 · 逗号表达式陷阱
🧩 试题片段 · 仅用于读代码求结果
c
#include <stdio.h>
int main(void)
{
int a[3][2] = {(0, 1), (2, 3), (4, 5)};
int *p;
p = a[0];
printf("%d", p[0]);
return 0;
}输出 1。初始化里的 (0,1) 不是两行数据,而是逗号表达式,值取最后一个操作数:实际初始化值是 1、3、5,数组为 {1,3},{5,0},{0,0}。gcc 会给 -Wmissing-braces 提醒,但程序合法。
B4 · 不同行宽的数组指针
🧩 试题片段(已按现代编译器加修正) · 仅用于读代码求结果
c
#include <stdio.h>
#include <stdint.h>
int main(void)
{
int a[5][5];
int (*p)[4];
p = (int (*)[4])a; // 原题写 p = a; gcc 14+ 报 incompatible-pointer-types 错误
ptrdiff_t diff = &p[4][2] - &a[4][2];
printf("%p,%d\n", (void *)(intptr_t)diff, (int)diff);
return 0;
}输出 fffffffffffffffc,-4。p 的行宽 4:p[4][2] 距起点 4×4+2 = 18 个元素;a 行宽 5:a[4][2] 为 4×5+2 = 22 个;指针相减 = 18−22 = −4。第一行用 %p 看待这个负数,64 位下就是它的补码。
B5 · 二维数组两种尾后定位
🧩 试题片段 · 仅用于读代码求结果
c
#include <stdio.h>
int main(void)
{
int aa[2][5] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}输出 10,5。ptr1 从"整个数组之后"回退一个元素 = 最后元素 10;ptr2 从第二行开头(aa[1])回退一个 = 第一行末尾 5。
B6 · 二级指针首行步进
🧩 试题片段 · 仅用于读代码求结果
c
#include <stdio.h>
int main(void)
{
char *a[] = {"work", "at", "alibaba"};
char **pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}输出 at。pa 指向数组首元素(一个 char*),pa++ 跨过一个指针,指向 a[1],其指向字符串 "at"。
B7 · 三级指针连环走
🧩 试题片段 · 仅用于读代码求结果
c
#include <stdio.h>
int main(void)
{
char *c[] = {"ENTER", "NEW", "POINT", "FIRST"};
char **cp[] = {c + 3, c + 2, c + 1, c};
char ***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;
}输出四行:POINT、ER、ST、EW。跟踪 cpp 的位置即可:第一行走 cp[1] 得 c+2 → POINT;第二行走到 cp[2]、自减后得 c+0,取字符串再加 3 → ER;此时 cpp 停在 cp[2],cpp[-2] 是 cp[0]=c+3 → FIRST+3 → ST;cpp[-1][-1] 由 cp[1]=c+2 再退一个得 c+1 → NEW+1 → EW。