C/C++编程规范
本规范制定的目的是通过详阐述如何进行C编码来减少团队开发中给项目管理带来的复杂性,增强代码的一致性,以利于项目成员间和后期维护中的交流。
保持统一编程风格,意味着可以轻松根据“模式匹配”规则推断各种符号的含义。创建通用的、必要的习惯用语和模式可以使代码更加容易理解,同时我们遵循一致性原则,尽量不创建独特的编程风格。
本规范的使用者对C应非常熟悉。
一、头文件
通常,每一个.c文件(C的源文件)都有一个对应的.h文件(头文件),也有一些例外,如单元测试代码和只包含main()的.c文件。
正确使用头文件可增强代码在可读性、文件大小和性能。
下面的规则将引导你规避使用头文件时的各种麻烦。
⒈ #define保护
⑴ 为保证唯一性,头文件的命名应基于其所在项目源代码树的全路径
命名格式为:
<PROJECT>_<PATH>_<FILE>_H_
例如,项目foo中的头文件foo\src\bar\baz.h
⑵ 所有头文件都应该使用#ifndef防止头文件被多重包含(multiple inclusion)。
例如,
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
...
#endif // FOO_BAR_BAZ_H_
避免多重包含是学习编程时最基本的要求
⒉ 包含文件的名称及次序
⑴ 包含文件的排列次序
C系统库
其他库的.h
项目内的.h。
将包含次序标准化可增强程序的可读性、有效减少隐藏依赖避免隐藏依赖(hidden dependencies,隐藏依赖主要是指包含的文件编译)。
⑵ 项目内头文件应按照项目源代码目录树结构排列,并且避免使用Windows的文件路径.(当前目录)和..(父目录)
例如,google-awesome-project\src\base\logging.h
#include "base\logging.h"
dir\foo.c的主要作用是执行或测试dir2/foo2.h的功能。
对应的.c和.h文件,通常位于相同目录下。
⒊ 函数参数顺序(Function Parameter Ordering)
定义函数时,参数顺序为:输入参数在前,输出参数在后。
C函数参数分为输入参数和输出参数两种,有时输入参数也会输出(注:值被修改时)。
这一点并不是必须遵循的规则,输入/输出两用参数(通常是结构体变量)混在其中,会使得规则难以遵循。
二、作用域
作用域的使用,除了考虑名称污染、可读性之外,主要是为降低耦合度,提高编译、执行效率。
⒈ 局部变量(Local Variables)
⑴ 将函数变量尽可能置于最小作用域内,在声明变量时将其初始化
我们提倡在尽可能小的作用域中声明变量,离第一次使用越近越好。这使得代码易于阅读,易于定位变量的声明位置、变量类型和初始值。
C可正确执行for (int i = 0; i < 10; ++i)(i的作用域仅限for循环),因此其他for循环中可重用i。if和while等语句中可同样使用。
⑵ 使用初始化代替声明+赋值的方式。
int i;
i = f(); // 坏——初始化和声明分离
int i = g(); // 好——初始化时声明
⒉ 全局变量(Global Variables)
⑴ 尽量不用全局函数和全局变量,考虑作用域的限制,尽量单独形成编译单元;
⑵ 永远不要使用函数返回值初始化全局发量。
三、C特性
⒈编写短小函数(Write Short Functions)
长函数有时是恰当的,因此对于函数长度并没有严格限制。更倾向于选择短小、凝练的函数,函数体尽量短小、紧凑,功能单一。
如果函数超过40行,可以考虑在不影响程序结构的情冴下将其分割一下。 即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的bugs。使函数尽量短小、简单,便于他人阅诺和修改代码。
在处理代码时,你可能会发现复杂的长函数,不要害怕修改现有代码:如果证实这些代码使用、调试困难,或者你需要使用其中的一小块,考虑将其分割为更加短小、易于管理的若干函数。
⒉ 声明次序(Declaration Order)
⑴ typedefs和enums
⑵ 常量(defines)
⑶ 函数
⑷ 变量
⒊ 引用参数
如果函数需要修改变量的值,形参(parameter)必须为指针。
⒋ 前置自增和自减(Preincrement and Predecrement)
⑴ 能用前置自增/减不用后置自增/减
⑵ 对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符
⑶ 定义
对于变量在自增(++i或i++)或自减(--i或i--)后表达式的值又没有没用到的情冴下,需要确定到底是使用前置还是后置的自增自减。
⑷ 优点
不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高,因为后置的自增自减需要对表达式的值i进行一次拷贝,如果i是迭代器或其他非数值类型,拷贝的代价是比较大的。既然两种自增方式动作一样(注,不考虑表达式的值),为什么不直接使用前置自增呢?
⑸ 缺点
C语言中,当表达式的值没有使用时,传统的做法是使用后置自增,特别是在for循环中,有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前。
⑹ 结论
对简单数值(非对象)来说,两种都无所谓,对迭代器和模板类型来说,要使用前置自增(自减)。
⒌ 无符号整型(Unsigned Integer Types)
一些教科书作者,推荐使用无符号类型表示非负数,类型表明了数值取值形式。但是,在C语言中,这一优点被由其导致的bugs所淹没。如:
for (unsigned int i = foo.Length()-1; i >= 0; --i) ...
上述代码永远不会终止!有时C会发现该bug并报警,但通常不会。
类似的bug还会出现在比较有符号变量和无符号变量时,主要是C的类型提升机制(type-promotion scheme,C语言中各种内建类型之间的提升转换关系)会致使无符号类型的行为出乎你的意料。
因此,使用确定大小的整型,除位组(bit pattern)外不要使用无符号型。
⒍ 预处理宏(Preprocessor Macros)
使用宏时要谨慎,尽量以枚举和常量代替之。
宏意味着你和编译器看到的代码是不同的,因此可能导致异常行为,尤其是当宏存在于全局作用域中时。
使用宏进行条件编译,最好不要返么做,会令测试更加痛苦(#define防止头文件重包含当然是个例外)。
宏可以做一些其他技术无法实现的事情,在一些代码库(尤其是底层库中)可以看到宏的某些特性(如字符串化(stringifying,使用#)、连接(concatenation,使用##)等等)。但在使用前,仔细考虑一下能不能不使用宏实现同样效果。
关于宏的高级应用,可以参考《C语言宏的高级应用》。
因此,除字符串化、连接外尽量避免使用宏
下面给出的用法模式可以避免一些使用宏的问题,供使用宏时参考:
⑴ 不要在.h文件中定义宏;
⑵ 使用前正确#define,使用后正确#undef;
⑶ 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称。
⒎ 0和NULL(0 and NULL)
整数用0,实数用0.0,指针用NULL,字符(串)用'\0'。
整数用0,实数用0.0,这一点是毫无争议的。
对于指针(地址值),到底是用0还是NULL,Bjarne Stroustrup建议使用最原始的0,我们建议使用看上去像是指针的NULL,事实上一些C编译器专门提供了NULL的定义,可以给出有用的警告,尤其是sizeof(NULL)和sizeof(0)不相等的情况。
字符(串)用'\0',不仅类型正确而且可读性好。
⒏ sizeof(sizeof)
尽可能用sizeof(varname)代替sizeof(type)。
使用sizeof(varname)是因为当变量类型改变时代码自动同步,有些情况下sizeof(type)或许有意义,还是要尽量避免,如果变量类型改变的话不能同步。
Struct data;
<