Google C++代码规范

Google C++代码规范

Google官方文档:https://google.github.io/styleguide/cppguide.html

一张图总结规范:https://blog.csdn.net/zyy617532750/article/details/81264648

为什么需要对代码进行规范?

  • 提高可读性:通过规范化代码格式、命名方式和结构,可以让团队中的所有成员更容易理解和维护代码。
  • 减少沟通成本:避免个人风格的冲突,注于逻辑和功能
  • 减少潜在的错误:代码规范包含最佳实践(如命名规则、指针使用的注意事项等),遵守这些规范可以有效减少因风格不一致引起的潜在错误。
  • Google Style Guide 是业界广泛认可的代码风格指南,其权威性和普适性使其成为许多团队和个人学习和参考的对象

以下是我个人的自我总结 Google Style Guide的相关知识(减少了一些模块(个人暂时未使用到的)

一、头文件

1.1 #define 保护

The #define Guard

目的:通过#define来防止头文件被多重包含

命名格式:_ __H

//示例
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
// 头文件相关声明...
#endif  // FOO_BAR_BAZ_H_

1.2 避免前置声明

Forward Declarations

什么是前置声明?

  • 定义:指在程序中提前声明一个类、函数或其他标识符,而不提供其完整定义。

    • class MyClass;  // 前置声明
      
  • 作用(优点):用于解决依赖问题或减少头文件的耦合,节约编译时间。

    • 如果头文件只需要引用某个类的指针或引用类型,可以通过前置声明避免包含整个类的定义,从而减少编译依赖。

      • class MyClass;  // 前置声明
        
        class AnotherClass {
            MyClass* ptr;  // 指针可以使用前置声明
        };
        

为什么避免前置声明?(缺点)

  • 代码的可维护性降低,隐藏了依赖关系(可能会让人忽略头文件变化后必要的重新编译过程)。

  • 缺乏上下文信息:前置声明只能告诉编译器类的存在,无法检查类的具体内容(例如继承关系或成员函数)。如果代码中需要类的定义,就必须引入对应的头文件。

  • 容易引发错误: 当前置声明的类被错误使用(如声明对象、调用成员函数等)时,可能导致编译错误或逻辑问题。

    • class MyClass;  // 前置声明
      
      MyClass obj;    // 错误:无法实例化类
      

1.3 内联函数

Inline Functions

只把 10 行以下的小函数定义为内联 (inline).(不要包含循环、switch、递归、析构、虚函数)

1.4 #include 路径及顺序

Names and Order of Includes

注意事项: 不能出现 UNIX 目录别名 (alias) . (当前目录) 或 .. (上级目录)

  • //路径:google-awesome-project/src/base/logging.h
    #include "base/logging.h"
    

顺序:

  • 源文件的头文件
  • C头文件
  • C++头文件
  • 第三方库的头文件
  • 本项目的头文件
//举例
#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"

二 、作用域

2.1 命名空间

Namespaces

目的:命名空间将全局范围细分为不同的命名范围,因此有助于防止全局范围内的名称冲突。

命名: _

注意事项:应该在namespace内放置代码,禁止使用using(例如:using namespace foo),禁止内联 (inline) 命名空间

// .h 文件
namespace mynamespace {

// 所有声明都位于命名空间中.
// 注意没有缩进.
class MyClass {
 public:
 ...
 void Foo();
};

}  // namespace mynamespace
// .cc 文件
namespace mynamespace {

// 函数定义位于命名空间中.
void MyClass::Foo() {
 ...
}

}  // namespace mynamespace

2.2 内部连接

Internal Linkage

什么是内部连接?

  • 内部链接(Internal Linkage)用于描述标识符(如变量、函数)在编译单元(Translation Unit)内的可见性和作用范围。具有内部链接的标识符仅在定义它的编译单元中可见,其他编译单元无法直接访问这些标识符。

说明:若其他文件不需要使用 .cpp 文件中的定义, 可以将这些定义放入匿名命名空间 (unnamed namespace) 或声明为 static. 但是不要在 .h 文件中使用这些手段.

//实现内部链接的两种方法
// file1.cpp
static int counter = 0;  // 内部链接,仅在 file1.cpp 中可见

static void incrementCounter() {
    ++counter;
}
// file2.cpp
namespace {
    int value = 42;  // 内部链接,仅在 file2.cpp 中可见

    void printValue() {
        std::cout << value << std::endl;
    }
}

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

Nonmember, Static Member, and Global Functions

说明:建议非成员函数放入命名空间,不要使用全局函数,类的静态函数应当和类的实例或静态数据紧密相关.

结论:

  • 定义一个和类的实例无关的函数 -> 非成员函数、静态成员函数

  • 非成员函数 -> 位于命名空间内,且不应该依赖外部变量

2.4 局部变量

应该尽可能缩小函数变量的作用域 (scope),且声明离第一次使用的位置越近越好。并在声明的同时初始化。

注意:如果变量是一个对象, 那么它每次进入作用域时会调用构造函数, 每次退出作用域时都会调用析构函数.

//示例
int i = f(); // 良好: 声明时初始化。
vector<int> v = {1, 2}; // 良好: 立即初始化 v.

int jobs = NumJobs();
f(jobs);      // 良好: 初始化以后立即 (或很快) 使用.

//如果变量是一个对象:循环的作用域外面声明这类变量更高效
Foo f;  // 调用 1 次构造函数和析构函数.
for (int i = 0; i < 1000; ++i) {
    f.DoSomething(i);
}

2.5 静态和全局变量

禁止使用该类变量(基本数据类型可以)

原因:

1. 破坏封装性
  • 静态和全局变量可以被整个程序访问,使得数据暴露在外部,无形中增加了模块之间的耦合。
  • 这违背了封装的设计原则,破坏了模块化代码的可维护性和灵活性。
2. 引发隐式依赖
  • 静态和全局变量的修改可能影响程序中的多个模块,但这些依赖关系通常是隐式的,难以追踪和理解。
  • 当一个模块修改了全局变量,其他模块的行为可能会受到影响,从而引发意料之外的错误。
3. 难以调试
  • 静态和全局变量的值可以在程序的任何地方被修改,这种全局可变状态使得调试变得困难。
  • 如果某个地方修改了全局变量,找到问题的根源可能非常耗时。
4. 线程安全问题
  • 在多线程程序中,全局和静态变量需要显式加锁来防止数据竞争(race conditions)。
  • 使用不当可能导致死锁、竞态条件或其他并发问题,增加代码的复杂性。
5. 降低代码的可测试性
  • 单元测试通常要求代码模块是独立的,能够隔离测试其行为。静态和全局变量引入了共享状态,导致模块之间相互依赖,从而难以进行隔离测试。
6. 难以扩展
  • 静态和全局变量使得模块的状态依赖外部数据,从而限制了代码的可扩展性。例如,在需要重构或扩展某个模块时,可能不得不修改全局变量的逻辑,导致大范围的代码调整。

三、类

3.1 构造函数的职责

构造函数 (constructor) 中不得调用虚函数 (virtual method)。不要在没有错误处理机制的情况下进行可能失败的初始化。

不能调用虚函数的原因:

  • 对象的构造是按照类的继承层次从基类到派生类依次构造进行的

    • 在构造基类对象时,派生类的部分尚未初始化完成。
    • 直到派生类的构造函数被执行完毕,派生类的成员变量和虚表(vtable)才完全设置完成。
  • 如果在基类构造函数中调用虚函数,由于派生类尚未完全初始化,调用的将是基类版本的虚函数,而不是派生类的重写版本。这可能会导致行为不符合预期。

    • //示例
      #include <iostream>
      class Base {
      public:
          Base() {
              // 虚函数调用
              show(); //此时构造调用的是基类的虚函数
          }
          virtual void show() {
              std::cout << "Base::show()" << std::endl;
          }
      };
      
      class Derived : public Base {
      public:
          Derived() : Base() {}
          void show() override {
              std::cout << "Derived::show()" << std::endl;
          }
      };
      
      int main() {
          Derived obj;//明明我定义的是派生类的对象,但是调用的确实基类的虚函数
          return 0;
      }
      
      

3.2 隐式类型转换

抑制隐式类型转换,定义类型转换运算符和单个参数的构造函数时, 请使用 explicit 关键字

3.3 可拷贝类型和可移动类型

必须明确指明该类是可拷贝的、仅可移动的、否则把隐式产生的拷贝和移动函数禁用。

//禁止拷贝、移动
class NotCopyableOrMovable {
public:
// 既不可复制也不可移动.
NotCopyableOrMovable(const NotCopyableOrMovable&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&) = delete;

// 移动操作被隐式删除了, 但您也可以显式声明:
NotCopyableOrMovable(NotCopyableOrMovable&&) = delete;
NotCopyableOrMovable& operator=(NotCopyableOrMovable&&) = delete;
};

什么时候要禁止拷贝、移动?

  • 需要独占资源时:资源(动态内存、文件句柄、网络连接)不能被多个实例共享或复制,则应禁用拷贝。
  • 逻辑上不应该被拷贝:如 单例模式。独占访问的资源(如线程、锁)。
  • 成员变量本身不可拷贝或不可移动:类的成员包含 不可拷贝不可移动 的对象(如 std::unique_ptrstd::mutex),需要明确禁用拷贝或移动。

3.4 组合 > 继承

组合 (composition) 比继承 (inheritance) 更合适,如需继承请定义为public

什么是组合关系?

  • “has-a”(有一个)关系,一个对象由其他对象作为其成员变量组成

  • 被组合的类的对象随着宿主类的对象的创建和销毁而自动管理。

    //举例:小汽车有一个发动机,小汽车启动调用发动机启动,小汽车销毁则发动机销毁
    class Engine {
    public:
        void start() {
            std::cout << "Engine started" << std::endl;
        }
    };
    
    class Car {
    private:
        Engine engine; // Car 组合了 Engine
    
    public:
        void start() {
            engine.start();
            std::cout << "Car started" << std::endl;
        }
    };
    
    int main() {
        Car car;
        car.start();
        return 0;
    }
    
    

组合的优点:

代码复用

  • 通过组合类可以直接复用已实现的类,而不需要重新编写代码。

灵活性

  • 组合提供了更大的灵活性,可以自由选择被组合的类,而不会像继承那样被基类限制。

解耦性

  • 组合优先于继承,因为它避免了子类和基类之间的强耦合,便于扩展和维护。

3.5 访问控制

类的 数据成员应该声明为私有 (private), 除非是常量. 这样做可以简化类的不变式 (invariant) 逻辑, 代价是需要增加一些冗余的访问器 (accessor) 代码 (通常是 const 方法).

3.6 声明顺序

类的定义通常以 public: 开头, 其次是 protected:, 最后以 private: 结尾.

在各个部分中, 应该将相似的声明分组, 并建议使用以下顺序:

  1. 类型和类型别名 (typedef, using, enum, 嵌套结构体和类, 友元类型)
  2. (可选, 仅适用于结构体) 非静态数据成员
  3. 静态常量
  4. 工厂函数 (factory function)
  5. 构造函数和赋值运算符
  6. 析构函数
  7. 所有其他函数 (包括静态与非静态成员函数, 还有友元函数)
  8. 所有其他数据成员 (包括静态和非静态的)
class MyClass {
public:
// 类型和类型别名
    typedef int Integer;
    using String = std::string;
    enum Color { Red, Green, Blue };
    struct Point {    };
    friend void someFriendFunction(MyClass&);

// (可选) 非静态数据成员
    int id_;
    std::string name_;

// 静态常量
    static const int MAX_SIZE = 100;

 // 工厂函数
    static MyClass createWithId(int id) {
        return MyClass(id);
    }

// 构造函数和赋值运算符
    MyClass(int id, const std::string& name) : id_(id), name_(name) {}
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            id_ = other.id_;
            name_ = other.name_;
        }
        return *this;
    }

 // 析构函数
    ~MyClass() {}

    // 所有其他函数
    void display() const {
        std::cout << "ID: " << id_ << ", Name: " << name_ << std::endl;
    }

