新闻详情

新闻详情

首页 / 资讯中心 / 详情

深入理解 C 语言中复杂的函数指针声明

发布时间:2026/9/26 12:45:10来源:尧图网络
深入理解 C 语言中复杂的函数指针声明
1. 引言先看一行代码不用着急理解(*(void (*)())0)();。作者在 2.1 节开头说这样的表达式恐怕会令每个 C 程序员的内心都不寒而栗。它是怎么来的书里说有位程序员在给某种微处理器写程序那台机器启动时硬件会调用首地址为 0 位置的子例程。他要写一条 C 语句显式地去调用那个子例程。结果就是我们看到的这一行。void(*)()与0这两块似懂非懂更要命的是为什么非要套这么多层括号还在最外面绕一圈*规则书里紧接着就把这条规则给了出来然而他们大可不必对此望而生畏因为构造这类表达式其实只有一条简单的规则按照使用的方式来声明。它的意思是声明符不是描述这个变量是什么而是描述这个变量在表达式里会怎么被用。2. 声明符的本质书里给了两句话解释任何 C 变量的声明都由两部分组成类型以及一组类似表达式的声明符declarator。声明符从表面上看与表达式有些类似对它求值应该返回一个声明中给定类型的结果。以float *pf为例float是类型*pf叫声明符——变量名加上它身上套的符号。对它求值应该返回一个声明中给定类型的结果也就是把声明符当成一个算式去算算出来的结果必须正好等于声明开头那个类型。开头写的是float声明符是*pf也就是对 pf 做解引用。规则要求*pf算出来得是float那pf就不可能是float了——它得是解引用之后能得到 float的那种东西。结论pf是指向float的指针。3. 拿它走一遍所以读声明的正确方法是把变量名为中心问一句它在这个表达式里被用完之后剩下的是什么类型。一条一条试float f, g;—— f 求值是 float所以 f 是 float。float ((f));—— 括号随便加跟表达式一样意思是同一个。float ff();—— ff() 求值是 float所以 ff 是函数返回 float。float *pf;—— *pf 求值是 float所以 pf 是指针指向 float。第一个就是最常用的变量声明第二个float ((f))括号只是把同一个东西括起来跟((3))就是 3 一样什么都没改变。第三个float ff();ff()算出来是 float括号这个动作叫调用调用完得到 float。也就是找到变量名看它身上套了什么*是解引用跟在表达式后的()是调用在表达式外面的()是普通分组括号[]是取下标。一层层剥掉剥完剩下的类型必须等于声明开头那个类型。至于声明符长得像表达式不是巧合是设计便于阅读。4. float *g() 与 (*h)()书里把上面那些形式组合起来给了这么一行float *g(), (*h)();。同一行两个声明符一个没括号一个有括号。它们完全不是一类东西。按刚才的规则读*g()要先看()。函数运算符()的优先级高于单目*所以*g()实际上是*(g())。也就是先调用 g再解引用结果是 float。倒推g 是一个函数返回值类型是指向 float 的指针。(*h)()里括号把*h圈住了。所以*h是一个函数h 才是指针。倒推h 是一个函数指针它指向的函数返回 float。两个形式都一样差别全在那一对括号上。5. 机械规则把声明变成类型转换符上一节的推法已经了解。书里接着给了一个更省事的机械做法一旦我们知道了如何声明一个给定类型的变量那么该类型的类型转换符就很容易得到了只需要把声明中的变量名和声明末尾的分号去掉再将剩余的部分用一个括号整个封装起来即可。类型转换符是什么类型转换符就是(类型)这个东西写在某个表达式前面对编译器说后面这一坨按我说的类型处理。(int)3.7—— 把 3.7 按 int 处理得到 3。(float)5—— 把 5 按 float 处理得到 5.0f。(char)300—— 把 300 按 char 处理得到 44。(void (*)())0—— 把 0 按函数指针处理得到一个地址。之后以float (*h)();为例float (*h)(); /* 原声明 */ float (*)(); /* 去掉变量名 h 和末尾分号 */ (float (*)()) /* 整体用括号封装 —— 这就是类型转换符 */所以(float (*)())的意思就是指向返回 float 的函数的指针。实测这条规则确实能直接用#include stdio.h float ff(void) { return 1.5f; } int main(void) { /* 带类型转换符的写法 */ float (*q)(void) (float (*)(void))ff; /* q 是一个函数指针——指向不接受参数、返回 float的函数 */ printf(q() %f\n, q()); /* () 的操作数本来就该是函数指针 */ /* 不带类型转换符的写法一样能跑函数名会自动退化成指针 */ float (*r)(void) ff; printf(r() %f\n, r()); return 0; }这里的ff是函数名会自动退化所以两种都是可以的进行强转是多余的。但有些情况必须强转。注意这个规则是可逆的。会读声明就会写类型转换符会写类型转换符也就懂声明了。6. 回到开头现在回头看最上面那句。书里的做法是分两步第一步假定变量fp是一个函数指针怎么调用它指向的函数(*fp)();。因为fp是指针*fp就是它指向的函数所以(*fp)()是在调用那个函数。书里在这里专门提醒了一句ANSI C 标准允许程序员将上式简写为fp()但是一定要记住这种写法只是一种简写形式。第二步把fp替换掉。先试最朴素的写法(*0)();不行。因为*需要一个指针当操作数而0是整数error C2100: 无法取消引用类型为int的操作数 error C2064: 项不会计算为接受 0 个参数的函数所以必须对0先做类型转换转成指向返回值为 void 类型的函数的指针。那个类型怎么拼用上一节的机械规则反推——从void (*fp)();去掉fp和分号、加括号void (*)() /* 类型转换符 (void (*)()) */ (void (*)())0 /* 把 0 转成这个类型 */替换进去得到开头那一行(*(void (*)())0)();。也就是把整数 0 强制转换为不接受参数、返回 void 的函数指针解引用该指针得到函数调用该函数。书里也说还可以用 typedef 让它好读一点typedef void (*funcptr)(); /* 定义 funcptr 是 void (*)() 类型 */ (*(funcptr)0)();7. 那两个括号为什么不能省书里这一段说得很严格在表达式(*fp)()中*fp两侧的括号非常重要因为函数运算符()的优先级高于单目运算符*。如果*fp两侧没有括号那么*fp()实际上与*(fp())的含义完全一致。写法读法意思(*fp)()先解引用再调用调用 fp 指向的函数*fp()先调用再解引用调用 fp 这个函数然后对返回值解引用括号不是装饰它决定*落在谁身上。再补一条能彻底分开函数名和函数指针的sizeof。printf(sizeof(pf) %u\n, (unsigned)sizeof(pf)); /* 8 */ unsigned n (unsigned)sizeof(ff); /* 编译不过 */sizeof(pf) 8 error C2070: float (__cdecl *)(void): 非法的 sizeof 操作数函数没有大小函数指针才有大小。8. signal 那个著名声明书里拿这个例子收尾因为它不是孤例。signal 库函数有两个参数一个整型的信号编号一个指向用户信号处理函数的指针。它的返回值也是这种指针。先用刚才的规则写指向处理函数的指针void (*sfp)(int); /* sfp 是指向接受 int、返回 void 的函数的指针 */然后推 signal 的完整声明void (*signal(int, void (*)(int)))(int);之前见到的返回值表示都是这样的int f(void);—— 返回 int。int *f(void);—— 返回 int *。char **f(void);—— 返回 char * *。struct Point f(void);—— 返回 struct Point。就是返回类型在前但返回值是一个函数的指针的情况就不能这样表示了void (*)(int) signal(int); /* 错误写法 */因为*被包住了只能这样void (*signal(int, void(*)(int)))(int);拆开看void (*)(int)是返回类型signal()是函数声明函数名与参数int, void (*)(int)是一个参数是 int一个参数是一个指针——指向一个接受 int 返回 void 的函数。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

