C语言_Misc(上)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

248.1.6C语言_Misc(上)

const int count = 1;          // 视频中的 const 实验:count 经初始化后不能再通过这个标识符修改。
while (count++)               // 视频故意对只读对象执行后缀自增,编译器因此报告“increment of read-only variable”。
{                             // 循环体开始;只有删去 const 或删去自增操作,这段实验代码才能通过编译。
    rt_thread_mdelay(1000);   // 当前 RT-Thread 线程延时约 1000 ms。
}                             // 循环体结束。

static int i = 0;             // 视频回看 51 串口中断代码:i 具有块作用域,但其值能在多次中断调用之间保留。

static int wfeng;             // 视频的文件作用域实验:static 使 wfeng 具有内部链接,只在当前翻译单元中可见。
extern wfeng;                 // 视频在另一头文件中临时写下的原句;它省略了类型,现代 C 应写 extern int wfeng,但仍不能引用上面的内部链接对象。

一、Misc 的含义

Misc 是 miscellaneous 的缩写,意为“杂项”。本课把几个篇幅较短、但在阅读嵌入式源码和面试中经常出现的 C 语言知识放在一起讲。本篇视频实际讲到三个主题:const、static 和内联函数;思维导图中露出的大小端、枚举等内容留到下一课。

二、const:给类型增加只读约束

2.1 const 的准确含义

const 是类型限定符。它让编译器禁止程序通过这个受限定的左值修改对象,可以尽早发现“这里本来只应读取,却误写了数据”的错误。它首先是一条语言层面的访问约束,并不等于对象一定存进物理只读存储器:局部 const 对象仍可能位于栈或寄存器,全局只读对象在本工程中则通常会进入 Flash 中的只读段。

如果对象本身确实被定义为 const,即使强制转换指针后绕过编译检查,写入它仍属于未定义行为;对象也可能恰好位于不可写的 Flash。因此,强制去掉 const 不能把只读对象安全地变成可写对象。

2.2 视频中的编译错误

视频把原来的 int count = 1; 临时改为 const int count = 1;,但循环条件仍为 while (count++)。后缀 ++ 不是单纯读取:它要先取得旧值,再把 count 加 1 写回。于是 GCC 报出 increment of read-only variable 'count',错误位置就在 count++。

视频随后把循环条件临时改成不修改 count 的常量表达式,工程就能编译。这说明编译器阻止的是“通过只读限定的对象名进行写操作”,读取它参与判断或计算没有问题。

2.3 const 修饰变量、参数和返回类型

  • 修饰对象:const int count 表示不能通过 count 这个左值给对象重新赋值或自增、自减。
  • 修饰值传递参数:函数内部不能修改该形参的副本。由于值传递本来就不会改动调用者的实参,这种写法主要用于表达函数内部的设计意图。
  • 修饰指针参数所指对象:例如链表源码的 const rt_list_t *l 表示不能通过 l 修改它所指向的链表结点,适合只读查询函数。
  • 修饰返回类型:要先辨认它修饰的是返回对象还是指针所指对象。返回 const T * 常用于声明“调用者不应通过该指针改写 T”;给按值返回的标量加顶层 const 通常没有实际价值。

读指针声明时可以从变量名向外看:

声明含义可以做什么
const int *p 或 int const *p指向只读 int 的指针可以让 p 指向别处,不能通过 *p 写值
int *const p只读的指针不能改变 p 的指向,可以通过 *p 改写所指的非 const 对象
const int *const p只读指针,且所指对象也只读指向和值都不能通过 p 修改

课程打开的 RT-Thread rt_list_len 声明为 rt_inline unsigned int rt_list_len(const rt_list_t *l)。这里的 const 限定的是 l 所指的 rt_list_t,不是指针变量 l 自身。函数内部另有 const rt_list_t *p = l,随后执行 p = p->next:改变的是指针 p 保存的地址,并没有通过 p 改写链表结点,所以不违反 const。

const 能加强接口约束,但它不自动解决并发访问、对象生命周期或硬件寄存器访问问题。对可能被中断、DMA 或硬件改变的对象,后续还会涉及 volatile;const volatile 也可以同时出现,分别表达“本段代码不写”和“值可能在代码之外变化”。

三、static 的三种常见用法

同一个 static 关键字会根据声明位置产生两类核心效果:对块内局部变量,它改变存储期;对文件作用域的变量或函数,它改变链接属性。课程把它概括成“修饰局部变量、全局变量、函数”三种情形。

3.1 修饰局部变量:值跨调用保留

视频回看了 51 单片机串口接收中断函数中的 static int i = 0;。i 的名字仍只有所在代码块能够使用,这叫块作用域;但它具有静态存储期,程序整个运行期间只有这一份对象,不会在每次进入中断函数时重新创建。因此,上一次接收后留下的索引值可以在下一次串口中断到来时继续使用。

把静态局部变量说成“变成全局变量”不够准确。它只是寿命和全局对象一样长,名字仍被限制在声明它的代码块中。两者应分开记:

