这篇文章总结了个人编写C++的风格,确保自己的代码拥有统一风格。之后上传的自己写的代码都会遵从这个风格和规范。
排版
- 系统头文件与用户头文件包含区分开
- 只引用需要的头文件
- 头文件除了特殊情况,应使用
#ifdef
控制块; 头文件#endif
应采用行尾注释 - 文件应包含文件头注释和内容。 #####
- 函数体类体之间原则上用1个空行
- 如果不能避免函数参数比较多,应在排版上可考虑相似含义的参数占用一行,参数名竖向对齐, 甚至每个参数一行。
- 类,结构,枚举,联合:大括号另起一行
- 函数体的”{“需要新起一行
- 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔
- “if”、”for”、”while”、”do”、”try”、”catch” 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加 “{ }” 。这样可以防止书写和修改代码时出现失误。
5 缩进和空格
- 最好使用2空格,不使用
tab
。因为空格在不同地方的表现一致,而tab
则和用户的设置有关。 - 在代码行的结尾部分不能出现多余的空格。
- 代码行最大长度宜控制在一定个字符以内,能在当前屏幕内全部可见为宜。建议80或120个
1 命名
- 风格统一;
- 可以长但含义必须清楚;
- 类型名采用大写驼峰,无下划线;
- constexpr,const,static,external 可根据需要添加表示;
- 临时使用的变量以小写字母t开头;
- 如果变量名需要写明类型,把类型写在最后;
- 避免过于相似, 例如仅靠大小写区分的相似的标识符
- 用正确的反义词组命名具有互斥意义的标识符, 例如”minValue” 和 “maxValue”
- 避免名字中出现wu逻辑yiyi的数字编号
- 函数名称不推荐用下划线,例如,handle_color。
2 注释
- 文件头注释: 作者,文件名称,文件说明,日期
- 函数的注释在声明和定义处都需要注释;
- 变量的注释可在变量前一行或该行的末尾;
- 为注释选择合适的语言,既能看懂又能防止乱码
- 对每个
#else
或#endif
给出行末注释 - “TODO”, “DEBUG”(调试的代码), “NOTE”(需要引起关注的代码)
-
对于较大的代码块结尾,如for,while,do等,可加上 “// end for while do” 或者同等含义的注释。
3 符号
- 如果不同的符号有相同含义,在使用相同含义时,使用一致的符号表示???
4 异常处理
- 及时发现错误并给出提示,try catch,输出和打印相应等级日志;
- 抛出异常而非使程序崩溃退出
6 常数设置
- 对函数内使用的常数,例如
PI
,设置为constexpr
等类型,不要在函数内直接使用相关常量的数字; - 如果”cos60”之类的数作为常数在程序中使用, 依照此规则
7 嵌套三角
- 嵌套的三角法则——>函数+return/循环+continue (有大量if但没有对应的else时, if语句之后所在函数就结束时, if语句结束之后本次循环结束)???
8 参数传递
- 一系列参数和方法如果一起实现,代码要适当分行,例如传入参数太多,就分类分行写
9. 调用深度
- 同一个文件中,函数从上到下颗粒度和调用深度增加;
- 注意调用深度,最好小于10
10. 功能分割
- 合理分割功能,通过函数和类实现模块化
11. 参数传递
对于传递的参数较多的函数,
- 传入和传出最好使用通用的标准的数据类型,如vector、Eigen::MatrixXd,独立性和可移植性好,传递的可读性差可以用良好的注释弥补;???
- 定义新的类(数据类型)把参数整合起来,参数传递过程的可读性强;???
12. 访问控制
- 如果不涉及作为继承关系中的基类,类内使用的成员变量声明为私有;
- 当需要使用成员变量时,声明和定义一个成员函数来访问这个变量的值(如果这个操作不是为了修改成员,不要返回指针或引用;似乎返回常量引用也可以)font color=Red>???</font>
14. 代码测试
- 如有必要保存,使用统一的方式控制所有测试代码,例如用同一个布尔型变量控制是否进入测试代码;
- 最好的方式是使用gtest等单元测试
15. 参数缺省
- 不要为含参数构造函数的参数都添加缺省值???
16
- 定义指针和引用时*和&紧跟变量名
- 用typedef简化程序中的复杂语法
- 尽量用引用取代指针
- 不要使用魔鬼数字
17
- 不要对浮点类型做等于或不等于判断
18
- 当函数返回引用或指针时,用文字描述其有效期
- 只有简单的函数才有必要设计为内联函数,复杂业务逻辑的函数不要这么做
-
虚函数不要设计为内联函数???
- 只读取该参数的内容,不对其内容做修改,用常量引用
-
简单数据类型用传值方式, 复杂数据类型用引用或指针方式
-
输入参数排在前面,输出参数排在后面,默认参数除外
- 类按照 public, protected, private 的顺序分块
- 每一块中,按照下面顺序排列:typedef,enum,struct,class 定义的嵌套类型->常量->构造函数->析构函数->成员函数,含静态成员函数->数据成员,含静态数据成员
- 构造函数
- 构造函数的初始化列表,应和类里成员变量的顺序一致
- 初始化列表中的每个项,应独占一行
- 构造函数应初始化所有成员,尤其是指针
- 不要在构造函数和析构函数中抛出异常
- 接口类的虚函数应设计为纯虚函数
- 如果类可以继承,则应将类析构函数设计为虚函数。如果类不允许继承,则应将类析构函数设计为非虚函数
- 如果类不能被复制,则应将拷贝构造函数和赋值运算符设计为私有的
- 尽量避免使用公有(public)成员变量
- 如果是子类型重写父类的虚函数,应该在函数声明后面添加”override”,让编译器来检查是否重新定义非虚函数
-
不想被子类重写的虚函数,函数声明后面添加final
- 应避免设计大而全的虚函数,虚函数功能要单一
-
避免将基类强制转换成派生类
- 释放内存完成后将指针赋空,避免出现野指针。
性能
- 卫句风格:先处理所有可能发生错误的情况,再处理正常情况
- 尽量避免在循环体内部定义对象
- 尽量使用标准库中封装的算法
语法
整型数
- 如果确定整数非负,应该使用
unsigned int
而非int
。处理器处理无符号整型的速度可能远快于有符号整型,也有利于代码的自解释。 - 整形
int
的运算速度高浮点型float
,并且可以被处理器直接完成运算,而不需要借助于FPU(浮点运算单元)或者浮点型运算库。例如在一个计算包中,如果需要结果精确到小数点后两位,我们可以将其乘以100,然后尽可能晚的把它转换为浮点型数字。
除法和取余数
除法函数消耗的时间包括一个常量时间加上每一位除法消耗的时间.如果确定操作数是无符号unsigned
的,使用无符号unsigned
除法更好一些,因为它比有符号signed
除法效率高。
合并除法和取余数
如下代码,编译器可以通过调用一次除法操作返回除法的结果和余数。
int func_div_and_mod (int a, int b)
{
return (a / b) + (a % b);
}
通过2的幂次进行除法和取余数
编译器使用移位操作来执行除法。因此,我们需要尽可能的设置除数为2的幂次(例如64而不是66)。
全局变量
全局变量绝不会位于寄存器中,所以,在重要的循环中我们不建议使用全局变量。
使用别名
考虑如下的例子:
void func1( int *data )
{
int i;
for(i=0; i<10; i++)
{
anyfunc( *data, i);
}
}
尽管*data的值可能从未被改变,但编译器并不知道anyfunc函数不会修改它,所以程序必须在每次使用它的时候从内存中读取它。如果我们知道变量的值不会被改变,那么就应该使用如下的编码:
void func1( int *data )
{
int i;
int localdata;
localdata = *data;
for(i=0; i<10; i++)
{
anyfunc (localdata, i);
}
}
这为编译器优化代码提供了条件。
变量的生命周期分割
变量的生命周期开始于对它进行的最后一次赋值,结束于下次赋值前的最后一次使用。需要使寄存器分配的变量数目需要超过函数中不同变量生命周期的个数。如果不同变量生命周期的个数超过了寄存器的数目,那么一些变量必须临时存储于内存。这个过程就称之为分割。
局部变量
我们应该尽可能的不使用char和short类型的局部变量。对于char和short类型,编译器需要在每次赋值的时候将局部变量减少到8或者16位。这对于有符号变量称之为有符号扩展,对于无符号变量称之为零扩展。这些扩展可以通过寄存器左移24或者16位,然后根据有无符号标志右移相同的位数实现,这会消耗两次计算机指令操作(无符号char类型的零扩展仅需要消耗一次计算机指令)。
可以通过使用int和unsigned int类型的局部变量来避免这样的移位操作。
指针链
p->pos->x
—> ` *pos = p->pos; pos->x`
布尔表达式和范围检查
x>min && x<max
可以转换为(unsigned)(x-min)<(max-min)
布尔表达式和零值比较
处理器的标志位在比较指令操作后被设置。标志位同样可以被诸如MOV、ADD、AND、MUL等基本算术和裸机指令改写。如果数据指令设置了标志位,N和Z标志位也将与结果与0比较一样进行设置。N标志表示结果是否是负值,Z标志表示结果是否是0。
C代码中每次关系运算符的调用,编译器都会发出一个比较指令。如果操作符是上面提到的,编译器便会优化掉比较指令。
二分中断
if(a==1) {
} else if(a==2) {
} else if(a==3) {
} else if(a==4) {
} else if(a==5) {
} else if(a==6) {
} else if(a==7) {
} else if(a==8)
{
}
使用下面的二分方式替代它,如下:
if(a<=4)
{
if(a<=2)
{
if(a==1)
{
/* a is 1 */
}
else
{
/* a must be 2 */
}
}
else
{
if(a==3)
{
/* a is 3 */
}
else
{
/* a must be 4 */
}
}
}
else
{
if(a<=6)
{
if(a==5)
{
/* a is 5 */
}
else
{
/* a must be 6 */
}
}
else
{
if(a==7)
{
/* a is 7 */
}
else
{
/* a must be 8 */
}
}
}
switch语句vs查找表
char * Condition_String1(int condition) {
switch(condition) {
case 0: return "EQ";
case 1: return "NE";
case 2: return "CS";
case 3: return "CC";
case 4: return "MI";
case 5: return "PL";
case 6: return "VS";
case 7: return "VC";
case 8: return "HI";
case 9: return "LS";
case 10: return "GE";
case 11: return "LT";
case 12: return "GT";
case 13: return "LE";
case 14: return "";
default: return 0;
}
}
char * Condition_String2(int condition) {
if ((unsigned) condition >= 15) return 0;
return
"EQ\0NE\0CS\0CC\0MI\0PL\0VS\0VC\0HI\0LS\0GE\0LT\0GT\0LE\0\0" +
3 * condition;
}
第一个程序需要240 bytes,而第二个仅仅需要72 bytes。
循环
循环终止
简单的终止条件消耗更少的时间。
int fact1_func (int n)
{
int i, fact = 1;
for (i = 1; i <= n; i++)
fact *= i;
return (fact);
}
int fact2_func(int n)
{
int i, fact = 1;
for (i = n; i != 0; i--)
fact *= i;
return (fact);
}
更快的for()循环
第二个程序的fact2_func执行效率高于第一个。
for( i=0; i<10; i++){ ... }
for( i=10; i--; ) { ... }
合并循环
如果你需要在循环中做很多工作,这坑你并不适合处理器的指令缓存。这种情况下,两个分开的循环可能会比单个循环执行的更快。
循环展开
简单的循环可以展开以获取更好的性能,但需要付出代码体积增加的代价。
函数设计
减少函数参数传递消耗
- 尽量保证函数使用少于四个参数。这样就不会使用栈来存储参数值。
- 如果函数需要多于四个的参数,尽量确保使用后面参数的价值高于让其存储于栈所付出的代价。
使用查找表
查找表的精确度比通常的计算低,但对于一般的程序并没什么差异。
许多信号处理程序(例如,调制解调器解调软件)使用很多非常消耗计算性能的sin和cos函数。对于实时系统,精确性不是特别重要,sin、cos查找表可能更合适。当使用查找表时,尽可能将相似的操作放入查找表,这样比使用多个查找表更快,更能节省存储空间。
浮点运算
- 浮点除法很慢。浮点除法比加法或者乘法慢两倍。
- 避免使用先验函数。先验函数,例如sin、exp和log是通过一系列的乘法和加法实现的(使用了精度扩展)。这些操作比通常的乘法至少慢十倍。
- 编译器并不能将应用于整型操作的优化手段应用于浮点操作。。例如,
3*(x/3)
可以优化为x,而浮点运算就会损失精度。
其他技巧
- 尽可能使用一个字大小的变量(int、long等),使用它们(而不是char,short,double,位域等)机器可能运行的更快。
- 编译器可以在一个文件中进行优化-避免将相关的函数拆分到不同的文件中,如果将它们放在一起,编译器可以更好的处理它们(例如可以使用inline)。
- 加法操作比乘法快-使用
val+val+val
而不是val*3
。 - 使用#define宏取代常用的小函数。
- 二进制/未格式化的文件访问比格式化的文件访问更快,因为程序不需要在人为可读的ASCII和机器可读的二进制之间转化。如果你不需要阅读文件的内容,将它保存为二进制。