指针与函数

官方信息技术老师·19 页·深入(追求细节与边界)·0 次浏览·2 天前
函数指针回调机制类型签名调用约定

指针与函数

看清函数指针与数据指针的本质差异,掌握声明、调用与回调

按 空格/→ 演示下一步

1 / 19 页

全部页面点击任意一页,跳回舞台从这页播放

函数指针回调机制类型签名调用约定

指针与函数

看清函数指针与数据指针的本质差异,掌握声明、调用与回调

1第 1 页 · 指针与函数

指针能指向什么

上一页我们让指针指向函数——这背后其实藏着一个更基本的事实:函数也有地址。今天我们顺着这条线往回追问一次:指针这个东西,到底能指向哪些东西?

数据对象
变量、数组、结构体成员,能取地址就能被指
函数
函数体在代码段,函数名就是它的入口地址
堆内存
malloc/new 申请的堆空间,没名字但有地址
空指针 NULL
合法值,明确表示「哪里都不指向」
另一枚指针
指针也是变量,可被二级、多级指针指向
书签对应 →指针

书签不是书的内容,只记一页的位置,插在哪本就指向哪本

2第 2 页 · 指针能指向什么

为什么要指向函数

上一页说指针可以指向函数,但「能」不等于「值得」。真正的问题是:什么场景下必须把函数当数据来传?三个核心用途:回调、动态分发、抽象接口。

回调
把函数当作参数传给别人,让它在合适的时机替你调用
动态分发
调用哪个函数不在写代码时定,而是运行时根据数据决定
抽象接口
对外暴露统一的调用入口,内部实现可替换,调用方不必关心
遥控器按键对应 →函数指针

按键是固定入口,触发哪个函数可以随时重新配置

3第 3 页 · 为什么要指向函数

函数指针的类型长什么样

上一页我们谈了为什么要让指针指向函数——回调、跳转表、动态行为。光有动机还不够,编译器必须先知道:我要指向的函数长啥样。

返回值类型
函数执行完返回什么数据,决定调用结果能赋给谁
(*指针名)
星号和名字必须被小括号包住——是指针而非函数的标记
参数列表
调用时要传几个、什么类型的参数进去
括号不能省
int *fp(int) 是函数;int (*fp)(int) 才是函数指针
插座规格对应 →函数指针类型

返回值=输出电压,参数列表=插孔形状,决定什么电器能插进来

返回类型  (指针名)(参数列表)\text{返回类型}\;(*\text{指针名})(\text{参数列表})
4第 4 页 · 函数指针的类型长什么样

声明语法详解

c

一段 C 代码逐符号拆解 `int (*pf)(int, int)` 的声明,并展示调用方式。

代码高亮加载中…

L7 是核心:`int` 返回值、(*pf) 强调 pf 是指针、(int,int) 是参数列表。L10-L12 展示赋值与两种等价调用写法。

5第 5 页 · 声明语法详解

函数指针 vs 函数原型

写法相似但身份完全不同:原型声明函数,指针声明装着函数地址的变量。

函数原型
  • 声明的是一个函数本身
  • 编译期签名契约,不占运行内存
  • 作用是让编译器允许你调用
函数指针
  • 声明的是一个变量(装函数地址)
  • 运行时对象,占内存存地址
  • 作用是间接调用、传递、切换函数
需要声明一个函数时写原型;需要持有地址做间接调用时写函数指针。混用会编译失败或运行错乱。
6第 6 页 · 函数指针 vs 函数原型

函数名就是地址

前面几页我们写声明、画括号,把类型对齐了。现在直接问:调用处写 func,编译器拿到的是函数本身,还是它的地址?

自动转指针
表达式里出现函数名,编译器自动转成函数指针
func 即 &func
对函数而言,写 func 与 &func 值与类型完全相同
与数组不同
数组名与 &数组名 类型不同;函数没这个区分
调用自动解引用
pf(args) 与 (*pf)(args) 等价,调用语法会帮你解引用
说「去小王家」对应 →直接写函数名

名字本身就指明地点,不必额外「取地址」

func&func\texttt{func} \equiv \texttt{\&func}
7第 7 页 · 函数名就是地址

赋值与调用示例

c

从定义函数到通过指针调用,一段最小可运行代码完整跑通四个步骤。

代码高亮加载中…

L8 声明一个空指针 fp;L9 把函数名 add 这个地址塞进去,指针"有内容";L10 用 fp(3,4) 即可调用,效果和 add(3,4) 完全一样——这是赋值与调用的完整闭环。

