news 2026/10/14 5:03:31

函数指针数组实战:用C语言写一个带菜单的计算器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数指针数组实战:用C语言写一个带菜单的计算器

1. 拿到练习73之后,我建议你先别急着敲代码

如果你也是照着网上流传的“C语言经典100例”在刷题,大概刷到七十几题会有一个很明显的感觉:前面那些题目(水仙花数、九九乘法表、冒泡排序)闭着眼都能写,到七十多题开始突然“变味”了——题面看起来还是在练习控制台菜单、练习分支循环,但真上手写,却总差那么一口气。

先说一个很多人没意识到的点:经典100例因为被反复转载、整理、改版,“第73题”在不同版本里对应的内容并不一样。我手里这版练习73,核心是“用函数指针数组实现一个带菜单的计算器”,本质上考的是三件事:函数指针怎么声明、函数指针数组怎么组织、怎么通过索引直接调用不同函数。而另一个流传较广的版本,第73题是“反向输出一个链表”,两者完全不同。

所以拿到题目第一步不是打开编辑器,而是先确认你的题目描述到底长什么样。如果题面出现了“函数指针”“菜单选择”“加减乘除”这类关键词,那就和我这版对上了。这篇就围绕这一版展开,把函数指针这个知识点从原理到代码再到编译报错,完整盘一遍。

明确目标之后,我们拆一下这道题的隐藏考点:表面上是个计算器,实际考察的是“函数的地址”这个抽象概念。很多人在前面章节学过“数组名是数组首元素的地址”,却很少意识到“函数名本身也是一个地址”。练习73就是逼你把函数名当成一个可存放、可索引、可调用的对象去理解。学完这道题,你顺手就掌握了回调、命令分发、菜单系统这些后面真正有用的工程技巧,所以它值得好好写。

2. 函数指针为什么会劝退一堆人:三个类比帮你建立直觉

2.1 变量存数据,指针存地址,函数指针存“函数这栋楼的楼门号”

很多教程一上来就丢语法:int (*func)(int, int);然后告诉你“声明了一个函数指针”。对新手来说,这完全不能建立画面感,只会背格式,换个场景就懵。

我的解释方式是这样的:普通变量好比一栋单人公寓,里面住着一个整数,你通过门牌号(变量名)找到它;普通指针变量,就像手里攥着一张写着地址的纸条,它不是数据本身,但它能告诉你“你要找的人住在哪”;而函数是什么呢?函数是一栋楼,里面住着一段完整的逻辑:进门要带两个整数做凭证,出来时给你一个整数当结论。

函数指针,就是把“这栋楼的楼门号”写在一张纸条上。调用的时候,你不必直接叫出这栋楼的名字(函数名),你只要把纸条上的门牌号交给CPU,CPU自己找上门去执行整段逻辑。

有了这个画面,很多问题迎刃而解:

  • add和&add,在绝大多数编译器里结果一模一样,都是那栋楼的楼门号。
  • add()才是“直接进去执行”,而add只是“告诉你楼门号”。写代码时括号加错位置,等于把“地址纸条”和“执行结果”混淆了。

2.2 语法三件套:声明、赋值、调用,拆开记就不难

函数指针的语法确实反直觉,我建议拆成三块记忆:

声明:int (*calc)(int, int);

读法是:先看*calc,说明 calc 是一个指针;再看外面的int (int, int),说明它指向的函数接收两个 int 参数、返回 int。连起来就是“calc 是一个指向‘接收两个 int 并返回 int 的函数’的指针”。注意那个括号,int *calc(int, int)是完全不同的东西,那是在声明一个“返回 int 指针的普通函数”。

赋值:calc = add;

等号右边写函数名,不写括号。函数名本身就是地址,这和“数组名就是首地址”是同一种思维。此时 calc 和 add 指向同一个逻辑块。

调用:calc(3, 5);

声明、赋值、调用,三件事做完,你会发现和普通函数唯一的区别就是“函数名前面加了一个星号声明”。实际工程里,用函数指针实现回调、命令表,核心就是这套三件套,没有任何多余的花样。

2.3 数组来了:把函数指针放进一个“登记表”

练习73真正的精髓,是把函数指针再套一层数组。所谓函数指针数组,就是一张登记表:表上第 0 行写着“加法这栋楼的楼门号”,第 1 行写着“减法这栋楼的楼门号”,依次排开。

