Skip to main content

Li Zheng flyskywhy@gmail.com

C 语言编码规范

为了统一自己项目中各种来源的源代码(主要是 C )的代码规范,特制定本规则。

基本原则

一般一个项目中的源代码来自好几个 IP 供应商,就容易出现编码规范不统一的现象,为了能够最大限度使用供应商的人力以弥补某些项目人力较少的缺陷,需要能够快速合并供应商的最新更新,因此基本原则为:下文所示规范如果与您正在编写的代码附近供应商的规范相矛盾的,以供应商的为主(当产品经理明确要求以自己的为主时除外)。当然这不是说您就不需要查看本文了,因为有矛盾的地方一般是较少的,大部分规范需要我们看了本文后才能一起去遵守。

文本编码

使用软件行业普遍使用的 UTF-8 编码格式的源代码,之所以这里特地强调,是因为有些 IDE 或文本编辑器的 Windows 版打开文本文件时默认使用的是 ANSI 编码甚至是 GB2312 编码,所以为了让文件中所有的中文字符都能正常显示而不至于出现一部分乱码,请在编辑每个文件前确认 IDE 菜单选项中是否选择了 UTF-8 。或者可以使用其它编辑器比如 Sublime Text 软件进行编辑,而 IDE 仅仅只是用来编译。

换行符

为了避免一些命令行工具可能的换行符处理 BUG ,以及避免 git 提交点出现整个文件所有行都有差异的现象,请使用软件行业普遍使用的 LF 换行符的源代码。可应用 Sublime Text 来保证,详见 Sublime Text 使用详解 中的“ 设置换行符为 LF ”小节。使用 geany 图形编辑器或 dos2unix 命令行工具来转换。

为了避免一些命令行工具可能的处理 BUG ,请确保文件最后一行是空行。

注释