private:
// 所有其他数据成员
    static int count_;  // 静态数据成员
};

四、函数

4.1 输入和输出

函数的输出倾向于按值返回(而非输出参数), 否则按引用返回。 避免返回指针

输入参数为值或者为const引用

排序函数时,输入参数放在任何输出参数之前

4.2 简短函数

如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.

4.3 函数重载

若要使用函数重载, 则必须能让读者一看调用点就胸有成竹, 而不用花心思猜测调用的重载函数到底是哪一种

4.4 缺省参数

不建议使用缺省参数,而是用函数重载

原因:

  • 接口的二义性, 隐藏了调用意图

    • 函数调用 print(5)print() 的意图不够直观,用户需要查阅函数定义才能确定行为。

    • void print(int x = 0, int y = 10) {
          std::cout << "x: " << x << ", y: " << y << std::endl;
      }
      
      print();        // 输出: x: 0, y: 10
      print(5);       // 输出: x: 5, y: 10
      print(5, 15);   // 输出: x: 5, y: 15
      

五、其它C++特性

5.1 右值引用

只在定义移动构造函数与移动赋值操作时使用右值引用, 不要使用 std::forward 功能函数. 你可能会使用 std::move 来表示将值从一个对象移动而不是复制到另一个对象.

5.2 友元

