C++编程风格

命名约定

通用命名规则

函数命名、变量命名、文件命名应具有描述性,不要过度缩写,类型和变量应该是名词,函数名可以用”命令性“动词。 如何命名?尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要,好的命名选择:

int numErrors; // Good
int numCompletedConnections; // Good

丑陋的命名使用模糊的缩写或随意的字符:

int n; // Bad - 指代不明
int nerr; // Bad - 含混的缩写
int compConns; // Bad - 含混的缩写

类型和变量名一般为名词:如FileOpener、numErrors。 函数名通常是指令性的,如openFile()、setNumErrors(),访问函数需要描述的更细致,要与其访问的变量相吻合。

尽量不要使用缩写,除非放到项目外也非常明了,例如:

// Good
int numDnsConnections; // 基本上大家都知道DNS是什么

// Bad!
int svrhostConnections; // svrhost只在项目组内有意义

不要用省略字母的缩写:

int errorCount; // Good
int errorCnt; // Bad

文件命名

文件名要全部小写,可以包含下划线(_)。 可接受的文件命名:

my_useful_class.cpp
myusefulclass.cpp

C++文件以.cpp结尾,头文件以.h结尾。不要使用已经存在于/usr/include下的文件名。 通常,尽量让文件名更加明确,http_server_logs.h就比logs.h要好。 定义类时文件名一般成对出现,如foo_bar.hfoo_bar.cpp,对应类FooBar。

类型命名

每个单词以大写字母开头,不包含下划线:MyExcitingClass、MyExcitingEnum。 所有类型命名:类、结构体、类型定义(typedef)、枚举,使用相同约定,例如:

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...
// typedefs
typedef hash_map<UrlTableProperties *, string> PropertiesMap;
// enums
enum UrlTableErrors { ...

变量命名

变量名以小写字母开头,后跟的单词以大写字母开头,类的成员变量以下划线结尾,如: myExcitingLocalVariable、myExcitingMemberVariable_。

普通变量命名:

string table_name; // Bad
string tablename; // Bad
string tableName; // Ok

结构体的数据成员可以和普通变量一样,不用像类那样接下划线:

struct UrlTableProperties
{
    string name;
    int numEntries;
}

静态全局变量以s_开头,全局变量以g_开头。

常量命名

函数命名

函数名以小写字母开头,后跟的单词首字母大写:

addTableEntry()
deleteUrl()

命名空间

命名空间命名全小写,单词间以下划线(_)连接。

枚举命名

枚举值全部大写,单词间以下划线相连:MY_EXCITING_ENUM_VALUE。 枚举名称属于类型,因此大小写混合:UrlTableErrors。

enum UrlTableErrors {
    OK = 0,
    ERROR_OUT_OF_MEMORY,
    ERROR_MALFORMED_INPUT,
};

宏命名

宏命名全部大写,单词间以下划线相连:

#define ROUND(x) ...
#define PI_ROUNDED 3.0

命名约定总结

  1. 总体规则:不要随意缩写,如果说ChangeLocalValue写作ChgLocVal还有情可原的话, 把ModifyPlayerName写作MdfPlyNm就太过分了,除函数名可适当为动词外,其他命名尽量使用清晰易懂的名词;
  2. 宏、枚举等使用全部大写+下划线;
  3. 类、结构体的单词首字母大写,不加下划线;
  4. 文件、命名空间等使用全部小写+下划线;
  5. 变量(含类、结构体成员变量)、函数首字母大写,后跟的单词以大写开头,不加下划线。 类成员变量以下划线结尾,静态全局变量以s_开头,全局变量以g_开头;

头文件

#define保护

所有头文件都应该使用#define防止头文件被多重包含(multiple inclusion),命名格式是:__<FILE>_H__

头文件依赖

使用前置声明(forward declarations)尽量减少.h文件中#include的数量。

当一个头文件被包含的同时也引入了一项新的依赖(dependency),只要该头文件被修改,代码就要重新编译。 如果你的头文件包含了其他头文件,这些头文件的任何改变也将导致那些包含了你的头文件的代码重新编译。 因此,我们应尽量少包含头文件,尤其是那些包含在其他头文件中的头文件。

使用前置声明可以显著减少需要包含的头文件数量。 如:头文件中用到类File,但不需要访问File的声明,则头文件中只需前置声明class File; 而无需#include "file/base/file.h"

在头文件如何做到使用类Foo而无需访问类的定义?