中英文混写时,英文与中文之间要有空格。数字与中文之间要有空格。总之一个基本原则是,使得当双击英文或数字时,不会产生选中一整段中英文句子的现象,比如在写文章而非写注释时,有时会在英文左右添加 ` 符号,此时 ` 符号与中文之间就无需空格了,因为此时双击两个 ` 符号间的内容只会选中该内容本身。

注释要简单明了,边写代码边注释,修改代码同时修改相应的注释,以保证注释与代码的一致性,在必要的地方注释,注释量要适中。注释的内容要清楚、明了,含义准确,防止注释二义性。

保持注释与其描述的代码相邻,即注释的就近原则,对代码的注释应放在其上方相邻位置或右边,不可放在下面,如放于上方则需与其上面的代码用空行隔开,并与所描述内容进行同样的缩排;对数据结构的注释应放在其上方相邻位置,不可放在下面,对结构中的每个域的注释应放在此域的右方,同一结构中不同域的注释要对齐;全局变量要有较详细的注释,包括对其功能、取值范围、哪些函数或过程存取它,以及存取时注意事项等的说明。

注释的第一个字一般是空格。

如果想让注释能被包含在 Doxygen 所自动生成的文档中,则使用////**格式开头的注释放在代码的上方,或是///</**<在右方,更详细的 Doxygen 文档格式生成方法请参见 Doxygen 的相关资料。

几种常用注释的示例:

文件开头的版权和版本声明

每个编码文件及相关说明文档都必须有,如下所示:

/**
* @file
*
* @brief Driver, Memory Stick Driver
*
* Copyright 2007, Silan, Inc.
*
* @verbatim
*
* this for EAUX3_1 -- sdio
* WR0: LD0 -- sclk, LD1 -- bs
* EAUX3_0 -- ins
* HAVE CHANGED ioport.h
*
* @endverbatim
*/

文件内函数、变量的注释

/**
* function description
*
* @param i description of i
* @param j description of j
*
* @return 1: error
* 0: ok
*/
int foo(int i, int j)
{
if (i) {
j++; ///< some comment in one line
return 1;
} else {
/**
* some comments in
* several lines
*/
return 0;
}
}

对于 switch 语句下的 case 语句,如果因为特殊情况需要处理完一个 case 后进入下一个 case 处理,必须在该 case 语句处理完、下一个 case 语句前加上明确的注释。

通过对函数或过程、变量、结构等正确的命名以及合理地组织代码的结构,使代码成为自注释的,这样可增加代码可读性,并减少不必要的注释。

头文件

尽量保持每个.c文件对应一个.h文件。

头文件由三部分内容组成:

  1. 头文件开头处的版权和版本声明
  2. 预处理块
  3. 函数和类结构声明等

为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。

#include <filename.h>格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。

#include “filename.h”格式来引用非标准库的头文件(编译器将从用户的工作目录及 gcc 的-I参数指定的路径开始搜索)。

头文件中只存放“声明”而不存放“定义”。

不提倡使用全局变量,尽量不要在头文件中出现象extern int value这类声明。

命名规则

在我们维护别人的代码时,请使用他们的命名规则(当产品经理明确要求也使用自己的命名规则时除外)。

在我们新建项目进行编码时,请使用本文所列的命名规则。

变量的名字应当使用“名词”或者“形容词+名词”。全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。( C++ 等语言的)类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。

程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。

用正确的反义词组命名具有互斥意义的变量或相反动作的函数等,如:

int min_value;
int max_value;

int set_value(...);
int get_value();

常量

用大写字母,如:

#define ID3_DECODED 0x03 /* C 语言的宏常量 */
const int MAX_LENGTH = 100; // C++ 语言的 const 常量

局部变量、全局函数

用小写字母,单词之间用下划线隔开,如:

u16 buffer;
bool is_wave_file;
void stop();
bool is_strong_signal();

静态变量

加前缀s_(表示 static ),如:

void init()
{
static int s_init_value;
...
}

全局变量

加前缀g_(表示 global ),如:

int g_how_many_people;

类名用大写字母开头的单词组合而成,类名尽量不超过 2 个单词。如:

class FontStyle

类的数据成员

加前缀m_(表示 member ),这样可以避免数据成员与成员函数的参数同名。如:

void set_value(int width, int height)
{
m_width = width;
m_height = height;
}

代码风格

在 Package Control 中安装 SublimeAStyleFormatter 并配置后的 Sublime 中编辑代码然后右键菜单 AStyleFormatter | Format 就可格式化代码风格,如果使用其它编辑器的则自行安装 astyle 插件或命令行工具并进行相应配置。

经过如上配置后的 astyle 基本可以自动规范下面提到的代码风格。

缩进

缩进以四个空格而不是 Tab 为单位。预处理语句、全局数据、函数原型、标题、附加说明、函数说明、标号等均顶格书写。

空行

在每个类声明之后、每个函数定义结束之后都要加空行。在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。

代码行

一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。ifforwhiledo等语句自占一行,执行语句不得紧跟其后,且不论执行语句有多少都要加{},这样可以防止书写失误。

空格

数据和函数在其类型,修饰名称之间适当空格并据情况对齐。关键字之后要留空格,如:象ifforwhile 等关键字之后应留一个空格再跟左括号(,以突出关键字。调用的函数名之后不要留空格,紧跟左括号(,以与关键字区别。 (向后紧跟,),;向前紧跟,紧跟处不留空格。,之后要留空格,如function(x, y, z),需对齐时也可不空或多空格。如果;不是一行的结束符号,其后要留空格,如for (initialization; condition; update)

赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如=+=>=<=+*%&&||<<^等二元操作符和三目运算符?:的前后应当加空格。一元操作符如!~++--&(地址运算符)等前后不加空格。象[].->这类操作符前后不加空格。

对语句行后加的注释应用适当空格与语句隔开并尽可能对齐。

对齐

函数自己的{}应独占一行并且位于同一列这样的上下对齐方式,其它{}比如函数内的代码、结构体定义等则采用 Linux Kernel 那样的 kr 风格的大括号斜角对齐方式,同时与引用它们的语句左对齐。{ }之内的代码块在}右边一个缩进单位格处左对齐。

长行拆分

代码行最大长度宜控制在 70 至 80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。长表达式要在,;或低优先级操作符处拆分成新行,且这些符号放在上一行之尾。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。

预编译

在编写#if类的预编译语句时,须采用#if defined (ABC)的写法,不允许#if defined ABC的写法。

编码要求

变量

去掉没必要的全局变量。

构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象。

定义并明确全局变量的含义、作用、取值范围及全局变量间的关系。明确全局变量与操作此全局变量的函数或过程的关系,如访问、修改及创建等。当向全局变量传递数据时,要十分小心,防止赋予不合理的值或越界等现象发生。

仔细设计结构中元素的布局与排列顺序,使结构容易理解、节省占用空间,并减少引起误用现象。结构的设计要尽量考虑向前兼容和以后的版本升级,并为某些未来可能的应用保留余地(如预留一些空间等)。

严禁使用未经初始化的变量。尽可能在定义变量的同时对变量进行初始化(就近原则)。

合理地设计数据并使用自定义数据类型,避免数据间进行不必要的类型转换。

当声明用于分布式环境或不同 CPU 间通信环境的数据结构时,必须考虑机器的字节顺序、使用的位域及字节对齐等问题。

函数、过程

函数的规模尽量限制在 200 行以内。

一个函数最好仅完成一件功能。为简单功能编写函数。

函数的功能应该是可以预测的,也就是只要输入数据相同就应产生同样的输出。带有内部“存储器”(如函数的 static 局部变量)的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。

避免设计多参数函数,不使用的参数从接口中去掉。用注释详细说明每个参数的作用、取值范围及参数间的关系。检查函数所有参数输入的有效性。检查函数所有非参数输入的有效性,如数据文件、全局变量等。

在同一项目组应明确规定对接口函数参数的合法性检查应由函数的调用者负责还是由接口函数本身负责,缺省是由函数调用者负责。

函数名应准确描述函数的功能。避免使用无意义或含义不清的动词为函数命名。

函数的返回值要清楚、明了,让使用者不容易忽视错误情况。

明确函数功能,精确(而不是近似)地实现函数设计。减少函数本身或函数间的递归调用。

如果多段代码重复做同一件事情,那么在函数的划分上可能存在问题。

编写可重入函数时,若使用全局变量,则应通过关中断、信号量等手段对其加以保护。

可测性

编写代码之前,应预先设计好程序调试与测试的方法和手段,并设计好各种调测开关及相应测试代码如打印函数等。

在进行集成测试/系统联调之前,要构造好测试环境、测试项目及测试用例,同时仔细分析并优化测试用例,以提高测试效率。

使用断言来发现软件问题,提高代码可测性。用断言来检查程序正常运行时不应发生但在调测时有可能发生的非法情况,不能用断言来检查最终产品肯定会出现且必须处理的错误情况。断言是对某种假设条件进行检查(可理解为若条件成立则无动作,否则应报告),它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深、用其它手段极难发现的问题进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。示例:下面是 C 语言中用宏来设计的一个断言。(其中 NULL 为 0L )

#ifdef _ASSERT_TEST_ // 若使用断言测试

void assert(char *fileName, unsigned int lineNo)
{
printf("\n[EXAM]Assert failed: %s, line %u\n", fileName, lineNo);
abort();
}

#define ASSERT(condition) \
if (condition) \ // 若条件成立,则无动作
NULL; \
else \ // 否则报告
assert(__FILE__, __LINE__)

#else // 若不使用断言测试

#define ASSERT(condition) NULL

#endif /* end of _ASSERT_TEST_ */

程序效率

编程时要经常注意代码的效率。在保证软件系统的正确性、稳定性、可读性及可测性的前提下,提高代码效率。但不能一味地追求代码效率,而对软件的正确性、稳定性、可读性及可测性造成影响。

要仔细地构造或直接用汇编编写调用频繁或性能要求极高的函数。

通过对系统数据结构划分与组织的改进,以及对程序算法的优化来提高空间效率。

在多重循环中,应将最忙的循环放在最内层。 尽量减少循环嵌套层次。避免循环体内含判断语句,应将循环语句置于判断语句的代码块之中。

尽量用乘法或其它方法代替除法,特别是浮点运算中的除法。

质量保证

代码质量保证优先原则。正确性,指程序要实现设计要求的功能。稳定性、安全性,指程序稳定、可靠、安全。可测试性,指程序要具有良好的可测试性。规范/可读性,指程序书写风格、命名规则等要符合规范。全局效率,指软件系统的整体效率。局部效率,指某个模块/子模块/函数的本身效率。个人表达方式/个人方便性,指个人编程习惯。

只引用属于自己的存贮空间。防止引用已经释放的内存空间。过程/函数中分配的内存,在过程/函数退出之前要释放。过程/函数中申请的(为打开文件而使用的)文件句柄,在过程/函数退出前要关闭。防止内存操作越界。时刻注意表达式是否会上溢、下溢。

系统运行之初,要初始化有关变量及运行环境,防止未经初始化的变量被引用。系统运行之初,要对加载到系统中的数据进行一致性检查。严禁随意更改其它模块或系统的有关设置和配置。不能随意改变与其它模块的接口。充分了解系统的接口之后,再使用系统提供的功能。

编程时,要防止差 1 错误。当编完程序后,应对<=<>=>这些操作符进行彻底检查。

使用类似if (NULL == abc)的表达式来代替if (abc == NULL),因为==容易被误写为=,而前一种写法发生这样的误写时,编译器会出错,从而很容易避免此类错误。

要小心地使用编辑器提供的块拷贝功能编程。

要时刻注意易混淆的操作符。当编完程序后,应从头至尾检查一遍这些操作符。不使用与硬件或操作系统关系很大的语句,而使用建议的标准语句。

使用第三方提供的软件开发工具包或控件时,要注意以下几点:充分了解应用接口、使用环境及使用时注意事项。不能过分相信其正确性。除非必要,不要使用不熟悉的第三方工具包与控件。

代码编译、测试、维护

写代码时要注意随时保存,并定期备份或上传,防止由于断电、硬盘损坏等原因造成代码丢失;合理地设计软件系统目录,方便开发人员使用;打开编译器的所有告警开关对程序进行编译。还可以使用 lint 、 C++Test 、 valgrind 等工具进行代码检查。

单元测试要求至少达到语句覆盖。单元测试开始要跟踪每一条语句,并观察数据流及变量的变化。

清理、整理或优化后的代码要经过审查及测试。代码版本升级要经过严格测试。

用宏定义表达式时,要使用完备的括号

示例:如下定义的宏都存在一定的风险。

#define RECTANGLE_AREA(a, b) a * b
#define RECTANGLE_AREA(a, b) (a * b)
#define RECTANGLE_AREA(a, b) (a) * (b)

正确的定义应为:

#define RECTANGLE_AREA(a, b) ((a) * (b))

将宏所定义的多条表达式放在大括号中

示例:下面的语句只有宏的第一条表达式被执行。为了说明问题,for语句的书写稍不符规范。

#define INIT_RECT_VALUE(a, b)\
a = 0;\
b = 0;

for (index = 0; index < RECT_TOTAL_NUM; index++)
INIT_RECT_VALUE( rect.a, rect.b );

正确的用法应为:

#define INTI_RECT_VALUE(a, b)\
{\
a = 0;\
b = 0;\
}

for (index = 0; index < RECT_TOTAL_NUM; index++) {
INIT_RECT_VALUE(rect[index].a, rect[index].b);
}

使用宏时,不允许参数发生变化

示例:如下用法可能导致错误。

#define SQUARE(a) ((a) * (a))
int a = 5;
int b;
b = SQUARE(a++); // 结果: a = 7 ,即执行了两次增 1 。

正确的用法是:

b = SQUARE(a);
a++; // 结果: a = 6 ,即只执行了一次增 1 。

volatile

有时代码会被不合适地优化掉,比如

void main()
{
int *p = (int *)(0x12345678);
*p = 1;
*p = 2;
}

此时如果在编译时选择了优化选项(如 gcc 后面添加了-O2参数),编译器就会把*p = 1给优化掉,因为编译器认为给 p 赋值后没有使用它而就又赋了新的值(*p = 2),所以*p = 1是没有意义的,但我们实际在操作硬件时却的确用得到这样的语句,解决的方法要么是将优化关掉,要么是作如下定义:

    volatile int *P;