> 文档中心 > Google 代码规范 C++ 解读一:头文件与作用域

Google 代码规范 C++ 解读一:头文件与作用域


引言:

C++ 有许多强大的特性,但强大也带来了复杂性,进一步我们的使代码更容易出错,更难阅读和维护。而且对于 C++,并没有一种强制的代码规范,我们经常能看到各种各样的代码规范,所以我在编程时总会有一种不自信感:无论写什么都感觉不正规、可能有 Bug,最终无从下笔或者不停修改,降低开发效率。

对于刚开始接触代码的同学,如果没有形成一个良好的代码规范,一旦形成手指肌肉记忆了,后面调整的成本会比较高。所以学习了解代码规范势在必行。

Google 代码规范 C++总结

本文背景

Google C++ Style Guide是一份不错的 C++ 编码指南,它通过详细描述编写 C++ 代码的注意事项来管理 C++ 语言的复杂性。 这些规则的存在是为了保持代码的可管理性,同时仍然允许编码人员高效地使用 C++ 语言功能。所以我仔细阅读了 Google C++ Style Guide 指南:对于我而言阅读规范让人有整体的感觉,而且在实际编程时知道该怎么写。

但是可能由于中英文翻译的问题,Google C++ Style Guide 的行文组织对我而言比较晦涩,所以笔者在此做了一些调整总结,希望能够更加读者友好。

当然阅读完原文,再整理一下并吸收其中自己认为最有用的东西,这样也能极大的减轻自己的记忆负担。这也是本文的目的之一。

友情提示

  • 本文不能代替原文,有时间的同学可以将 Google C++ Style Guide 认真阅读一遍,相信会有不一样的理解。
  • 与现有代码保持一致:虽然Google C++ Style Guide是一份不错的 C++ 编码规范,但是与团队保持一致也非常重要,如果你在一个文件中新加的代码和原有代码风格相去甚远的话,这就破坏了文件本身的整体美观也影响阅读,所以要尽量避免。
  • 学习的过程中我们应该尽量养成良好的代码习惯,不过也不要太过于拘泥于标准,毕竟,标准只是为了更好的代码。

Header Files 头文件

通常每一个 .cc(.cpp) 文件都有一个对应的 .h 文件。

特例:单元测试代码和只包含 main() 函数的 .cc(.cpp) 文件。

  • 头文件应该是独立的:自定义函数、自定义类必须在 .h 头文件中,而其定义部分将会放在 .cc(.cpp) 文件中实现 。

    特例1:一个模板或内联函数的声明和定义都放在 .h 头文件中。

    特例2:如果某函数模板为所有相关模板参数的显式实例化,或本身就是某类的一个私有成员,该模板定义在实例化该模板的 .cc(.cpp) 文件里。

    .inc 文件:一个文件是作为文本插入到代码某处或者文件内容实际上是其它头文件的特定平台扩展部分。这些文件并不是头文件,要用 .inc 文件扩展名,不允许分离出 -inl.h 头文件的做法。

  • #defifine 保护:所有头文件都应该使用 #define 来防止头文件被多重包含。

    命名格式: ___H_ ,格式应基于所在项目源代码树的全路径

    例如项目 foo 中的头文件 foo/src/bar/baz.h 头文件标头按照下面定义:

    #ifndef FOO_BAR_BAZ_H_#define FOO_BAR_BAZ_H_…#endif // FOO_BAR_BAZ_H_
  • 前置声明:C++中前置声明和头文件包含

  • 内联函数

    • 只有当函数只有 10 行甚至更少时才将其定义为内联函数。
    • 析构函数声明为 inline 函数需要慎重:析构函数往往比其表面看起来要更长,因为有隐含的成员和基类析构函数被调用!
    • 一般包含循环或 switch 语句的函数,不将其声明为 inline 函数,除非在大多数情况下,这些循环或 switch 语句从不被执行
    • 有些函数即使声明为内联的也不一定会被编译器内联,比如虚函数和递归函数就不会被正常内联。
      • 通常, 递归函数不应该声明成内联函数。
      • 虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数。
  • #include 的路径及顺序:C库 .h、C++库 .h、其他库 .h、项目内的库 .h,同种头文件按字母序进行排列,不同种类头文件以空行隔开。

    • 项目内头文件应按照项目源代码目录树结构排列,避免使用 UNIX 特殊的快捷目录 . (当前目录)或 … (上级目录)。

      // 例如头文件: google-awesome-project/src/base/logging.h #include "base/logging.h"
    • 有时候,由于系统不同,引用的头文件也不同,为了实现跨平台特征,可以用宏:

      #ifdef WIN32#include #elif __linux__#include #endif
    • 举例:

      #include "foo/public/fooserver.h" // 优先位置 #include //C#include  #include //C++#include  #include "base/basictypes.h"//项目所需自定义库#include "base/commandlineflags.h"#include "foo/public/bar.h"

