跳转至

📘 C 语言编程规范与底层原理笔记

1. 编程规范概览 (省流版) ⚡

  • 命名规范:使用下划线命名法,宏定义全大写,预编译命令 #else, #endif 行末加注释说明条件。
  • 前缀规范:全局变量加 g_ 前缀,全局指针加 p_ 前缀。
  • 类型定义:结构体/联合体/枚举等复杂数据类型,除非不公开,统一用 typedef 定义名称,并加上 _t 后缀。
  • 缩进限制:缩进最多接受 6 层,超过 6 层必须重写;预编译如果嵌套命令不缩进。
  • 嵌入式限制:嵌入式程序中不准使用递归。
  • 返回值:对于有返回值的函数,必须返回一个默认值,不能什么都不返回。

2. 命名禁忌与规则 🚫

一. 命名禁忌

  1. 单字母:int a,b,c;(但循环变量可以使用 i,j,k,前提是不要定义成全局变量)。
  2. 重复无意义:int aa, bb, cc, dd;。
  3. 拼音:char[30] mingZi, sb, nb; (对于一些公认的拼音可以使用)。
  4. 不规范缩写:如 int cont_freq。

二. 命名规则

  1. 风格:统一使用下划线命名法,对于第三方代码保留其命名风格,不要修改。
  2. 统一性:编写模块代码时使用统一前缀,如操作 NAND Flash 的代码加 nand_ 前缀。
  3. 作用域关联:变量命名长度与作用域有关,作用域越大命名越长,以避免冲突。
  4. 长度限制:尽量不要超过 6 个单词。
  5. 避免编号:如 number1, number2,应用数组代替;除非逻辑上必须使用编号(如 USART1, USART2)。

3. 代码格式与风格指南 📏

一. 命名语义

  • 函数:动词 + 名词,准确对应功能或行为。
  • 变量:形容词 + 名词。
  • 句柄:加 _handle 后缀,名称与类型功能匹配即可。

二. 文件规范

  • 引用:只引用需要的头文件,不要包含一堆;#include 对大小写敏感。
  • 目录结构:头文件放 Inc 目录,源文件放 Src 目录。
  • 后缀:C++ 头文件用 .hpp,源文件用 .cpp。
  • 路径:子目录包含头文件时使用相对路径,不要在 IncludePath 中添加。
  • 宏保护:头文件开头末尾必须加 #ifndef ... #endif。

三. 空格与符号

  • 二元/三元操作符:前后加空格 (= + - < > * / % ^ <= >= == != ? :)。
  • 一元操作符:后不加空格 (& * + - ~ !),但声明指针时 * 靠近变量一侧且左边有空格。
  • 自增减:前后都不加空格 (num++, --num)。
  • 逗号分号:只在后面加空格。
  • 注释:块注释 /* xxx */ 内容前后各一空格;行注释 // xxx 双斜线后一空格,且与行对齐。

四. 代码布局

  • 花括号:左花括号不换行,右花括号单独一行(else 的右花括号不换行),减少阅读负担。
  • 空行:文件头注释后、函数定义前加空行;注释函数/结构体/枚举时不加空行。
  • 单行限制:一行只做一件事;一行最多 80 字符。
  • 语句块:if, for, while, do 必须加花括号,即使只有一条语句。
  • 清理:及时清理死代码。

4. 函数与变量最佳实践 🛠️

一. 函数

  • 复杂度:尽量不超过 10 个局部变量。
  • 参数:最多 6 个。
  • 返回值:必须 return 默认值。
  • 无参函数:使用 void,如 int main(void)。
  • 功能单一:不要让函数或文件成为“大杂烩”。

二. 变量

  • 定义:哪里用哪里声明,使用前必须定义。
  • 初始化:确定初值写在一行;不确定初值变量给 0/1,指针给 NULL,结构体给 {0}。
  • Static 使用:如果全局变量仅本文件使用,加 static 避免外部访问。

三. 复杂数据类型与杂项

  • 结构体/联合体:公开的使用 typedef 定义并带 _t 后缀。
  • 枚举:成员全大写。
  • 内存:申请必须释放。
  • 嵌套限制:指针嵌套最多 3 层,数组最多 3 维。
  • 参数传递:结构体/联合体尽量传指针。
  • 浮点数:不要做相等/不等判断(精度问题)。
  • 编码:统一使用 UTF-8+。

5. C 语言编译原理 🏗️

一. 编译流程

  1. C 语言到机器码:高级语言 -> 汇编语言 -> 二进制文件(机器语言)。
  2. 后缀变化:.c -> .s -> .o -> .exe。
  3. 详细步骤:
  4. 预处理:处理头文件包含、宏定义、条件编译。生成代码(编译单元),注释被删除。
  5. 编译:将编译单元转换为目标架构的汇编代码。
  6. 汇编:将汇编代码生成机器指令(目标文件 .o),此时文件不可执行。
  7. 链接:符号解析、地址分配、重定位。组合目标文件生成可执行文件。

二. 文件区别

  • .obj 文件:编译后的中间文件,含相对地址和未解决符号。
  • .exe 文件:链接后的可执行文件,含绝对地址。