声明方式为:int (*funcs[4])(int, int);

读法:funcs 先和[4]结合,说明 funcs 是一个含有 4 个元素的数组;元素类型是int (*)(int, int),也就是指向“接收两个 int 返回 int 的函数”的指针。所以翻译成大白话:funcs 是一张能登记 4 个函数地址的表。

初始化更直白:int (*funcs[4])(int, int) = {add, sub, mul, divide};

这里每个元素直接就是一个函数名,编译器会把这些函数的入口地址依次放进数组。

调用时:funcs[choice](x, y);

choice 是用户输入的数字。传 0 就执行加法,传 1 就执行减法。这就是计算器菜单的核心机制——用一个整数当索引,代替一堆 if-else 或 switch-case。把代码从“每来一个操作写一个分支”变成“查表后直接跳转”,程序结构瞬间清爽。

3. 手写一版可直接运行的完整答案:带菜单的整数计算器

3.1 核心设计思路

这一版完整代码,我用四个函数分别实现加减乘除,再用一个函数指针数组去登记它们。主循环负责打印菜单、读用户输入、根据编号调用对应函数。这样设计天然贴练习题要求,也方便你后面改成浮点数版本或扩展开方、取余等操作。

3.2 上代码,完整可编译

#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 divide(int a, int b) { if (b == 0) { printf("除数不能为0,本次调用返回0。\n"); return 0; } return a / b; } int main(void) { int (*funcs[4])(int, int) = {add, sub, mul, divide}; char ops[4] = {'+', '-', '*', '/'}; int x, y, choice; while (1) { printf("\n======== 计算器菜单 ========\n"); printf("[0] 加法 [1] 减法 [2] 乘法 [3] 除法 [-1] 退出\n"); printf("请选择功能编号: "); scanf("%d", &choice); if (choice == -1) { break; } if (choice < 0 || choice > 3) { printf("无效选项,请输入0到3之间的数字。\n"); continue; } printf("请输入两个整数: "); scanf("%d %d", &x, &y); printf("%d %c %d = %d\n", x, ops[choice], y, funcs[choice](x, y)); } printf("程序结束。\n"); return 0; }

3.3 逐步说明,每一段都不白写

函数定义部分,加、减、乘很简单。除法的判断值得注意:如果你不在 divide 内部做除零保护,用户在菜单里选 3 再输入 5 0,程序会直接崩溃。练习题往往不要求这么细,但真正拿得出手的代码必须考虑运行时的异常输入。

main 函数里的int (*funcs[4])(int, int)是整道题的眼睛,前面已经拆过,它就是一张登记了 4 个函数地址的表。char ops[4]存运算符符号,只是为了输出结果时显示成 5 + 3 这种自然格式,和函数调用本身无关。

while 循环里先读 choice,判断退出条件再判断范围。这里的顺序有讲究:先判断是否等于 -1,再判断是否越界,避免输入 -1 时先掉进“无效选项”的分支导致无法退出。实际写的时候我确实见过很多同学把这个顺序写反,结果想退出却一直提示无效选项。

funcs[choice](x, y)是核心调用。这一行等价于:

switch (choice) { case 0: result = add(x, y); break; case 1: result = sub(x, y); break; case 2: result = mul(x, y); break; case 3: result = divide(x, y); break; }

区别在哪?每增加一个功能,switch 方案要加 case 分支、修逻辑;函数指针数组方案只需要加一个函数、把它的名字填进数组。代码的扩展点变少、更集中,这就是为什么命令分发、插件系统、菜单系统这些实际场景偏爱函数指针数组。

3.4 你可能遇到的第一个意外:函数指针数组越界

运行上面代码,输入 0 到 3 都没问题。但如果把越界判断注释掉,输入 5 会怎样?funcs[5]会去内存里某个随机地址“取函数入口”,然后跳过去执行。轻则跑出一堆乱码结果,重则直接段错误。我在排查类似问题时的明显感受是:越界访问指针数组比越界访问普通 int 数组更难定位,因为问题爆发点未必在数组访问那一行,而是在“跳到了错误地址之后”,所以练习时务必保留边界判断。

4. 编译期三个最常踩的坑,我挨个给你列出来

练习 73 本身不难,但我在给初学同学看代码的时候,见到的报错类型高度集中。这站点个名,省得大家反复踩。

4.1 赋值时函数名后面加了括号

