
1. 我遇到的问题编译器报了一串看不懂的“星号括号组合”今天本来想写一个计算器程序把加、减、乘、除四个功能分别写成函数再搞一个统一的调用入口传不同的函数进去就行。代码写到一半我信心满满地定义了一个指针结果编译器直接甩给我一行红字int (int, int) *p; // 我想定义一个指向函数的指针GCC 当场报错“expected identifier or ‘(’ before ‘*’ token”。我盯着这行代码看了五分钟又把星号往左挪挪、往右挪挪怎么改都不对。那一刻我突然意识到函数指针的声明语法根本不是我想的那样直接“类型 星号 变量名”。2. 错误写法/想法展示我最初的想法特别朴素既然普通变量可以写成“int *p;”表示指向 int 的指针那函数的“类型”不就是它的返回值和参数列表吗于是顺手写成了// 错误示范把函数类型当成普通类型来声明指针 int (int, int) *p; // 编译不过 int add(int, int) *p; // 同样编译不过 int *p(int, int); // 这会被解析成“返回 int* 的函数声明”也不是函数指针我把星号放在类型后面、放在函数名前面、放在括号里面全都试了一遍编译器要么报语法错误要么把东西解析成了另一个意思。我当时脑子里只有一个念头这语法到底谁定的为什么不把星号放在类型后面就完事了3. 为什么错了从内存、类型、编译原理角度讲清楚要理解这个问题得先搞清楚“函数名”在 C 语言里到底是什么。回忆一下我们之前学的数组名代表首元素地址而函数名其实也一样——函数名就是该函数的入口地址。也就是说add这个标识符本身就是一个地址值指向函数第一条指令在代码段里的位置。那么当我们想用一个指针变量存这个地址时这个指针的“类型”必须能描述它指向的是一段什么样的代码——也就是函数的返回值类型和参数列表。C 语言里把这个描述称为“函数类型”。比如// add 函数的类型是int (int, int) // 读作接收两个 int返回一个 intOK现在我们想定义一个能指向这类函数的指针。直觉上可能想写成int (int, int) *p;但 C 语言的声明语法并不是“类型在前、星号在中间、变量名在后”这么简单。实际上C 声明遵循“声明模仿使用”的原则——也就是“怎么用就怎么声明”。假如我们真的有一个函数指针 pfp想通过它调用函数并拿到返回值会写成int result (*pfp)(a, b); // 解引用指针然后调用函数那么根据“声明模仿使用”我们在声明 pfp 时就要让它的结构看起来像(*pfp)这一部分// 正确声明 int (*pfp)(int, int); // 解读pfp 是一个指针指向“接收两个 int、返回 int”的函数 // 把 (*pfp) 看成一个整体它后面跟参数列表、前面跟返回类型括号在这里至关重要。因为()的优先级高于*如果不加括号写成int *pfp(int, int)编译器会把它解释成pfp 是一个函数接收两个 int返回 int*——这就完全跑偏了。再从内存的角度理解一下这段代码在运行时代码段里存着 add、sub 这些函数的机器指令数据段或栈上有一个指针变量 pfp它里面存的数值就是某个函数第一条指令的地址。CPU 执行到(*pfp)(a, b)时先把参数压栈然后跳转到 pfp 指向的地址开始执行那条函数。如果类型不匹配比如你用 int(*)(int,int) 的指针去指向一个返回 double 的函数编译器会报警告甚至错误因为调用时返回值的字节数和解释方式都不一样会导致栈或寄存器的数据解析出错。所以函数指针的声明之所以“绕”不是因为谁闲得无聊搞语法而是它必须同时表达两个信息① 这是一个指针② 它指向的东西不是普通数据而是一段有特定签名的可执行代码。而 C 语言选择用“声明模仿使用”这种方式来统一表达这就是星号加括号的根本原因。4. 正确写法及注释说明搞清楚原理之后代码就可以写得明明白白了。下面是一个完整的计算器回调函数示例#include stdio.h // 四个运算函数类型都是 int (int, int) 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 div_op(int a, int b) { return b ! 0 ? a / b : 0; } // processData 接收两个操作数和一个函数指针 // int (*pfunc)(int, int) —— pfunc 是指向“返回 int、参数为两个 int”的函数的指针 void processData(int a, int b, int (*pfunc)(int, int)) { int result pfunc(a, b); // 通过函数指针间接调用 printf(result %d\n, result); } int main() { int x 20, y 4; processData(x, y, add); // add 是函数名也是入口地址 processData(x, y, sub); // 传 sub 的地址 processData(x, y, mul); processData(x, y, div_op); return 0; }注意看processData的第三个参数声明int (*pfunc)(int, int)。我们来拆解一下读法从里向外读*pfunc表示 pfunc 是一个指针向右看(int, int)表示它所指向的东西接收两个 int 参数向左看int表示那个东西的返回值是 int。合起来就是pfunc 是一个指针指向“接收两个 int、返回 int”的函数。这样读几遍以后看到int (*cmp)(const void *, const void *)这种系统 API 的声明也就不会发怵了。5. 一句人话总结函数指针的“星号加括号”语法不是故意搞人心态而是因为 C 语言必须同时说清楚“这是一个指针”以及“它指向的函数长什么样”而括号就是用来对抗运算符优先级、把星号和变量名绑成“指针变量”这一整体的。6. qsort 为什么需要传一个函数进去6.1 我遇到的问题这排序函数怎么连“谁大谁小”都不知道学完函数指针的语法我翻到 C 标准库里的 qsort 函数发现它的原型长这样void qsort( void *base, // 要排序的数组起始地址 size_t nmemb, // 元素个数 size_t size, // 每个元素的大小字节 int (*compar)(const void *, const void *) // 比较规则的回调函数 );我当时第一个反应是这函数也太离谱了吧一个排序函数居然不知道元素怎么比大小还得我传一个比较函数给它我们自己写冒泡排序的时候不是直接在循环里用或就搞定了吗6.2 错误想法展示我的困惑其实来自一个惯性思维以为排序函数只能排一种数据类型。比如下面这种自己写的冒泡排序// 只能排 int 数组的冒泡排序 void bubble_sort(int arr[], int n) { for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { // 比较规则写死了 int tmp arr[j]; arr[j] arr[j 1]; arr[j 1] tmp; } } } }这个函数只能排 int如果要排 double、字符串、结构体就得再写 N 个几乎一模一样的函数。qsort 的设计者显然不想这么干所以他们把比较规则“外挂”成回调函数让调用者自己定义。6.3 为什么必须传函数指针从内存和通用性角度qsort 是通用排序函数它不知道你传给它的是整型数组、浮点数组还是结构体数组。它只知道每个元素占多少字节size和一共有多少个元素nmemb而 base 是一个 void* 类型的万能指针。void* 的意思是“可以接收任意类型的地址”但它不能直接用来解引用——因为编译器不知道指向的空间有多大、按什么格式解析。所以 qsort 在内部做冒泡或快排的时候每次需要比较两个元素就必须“回调”你提供的那段比较代码。你通过 compar 函数告诉它// 比较两个元素 a 和 b // 返回值 // 0 表示 a b // 0 表示 a b // 0 表示 a b int compar(const void *a, const void *b);a 和 b 是指向待比较元素的指针void* 类型在你的比较函数里你需要先把它强转成具体类型的指针再进行解引用比较。比如排 int 数组int cmp_int(const void *a, const void *b) { int va *(const int *)a; // 把 a 强转成 int*再解引用拿到值 int vb *(const int *)b; return va - vb; // 升序a 小则返回负数 }排字符串二维字符数组元素int cmp_str(const void *a, const void *b) { // a 和 b 实际上是“数组元素”的地址而元素是 char[10] // 所以强转成 char (*)[10]再解引用得到“该行的数组名”即首字符地址 return strcmp(*(const char (*)[10])a, *(const char (*)[10])b); }这样qsort 不需要知道你在排什么类型它只需要在适当的时候调用 compar把两个元素的地址传进去由你告诉它大小关系。这就是“回调函数”的核心思想把行为当作参数传递。**不是 qsort 不知道大小而是它的通用性要求它不自己做这个判断。**6.4 正确写法用 qsort 排 char s[][10] 数组下面是一个实际可运行的例子用 qsort 对二维字符数组存放三个单词进行排序#include stdio.h #include stdlib.h #include string.h // 比较函数排的是 char[][10]每行是一个字符串 int cmp_word(const void *a, const void *b) { // a 是指向数组元素即 char[10]的指针类型是 char ()[10] const char *str_a (const char (*)[10])a; const char *str_b (const char (*)[10])b; return strcmp(str_a, str_b); // 按字典序比较 } int main() { char words[][10] {hello, world, china}; int n sizeof(words) / sizeof(words[0]); // qsort 排序传数组首地址、元素个数、单个元素大小、比较函数 qsort(words, n, sizeof(words[0]), cmp_word); for (int i 0; i lt; n; i) { printf(%s\n, words[i]); // 输出china hello world } return 0; }其中qsort(words, n, sizeof(words[0]), cmp_word);这一行里cmp_word就是函数名入口地址直接传给 qsortqsort 内部会通过那个 compar 参数来调用它。看到这里你会发现函数指针不只是语法概念它是实现“算法与数据分离”的关键工具。6.5 一句话总结qsort 传函数指针本质上是把“怎么比大小”这个决策权交给使用者这样同一个 qsort 可以排 int、排字符串、排结构体而不用为每种类型重写一套排序逻辑。7. 指针函数和堆区内存Day14 笔记补充今天还接触到两个新概念一个是和“函数指针”只有一字之差的“指针函数”一个是向堆区借空间的 malloc。前者容易搞混后者是内存管理的必修课。“指针函数”就是返回值类型为指针的函数比如char *strcpy(char *dest, const char *src)。它和函数指针的区别一目了然函数指针int (*p)(int, int)—— 变量 p 是一个指针指向函数指针函数int *func(int, int)—— func 是一个函数返回指针。记住“星号跟谁走”就清楚了星号跟变量名绑在一起的是函数指针星号跟返回类型绑在一起的是指针函数。至于 malloc它是向堆区申请内存的函数返回 void* 万能指针需要自己强转后使用。关键记住几条铁律malloc 和 free 必须成对出现只申请不释放会造成内存泄露free(p) 之后要把 p 置为 NULL避免“悬空指针”指针函数里绝对不能返回局部变量的地址因为局部变量在栈上函数返回后那片空间就无效了。今日避坑指南坑点说明函数指针声明少括号写成 int *p(int,int) 会被当成“返回指针的函数”必须用 int (*p)(int,int) 把星号和变量名绑在一起。void* 万能指针不能直接解引用必须先强转成具体类型指针比如 *(int*)a否则编译报错或行为未定义。qsort 的 compar 参数传错签名比较函数必须符合 int (*)(const void*, const void*) 的格式否则编译警告且运行可能崩溃。返回值只写 1 或 -1 不够要用差值驱动升序/降序一目了然。指针函数返回局部变量地址栈上局部变量在函数返回后释放返回其地址是典型的“悬空引用”应使用堆区或静态存储区。free 后没有置 NULL悬空指针仍指向已释放空间后续误用会导致未定义行为free 后立即赋 NULL 是良好习惯。多次 free 同一块空间导致 double free程序直接崩溃用 NULL 保护后 free(NULL) 是安全的什么都不做。今日学习感悟Day14 学完最大的感触是“语法只是表象设计意图才是骨架”。函数指针那堆星号括号初看像乱码但一旦理解“C 语言怎么用就怎么声明”和“库函数为什么要用回调”这些符号就不再是障碍反而成了表达力的工具。qsort 通过一个函数指针就实现了算法与数据类型的解耦这种思路在后面的嵌入式回调、中断服务函数里还会反复出现。指针真的越学越有意思今天写代码时终于不再害怕那对int (*)(int,int)了。Day14 学习记录明天继续。