友元应该定义在同一文件内, 避免代码读者跑到其它文件查找使用该私有成员的类。

如果你只允许另一个类访问该类的私有成员时,使用友元是更好的选择

5.3 禁用运行时类型识别

禁止使用typeid 或者 dynamic_cast

5.4 类型转换

不要使用 C 风格类型转换. 而应该使用 C++ 风格.

  • static_cast 替代 C 风格的值转换, 或某个类指针需要明确的向上转换为父类指针时.
  • const_cast 去掉 const 限定符.
  • reinterpret_cast 指针类型和整型或其它指针之间进行不安全的相互转换. 仅在你对所做一切了然于心时使用.

六、命名约定

类型命名规则
文件命名全部小写,各单词用"_"连接(my_useful_class.cpp
类型命名
类、结构体、typedef、enum
首字母大写,不用下划线(MyExcitingClass
变量命名普通/结构体变量:全部小写,各单词用"“连接(string table_name;
类成员变量:在普通变量的末尾添加下划线”
"(string table_name_;
常量命名”K“开头,后续所有单词首字母大写(const int kDaysInAWeek = 7;
函数命名驼峰命名,没有下划线(void AddTableEntry()
命名空间命名全部小写,各单词用"_"连接
枚举值命名”K“开头,后续所有单词首字母大写(kOK = 0
宏命名全部大写,单词之间下划线连接(#define PI_ROUNDED 3.0

七、注释

采用Doxygen注释风格

  • 单行注释///
  • 行尾注释///<
  • 多行注释/***/

7.1文件开头注释

在每一个文件开头加入版权公告.

文件注释描述了该文件的内容. 如果一个文件只声明, 或实现, 或测试了一个对象, 并且这个对象已经在它的声明处进行了详细的注释, 那么就没必要再加上文件注释. 除此之外的其他文件都需要文件注释.

/**
 * @file        <filename>.cpp
 * @brief       <简要描述文件的功能或用途>
 * 
 * @author      <作者姓名或团队名称>
 * @date        <创建日期,例如 YYYY-MM-DD>
 * @version     <版本号,例如 1.0.0>
 * 
 * @details     <详细描述文件的内容,包括主要功能、算法或实现逻辑>
 *              <可选: 对使用方法或特殊注意事项进行说明>
 */

7.2 类注释

每个类的定义都要附带一份注释, 描述类的功能和用法,

/**
* @brief       <简要描述类的功能或用途>
*	类的详细介绍
*/

7.3 函数注释

每个函数声明处前都应当加上注释, 描述函数的功能和用途

函数声明处注释的内容:

  • 函数的输入输出.
  • 对类成员函数而言: 函数调用期间对象是否需要保持引用参数, 是否会释放这些参数.
  • 函数是否分配了必须由调用者释放的空间.
  • 参数是否可以为空指针.

函数定义处应介绍实现时的关键步骤

/**
 * @brief       <函数的简要描述>
 * 
 * @details     <函数的详细描述,包括逻辑、算法等>
 * 
 * @param[in]   <参数名> <参数的描述,标明输入>
 * @param[out]  <参数名> <参数的描述,标明输出>
 * @param[in,out] <参数名> <既是输入也是输出的参数>
 * @return      <返回值的描述,如果是 void 可忽略>
 */

八、格式

  1. 行宽原则上不超过 80 列, 把 22 寸的显示屏都占完, 怎么也说不过去;
  2. 尽量不使用非 ASCII 字符, 如果使用的话, 参考 UTF-8 格式 (尤其是 UNIX/Linux 下, Windows 下可以考虑宽字符), 尽量不将字符串常量耦合到代码中, 比如独立出资源文件, 这不仅仅是风格问题了;
  3. UNIX/Linux 下无条件使用空格,(修改IDE配置);
  4. 函数参数, 逻辑条件, 初始化列表: 要么所有参数和函数名放在同一行, 要么所有参数并排分行;
  5. 个人习惯更喜欢Windows风格)除函数定义的左大括号可以置于行首外, 包括函数/类/结构体/枚举声明, 各种语句的左大括号置于行尾, 所有右大括号独立成行;
  6. ./-> 操作符前后不留空格, */& 不要前后都留, 一个就可, 靠左靠右依各人喜好;
  7. 预处理指令/命名空间不使用额外缩进, 类/结构体/枚举/函数/语句使用缩进;
  8. 初始化用 = 还是 () 依个人喜好, 统一就好;
  9. return 不要加 ();
  10. 水平/垂直留白不要滥用, 怎么易读怎么来.
  11. 关于 UNIX/Linux 风格为什么要把左大括号置于行尾 (.cc 文件的函数实现处, 左大括号位于行首), 我的理解是代码看上去比较简约, 想想行首除了函数体被一对大括号封在一起之外, 只有右大括号的代码看上去确实也舒服; Windows 风格将左大括号置于行首的优点是匹配情况一目了然.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值