int (*funcs[4])(int, int) = {add(), sub(), mul(), divide()};

这是新手最容易写错的一行。问题是:add()是“立刻执行 add 函数并拿返回值”,而add才是“拿函数的地址”。加法函数返回整数,于是这行代码试图把 4 个整数塞进一个函数指针数组,编译器直接报类型不兼容。理解上可以记一句话:函数指针数组初始化时,右边只写函数名,永远不要跟括号。

4.2 声明时括号位置不对

int *funcs[4](int, int); // 错误 int (*funcs[4])(int, int); // 正确

直接把声明读出来:第一种是“funcs 是一个数组,元素是返回 int 指针的函数”?不对,元素如果是函数,那它必须有函数名位置,这行代码编译器甚至不知道怎么解析。第二种是“funcs 是一个 4 元素数组,每个元素是指向函数的指针”。两者差距毫厘,意义天壤之别。我建议判断规则:先用星号加括号把“指针”身份锁死,再写数组维度,顺序不要反。

4.3 函数参数类型对不上

如果你把 add 声明成int add(double a, double b),前面初始化时 funcs 数组的元素类型却是int (*)(int, int),编译器一样报错。概念上很好理解:两个楼门牌号虽然都是“地址”,但一栋楼进门需要两个 int,另一栋楼需要两个 double,你不能把 double 楼的地址登记进 int 楼的通讯录里。练习73里统一参数类型为 int 是最省事的,如果后面想升级成 double 版本,记得把所有函数签名和函数指针类型全部同步替换,代码量不大,最怕漏改其中一两个。

4.4 我的排查思路,供你参考

遇到编译报错,先看冒号前的行号,再看待处理类型。这类报错几乎都会提到 incompatible types、expected ... but got ... 之类的关键字。定位到具体行之后,对照上面三件事逐一检查:赋值是否加了括号、声明括号是否错位、参数类型是否一致。

如果代码能编译但运行结果诡异,比如输入 0 却不执行加法,先打印函数地址验证登记表是否初始化正确:

printf("add 的地址: %p\n", (void *)add); printf("funcs[0] 的内容: %p\n", (void *)funcs[0]);

如果两个值一致,说明登记正确,问题在别处;如果 funcs[0] 是未初始化垃圾值,说明数组初始化语句有误。这种“先打印地址”的验证思路,也是以后做指针相关项目时最常用的排错手法。

5. 同样一套思路,还能用在哪些场景

练习73写完之后别急着翻下一题,函数指针数组这个模式能迁移到很多地方,往下延展的价值比题目本身大得多。

5.1 成绩统计:按不同字段排序

假设你写一个学生成绩程序,要求支持按学号排序、按姓名排序、按总分排序。最笨的做法是写三个排序函数,然后复制粘贴三份排序逻辑,只在比较那一行做改动。函数指针数组的做法是:写一个通用的排序函数,接收一个“比较器”函数指针参数,再写三个比较函数,分别比较学号、姓名、总分。调用时把对应比较器的地址传进去。

实际做法如下:参与比较的两个学生结构体指针分别为 p1、p2,比较学号时写成return p1->id - p2->id;;比较总分时写成return p1->total - p2->total;。排序算法本身完全不动。这比直接复制排序代码少维护至少 70% 的重复逻辑,出 bug 的概率也大幅下降。练习73教你的是“怎么把函数地址存进数组”,这个场景教你的是“怎么把函数地址作为参数传递”,本质是同一条知识线的两级台阶。

5.2 命令分发:文本菜单系统

写一个文本界面的小工具,用户输入 help、quit、list 等命令,程序分别执行对应处理函数。工程上常用的做法是维护一张结构体表:

struct Command { char *name; void (*handler)(void); };

每个命令对应一个结构体元素,比如{"help", showHelp}、{"quit", quitProgram}。程序收到字符串命令后,遍历这张表,匹配到 name 就调用 handler,匹配失败输出未知命令。

这和练习73简直一模一样,只是把“整数索引查表”换成了“字符串匹配查表”。你甚至可以把表中的 handler 定义成“接收字符串参数并返回整数”的函数指针,用于支持带参数的命令,比如move 100、count 3 5。凡是要做命令解析的地方,几乎都逃不出这个模式。

5.3 回调机制:库函数 qsort 的比较器