三. 编译器分类

  • 基于源语言(C/C++/Java 等)。
  • 基于目标语言(机器码/中间语言)。
  • 跨编译器 (Cross-compiler):在一个平台运行,生成另一个平台的代码。

四. 嵌入式编译器 vs PC 编译器

特性 嵌入式编译器 PC 编译器
目标平台 特定硬件 (ARM, STM32),需交叉编译 通用架构 (x86),本地编译
优化方向 代码体积、功耗、实时性优先 执行速度优先
开发环境 含硬件调试 (JTAG),需手动配置硬件细节 侧重软件调试,依赖系统 API
流程复杂度 需配置链接脚本,处理中断向量表 自动处理链接和内存

导出到 Google 表格

6. 位运算与 Static 关键字 🔢

一. 位运算

  • 操作:置零用 &,置 1 用 |。
  • 表示:bit[y:x] 表示从 x 位到 y 位。
  • 优化:对 2 的倍数的取余、乘除运算,编译器会优化为位运算。

二. Static 关键字详解

修饰对象 作用域 生命周期 说明
全局变量 仅当前文件可见 程序运行期间 隐藏变量,避免命名冲突
局部变量 仅函数内部 程序运行期间 仅初始化一次,函数结束后值保留
类成员变量 属于类 程序运行期间 所有对象共享,不占对象空间
类成员函数 属于类 - 不依赖对象,无 this 指针

导出到 Google 表格

7. 整数与数据类型 💾

一. 整数

  • 类型:uint8 占用 8 个二进制位;stdint.h 根据平台确定范围;inttypes.h 处理类型输出。
  • 隐式转换优先级:char, short --> int --> unsigned --> long --> double <-- float。
  • 比较陷阱:有符号数与无符号数比较时,有符号数会被隐式转换为无符号数(例如 -1 变成巨大的正数)。

二. 枚举与结构体

  • 枚举:用于状态/错误处理,默认从 0 递增。
  • 结构体初始化:可使用 .成员名 方式赋值。
  • 结构体传参:作为参数会发生压栈复制,建议传指针以节省时间和空间。

三. 结构体内存对齐

  • 规则:
  • 第一个成员偏移为 0。
  • 成员偏移地址必须是对齐数的整数倍。
  • 结构体总大小必须是最大对齐数的整数倍。
  • 对齐数:min(成员大小, 默认对齐数)。
  • 工具:offsetof 获取偏移,#pragma pack(n) 或 __attribute__ 设置对齐。

四. 位域与联合体

  • 位域:类型 成员 : 位数;不能用浮点和指针。
  • 联合体 (Union):
  • 通信应用:传输多字节数据时,发送拆分为字节,接收组合为数据。
  • 特性:修改一个成员会影响其他成员。

8. 数组与指针 📌

  • 零长数组:常用于变长结构体末尾,减少空间占用。
  • 变长数组:全局数组不可用变量定义长度。
  • 返回局部数组:不允许,编译器会报错或返回空指针;加 static 可解决。
  • 数组退化:int [] 隐式转换为 int * 是允许的,反之不行。
  • 指针定义:int *ptr1, ptr2; 中 ptr2 是整型,正确写法是 int *ptr1, *ptr2;。

9. 内存管理:栈与堆 🧠

一. C 语言中的内存区域

区域 管理方式 用途
栈 (Stack) 操作系统自动管理 局部变量、函数调用信息
堆 (Heap) 程序员手动管理 动态分配 (malloc, free)

导出到 Google 表格

二. 数据结构中的抽象

  • 栈:LIFO(后进先出),仅允许栈顶操作。
  • 堆:完全二叉树,满足堆属性(最大堆/最小堆)。

三. 内存段划分

程序内存分为 5 部分:程序段、初始化数据段、未初始化数据段、栈、堆。

四. 动态分配细节

  • malloc vs calloc:malloc 分配后数据随机;calloc 会将区域清零。
  • calloc 参数:成员个数 * 单个成员大小。
  • 指针类型:指向零地址叫空指针,指向不存在地址叫野指针。

10. 预处理与 GNU 扩展 ⚙️

一. 预处理

  • 指令:以 # 开头,不以分号结尾,\ 为续行符。
  • 头文件保护:使用 #ifndef ... #define ... #endif 防止重复包含。
  • 路径查找:双引号优先查源文件目录,尖括号查编译器路径和环境变量。
  • 检查:__has_include 判断头文件是否存在。

二. 函数调用约定

  • 规定参数压栈顺序、谁清理栈、返回值如何返回(如 __stdcall, __cdecl)。

三. GNU 扩展 (__attribute__)

  • 变量属性:
  • packed:取消对齐。
  • align(n):设置对齐。
  • 函数属性:
  • weak:弱定义,可被重定义。
  • nonnull:限制传入空指针。
  • noreturn:函数不返回 (如死循环错误处理)。
  • 头文件包装:#include_next "filename" 用于引用同名旧头文件。
  • 数组初始化:[0 ... 2] = 1 (范围初始化)。