  1. 将数据成员类型声明为Foo *或Foo &;
  2. 参数、返回值类型为Foo的函数只声明,但不定义实现;
  3. 静态数据成员的类型可以被声明为Foo,因为静态数据成员的定义在类定义之外;

另一方面,如果你的类是Foo的子类,或者含有类型为Foo的非静态数据成员,则必须为之包含头文件。 有时,使用指针成员(pointer members)替代对象成员(object members)的确更有意义。 然而,这样的做法会降低代码可读性及执行效率。如果仅仅为了少包含头文件,还是不要这样替代的好。 当然,.cpp文件无论如何都需要所使用类的定义部分,自然也就会包含若干头文件。 总之,能依赖声明的就不要依赖定义。

内联函数

只有当函数只有10行甚至更少时才会将其定义为内联函数(inline function)。 当函数被声明为内联函数之后,编译器可能会将其内联展开,无需按通常的函数调用机制调用内联函数。

优点:当函数体比较小的时候,内联该函数可以令目标代码更加高效。

缺点:滥用内联将导致程序变慢,内联有可能是目标代码量或增或减,这取决于被内联的函数的大小。 内联较短小的存取函数通常会减少代码量,但内联一个很大的函数将增加代码量。 现代处理器,由于能更好的利用指令缓存(instruction cache),小巧的代码往往执行更快。

对于内联函数,一个比较得当的处理规则是,不要内联超过10行的函数。 另外,内联那些包含循环或switch语句的函数是得不偿失的,除非在大多数情况下,这些循环或switch语句从不执行。 虚函数和递归函数不应被声明为内联函数,很多编译器并不支持虚函数和递归函数内联。

函数参数顺序

定义函数时,参数顺序为:输入参数在前,输出参数在后。 C/C++函数参数分为输入参数和输出参数两种,有时输入参数也会输出(值结果参数)。 输入参数一般传值或常数引用(const references),输出参数为非常数指针(non-const pointers)。 应该总是将所有输入参数置于输出参数之前,不要仅仅因为是新添加的参数,就将其置于最后,而应该依然置于输出参数之前。

包含文件的次序

将包含次序标准化可增强可读性、避免隐藏依赖,次序如下:C库、C++库、其他库的.h、项目内的.h。

构造函数的职责

在构造函数中执行操作引起的问题有:

  1. 构造函数中不易报告错误,不能使用异常
  2. 操作失败会造成对象初始化失败,引起不确定状态
  3. 构造函数内调用虚函数,调用不会派发到子类实现中,即使当前没有子类化实现,将来仍是隐患
  4. 如果有人创建该类型的全局变量,构造函数将在main之前被调用,有可能破坏构造函数中暗含的假设条件

所以,构造函数中只进行那些没有实际意义的(即:对于程序执行没有实际的逻辑意义)初始化, 尽可能使用init方法集中初始化为有意义的数据。

默认构造函数

如果一个类定义了若干成员变量又没有其他构造函数,需要定义一个默认构造函数,否则编译器将自动生产默认构造函数。

明确的构造函数

对单参数构造函数使用C++关键字explicit。

拷贝构造函数和赋值运算符

仅在代码中需要拷贝一个类对象的时候使用拷贝构造函数和赋值运算符; 不需要拷贝时应禁用拷贝构造函数和赋值操作符。在C11中可通过如下方式禁用拷贝构造函数和赋值运算符:

class MyClass
{
  public:
    MyClass() = default;
    ~MyClass() = default;

    MyClass(const MyClass &) = delete;
    MyClass& operator=(const MyClass &) = delete;
};

结构体和类

仅当只有数据时使用struct,其它一概使用class。

在C++中,关键字struct和class几乎含义等同,我们为其人为添加语义,以便为定义的数据类型合理选择使用哪个关键字。 struct被用在仅包含数据的消极对象上,可能包括有关联的常量,但没有存取数据成员之外的函数功能,而存取功能通过直接访问实现而无需方法调用。 这儿提到的方法是指只用于处理数据成员的,如构造函数、析构函数、init()、get()、set()。

如果需要更多的函数功能,class更适合,如果不确定的话,直接使用class。 如果与STL结合,对于仿函数(functors)和型别萃取(traits)可以不用class而是使用struct。 注意:类和结构体的成员变量使用不同的命名规则。

继承

使用组合通常比使用继承更适宜,如果使用继承的话,只使用公共继承。

多重继承

真正需要用到多重继承的时候非常少,只有当最多一个基类中含有实现,其他基类都是纯接口类时才使用多重继承。

操作符重载

除少数特定环境外,不要重载操作符。 虽然操作符重载令代码更加直观,但有如下不足:

  1. 混淆直觉,让你误以为一些耗时的操作像内建操作那样轻巧
  2. 查找重载操作符的调用处更加困难,如:查找equals()显然比同等调用==容易的多
  3. 有的操作符可以对指针进行操作,容易导致bug。 Foo + 4做的是一件事,而&Foo + 4可能做的是完全不同的另一件事,对于二者,编译器都不会报错,使其很难调试
  4. 重载还有潜规则,比如,重载操作符&的类不能被前置声明。

所以,一般不要重载操作符,如果实在需要的话,可以定义类似equals()、copyFrom()等函数。 然而,极少数情况下需要重载操作符以便与模板或“标准”C++类衔接(如operator<<(ostream&, const T&)), 如果被证明是正当的尚可接受,但你要尽可能避免这样做。 尤其是不要仅仅为了在STL容器中作为key使用就重载operator==或operator<; 取而代之,你应该在声明容器的时候,创建相等判断和大小比较的仿函数类型。

有些STL算法确实需要重载operator==时可以这么做,但不要忘了提供文档说明原因。

编写短小函数

尽量编写短小、凝练的函数。 即使一个长函数现在工作的非常好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的bug。 使函数尽量短小、简单,便于他人阅读和修改代码。

总结

  1. 不在构造函数中做太多逻辑相关的初始化
  2. 为避免隐式转换,需将单参数构造函数声明为explicit
  3. 为避免拷贝构造函数、赋值操作的滥用和编译器自动生成,可通过delete关键字禁用它们
  4. 仅在作为数据集合时使用struct
  5. 组合>实现继承>接口继承>私有继承
  6. 避免使用多重继承,使用时,除一个基类含有实现外,其他基类均为纯接口
  7. 为降低复杂性,尽量不重载操作符,模板、标准类中使用时提供文档说明
  8. 存取函数一般内联在头文件中
  9. 声明次序:public->protected->private;
  10. 函数体尽量短小、紧凑,功能单一