C语言_函数指针、指针函数
243.1.1C语言_函数指针、指针函数
float *fun() /* 课程原例:fun 是一个返回 float 指针的函数;旧式空括号不等于严格声明“无参数”。 */
{ /* 函数体开始。 */
/* 函数内容 */ /* 原视频中的占位注释。 */
float *a; /* 定义一个指向 float 的指针变量,但此时尚未初始化。 */
float b=3; /* 定义自动存储期的局部变量 b,它只在本次函数调用期间存活。 */
a=&b; /* 让 a 指向局部变量 b。 */
return a; /* 危险:函数返回后 b 的生命周期结束,调用者得到悬空指针。 */
} /* 离开此处时局部变量 b 不再存活。 */
int main() /* 课程原例:在现代代码中,无参数通常明确写成 main(void)。 */
{ /* main 函数体开始。 */
float *p; /* 定义接收 float 地址的指针变量。 */
p = fun(); /* p 得到已经失效的局部对象地址;后续不能通过 *p 访问。 */
} /* 托管 C 环境中从 main 末尾退出等价于返回 0;嵌入式入口行为由运行环境决定。 */
这段代码来自视频中的“指针函数”示例。它能说明声明形式,却包含典型的生命周期错误,因此不能作为可用代码照抄。函数若要返回指针,必须保证所指对象在调用者使用期间仍然有效。
一、C语言知识补充
RT-Thread 和 Linux 内核代码大量使用指针、结构体、链表、回调函数和动态内存。掌握这些概念的目标是能追踪对象的类型、地址、所有权和生命周期,而不只是背诵“函数指针带括号、指针函数不带括号”。
本组课程还会补充链表、内存分配、大小端以及 static、const、volatile 等关键字。本节只聚焦两个容易混淆的声明:
float *fun(void):fun是函数,返回float *,常被称为“指针函数”或“返回指针的函数”。float (*p)(void):p是指针,指向一个返回float的函数,即函数指针。
阅读复杂声明时从标识符向外看:fun 后面先遇到 (),所以它首先是函数;p 被 (*p) 括住,先与 * 结合,所以它首先是指针。括号不是记忆口诀,而是在改变声明符的结合顺序。
1.1 指针
指针对象保存某种可寻址实体的位置,并带有指向类型。对象指针、函数指针、空指针以及失效指针的使用规则不同。类型决定编译器如何检查调用或解引用,生命周期决定该地址现在是否仍可使用。
void * 是通用的对象指针,可以在 C 中与其他对象指针互相转换;它不能在严格可移植 C 中被当作通用函数指针。回调参数使用 void *,是为了传递任意对象上下文,并不是说任何地址都能不受约束地混用。
1.1.1 函数
函数定义描述参数、返回类型和函数体。编译、链接后,未被优化掉的函数通常形成可执行代码,并由链接器放入代码段;但源代码中的“定义一个函数”不能简单等同于运行时“开辟一块内存”。编译器可能内联、合并、拆分或删除不可达函数。
在大多数表达式中,函数标识符会自动转换为指向该函数的指针。因此,把 add 或 &add 赋给兼容的函数指针会得到等价结果。
更严谨的说法是“add 在赋值表达式中转换为函数指针”,而不是无条件地说“函数名永远就是首地址”。调用方必须通过兼容的函数类型调用;把地址强制转换成不匹配的参数或返回类型再调用,会产生未定义行为。
数组名与函数名都存在表达式中的自动转换,但规则并不完全相同。数组有元素和对象大小,函数不是对象,不能对函数使用 sizeof,也不能像数据那样读写函数体字节。
1.1.2 指针函数
“指针函数”不是 C 标准中的独立语法类别,通常只是“返回值为指针的函数”的简称。例如 float *find_value(float *buffer, unsigned int count); 表示 find_value 是函数,返回一个指向 float 的指针。
返回指针时必须先回答“它指向谁、谁负责释放、能用多久”:
- 指向调用者传入的缓冲区:生命周期由调用者保证。
- 指向静态或全局对象:程序运行期间有效,但共享访问可能需要同步。
- 指向动态分配对象:在释放前有效,接口应说明由谁
free。 - 指向被管理对象:按库的句柄/引用规则使用。
- 指向本函数的普通局部变量:函数返回后立即失效,不能返回给调用者使用。
本文开头的 b 是自动存储期局部变量,fun() 返回后它的生命周期结束。即使调试时栈上的数值暂时还是 3,解引用 p 仍属于未定义行为;下一次函数调用或中断随时可能覆盖那片栈空间。正确教学示例应改成由调用者传入存储、返回静态对象,或在明确所有权后返回动态分配内存。
另外,课程写的是 float *fun()。在课程使用的 C 版本中,空参数列表表示“参数未指定”;明确无参数应写 float *fun(void)。这与 C++ 中 fun() 表示无参数不同。
1.1.3 函数指针
函数指针保存某一类函数的地址。完整类型同时包含返回类型和参数类型;只有类型兼容的函数才能安全赋值并间接调用。常见用途包括回调、状态机动作表、中断向量、驱动操作表和 RTOS 线程入口。
int (*p)(int, int) 的解析顺序是:p 先与 * 结合,因此 p 是指针;外层 (int, int) 说明它指向函数;最左侧 int 是该函数的返回类型。若去掉关键括号写成 int *p(int, int),p 就变成“返回 int * 的函数”。
1.1.3.1 例题: 函数指针赋值使用
视频中的示例为:
int add(int a,int b) /* 普通加法函数:参数为两个 int,返回 int。 */
{ /* add 函数体开始。 */
return a + b; /* 返回两个参数之和。 */
} /* add 函数体结束。 */
int (*p)(int ,int); /* 定义 p:它指向“接收两个 int、返回 int”的函数。 */
p = add; /* 把兼容函数 add 的地址保存到 p;这里不需要写 &add。 */
int a = (*p)(10,10); /* 经 p 间接调用 add,结果为 20;也可以写成 p(10, 10)。 */
最后三行在视频中是用于解释声明的片段,没有放进完整函数体;若要单独编译,应把赋值和调用语句放入函数中。全局作用域只允许声明/定义,不能直接出现普通赋值语句 p = add;。
(*p)(10, 10) 中的 * 可以省略,因为函数调用运算符会按函数指针完成调用,p(10, 10) 更常见。真正不能省略的是声明里的括号:int (*p)(int, int) 与 int *p(int, int) 类型完全不同。
工程中常用 typedef 给函数指针类型取别名。例如 typedef int (*binary_op_t)(int, int); 把这一签名命名为 binary_op_t,后续变量仍只能保存兼容的函数地址。
函数指针让调用者选择行为,而被调用框架不必知道具体实现。例如排序函数接收比较回调,驱动框架保存设备操作函数,状态机用表项选择当前状态的处理器。间接调用带来灵活性,也要求回调签名、上下文指针和生命周期保持一致。
1.1.3.2 例题: RT-Thread函数指针应用
课程使用 RT-Thread 5.0.2 的 rt_thread_create() 说明真实用法,其源码声明是:
rt_thread_t rt_thread_create(const char *name, /* name 指向线程名称字符串,const 表示函数不应通过它修改字符。 */
void (*entry)(void *parameter), /* entry 是线程入口函数指针:返回 void,接收一个 void * 参数。 */
void *parameter, /* parameter 是传给 entry 的用户上下文对象指针。 */
rt_uint32_t stack_size, /* stack_size 指定动态分配的线程栈大小,单位为字节。 */
rt_uint8_t priority, /* priority 指定线程优先级;RT-Thread 中数值越小优先级越高。 */
rt_uint32_t tick) /* tick 指定同优先级线程之间轮转使用的初始时间片,单位为系统 tick。 */
第二个参数 entry 正是函数指针。传入的线程入口必须兼容 void entry(void *parameter);第三个参数会原样交给该入口,使同一个线程函数可以处理不同对象。线程并不“本质上只是一个函数”:它还包括线程控制块、独立栈、入口与参数、优先级、状态、剩余时间片、内置定时器等运行上下文。
rt_thread_create() 自身又是一个返回指针的函数。课程源码定义:
typedef struct rt_thread *rt_thread_t; /* rt_thread_t 是指向线程控制块 struct rt_thread 的句柄类型。 */
因此 rt_thread_create() 成功时返回新分配的线程对象指针,失败时返回 RT_NULL。源码内部先用 rt_object_allocate() 分配线程控制块,再为线程栈分配内存并调用 _thread_init();创建成功后线程仍需 rt_thread_startup() 才进入调度。
课程把 struct rt_thread 的一百多行条件编译源码理解为“线程的很多属性”,方向是对的,但结构体实际大小取决于配置。比如是否启用 SMP、互斥量、事件、信号、CPU 使用率、Pthreads 和 RT-Smart,都会决定哪些成员真正编译进去。rt_thread_t 只是指针别名,不会把整份结构体复制给调用者。
时间片也需要精确理解:
- 调度器始终优先选择最高优先级的就绪线程;高优先级线程就绪时可抢占低优先级线程。
- 时间片主要用于同优先级、同时就绪线程之间的轮转,并不强迫所有线程按固定顺序各跑一遍。
- 线程阻塞、延时或主动让出时,可以在时间片耗尽前离开 CPU。
tick的单位是系统节拍,不天然等于毫秒。本课程rtconfig.h配置RT_TICK_PER_SECOND = 1000,所以这里 1 tick 才对应约 1 ms。
课程建议不要修改内核源码,适合作为初学阶段的工程纪律:业务代码优先放在 applications/,升级 SDK 时更容易维护。但 RT-Thread 是开源软件,做 BSP 移植、问题定位或向上游修复时可以修改内核;正确做法是理解影响、保留补丁、增加验证并尽量向上游反馈,而不是把官方代码视为永远不会出错。
学完本节应能独立回答:float *fun(void) 和 float (*fun)(void) 各是什么;为什么不能返回普通局部变量地址;rt_thread_create() 的第二、第三个参数怎样配合;rt_thread_t 为什么既是返回类型又表示一个线程句柄;时间片为何只在同优先级就绪线程间起轮转作用。