SSE流式传输实战:从协议原理到AI对话生产环境避坑指南 2026/9/26 15:02:25

SSE流式传输实战:从协议原理到AI对话生产环境避坑指南

1. 从一个真实场景说起:为什么我们需要流式传输 前阵子帮一个朋友排查他做的智能问答页面,问题很典型:用户问一个问题,前端要转圈等七八秒,然后“啪”一下整段答案全冒出来。他自己也觉得别扭,说看别人家的…

阅读更多 →
基于大数据反电信诈骗系统:Python课程设计完整项目实战解析 2026/9/26 15:02:25

基于大数据反电信诈骗系统:Python课程设计完整项目实战解析

简介:一套基于大数据反电信诈骗管理系统的Python课程设计项目源码包,面向高校计算机、大数据专业学生及安全领域初级开发者。系统整合大数据分析、NLP与机器学习,覆盖实时通信监控、智能报告、用户反馈、风险评估等核心模块,并配有…

阅读更多 →
残差不是噪声:两阶段校正框架在8个时序基准上屠榜,最高提升92.85% 2026/9/26 15:02:25

残差不是噪声:两阶段校正框架在8个时序基准上屠榜,最高提升92.85%

1. 时序预测里的“残差”到底冤不冤做时间序列预测的朋友,大概率都经历过这样一个场景:模型在训练集上拟合得漂漂亮亮,一到验证集或者线上就拉胯,误差曲线像心电图一样上下乱跳。这时候很多人的第一反应是“数据噪声太大”&#x…

阅读更多 →
图生图提示词固定模板:四套高频场景与重绘幅度调参指南 2026/9/26 15:02:25

图生图提示词固定模板:四套高频场景与重绘幅度调参指南

1. 为什么我最终把图生图提示词固定成了这几套先说结论:过去两个月,我手机相册里新增的图,大概有七成不是拍出来的,而是用图生图跑出来的。不是那种“输入一句话等半天出一张盲盒”的玩法,而是拿一张底图,配…

阅读更多 →
Flow Matching实战指南:从原理到训练与采样 2026/9/26 15:02:25

Flow Matching实战指南:从原理到训练与采样

1. 从“为什么需要Flow Matching”说起 如果你最近在关注生成模型,大概率会频繁刷到“flow matching”这个词。我第一次接触它是在做图像生成实验的时候,当时用扩散模型跑一个中等规模的数据集,采样步数动辄几百上千步,推理成本高…

阅读更多 →
轴向磁通电机电磁仿真与实测对标:0.3毫米气隙偏差的教训 2026/9/26 15:02:19

轴向磁通电机电磁仿真与实测对标:0.3毫米气隙偏差的教训

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