8第 8 页 · 赋值与调用示例

两种调用方式等价性

c

运行结果说明:(*pf)(3,4) 和 pf(3,4) 输出完全相同。

代码高亮加载中…

函数调用运算符 () 本就自带解引用,所以 * 是冗余但合法的——编译器把两者当成一回事。

9第 9 页 · 两种调用方式等价性

回调函数机制图解

左侧用户代码,右侧库代码。重点看 l4→u3 那根"回"字箭头——库通过存贮的指针,把控制权反向交还用户。

图解渲染中…
u2注册阶段:用户把函数指针交给库l2指针的存贮位置(栈/堆/全局)l4→u3这就是"回调"——库反向调用用户代码e1触发源:数据就绪、定时器、信号等
10第 10 页 · 回调函数机制图解

qsort中的回调实战

c

用 qsort 把整型数组降序排:自定义比较器作为函数指针传入。

代码高亮加载中…

qsort 第四参期待特定函数指针类型,cmp_desc 签名匹配后函数名自动 decay 成指针传入——这正是回调的本质:把比较规则从算法骨架里"挖"出来交给调用方。

11第 11 页 · qsort中的回调实战

函数指针数组

c

用状态编号索引函数指针数组,依次执行三个状态处理函数,展示状态机的分发表。

代码高亮加载中…

table[state] 先按当前状态选中函数,再调用并取得新状态;数组下标同时负责选择处理函数和驱动状态转移。

12第 12 页 · 函数指针数组

返回指针的函数声明

前面学了用指针调用函数。现在反过来:函数本身能不能返回一个指针?很多场景下函数不直接给出值,而是返回一个『地址』让调用者自己取——比如 malloc、字符串查找函数。

返回类型可以是指针
int* func() 表示这个函数返回一个 int 指针
星号属于返回类型
int* a, b 中只有 a 是指针,b 是普通 int
常见使用场景
动态内存分配返回值、字符串查找、容器取元素
读法:从右往左
int* func() 读作 func 是函数,返回 int 指针
生命周期陷阱
不能返回局部变量地址,函数结束栈帧就销毁
图书馆管理员对应 →返回指针的函数

管理员不递书,只给写着书架号的纸条——书被下架纸条就失效

int* func()    func 是函数,返回 int*\texttt{int* func()} \;\Longrightarrow\; \text{func 是函数,返回 int*}
13第 13 页 · 返回指针的函数声明

正确返回指针的场景

c

三个返回指针的合法姿势:静态常量、堆分配、调用者传入的缓冲区。

代码高亮加载中…

返回值有合法归宿——静态区永生、堆上 malloc/free 配对、或者就是调用者自己那块地皮。共同点:函数返回时那块内存还活着。

14第 14 页 · 正确返回指针的场景

返回局部变量的危险

c

用一段经典错误代码看:返回栈上变量地址后会发生什么。

代码高亮加载中…

函数结束后栈帧立即回收,&local 成了悬空地址;继续解引用 *p 即触发未定义行为。

15第 15 页 · 返回局部变量的危险

返回指针 vs 返回值

函数可以返回「值」也可以返回「指针」,对大对象而言差异显著——性能、所有权、失败表达都不一样。

返回值
  • 在栈上拷贝一份,调用者独占
  • 函数退出即确定,无需关心释放
  • 失败靠返回特定值(如 0、-1),需文档约定
返回指针
  • 只把地址交出去,零拷贝
  • 谁负责释放、何时失效要约定清楚
  • 返回 nullptr 即失败,类型自洽
对象小、需作用域自动管理 → 返回值;对象大、或需表达「可选/失败」→ 返回指针并明确所有权。
16第 16 页 · 返回指针 vs 返回值

综合示例:计算器

c

把函数指针数组与回调合在一起:一个循环跑完四则运算。

代码高亮加载中…

typedef 让声明变干净、数组把函数捆成一捆、回调参数把『何时调用』交给被调方——三者组合即可替代一连串 if/else。

17第 17 页 · 综合示例:计算器

自测题

点击作答

函数返回其内部局部变量的地址,调用方用这个指针访问时会怎样?

18第 18 页 · 自测题

课后思考

三个开放问题,探讨函数指针的深层边界

课后思考
三个开放问题,探讨函数指针的深层边界
19第 19 页 · 课后思考
指针与函数 · 知识图解