Scoping 作用域

  • 名字空间

    命令空间的主要作用是避免命名冲突。例如常见的std::其实就是标准的命名空间。

    • A 匿名名字空间

      • 在 .cc(.cpp) 文件中,鼓励使用匿名名字空间,以避免运行时的命名冲突 :

        namespace { // .cc(.cpp) 文件中    // 名字空间的内容无需缩进enum { kUNUSED, kEOF, kERROR }; // 经常使用的符号bool AtEof() { return pos_ == kEOF; } // 使用本名字空间内的符号 EOF    } // namespace

        如上例所示,匿名空间结束时用注释 // namespace 标识。

      • 不要在 .h 文件中使用匿名名字空间。

    • B 具名的名字空间

      • 命名空间名字建议全部使用小写、尽量不要使用缩写并避免与预定义的命名空间重名,如std。
      • 在命名空间结束的位置写上注释 // namespace mynamespace;
      • 用名字空间把文件包含,gflflags 的声明/定义,以及类的前置声明以外的整个源文件封装起来,以区别于其它名字空间。
      • 禁止使用 using 指示,例如:using namespace std; ,以保证名字空间下的所有名称都可以正常使用.
      • 在 .cc(.cpp) 文件和.h 文件的函数、方法和类中, 可以使用 using 声明。
      • 在 .cc(.cpp) 文件和.h 文件的函数、方法和类中, 允许使用名字空间别名。
      • 禁止用内联命名空间。
      // .h 文件namespace mynamespace {    // 所有声明都置于命名空间中// 注意不要使用缩进class MyClass {public:…void Foo();};    } // namespace mynamespace
      // .cc 文件namespace mynamespace {// 函数定义都置于命名空间中void MyClass::Foo() {… }    } // namespace mynamespace
      #include “a.h”DEFINE_bool(someflag, false, “dummy flag”);class C; // 全局名字空间中类 C 的前置声明namespace a { class A; } // a::A 的前置声明namespace b {    …code for b… // b 中的代码    } // namespace b
  • 嵌套类:不要将嵌套类定义成公有,除非它们是接口的一部分, 比如,嵌套类含有某些方法的一组选项。当公有嵌套类作为接口的一部分时,虽然可以直接将他们保持在全局作用域中,但将嵌套类的声明名字空间内是更好的选择。

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

    • 对于非成员函数,应该放在一个命名空间内,以避免污染全局;
    • 尽可能少地使用全局函数
    • 不要用一个类简单地集中静态成员。静态成员应该尽可能接近类的实例或者类的静态数据。
    • 静态成员和非成员函数可以作为新类的成员,尤其是如果它们需要获取外部资源或者有很多依赖的时候。
  • 局部变量

    • 将局部变量尽可能置于最小作用域内;
    • 尽量在变量定义时进行初始化;
    • 尽量把局部变量定义在一起,并接近第一次使用的地方;
    • 如果变量是一个对象,尽量不要定义在循环内,每次循环都要调用构造和析构函数,导致代码效率很低。
    int i;i = f(); // 坏——初始化和声明分离int j = g(); // 好——初始化时声明vector v;v.push_back(1); // 用花括号初始化更好v.push_back(2);vector v = {1, 2}; // 好——v 一开始就初始化// 低效的实现for (int i = 0; i < 1000000; ++i) {Foo f; // 构造函数和析构函数分别调用 1000000 次!f.DoSomething(i);}Foo f; // 构造函数和析构函数只调用 1 次for (int i = 0; i < 1000000; ++i) {f.DoSomething(i);}
  • 静态和全局变量:禁止使用 class 类型的静态或全局变量:它们会导致难以发现的 bug 和不确定的构造和析构函数调用顺序。不过 constexpr 变量除外,毕竟它们又不涉及动态初始化或析构。

推荐一个零声学院免费公开课程,个人觉得老师讲得不错,
分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,
fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,
TCP/IP,协程,DPDK等技术内容,点击立即学习:服务器课程