很多人用 qsort 时只记住了格式,不知道那个比较器参数到底是什么。实际上qsort(数组, 元素个数, 元素大小, 比较器)的核心就是把一个函数指针交给库函数,让库函数在排序过程中调用它。你提供一个“如何在两个元素之间比较大小”的函数,排序算法就完成了泛型化。

标准库头文件 stdlib.h 里的 qsort 函数,其最后一个参数的类型就是函数指针。写比较器时,示例写法为:

int compareInts(const void *a, const void *b) { int ia = *(const int *)a; int ib = *(const int *)b; return (ia > ib) - (ia < ib); }

然后调用qsort(arr, n, sizeof(int), compareInts);即可。这里的 compareInts 就是一个回调函数。理解了练习73的函数指针赋值与调用,再看这个参数就没什么神秘感了。往后接触 gui 按钮事件回调、网络请求完成回调、定时器回调,底层都是同一件事。

5.4 最后分享一个练习建议

刷这种经典题目,我比较推荐的节奏是:第一遍看着答案把代码敲通,重点标注自己卡住的地方;第二天合上答案默写一遍,你会发现函数指针声明那一行大概率还是错,这恰恰说明知识还没真正长在你身上;过一周再把这题改造成浮点数计算器,顺便增加一个“取余”功能。三遍走完,函数指针数组基本就熟得和 for 循环一样自然了。

经典100例的价值从来不在“刷完”,而在“每个例子背后的那个模式,你能不能在下个项目里想起来用”。练习73这种题,就是给函数指针这个重要模式留的一个锚点,想清楚这层,你就没白做这道题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 5:03:14

工业AI落地资料合集:框架、指南与标准协同使用指南

1. 这套资料合集到底装了什么工业AI这个词这两年热得发烫&#xff0c;但真正落到产线、设备、质检、能耗这些具体场景里&#xff0c;大多数人第一反应是懵的——算法团队不懂工艺&#xff0c;工艺团队不懂模型&#xff0c;中间隔着一道巨大的沟通鸿沟。我见过太多项目死在“技术…

作者头像 李华
网站建设 2026/10/14 5:03:06

MATLAB性能优化与疑难排解实战指南

1. MATLAB性能优化与疑难排解全攻略作为一名长期与MATLAB打交道的工程师&#xff0c;我深知性能瓶颈和诡异报错对工作效率的致命影响。记得有次处理百万级气象数据时&#xff0c;一个未优化的循环让脚本跑了整整8小时——而经过调优后同样任务只需3分钟。本文将分享我积累的实战…

作者头像 李华
网站建设 2026/10/14 5:02:35

研发团队结构化知识管理与智能问答实战:告别文档吃灰

研发团队的知识管理一直是个老大难问题&#xff0c;尤其是当团队从几个人扩张到几十个人之后&#xff0c;你会发现一个很尴尬的现象&#xff1a;文档不是没有&#xff0c;而是没人看、没人更新、更没人找得到。我最近在一个二十多人的研发中心试点了一套叫 PandaWiki 的内部知识…

作者头像 李华
网站建设 2026/10/14 5:01:56

Power Query描述性统计:M语言函数与批量模板实战指南

做Power BI的人基本都遇过这类时刻&#xff1a;数据清洗已经在Power Query里做完了&#xff0c;下一步想快速知道当前这张表里各数值字段的均值、中位数、标准差大概长什么样&#xff0c;有没有异常高或异常低的离群值。多数人的第一反应是把数据复制回Excel算一遍&#xff0c;…

作者头像 李华
网站建设 2026/10/14 5:01:36

Cesium自定义影像图层变形与空白问题排查实战

做 Cesium 开发这几年&#xff0c;被影像图层折腾的次数真不少。尤其是自定义影像源&#xff0c;地图上的影像要么拉伸得不像样&#xff0c;要么边缘全是空白&#xff0c;偶尔还会出现整块扭曲变形。前一阵子给某个三维可视化项目对接本地影像数据&#xff0c;这三种症状一次性…

作者头像 李华
网站建设 2026/10/14 5:01:09

系统性能指标全解:从响应时间到资源利用率的实战指南

技术圈聊架构&#xff0c;最后都会落到一个很朴素的问题上&#xff1a;系统到底扛不扛得住。而判断"扛不扛得住"&#xff0c;靠的不是感觉&#xff0c;是一系列量化的性能指标。这些年我参与过不少系统的架构评审、性能压测和线上问题排查&#xff0c;见过太多"…

作者头像 李华