c++代码规范

文件后缀名

实现:.cc

声明:.h(模板声明和定义都写在.h里)

内联:.inc(不允许有-inl.h,也就是声明和定义写在一起)或者用于插入文本

实现和声明一一对应,除非如单元测试和只包含main函数的可以只有.cc

目录

头文件

一个.h文件应该自给自足,其自身就应该include所有其他依赖头文件,并不要求定义任何特别symbols。

除了为模板提供实例化的模板函数需要定义在实例化改模板的.cc文件里。

  1. define

    • 头文件应该有#define保护来防止被多重包含,名字的格式是H,全部都要大写。

      • 22

        2

    • 1

    • 使用

    1
    2
    3
    4
    #ifndef 名字
    #define 名字
    ...
    #endif
  2. 前置声明

    • 尽量避免使用前置声明。只在需要避免循环嵌套的时候使用。一般来说前置声明是用来定义一个指针,用于避免重复编译。

      不使用的原因:没声明关系,声明冗长,更改后跳过必要的重新编译,是不完全类型。

  3. 内联函数

    • 只有函数少于10行且不是折构,不包含循环和switch才应该内联。虚函数一般也不内联。

      不使用的原因:内联大量的代码是更低效率的,折构往往实际上很长。

    类内部的函数会自动内联。

  4. include

    • 头文件顺序:相关头文件,C库,C++库,其他库的.h,本项目的.h。并按照字典序排序,每个大部分用空行隔开。

      此处本项目的.h是指为了预留扩展性和满足接口编程的需要而引入的?。

      原因:为了保证本模块的问题先暴露

    • 路径格式:项目完整路径

      以减少隐藏依赖

    • 包含的头文件:实现文件只需要包含.h,其他文件包含一切直接用到的名字的头文件,即便这个头文件被包含在其他头文件内,除非其他头文件明确表示会提供这个名字

    • 例外:需要特殊宏判断的可以放在最后面。

YuleFox

标准化函数参数顺序可以提高可读性和易维护性(对函数参数的堆栈空间有轻微影响, 我以前大多
是相同类型放在一起);

