C 语言编程 — 编程规范

目录

前文列表

程序编译流程与 GCC 编译器
C 语言编程 — 基本语法
C 语言编程 — 基本数据类型
C 语言编程 — 变量与常量
C 语言编程 — 运算符
C 语言编程 — 逻辑控制语句
C 语言编程 — 函数
C 语言编程 — 高级数据类型 — 指针
C 语言编程 — 高级数据类型 — 数组
C 语言编程 — 高级数据类型 — 字符串
C 语言编程 — 高级数据类型 — 枚举
C 语言编程 — 高级数据类型 — 结构体与位域
C 语言编程 — 高级数据类型 — 共用体
C 语言编程 — 高级数据类型 — void 类型
C 语言编程 — 数据类型的别名
C 语言编程 — 数据类型转换
C 语言编程 — 宏定义与预处理器指令
C 语言编程 — 异常处理
C 语言编程 — 头文件
C 语言编程 — 输入/输出与文件操作
C 语言编程 — 堆栈与内存管理
C 语言编程 — 指令行参数
C 语言编程 — GDB 调试工具

排版

空行:起着分隔程序段落的作用,空行得体将使程序的布局更加清晰,更具可读性。

  1. 定义变量后要空行。尽可能在定义变量的同时初始化变量,即遵循就近原则。如果变量的引用和定义相隔比较远,那么变量的初始化就很容易被忘记。若引用了未被初始化的变量,就会导致程序出错。
  2. 每个函数定义结束之后都要加空行
  3. 两个相对独立的程序块必须要加空行。
  4. 一行只写一条语句。这样的代码容易阅读,并且便于写注释。
  5. if、for、do、while、case、switch、default 等语句独占一行,执行语句不得紧跟其后,就算只有一行也要加 {},并且遵循对齐的原则,这样可以防止书写失误。

举例:

if (x == 0)
    if (y == 0)
        error();
else {
    z = x + y;
}

实现的效果为:

if (x == 0) {
    if (y == 0) {
        error();
    }
    else {
        z = x + y;
    }
}

不带 {} 的结果显然是不对的,因为 C 语言中的 else 始终优先匹配同一对括号中最近未匹配的 if。所以,逻辑控制语句应该养成写 {} 的习惯。

