C语言与编码规范问答总集
本篇将本目录全部问答帖按知识相关性合并重组,共收录 50 个知识点。同类问题多来源已合并为一条,条目末尾给出原帖与作者。仅图片题目、无回答帖已在原笔记标记
review: 建议隔离,未收录。
知识地图
- 一、类型与内存模型:类型大小 / static 变量 / ZI-RW 数据 / 栈与满减栈 / 变量生命周期
- 二、指针与 const:数组退化 / 双重 const / const 保护数据
- 三、volatile 与编译优化:语义边界 / 优化踩坑 / 为什么要会反汇编
- 四、结构体、联合体与 C 面向对象:联合体应用 / 可见域控制 / 函数指针接口 / 状态机
- 五、函数传参与调用约定:AAPCS / 回调与钩子 / 可重入 / 接口概念
- 六、编译链接与启动:地址确定时机 / map 文件 / 总线取指 / LR 与 Thumb / __main
- 七、编码规范与工程实践:MISRA/华为/Google 资源 / 规范落地 / uint8_t 与 bool / Git
- 八、防御性编程与错误处理:判空 / 资源确认 / 统一异常机制 / 溢出处理
一、类型与内存模型
char/int 等基本类型的长度由平台决定。 C 标准只规定最小长度和大小顺序(char ≤ int ≤ long),不固定字节数:char 恒为 1 字节,int 在 32 位 STM32 上是 4 字节,在 16 位机(如 8051)上可能只有 2 字节。写跨平台代码不能假设 int 就是 4 字节。
— char 类型在 C 标准中是 1 字节(通常为 8 位(Jack)
静态局部变量存放在静态存储区,本质是全局变量。 它定义在函数内,但不在栈上,生命周期与整个程序相同,只是可见性被限制在函数之内。
— 老师,静态局部变量存在哪?它的(Jack)
ZI 数据段不只包含全局/静态变量。 ZI 既含未初始化或初始化为 0 的全局、静态变量,也包含堆区和栈区;此外零初始化变量也可能被放进 RW 数据段(视链接器布局而定)。
— ZI_data 不仅包含 未初始化:初始化为0的全局和静态变量(Jack)
满减栈的含义拆开理解。 "满"指 SP 始终指向有数据的栈地址,必须先移动指针再写数据,否则破坏栈内已有数据;"空"栈则 SP 指向空位置,可先写后移。"减"指 SP 向地址减小方向生长(0x20001234 → 0x20001230),对应还有"增"栈。
— 老师,满减栈什么意思? 满意(Jack)
栈水位可以直接打印栈指针来检测:观察 SP 在运行中的最低点,即可评估栈的实际用量与余量。
— 栈水位的直接打印栈指针来检测(Jack)
用程序块(复合语句)管理变量生命周期。 把解包拆包等大数组处理包在 { } 程序块内,处理完数组随块释放,不占长期内存,也没有 malloc 的风险,代价只是栈要大一点。
— 用程序块来管理变量生命周期(Jack)
初始化后只读的数据用 const 指针保护更安全。 对"初始化前可写、之后只读"的配置数据,可以用 const u16 *p 指向 const u16 值的方式防止误改;代价是多消耗一个指针。也可以改用中间接口(类似队列)传递,让其他任务接触不到数据本体。
— 老师,一个u16变量只在初始化(在线程初始化之前,是上电后初始化)的时候会改(Jack)
声明、定义、构造是三个不同动作。 声明意味着链接时有个符号;定义意味着内存里有地址;构造意味着有数据填充的来源(即初始化)。Linux 里的对应是注册、初始化,C++ 里是定义、构造、初始化。
— 老师,构造的作用和意义是什么?(Jack)
二、指针与 const
数组名赋值给指针时自动退化。 char *fruits[] 是数组,赋给指针时退化为 char **,因为数组名本质是"指向首元素的指针"。
— 指针与数组 char :fru(Jack)
const void * const p 中两个 const 位置不同、约束不同。 前一个 const 修饰指向的数据(*p = 3 非法),后一个修饰指针本身(p = 0x20001234 非法);即"指向只读数据且指针自身不可改"。
— 老师,问一下const void : const pvItemToQueue(Jack)
三、volatile 与编译优化
volatile 的基本语义。 volatile 是类型修饰符,告诉编译器该变量可能被"代码之外"的因素(中断、硬件、DMA)改变:每次读写都必须真实访问内存,不能被省略或合并。它不保证原子性,也不是多线程同步原语。
— volatile 关键字的作用(立芯嵌入式)
volatile 专题(精华长文):从编译器优化讲到正确用法。 编译器会做寄存器分配、死代码消除、常量折叠、公共子表达式消除、指令重排;对被中断/硬件异步修改的变量,不加 volatile 可能读到寄存器缓存的过期值甚至死循环。必须加的场景:内存映射外设寄存器(只读状态寄存器用 const volatile)、中断与主循环共享的标志位、环形缓冲区的索引与计数器。不应滥用的场景:纯局部变量、大数组本身;多线程同步应改用 <stdatomic.h> 原子类型或 RTOS 锁/屏障。
— 在嵌入式开发里,几乎是一个“老生常谈”的关键字(刘子奇)
优化等级改变行为是真实存在的坑。 同样代码在 -O3 下队列接收数据异常、-O0 正常,加 volatile 也无效。工程上的对策:约定统一的优化等级;对关键变量用分散加载放到不被优化的地址;驱动时序用 ISB 等指令清流水线(参考 PendSV/SVC 实现)。
— 老师,我这优化等级和你们demo一样是3,为什么这队列接收进来的数据会变(Jack)
学会看 C 语言反汇编的用处。 编译器背着我们做了很多事,只有看汇编才能发现:同一个变量没加 volatile 时,高优化等级下的汇编处理完全不同——这正是变量"异常"的常见根因。
— 老师,我们学习看C语言的反汇编代码有什么用(Jack)
四、结构体、联合体与 C 面向对象
联合体的典型应用。 union 所有成员共享同一块内存(int i 与 float f 各占 4 字节),常用于同一段数据的多解释视角(如寄存器值按位/按字节拆解、数据类型转换)。
— 联合体的常见应用方式(Lili_iliLX)
C 语言限制结构体成员可见域的办法。 结构体一旦暴露,内部成员全部暴露;要限制可见域,应在文件内定义 static 全局变量,只通过模块接口(带初始化检查等条件)访问这些变量。
— 老师,请问有没有办法限制结构体成员的可见域,让外部文件只能访问结构体内的部分成员(Jack)
函数指针集中封装进结构体是为了稳定接口。 用结构体约定接口后,替换内部的函数指针不影响别人代码;架构改动会迫使所有依赖者跟着改,而接口化把改动限制在一处。
— 老师,有个问题是,为啥就一个函数指针都要写在一个结构体里面(Jack)
static 修饰模块私有函数 + 实例成员调用。 外部只能通过访问模块实例的结构体成员来调用其"私有函数",不能跨文件直呼其名,避免误调核心函数——相当于给贵重物品加了盖子。
— 老师,这样使用static修饰函数,外部只能通过访问aht21实例的成员的形式来调用(Jack)
面向对象先写 .h 后写 .c。 先对事物本质做抽象、归纳特点写成"类"(头文件即接口),函数指针注册不同实现即"对象";这个理解角度得到了认可:函数指针≈抽象出的类,注册的延时函数≈对象。
— 写面向对象的程序时,要先写.h文件,后写.c文件。要对事物的本质进行抽象(Jack)
实例化(inst)与初始化(init/deinit)的职责区分。 实例化只完成接口挂载;初始化涉及内存申请(队列)与硬件使能,这些才需要在 deinit 中对称释放。低功耗失能模块时只释放内存、关闭硬件,控制块结构体不必 free,因为恢复时还要用。
— 挂载inst:register和init的关系(Jack)
面向对象的本质是"用现实分工组织代码"。 定义机构分类=定义接口,建医院=实例化,机构功能与被服务者=数据与方法。继承=新医院继承挂号系统;多态=不同医院收费系统不同但接口一致;封装=外人只能经前台操作病历;抽象=定义"医疗机构基类"。面向过程则是"一个人包办所有事"。
— 到底是谁把面向对象编程搞得这么(Jack)
函数指针可读性差的解法是改变阅读方式。 熟练后应顺着结构体定义(目录)去找注册点、理解作者的对象思路,而不是靠 IDE 跳转逐个找实现。
— 习惯就好了,用多了,有技巧了,(DemonWu 问 / Jack 答)
C 语言实现状态机的三种方法。 1. switch-case 法(最常用);2. 表格驱动法;3. 函数指针法。
— C语言实现状态机有三种方法(Jack)
五、函数传参与调用约定
ARM 架构的参数传递规则。 参数不超过 4 个时用通用寄存器(R0-R3)传递;超过 4 个的部分用栈传递。
— ARM架构传参方式: 常规压(Jack)
AAPCS 要求栈 8 字节对齐,但不总是"看起来"对齐。 被调函数不用栈空间时,编译器可能不做对齐;一旦判断函数需要动用栈,就会加 padding 使入口处 SP 对齐 8 字节——所以实测 4 字节对齐也可能是正常现象。
— Q: 在网上看到 AAPCS 标准要求函数和中断入口时(花烛想迟到 问 / Jack 答)
回调函数与钩子函数不是一回事。 钩子是"做某件事时顺带做的附属动作",如任务切换时顺带打印当前与目标任务名;回调是把函数 B 作为参数传给函数 A、由 A 在合适时机调用。
— 老师,回调函数和钩子函数意义不(Jack)
回调函数的三组概念(精华)。 中断服务函数是启动时绑定固定地址、由硬件触发的函数;ISR 里默认执行的 __weak 回调(如 HAL_ADC_ConvCpltCallback)只是弱定义静态回调,真正的回调要经 HAL_ADC_RegisterCallback() 把函数指针注册进句柄,中断时经指针调用,多一步解引用但更灵活。同步回调:调用方阻塞等回调执行完;异步回调:调用方继续往下走,回调独立执行。
— 回调函数与钩子函数详解(Jack)
可重入函数的要求。 可重入指函数被多个任务或中断嵌套同时调用也不会把状态搞乱:不依赖共享的全局/静态变量、不返回静态缓冲区地址、不调用不可重入库函数(如某些 printf/malloc);纯计算函数天然可重入,用了共享资源就要加锁或做局部拷贝。
— 问题:什么是可重入函数? 回答(Jack)
函数名不需要死记,通用性要靠函数指针。 函数命名有规律(__ 前缀多表示靠近底层的内部函数);换平台时函数名必然不同,通用接口的做法是用同形参的函数指针代指一类函数,切平台时切换指针指向。
— 我有个问题,我看很多人在讨论技术的时候,都会讲某个具体的函数名。我不了解这些函数名(Jack)
接口就是对外提供的函数。 接口即 API:你提供一个函数让别人调用做事,这个函数就是接口。函数指针可以存放 API 和接口的地址;函数指针中存的地址与函数名背后的地址本质是同一个地址。
— 老师,什么是接口? 前面我看视频的时候理解的接口,就是API函数,但是有些逻辑理不清(Jack)
六、编译链接与启动
全局变量的地址在链接阶段确定。 .o 文件里只存放全局变量相对本文件的 offset,链接器再根据链接脚本确定绝对地址,最终回填到汇编码中。
— 全局变量的地址,是在编译阶段还是链接阶段确定(Jack)
查看函数(段)大小去 map 文件里找。 函数 A、B 分别用 section("sram1")、section("sram2") 定位到 SRAM 后,各自大小可以在 map 文件中查到。
— 在map文件中有(像大海一样 问 / Jack 答)
map/镜像信息里 size 的单位是字节。
— 老师,这个size的单位是什么(Jack)
I-Code-Bus 与 D-Code-Bus 的分工。 I-Code 专门取指令:从 Flash/SRAM 取指送译码再进 ALU,运算后更新 LR、PC 等;D-Code 取数据:把 Flash 中的 const 常量取入通用寄存器(不含 PC/LR)。SRAM 能取指令是因为启动时可把 SRAM 映射到 0x00000000 进入 I-Code 空间,下载算法中会用到。
— I-Code-Bus, D-Code-Bus有什么区别?它们在如何取指令和数据(Jack)
LR 最低位为 1 表示 Thumb 状态。 LR 保存返回地址,其最低位是 Thumb/ARM 模式标志(Thumb 16 位、ARM 32 位);返回时硬件自动清除最低位得到真实跳转地址——所以看到的 LR 地址常以奇数(如 9)结尾。
— 老师,LR保存函数的返回地址,我理解为函数跳转前的PC指针的值(Jack)
栈、堆与 __main 的初始化时机。 复位时硬件把向量表[0] 的初始栈顶装入 MSP,调用栈早已可用;__main(ARMCC)/__libc_init_array(GCC)负责 .data 搬运、.bss 清零、C++ 全局构造,最后进 main。栈空间由链接脚本静态预留,"填色"(如 0xA5)要自己在启动文件里做;堆边界由 _end 等符号确定,真正的分配机制在第一次 malloc 时才建立。
— 关于堆栈和__main()在什么时候初始化(Jack)
七、编码规范与工程实践
三份官方规范资源。 华为《C语言编程规范》全文 PDF;兼容 MISRA C 的 Barr C 编码标准(2018,中英对照);Google 代码风格指南(中英双语,建议参考原版仓库)。
— 华为C语言编码规范(Jack)、嵌入式C编码规范,兼容MISRA C,中英对照(立芯嵌入式)、Google的代码风格指南,中英双语链接都在下面了,有能力的同学建议参考原版仓库(立芯嵌入式)
注释规范要点。 // 是被接受的注释形式(替代传统 /* */);注释应避免包含预处理相关内容(完整规范见原长文)。
— 嵌入式C代码规范(二)——注释篇(立芯嵌入式)
公司没有代码规范怎么办。 好代码必须有自解释性,不守规范是在破坏团队劳动成果;成熟公司会把规范与开发流程绑定(如 git 提交的 fix/feat 约定、注释规范),制定时多参考 Google、华为规范,特定领域再叠加(如汽车电子的 MISRA C)。规范是经验的凝结,自动化后就是静态检查。
— 大佬们,我想咨询一下,你们写嵌入式代码的时候,“代码规范“有没有遵循哪个文档(Jack)
用 uint8_t 代替 bool 的规范值得认同。 理由:1. bool 到 C99 才确定且各编译器支持不一,uint8_t 跨平台能力更强;2. bool 在 32 位机上可能按 4 字节对齐而浪费空间;3. uint8_t 能表达 0-255 的更多 API 状态,便于扩展(CMSIS Driver 即如此)。代价是需要约束:设计 API 时明确各返回值对应的状态。
— 是否认同对于用uint8_t代替bool的规范(Jack)
技术结论要有"5 连问"意识。 结论怎么来的?参考依据是什么?有什么数据支撑?为什么有这个结论?为什么这么设计?
— 嵌入式5连问 结论是怎么来(Jack)
Git 分支是指针不是文件夹。 新建/切换分支只更新工作区文件内容并记录提交指针,不会创建同名文件夹;所有分支共享同一工作目录,由 .git 目录区分,git branch 可查看当前分支(带 * 标记)。
— 在 Git 中,分支是一个指向提交的指针,而不是一个实际的文件夹或目录。因此(Jack)
大数据量拷贝的企业级优化。 无法零拷贝时 memcpy 常成为标配,但微库的 memcpy 在高实时场景可能不够用;可用纯数据拷贝优化方案提升大块数据传输效率(方案见原长文)。
— 在嵌入式开发场景中,项目常面临大数据量传递的挑战(zzz)
八、防御性编程与错误处理
用指针必须先判空。 无论前面业务逻辑是否已保证非空,使用处仍要判断——这是代码习惯,不判断不能用。
— 为什么老师的代码里面在反复判断指针是否为空(Jack)
调用 API 前先确认其依赖资源已初始化。 "判断两次"是代码规范的要求:先确保 API 所需资源初始化成功,再去取值,否则取到的可能是脏值。
— 老师,这里为什么要判断两次?(Jack)
系统集成后用统一异常机制取代层层返回值。 集成后调用链变长,若每层都返回状态,错误处理代码会重复冗余;大型架构通常由系统层统一管理各模块错误码,porting/wrapper 层不再逐层返回状态。
— 老师你好,我想问问为什么系统集成后的porting层和warpper层都不要返回值表(Jack)
tick 计数回绕并不可怕,取模运算天然正确。 u16 计数从 65535 加 1 变 0 后,无符号减法/取模结果依然正确;担心编译器差异可显式做取模。更严谨的项目会维护 64 位计数,或使用 GorgonMeducer.perf_counter 这类开源库。
— 老师,这样写能避免溢出问题? pf_get_tick_count归零不就不准(Jack)
来源笔记
- char 类型在 C 标准中是 1 字节(通常为 8 位|老师,静态局部变量存在哪?它的|ZI_data 不仅包含 未初始化:初始化为0的全局和静态变量|老师,满减栈什么意思? 满意|栈水位的直接打印栈指针来检测|用程序块来管理变量生命周期|老师,一个u16变量只在初始化(在线程初始化之前,是上电后初始化)的时候会改|老师,构造的作用和意义是什么?
- 指针与数组 char :fru|老师,问一下const void : const pvItemToQueue
- volatile 关键字的作用|在嵌入式开发里,几乎是一个“老生常谈”的关键字|老师,我这优化等级和你们demo一样是3,为什么这队列接收进来的数据会变|老师,我们学习看C语言的反汇编代码有什么用
- 联合体的常见应用方式|老师,请问有没有办法限制结构体成员的可见域,让外部文件只能访问结构体内的部分成员|老师,有个问题是,为啥就一个函数指针都要写在一个结构体里面|老师,这样使用static修饰函数,外部只能通过访问aht21实例的成员的形式来调用|写面向对象的程序时,要先写.h文件,后写.c文件。要对事物的本质进行抽象|挂载inst:register和init的关系|到底是谁把面向对象编程搞得这么|习惯就好了,用多了,有技巧了,|C语言实现状态机有三种方法|老师,位域该不该用?是否需要刻意考虑在不同的平台间或编译环境下所产生的问题(收录自 06 目录)
- ARM架构传参方式: 常规压|Q: 在网上看到 AAPCS 标准要求函数和中断入口时|老师,回调函数和钩子函数意义不|回调函数与钩子函数详解|问题:什么是可重入函数? 回答|我有个问题,我看很多人在讨论技术的时候,都会讲某个具体的函数名。我不了解这些函数名|老师,什么是接口? 前面我看视频的时候理解的接口,就是API函数,但是有些逻辑理不清
- 全局变量的地址,是在编译阶段还是链接阶段确定|在map文件中有|老师,这个size的单位是什么|I-Code-Bus, D-Code-Bus有什么区别?它们在如何取指令和数据|老师,LR保存函数的返回地址,我理解为函数跳转前的PC指针的值|关于堆栈和__main()在什么时候初始化
- 华为C语言编码规范|嵌入式C编码规范,兼容MISRA C,中英对照|Google的代码风格指南,中英双语链接都在下面了,有能力的同学建议参考原版仓库|嵌入式C代码规范(二)——注释篇|大佬们,我想咨询一下,你们写嵌入式代码的时候,“代码规范“有没有遵循哪个文档|是否认同对于用uint8_t代替bool的规范|嵌入式5连问 结论是怎么来|在 Git 中,分支是一个指向提交的指针,而不是一个实际的文件夹或目录。因此|在嵌入式开发场景中,项目常面临大数据量传递的挑战
- 为什么老师的代码里面在反复判断指针是否为空|老师,这里为什么要判断两次?|老师你好,我想问问为什么系统集成后的porting层和warpper层都不要返回值表|老师,这样写能避免溢出问题? pf_get_tick_count归零不就不准
未收录说明:图片题目帖(每日面试题系列等 15 篇)已标记 review: 建议隔离;《老师,什么是接口? 前面我看视 (2)》与保留篇内容重复,也已标记。栈回溯总结与偶发性问题定位两篇虽在本目录,但主题属调试领域,收录于 调试与可靠性问答总集。