作用域

  1. 命名空间

    • 只在局部作用域中声明命名空间别名

      头文件的全局作用域中声明的命名空间别名会成为公开的,导致污染

    • 禁止使用using指示

      污染空间

    • 禁止使用内联命名空间

      内联命名空间一般用于大型版本控制,保持跨版本的ABI兼容性,也就是通过内联新版本的一些函数来代替旧版本的函数但是旧版本的函数仍然是可用的。

    • 不再std中声明任何东西

      是未定义的行为,会导致不可移植性

    格式

    • 除了gflags(谷歌的写的一个更方便在命令行给程序传入参数的库),头文件和类的前置声明以外的所有部分都应该被包含于一个命名空间中。命名空间内部内容不因为命名空间而缩进。

    • 在命名空间右大括号后注释上namespace name(会自动补全)

      在插入新函数时能更快确定作用域

    • 命名空间的命名可以根据绝对或相对路径

  2. 匿名命名空间和静态变量

    • 在.cc中定义不需要被外部引用的对象时,建议使用匿名命名空间或声明为static。但不要在.h中使用。

      这二者都能使对象具有内部连接性,也就是这个对象的名字只能在本文件被访问。

  3. 非成员函数、静态成员函数和全局函数

    • 尽量不要用裸的全局函数,尽量单独形成编译单元

      污染命名空间

    • 静态成员函数应该与静态数据成员高度相关,不要用来当做限定作用域的非成员函数

    • 非成员函数,不依赖于外部变量,应该被放于命名空间中

      避免污染

    • 当一个静态成员函数被超过一个编译单元(cpp文件)所使用,可以考虑将其提取到新类中。

      避免耦合和依赖。

  4. 局部变量

    • 局部变量限定于最小可能的作用域内,并在变量声明时进行初始化。if,while,for语句中需要的变量就在语句中声明即可,并且会在语句的作用域内可用。

      更易于理解。

    • 例外:如果变量是一个对象,并且每次进出作用域都要调用折构和构造函数,那么不如定义在外层。

      省时间

  5. 静态和全局变量

    • 对于静态存储周期的变量,只允许定义POD变量,并且不允许用使用了静态存储周期变量的函数来初始化它,除非变量是constexpr的。

      静态存储周期是指程序或者多线程开始时生成,程序结束时销毁的变量。包括全局变量,静态变量,静态类成员变量和函数静态变量。

      POD是指plain old data,是指不使用构造和折构函数的变量。

      由于初始化过程的顺序是未定义的,所以在程序开始初始化和程序结束折构的时候会出问题。

    • 如果一定想要用一个非POD类型的静态存储周期的变量,可以在main或者pthread_once中初始化一个指针并永不回收,且不能用智能指针,因为它也是非POD的。

  1. 构造函数的职责

    • 构造函数不允许调用虚函数。

      构造函数中的虚函数并不会真正的调用虚函数。

    • 如果构造函数出错,那么建议直接终止程序。否则应该用init()或者工厂函数来构造。

      因为错误处理被禁用了。因此并没有一个好的处理构造函数出错的解决方法。构造函数一旦出错,如果不报错,那就很难检查出这个呗构造出的对象是否正常。

  2. 隐式类型转换

    • 类型转换运算符和单参数构造函数都应当被标记为explicit。

      可能会在无意识中发生隐式类型转换。

      函数重载并不知道调用的是哪一个函数。

    • 拷贝和移动构造函数不应该被标记为explicit。

      因为拷贝和移动构造函数本身就不进行类型转换。

    • 不以一个参数进行调用的构造函数不应加explicit,如列表初始化?。

  3. 可拷贝类型和可移动类型

    • 如果一个类型不是很显然要拷贝移动操作的,就不要设置为可拷贝移动的。

      因为拷贝构造函数是隐式调用的,调用容易被忽略。

    • 如果定义了拷贝操作,那么就应该定义移动操作。

      移动操作拷贝临时的对象,那么也能拷贝非临时对象。并且效率更高。

    • 如果不定义拷贝移动操作,那么就应该定义为删除(=delete)的。

      避免使用时无意识的隐式调用。

      使用delete时,若是拷贝操作被delete了,那么编译器也不会自动合成移动操作。

    • 一旦定义,那么就应该是两个为1组出现的。

      赋值与构造。

    • 不要给可能有派生类的基类定义拷贝移动操作,如有需要,应提供一个clone来解决。

      给基类定义拷贝移动操作可能会产生切割。

  4. 结构体与类

    • 当且仅当只有数据成员时用struct,其他都用class。

      一般struct都是直接访问位域的,也就是直接通过点运算符,而class则通过函数。如果提供了用于设定数据成员的函数外的函数,那么就应该用class。

    • 仿函数可以用struct。

      与STL保持一致。

  5. 继承

    组合>实现继承>接口继承>私有继承

    • 所有继承都必须是public的。

      其他继承为什么不用组合来代替。

    • 只有关系是is的时候使用继承,其他时候使用组合。

      只有is才是一种继承关系,has就是组合关系了。

    • 如果类有虚函数,那么折构函数也应该是虚函数。

      显然,因为这个子类是虚的,得要避免基类指针只调用基类的折构函数。

    • 对于所有的重载函数都要用override标注。

      显式表明这是虚函数。

    • 只有对一定会被子类访问的成员函数设置为protected。

      封装。

  6. 多重继承

    • 只允许在最多一个基类是非抽象类其他基类都是以Interface为后缀的纯接口类时使用多重继承。

      一般都有别的可以来替代多重继承的方法。

  7. 接口

    • 接口类应该以Interface为后缀。

      用以提醒这是接口类。

      一个纯接口必须满足:

      1. 只有纯虚函数和静态函数
      2. 没有非静态数据成员
      3. 没有定义任何构造函数
      4. 如果要继承,也只能从纯接口类继承
      5. 折构函数没必要是纯虚函数
  8. 运算符重载

    • 只有在符号意义明确与内建运算符行为一致时才重载。

      重载本身就可能会出问题。如果意义与通常的约定不符时会难以理解。

    • 只对自己定义的类型进行重载运算符,并将其定义在同一个空间中。

      不去影响内建运算符的含义,并且在何处都能使用这个运算符而不出错,并且避免了多重定义。

    • 当定义了一个运算符,就应该将相关的运算符都重载。

      保证语义一致,不冲突。

    • 不要将不进行修改的二元运算符定义为成员函数。

      不然就会只对第二个参数进行隐式转换。

    • 不要强行去重载。

      如当一个类型不具备自然顺序,但我们希望将其放入set中,那么应该自定义一个比较运算符,而不是重载我们常用的<运算符。避免给它加上不必要的含义。

    • 不要重载&&,||和一元运算符&,也不要重载operator””。

    • 更不要重载运算符模板。

      除非适配了所有可能性。

  9. 存取控制

    • 所有数据成员都声明为private,除非是static const的。

      封装。

    • 存取函数一般内联。

      存取函数就是存取这些私有数据成员的函数。

  10. 声明顺序

    • 按public,protected,private的顺序,没有空行。

    • 在每个部分中按类型,常量,工厂函数,构造函数,赋值运算符,折构函数,其他函数,数据成员的顺序排。

    • 只将性能关键且短小的函数内联。