空格

  1. 关键字之后要留空格。像 const、case 等关键字之后至少要留一个空格,否则无法辨析关键字。像 if、for、while 等关键字之后应留一个空格再跟左括号 (,以突出关键字。
  2. 函数名之后不要留空格,应紧跟左括号 (,与关键字进行区别
  3. , 之后要留空格。
  4. 如果 ; 不是一行的结束符号,其后要留空格。
  5. 赋值运算符、关系运算符、算术运算符、逻辑运算符、位运算符,e.g. 如 =、==、!=、+=、-=、*=、/=、%=、>>=、<<=、&=、^=、|=、>、<=、>、>=、+、-、*、/、%、&、|、&&、||、<<、>>、^ 等**双目运算符(两端有操作数)**的前后要加空格。
  6. 单目运算符(只有一端有操作数),e.g. !、~、++、--、-、*、& 等前后不加空格。
    注意:规则 6 中的 - 是负号运算符、* 是指针运算符、& 是取地址运算符。
  7. 数组符号 []、结构体成员访问运算符 .、指向结构体成员运算符 ->,这类操作符前后不加空格。
  8. 对于表达式比较长的 for 语句和 if 语句,其表达式可以适当的删除一些空格,例如:for (i=0; i<10; i++){}

缩进

  1. 缩进为 4 个空格,通过键盘上的 Tab 键完成。缩进可以使程序更有层次感。原则是:如果地位相等,则不需要缩进;如果属于某一个代码的内部代码就需要缩进。

对齐:对齐主要是针对大括号 {} 说的,这里是风格纠纷的重灾区,有换行派、有不换行派,具体看项目的整体约定。

  1. {} 分别都要独占一行。互为一对的 {} 要位于同一列,并且与引用它们的语句左对齐。
  2. {} 之内的代码要向内缩进一个 Tab,且同一地位的要左对齐,地位不同的继续缩进。
#include <stdio.h>
int main(void)
{
    if () {}
    return 0;
}

注释

注释通常用于重要的代码行或段落提示。在一般情况下,源程序有效注释量必须在 20% 以上。虽然注释有助于理解代码,但注意不可过多地使用注释。

  1. 注释是对代码的 “提示”,而不是文档。程序中的注释不可喧宾夺主,注释太多会让人眼花缭乱。
  2. 优秀的代码可以自我解释,如果代码本来就是清楚的,则不必加注释。
  3. 注释应说明代码的功能或意图,而不是解释代码本身
  4. 保证注释与代码的一致性,修改代码的同时要修改相应的注释,以保证注释与代码的一致性,不再有用的注释要删除。
  5. 文头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明
  6. 函数声明处注释描述函数功能、性能及用法,包括:输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等
  7. 全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明
  8. 注释应放在其代码上方相邻位置或右方,不可放在下面。
  9. 对于 switch 语句下的 case 语句,如果因为特殊情况需要处理完一个 case 后进入下一个 case 处理,必须在该 case 语句处理完、下一个 case 语句前加上明确的注释。
  10. 注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达。对于有外籍员工的,由产品确定注释语言。
  11. 文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式
  12. 当代码比较长,特别是有多重嵌套的时候,应当在段落的结束处加注释,这样便于阅读。
  13. 每一条宏定义的右边必须要有注释,说明其作用。
  14. 注释内容在 // .../* ... */ 中间应该有空格。

头文件

对于 C 语言来说,头文件的设计体现了大部分的系统设计。

  1. 每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口
  2. 头文件放置适合的接口声明,对外提供的函数声明、宏定义、类型定义等,而不适合放置函数实现
  3. 头文件应当职责单一,高内聚低耦合。头文件的依赖过于复杂是导致编译时间过长的主要原因。
  4. 头文件应向稳定的方向演进。
  5. 禁止头文件循环依赖。例如:a.h 包含 b.h,b.h 包含 c.h,c.h 包含 a.h,导致任何一个头文件修改,都导致所有包含了 a.h、b.h、c.h 的代码全部重新编译一遍。
  6. .c、.h 文件禁止包含用不到的头文件
  7. 头文件应当自包含,即头文件均可独立编译。
  8. 总是编写内部 #include 保护符,即 #ifndef、#define 保护。让头文件在编译的时候只被包含一次
  9. 禁止在头文件中定义变量。否则,会由于头文件被其他 .c 文件包含而导致变量重复定义。
  10. 只能通过包含头文件的方式来使用其他 .c 提供的接口,禁止在 .c 中通过 extern 的方式使用外部函数接口、变量

函数

  1. 一个函数仅完成一件功能
  2. 重复代码应该尽可能提炼成函数
  3. 避免函数过长,新增函数不超过 50 行(非空非注释行)。
  4. 避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过 4 层。
  5. 可重入函数(可能被多个任务并发调用的函数)应避免使用共享变量(全局变量和 static 变量),若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护
  6. 对参数的合法性检查,由调用者负责还是由接口函数负责,在项目组、或模块内应统一规定。缺省由调用者负责。
  7. 对函数的错误返回码要全面处理
  8. 设计高扇入,合理扇出(小于 7)的函数。扇入是指有多少上级函数调用它,扇出是指一个函数直接调用(控制)其它函数的数目。
  9. 废弃代码(没有被调用的函数和变量)要及时清除。
  10. 函数中的不变参数使用 const 定义。例如:void *memcpy(void *s1, void const *s2, size_t size);,在编译时会对其进行检查,使代码更牢固/更安全。
  11. 函数应尽量保持幂等性,避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用。带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如:某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在 C 语言中,函数的 static 局部变量是函数的内部存储器,有可能使函数的功能不可预测,然而,当某函数的返回值为指针类型时,则必须是 static 的局部变量的地址作为返回值,若为 auto 类,则返回为错针。
  12. 检查函数所有非参数输入(如:数据文件、公共变量)的有效性。函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用非参数输入之前,应进行有效性检查。
  13. 函数的形参个数不超过 5 个
  14. 除打印类函数外,不要使用可变长参函数
  15. 如果一个函数只是在同一文件中的其他地方调用,那么就用 static 声明,确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

标识符命名与定义

  1. 风格统一。UNIX-link 风格:单词用小写字母,每个单词直接用下划线 _ 分割;Windows 风格:大小写字母混用,单词连在一起,每个单词首字母大写。
  2. 见名知意。
  3. 除了常见的通用缩写以外,不使用单词缩写
  4. 全局变量应增加 g_ 前缀。全局变量十分危险,通过前缀使得全局变量更加醒目,从根本上说,应当尽量不使用全局变量。
  5. 静态变量应增加 s_ 前缀
  6. 禁止使用单字节命名变量,但允许定义 i、j、k 作为局部循环变量。
  7. 数值、字符串等常量,以及枚举类型变量,建议采用全大写字母,单词之间加下划线 _ 的命名方式。
  8. 头文件或编译开关等特殊标识定义实体 _ 开头和结尾,此外的标识符不使用。
  9. 文件命名统一采用小写字母。
  10. 使用名词或者形容词+名词方式命名变量
  11. 动词或者动词+名词命名函数

变量

  1. 一个变量只有一个功能。
  2. 数据结构功能单一
  3. 不用或者少用全局变量
  4. 通讯协议程序中使用的结构,必须注意字节序
  5. 严禁使用未经初始化的变量作为右值
  6. 定义专门的函数来对全局变量进行写操作,其他函数只能进行读操作,通过锁来保证全局变量的一致性。
  7. 使用面向接口编程思想,通过 API 访问数据。如果本模块的数据需要对外部模块开放,应提供接口函数来设置、获取,同时注意全局数据的访问互斥。
  8. 初始化变量的的地方应该离使用的地方越近越好
  9. 明确全局变量的初始化顺序,避免跨模块的初始化依赖。
  10. 尽量减少没有必要的数据类型默认转换与强制转换。

宏、常量

  1. 用宏定义表达式时,要使用完备的括号,使用 do{}while(0) 结构进行处理。
#define ASSERT(condition)\
do {   \
    if (condition)\
       NULL; \
    else\
       Assert(__FILE__ , __LINE__);\
} while(0)
  1. 将宏所定义的多条表达式放在大括号中。
  2. 使用宏时,不允许参数发生变化。
  3. 不允许直接使用魔鬼数字。
  4. 除非必要,应尽可能使用函数代替宏。
  5. 建议使用 const 来定义常量,代替宏定义
  6. 宏定义中尽量不使用 return、goto、continue、break 等改变程序流程的语句

代码逻辑

  1. 防止差 1 错误。此类错误一般是由于把 <= 误写成 < 或 >= 误写成 > 等造成的。当写完程序后,应对这些操作符进行彻底检查。使用变量时要注意其边界值的情况。
  2. 所有的 if … else if 结构应该由 else 子句结束
  3. 所有 switch 语句必须有 default 分支
  4. 不要滥用 goto 语句。
  5. 当需要在代码中使用 == 比较一个常量和一个变量的数值是否相同时,使用 常量在左、变量在右 的写法。因为当我们错误的将 == 写错 = 时,如果变量在左就会变成 “赋值” 操作并返回为 True。通常的,这是一种非常难以排查的错误。而当常量在左时,编译的过程就会将这种错误排除掉。

内存操作

必须了解编译系统的内存分配方式,特别是编译系统对不同类型的变量的内存分配规则,如局部变量在何处分配、静态变量在何处分配等

禁止内存操作越界。内存操作主要是指对数组、指针、内存地址等的操作。内存操作越界是软件系统主要错误之一,后果往往非常严重:

  1. 数组的大小要考虑最大情况,避免数组分配空间不够。
  2. 避免使用危险函数 sprintf、vsprintf、strcpy、strcat、strcmp、gets 操作字符串,使用相对安全的函数 snprintf、strncpy、strncat、strncmp、fgets 代替
  3. 使用 memcpy、memset 时一定要确保长度不要越界。
  4. 字符串考虑最后的 ’\0’, 确保所有字符串是以 ’\0’ 结束
  5. 指针加减操作时,考虑指针类型长度。
  6. 数组下标进行检查
  7. 使用时 sizeof 或者 strlen 计算结构体、字符串的长度,避免手工计算

禁止内存泄漏,内存和资源(包括定时器、文件句柄、Socket、队列、信号量、GUI 等各种资源)泄漏是常见的错误:

  1. 异常出口处检查内存、定时器、文件句柄、Socket、队列、信号量、GUI 等资源是否全部释放
  2. 删除结构指针时,必须从底层向上层顺序删除
  3. 使用指针数组时,确保在释放数组时,数组中的每个元素指针是否已经提前被释放了
  4. 避免重复分配内存
  5. 小心使用有return、break 语句的宏,确保前面资源已经释放。
  6. 检查队列中每个成员是否释放

禁止引用已经释放的内存空间,在实际编程过程中,稍不留心就会出现在一个模块中释放了某个内存块,而另一模块在随后的某个时刻又使用了它:

  1. 内存释放后,把指针置为 NULL,使用内存指针前进行非空判断
  2. 耦合度较强的模块互相调用时,一定要仔细考虑其调用关系,防止已经删除的对象被再次使用。
  3. 避免操作已发送消息的内存。
  4. 函数中分配的内存,在函数退出之前要释放

日志打印

调测打印的日志要有统一的规定:

  1. 统一的日志分类以及日志级别
  2. 通过命令行、网管等方式可以配置和改变日志输出的内容和格式
  3. 在关键分支要记录日志,日志建议不要记录在原子函数中,否则难以定位;
  4. 调试日志记录的内容需要包括文件名/模块名、代码行号、函数名、被调用函数名、错误码、错误发生的环境等

质量保证

  1. 正确性,指程序要实现设计要求的功能。
  2. 简洁性,指程序易于理解并且易于实现。
  3. 可维护性,指程序被修改的能力,包括纠错、改进、新需求或功能规格变化的适应能力。
  4. 可靠性,指程序在给定时间间隔和环境条件下,按设计要求成功运行程序的概率。
  5. 代码可测试性,指软件发现故障并隔离、定位故障的能力,以及在一定的时间和成本前提下,进行测试设计、测试执行的能力。
  6. 代码性能高效,指是尽可能少地占用系统资源,包括内存和执行时间。
  7. 可移植性,指为了在原来设计的特定环境之外运行,对系统进行修改的能力。

单元测试

模块划分清晰,接口明确,耦合性小,有明确输入和输出,否则单元测试实施困难:

  1. 模块间的接口定义清楚、完整、稳定
  2. 模块功能的有明确的验收条件(包括:预置条件、输入和预期结果);
  3. 模块内部的关键状态和关键数据可以查询,可以修改
  4. 模块原子功能的入口唯一
  5. 模块原子功能的出口唯一
  6. 依赖集中处理:和模块相关的全局变量尽量的少,或者采用某种封装形式。
  7. 有一套统一的为集成测试与系统联调准备的调测开关及相应打印函数,并且要有详细的说明
  8. 单元测试关注单元的行为而不是实现,避免针对函数的测试。

断言

使用断言记录内部假设:断言是对某种内部模块的假设条件进行检查,如果假设不成立,说明存在编程、设计错误。断言可以对在系统中隐藏很深,用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。

不能用断言来检查运行时错误:断言是用来处理内部编程或设计是否符合假设;不能处理对于可能会发生的且必须处理的情况要写防错程序,而不是断言。如某模块收到其它模块或链路上的消息后,要对消息的合理性进行检查,此过程为正常的错误检查,不能用断言来实现。

安全性

  1. 对用户输入进行检查
  2. 确保所有字符串是以 NULL 结束。C 语言中 ‟\0‟ 作为字符串的结束符,即 NULL 结束符。标准字符串处理函数,如 strcpy、strlen 依赖 NULL 结束符来确定字符串的长度。没有正确使用 NULL 结束字符串会导致缓冲区溢出和其它未定义的行为。
  3. 不要将边界不明确的字符串写到固定长度的数组中
  4. 避免整数溢出。当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
  5. 避免符号错误。有时从带符号整型转换到无符号整型会发生符号错误,符号错误并不丢失数据,但数据失去了原来的含义。
  6. 避免截断错误。将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失。
  7. 确保格式字符和参数匹配。使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。
  8. 避免将用户输入作为格式化字符串的一部分或者全部。调用格式化 I/O 函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。
  9. 避免使用 strlen 计算二进制数据的长度
  10. 使用 int 类型变量来接受字符 I/O 函数的返回值。 字 符I/O 函数 fgetc()、getc() 和 getchar() 都从一个流读取一个字符,并把它以 int 值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回 EOF。fputc()、putc()、putchar() 和 ungetc() 也返回一个字符或 EOF。如果这些 I/O 函数的返回值需要与 EOF 进行比较,不要将返回值转换为 char 类型。因为 char 是有符号 8 位的值,int 是 32 位的值。如果 getchar() 返回的字符的 ASCII 值为 0xFF,转换为 char 类型后将被解释为 EOF。因为这个值被有符号扩展为 0xFFFFFFFF(EOF 的值)执行比较。
  11. 防止命令注入

可移植性

  1. 不能定义、重定义或取消定义标准库、平台中保留的标识符、宏和函数
  2. 不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句,以提高软件的可移植性和可重用性

参考文档

https://blog.csdn.net/LIAOYUANGANG/article/details/79174627?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值