google开源项目风格指南-C++

google开源项目风格指南-C++

引言

自从大学开始接触c++语言(平时写的都是类C代码),断断续续的已经有7、8年过去了,但是,始终没有规范的在工程上使用C++。现在的工作主要使用python语言,更多的是关注算法,而没有关注语言本身。但是python项目也给我带来了工程经验,经验之一就是,代码是写给别人看的,大家遵守共同的编程规范很重要。所谓的编程规范,也可以称为编程风格,一开始会让新手感觉是桎梏,但随着解了越来越多的bug,写了越来越多的代码,也体会到了,编程规范的而重要性,编程规范不但是一种风格,而且是避免埋坑、减少bug的重要措施。如果大家写的代码一致性高,那阅读起来也就方便多了。

在网上搜索了google的编程规范,应该是值得一读的,因为这些规范都是大厂内的有丰富工程经验的人总结出来的精华。因此,准备认真研读,并且,测试为什么这样规定。

头文件

一般来讲,每个cpp文件都应该对应一个.h文件(除了只包含main函数的文件),这是为了提升可读性与可维护性。一般头文件要遵守以下规范:

  • 头文件要加入#define保护,避免重复编译时导入;
  • 头文件只能声明变量,不能定义变量,全局的变量不可以声明,因为全局变量在声明时会初始化一个默认值。两次包含给头文件,编译时,会报重复定义的错误;
  • 头文件的包含顺序为自身头文件、c标准库、c++标准库、其他项目头文件、自身项目其他头文件;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 为了防止被多重导入,header文件要有#define保护 */
#ifndef HEADER_A_H
#define HEADER_A_H 1

// 头文件里面不要定义任何变量,也不声明全局变量,全局变量会在声明时初始化为默认值
// C++原则-唯一定义原则(One Definition Rule), 但一个变量可以声明多次
//int i; //error 两次被引入会错误
//int j=0; //error 不能定义变量

void print(void); // ok, declear function

// ok , declear a class
class A {
int a,b;
public:
A(int a, int b);

};

#endif /* HEADER_A_H */

作用域

命名空间

命名空间(namespace)可以把全局作用域细分为具名独立的作用域,使用命名空间,可以避免一些通用命名的冲突。但两个不同命名空间的相同变量名称放在一起会有迷惑性,而且不得不引用多层嵌套命名空间内的定义会使代码变得冗长。

命名空间的两个特殊特性:

  • 内联命名空间:内敛命名空间会自动把标识符放在外部命名空间。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    namespace foo1{
    inline namespace foo2 {

    int c = 3;

    } //foo2

    int a = 1, b = 2;
    void print() {
    std::cout << a * b * c << std::endl;
    }

    } // foo1

    foo1::c 与 foo1::foo2::c一致
  • 匿名命名空间:编译器会给匿名命名空间生成一个独立不可见标识,导致,其他文件不能引用匿名命名空间内的变量,两个不同匿名命名空间的相同定义不会冲突(两个不同cpp文件)。

    1
    2
    3
    namespace {
    ...
    } //namespace

规范:

  • 禁止使用内联命名空间,在大型版本控制里面使用
  • 鼓励在.cc文件中使用匿名命名空间或static声明
  • 不能在header里面定义匿名命名空间,会违背唯一定义原则
  • 不能使用using指示命名空间
  • 不要在std里面声明任何任何东西,否则会导致不可移植
  • 不要在头文件中使用命名空间别名

非成员函数、静态成员函数、全局函数

使用静态成员函数或命名空间内的非成员函数, 尽量不要用裸的全局函数. 将一系列函数直接置于命名空间中,不要用类的静态方法模拟出命名空间的效果,类的静态方法应当和类的实例或静态数据紧密相关.

局部变量

将函数的变量尽可能置于最小的作用域内,并在变量声明时进行初始化。

不要在循环内部初始化对象,否则每次调用构造函数与析构函数导致执行效率很低

静态与全局变量

禁止定义静态存储周期的非POD(Plain Old Data),POD数据如int、char、float,多编译单元中的静态变量执行的构造与析构顺序时未明确的。也不使用vector和string类型的静态变量。

构造函数

不要再构造函数内调用虚函数,也不要进行可能会失败的初始化。因为这样不用考虑类是否被正确的初始化,经过构造函数初始化的类型可以是const类型,也可以方便的被其他容器调用。

如果调用虚函数,这类调用不会重定向到子类的虚函数实现;如果初始化可能会报错,则初始化的对象可能进入不正常的状态,必须调用bool isValid()判断,而这个方法容易被忽视;构造函数的地址是无法被获得的,有构造函数完成的工作无法以简单的方式交给其他线程。

运算符重载

除少数特定环境下,不要重载运算符,这与《More Effective C++》里面第5条款的要求是类似的。

运算符重载是指,c++允许用户使用operator关键字对内建运算符进行重载定义,operator允许用户使用operator""定义新的字面量,并且定义类型转换函数如:

1
2
operator bool(); // 重载bool类型转换
operator double(); //重载double类型转换

重载运算符可以使代码更简洁易懂,使用户自定义的类型与内建类型拥有相似的行为。

但是过度使用操作运算法重载,会导致令人迷惑的bug。

不要为了避免重载操作符而走极端. 比如说, 应当定义 ==, =, 和 << 而不是 Equals(), CopyFrom()PrintTo(). 反过来说, 不要只是为了满足函数库需要而去定义运算符重载. 比如说, 如果你的类型没有自然顺序, 而你要将它们存入 std::set 中, 最好还是定义一个自定义的比较运算符而不是重载 <.

不要重载 &&, ||, , 或一元运算符 &. 不要重载 operator"", 也就是说, 不要引入用户定义字面量。

其他

  • 如果不需要拷贝,则把默认拷贝构造函数禁掉,使用= delete
  • 不要使用多继承,少使用继承。