函数体应短小且功能单一。

函数

  1. 输入和输出

    • 返回值和输出参数相比,返回值更好。

      输出参数就是通过引用的方式来在函数内部进行更改,进行“输出”。

      返回值更加直观,可读性更好且性能更好?

    • 输入参数用值传递或者const引用,其中超出生命周期的要用值传递。

      对于超出生命周期的对象进行const引用会访问不存在的对象。

    • 可选参数中值传递用std::optional,其他可选输入用const指针,可选输出用指针。

      std::optional就是使一个类型可以为空。

      其他使用指针是因为指针可以为空指针。

    • 将所有输入参数放在输出参数之前。

      可读性。

  2. 编写简短函数

    • 建议所有函数都不要超过40行,若超过40行,应将其分割为多个小函数。

      更加易于管理与调试。

  3. 引用参数

    • 输入参数如果是引用必须是const的,(输出参数一般为指针)。

      防止更改。另外实际上我们要的输入都是值输入,而引用就是有着指针属性的值输入。

    • 有的时候输入参数可以使const指针,除非const指针比const引用更明智。

      如在可能传递空指针或者传递的是地址的时候就应该使用const指针。

  4. 函数重载

    • 除非使用者能很容易分辨重载函数调用的是哪一个重载函数,否则不要重载。

      在函数名中加上参数信息或者使用初始化列表来解决同一类型的多个参数。

  5. 缺省参数(默认参数)(尽可能避免使用)

    • 虚函数不许用缺省参数。

      虚函数的缺省参数值取决于静态类型。

    • 如果缺省参数的值在变化,也不要用缺省参数。

      不仅用户会疑惑,缺省参数的值在每个调用点都会展开一次声明,然后就导致代码臃肿。

    • 如果要用函数指针,那么也不要用默认参数,去用重载函数。

      函数指针无法使用默认参数,且加上默认参数后地址也会变。

    • 缺省参数实际上也是一种重载函数,全部都可以使用重载函数代替。

    • 限制于局部文件的地方和构造函数可以使用。

      一般不会去获取地址。

  6. 后置返回语法

    • 只在后置返回语法能大幅度提升可读性时或者在使用lambda的时候使用。

Google的奇技

  1. 所有权

    • 如果用动态分配的内存,那么就应该使用unique_ptr来明确所有权和所有所有权的传递。

      明确所有权,方便释放。

    • 当且仅当拷贝的开销很大并且操作的对象不可变的时候可以使用shared_ptr共享所有权。

      拷贝的开销实际上不是很大,所有权一旦共享,可能就搞不清所有权了,并且共享所有权的运行开销很大。

  2. cpplint

    用于检测风格错误的一个py文件。