属性普通自动局部变量静态局部变量
作用域所在代码块所在代码块
存储期每次进入代码块时存在整个程序运行期
初始化每次执行到定义附近形成相应初值程序启动阶段初始化一次
多次调用通常不保留上次的值保留同一对象中的值

课程把 static int i = 0; 归到已初始化数据段的说法也需要修正。C 语言保证静态存储期对象在启动时完成初始化,但不规定 ELF 节名。对于值为零的对象,GCC/链接器通常把 static int i = 0; 与 static int i; 都放进 .bss,以免在 Flash 中保存四个零;非零初值通常才需要 .data 的初值镜像。最终结果要以当前 map/ELF 文件为准。

静态局部变量还会让函数变得不可重入:多个线程或中断嵌套调用同一函数时,它们访问的是同一份对象。若它会被并发读写,就要分析是否需要关中断、原子操作、互斥量或重新设计状态归属,不能只因为“值能保留”就直接使用。

3.2 修饰文件作用域变量:内部链接

文件作用域下的 static int wfeng; 具有内部链接。它的名字只在当前翻译单元中可见。翻译单元不是单指一个 .c 文件,而是该 .c 文件经过预处理、把所包含的头文件展开后的整体。

其他 .c 文件中的 extern int wfeng; 声明期望找到一个具有外部链接的同名定义,不能指向这个内部链接对象。若没有其他外部定义,最终通常表现为链接阶段的未定义符号。当前视频却把 extern wfeng; 临时写进了会被 main.c 包含的 rtdbg.h,随后又在同一翻译单元定义 static int wfeng;,所以编译器更早报告 static declaration of 'wfeng' follows non-static declaration:前面的外部链接声明与后面的内部链接声明冲突。

还要注意,视频中的 extern wfeng; 省略了类型。旧式 C 曾允许隐式 int,画面中的 GCC 也先给出 type defaults to 'int' 警告;现代 C 已不允许这种写法。正确的外部声明语法是 extern int wfeng;,通常应放在专用头文件里,但它仍只能配对一个非 static 的外部定义。

文件作用域 static 的价值是隐藏模块实现细节、减少跨文件命名冲突。它并不禁止同一文件中的其他函数访问变量;限制的是其他翻译单元通过这个名字进行链接。本课 03MEM 的 wfeng 没有被使用,当前归档构建还启用了节垃圾回收,因此它对应的输入节最终被链接器丢弃。

3.3 修饰函数:函数只供本翻译单元使用

文件作用域函数加 static 同样获得内部链接。当前 .c 文件里的函数可以正常调用它,其他 .c 文件不能用同名外部声明链接到它。驱动源码常用这种方式隐藏只服务于本模块的辅助函数,并把真正需要跨模块调用的接口声明在头文件中。

四、内联函数

4.1 RT-Thread 中的 rt_inline

视频在 rtservice.h 中查看了 rt_list_remove、rt_list_isempty 和 rt_list_len。这些短小且调用频繁的链表操作前都带有 rt_inline。继续追到 rtdef.h 可以看到,在本工程支持的不同编译器分支中,它会展开为 static inline 或 static __inline:

  • static 让头文件中的函数在每个使用它的翻译单元内保持内部链接,避免多个外部同名定义互相冲突;
  • inline 或编译器形式的 __inline 告诉编译器,这个函数适合考虑内联展开。

这里查看 RT-Thread 库源码是为了解释关键字,不把整个链表库函数复制成本课业务代码;第 244 篇已经逐行整理了这些函数的实际实现。

4.2 内联发生在编译优化阶段

内联后,编译器可以把被调用函数的操作放到调用点附近,从而消除一次普通函数调用可能需要的跳转、返回和寄存器保存等开销,并获得跨函数常量传播等优化机会。课程把它类比为“把函数体直接替换到调用位置”,适合作为结果示意,但它不是预处理器在编译前做的文本替换;头文件的 #include 展开和宏替换才属于预处理阶段。

inline 只是对编译器的提示或影响 C 的定义规则,不保证一定展开。编译器可能因为函数过大、递归、需要保留函数地址、优化级别较低等原因仍生成真实调用;反过来,优化器也可以内联没有写 inline 的普通函数。判断是否真正内联,应查看优化报告、反汇编或 map 文件,而不能只看源码关键字。

4.3 时间和空间的权衡

把多个调用点都展开,常会增加机器码体积,因此内联经常被概括为“用空间换时间”;代码体积增大还可能降低指令缓存命中率。这个规律也不是绝对的:极短函数的调用序列本身可能比展开后的指令更大,内联再配合常量折叠和删除无用代码,最终映像反而可能缩小。

实际使用时可以这样判断:短小、调用频繁、位于性能关键路径的函数适合让编译器考虑内联;大函数、递归函数、很少调用的函数通常不应强求。先保证接口和逻辑正确,再用编译产物和测量结果判断是否值得为性能增加代码体积。