其他C++特性

  1. 右值引用

    • 只在移动构造函数和移动拷贝函数时使用右值引用。

      可以很大提高效率。

    • 不要使用std::forward。

      太新了?,虽然完美转发需要std::forward。

  2. 友元

    • 一般友元定义在同一文件内,一般工厂类和单元测试会被声明为友元。

      仍然保持着封装性。

  3. 异常

    • 异常肯定是优于错误码的,适当的正确的使用异常时可行的。

      但是google禁用异常,大概率是因为历史包袱。

  4. RTTI

    • RTTI可以进行合理的使用,可以在单元测试中使用。一般都有替代方案,虚函数和dynamic_cast(能保证基类对象都是指向同一派生类)。

      使用RTTI本身就表明设计是有问题的,且难以维护。

  5. 类型转换

    • 使用c++的类型转换。

      const_cast去const。

      static_cast:值转换或者类指针向上转为基类指针。

      reinterpret_cast进行不安全的相互转换。

      dynamic_cast见上。

    • 只在日志的时候使用流,其他时候都使用printf和read/write。

      流可读性差,格式化慢,并且在输出时不容易关注到输出类型,导致错误。

  6. 自增自减

    • 使用前置自增自减

      后置自增自减会产生一次拷贝,运行效率低。

  7. const

    • 在任何可能用的地方用const,使用时前置加一个const即可,不需对于部分都加一个定语。

      提供一层保护,对程序员自己的保护。

  8. constexpr

    • 在定义真.常量(运行和编译时都保持不变)的时候使用,或者在给常量初始化时使用。

      提高效率。

  9. 整型

    • 内建类型的整型只用int,并且只认为它至少32位,但不多于32位。

      int是最常用的。

    • 其他应该使用如int16_t或者int64_t,而不应该使用long long或者short等。

      在不同的平台上位数可能不同。

    • 只在位域的时候使用无符号整数,用断言来处理无符号整数。

      无符号整数会导致很多抽象的bug。

    • 专门的类型就用专门的类型别名,如size_t和ptrdiff_t。

      在不同的环境下也会随之发生改变。

    • 宁愿更大,不要更小。

      避免溢出。

  10. 64位下的可移植性

    评价是不懂

  11. 预处理宏

    • 尽量少用宏,可以用内联,枚举或常量取代。

      宏很复杂。

    • 防止重包含是例外。

    • 除非有的地方其他实现不了,只有宏能行。

  12. 零赋值

    • 字符串用’\0’,指针用nullptr,整数用0,实数用0.0。

      sizeof(NULL)和sizeof(0)还是不一样的。

  13. sizeof

    • 尽可能使用sizeof(变量)而非sizeof(类型)。

      使得变量即便类型改变也不会出问题。

  14. auto

    • 只在局部变量和尾置返回一起使用。

      还是为了易读性。在文件作用域,命名空间作用域和类数据成员都不要用auto,要显式指明。

  15. 列表初始化

    • 评价是非常好用,只不过不要用来给auto赋值,除非你想创建一个initializer_list。
  16. Lambda

    • 可以适度按格式使用lambda。

      很好用。

    • 捕获列表要写完整,最好显式写出返回类型。

      可读性好。

    • lambda应该简短,不超过5行。

      否则不如创建一个函数。

  17. 模板编程

    • 减小模板的复杂性,并减少暴露在外的模板。

      模板本身的维护成本可能就超过了模板使用的简便性。

  18. Boost库

    里面很多库都很好,可以学习。

命名规则

  1. 通用命名规则

    • 使用描述性的命名,少用缩写。
  2. 文件命名规则

    • 使用下划线分割,详细。
  3. 类型命名规则

    • 类型名称的每个单词的首字母要大写,不用下划线。
  4. 变量命名规则

    • 普通变量和结构体成员变量全小写并用_隔开,而类的成员变量在结尾需要再加一个_。
  5. 常量命名规则

    • 常量的每个单词的首字母大写,不用下划线,在最前面加上k。

    • 全局变量和静态变量也应该遵循这个命名规则。

  6. 函数命名规则

    • 对于取值和设值函数,一般是与变量名匹配,设值会加一个set_。

    • 对于其他函数,每个单词首字母大写没下划线。

  7. 命名空间规则

    • 全小写字母,无下划线,注意不要冲突。
  8. 枚举类型规则

    • 类型名按照类型。其中每个元素的命名按照常量或者宏的方式。
  9. 宏命名规则

    • 全部大写,用下划线分割。

c++代码规范
https://lhish.github.io/project/c++代码规范/
作者
lhy
发布于
2024年6月30日
许可协议