Skip to content

CPP编码风格

taotianran edited this page Nov 29, 2022 · 1 revision

目录



1.风格规范

1.1 头文件

1.1.1 头文件引用顺序

强制 ①: 必须按以下顺序引用头文件,并进行分节: cpp对应头文件 → C(标准)库 → C++(标准)库 → 其它库 → 项目内其他头文件

解释 ①: 增强可读性, 同时避免隐藏依赖。

示例:

fooserver.cpp包含的头文件顺序如下:

// cpp对应头文件优先
#include "foo/public/fooserver.h" 

// C标准库
#include <stdint.h>

// C++库
#include <vector>

// 其他库
#include "paddle/paddle_api.h"

// 项目内其他头文件
#include "foo/public/bar.h"

1.1.2 #include<> 与 #include""

强制 ①:系统库使用 #include <>,其他库使用 #include ""


1.1.3 头文件保护符

强制 ①:头文件必须使用 #pragma once 保护符,放在头文件起始位置。

示例:

// Copyright (c) 2022 FlyCV Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once  // 放在代码起始位置

1.2 排版

1.2.1 文件组织

强制 ①:头文件按以下顺序组织:license声明 → 头文件保护符 → 头文件引用 → 自定义类型、函数声明

强制 ②:cpp 文件按以下顺序组织:license声明 → 头文件引用 → 代码部分

示例:

头文件:foo.h

// license声明
// Copyright (c) 2022 FlyCV Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// 头文件保护符
#pragma once

// 头文件引用
#include <iostream>

// 自定义类型/函数声明
class Foo { ... };

1.2.2 分行与空行

强制 ①:一条单独的语句必须独立成行。

强制 ②:避免连续(≥2)的空行。

强制 ③:使用适当的空行来分组代码的逻辑。

强制 ④:左大括号不独立,右大括号独立成行。并且与左大括号所在行进行垂直对齐。

  • 例外: 空函数体,左右大括号可以放在函数签名末尾。
  • 例外: 对于带分支的if语句, 右大括号后面还有 else/else if分支的, 需要与 else/else if语句合并成行。

示例:

// 左大括号不独立,右大括号独立成行
class Foo {
public:
    // 例外:函数体为空
    Foo() {}	
    
    int play() {
	if (...) {
	} else if () {	// 例外:右括号与else语句合并成行
	    ...
	}
    }
};

1.2.3 缩进

强制 ①:以4个空格为单位缩进,严禁使用制表符缩进。

强制 ②:命名空间不缩进。

强制 ③:预处理指令不缩进。

强制 ④:类成员访问修饰符( public/protected/private)缩进,和类声明对齐。

强制 ⑤:构造函数初始化列表缩进,初始化参数较多(≥3)时换行且按8空格缩进,冒号: 放在第一行。

强制 ⑥:函数声明缩进,短函数声明独占一行;长函数声明(≥3参数)每个参数独占一行,且相对于函数声明起始位置8个空格缩进。

强制 ⑦:函数调用缩进,过长函数调用每行不要超过 120 列,换行后 8 空格缩进。

强制 ⑧:if/while语句缩进,条件表达式很长时,在低优先级运算符前换行,换行后运算符(|| or &&)放在新行最前面,并额外缩进 8 个空格。

强制 ⑨:switch语句缩进,switch 对应的 case 语句需要与 switch 对齐,不要增加额外缩进。

示例:

namespace flycv {	// ② 命名空间之后不缩进
class Foo {	
public:         // ④ 类成员访问修饰符和类声明对齐
#ifdef	XXX     // ③ 预处理指令不缩进
    typedef A B;
#endif	        // ③ 预处理指令不缩进

    Foo(int val);

    int init(// ⑥长函数声明每个参数独占一行,相比上一行缩进增加8空格缩进。
            const int& src_width,
	    const int& src_height,
	    const int& src_channel,
	    const unsigned char* src_data);

    int run();
    int skip();

    int uninit();

private:
    int do_something_to_complete_work(
            const int& src_width,
	    const int& src_height,
	    const int& src_channel,
	    const unsigned char* src_data);
};

Foo::Foo(int val) : // ⑤ 初始化参数较多时换行,冒号放在第一行,每个参数独占一行,8空格缩进
        _val_a(val),							
	_val_b(val + 1),
	_val_c(val + 2) {
	...
}

int Foo::init(
        const int& src_width,
	const int& src_height,
	const int& src_channel,
	const unsigned char* src_data) {
    do_something_to_complete_work(src_width,
	    src_height, src_channel, src_data);	 // ⑦ 长函数调用每行不要超过120列,换行后8空格缩进
    ...
}

int Foo::run() {
    if (_val_a >= _val_b    // ⑧ 复杂的条件判断,换行后运算符(|| or &&)放在新行最前面,并额外缩进8个空格。
	    && (_val_a - _val_c < _val_b)
	    || _val_c < _val_b) {
	...
    }
}

int Foo::skip() {
    switch (a) {
    case 0:		// ⑨ case与switch对齐,不需要添加额外缩进
	break;
    case 1:
	break;
    default:
	break;
    };
}

}

1.2.4 空格的使用

强制 ①:if/switch/while/for/catch 与后边的圆括号之间加一个空格,圆括号内侧与判断表达式之间不加空格。

强制 ②:for语句圆括号内分号前不加空格,分号后加一个空格。

强制 ③:函数调用中,函数名和圆括号之间不要加空格。

强制 ④:类继承与构造函数的初始化列表的冒号前后加一个空格。

强制 ⑤:逗号表达式或参数列表中,逗号前不加空格,逗号后加一个空格。

强制 ⑥:一元运算符前后不加空格。二元、三元表达式前后各加一个空格。

强制 ⑦:成员访问或作用域运算符前后不加空格。如:a.b, a->b, a.*b, a->*b, a::b。

强制 ⑧:圆括号和方括号运算符前后和内部都不加空格。

强制 ⑨:基于范围的 for 循环冒号前后带有空格。

示例:

if (is_good && is_powerful)    // 正确
if(is_good && is_powerful)     // 错误 if之后没有空格
if ( is_good && is_powerful )  // 错误, 括号内加了额外空格

switch (type)       			// 正确
while (condition)   			// 正确
catch (std::exception& ex)		// 正确
for (int i = 0; i < 10; ++i)    // 正确
for (int i = 0;i<10;++i)        // 错误, 分号后无空格
for (int i = 0 ; i < 10 ; ++i)  // 错误, 分号前有空格
 
call_some_func(arg1, arg2, arg3);   // 正确
call_some_func (arg1, arg2, arg3);  // 错误, 函数名和圆括号之间有额外空格
call_some_func( arg1, arg2, arg3 ); // 错误, 圆括号内部有额外空格
call_some_func(arg1,arg2,arg3);     // 错误, 逗号后没有空格
call_some_func(arg1 , arg2 , arg3); // 错误, 逗号前有额外空格
 
template <typename T>     // 正确
template<typename T>      // 错误, template和<之间没有空格
 
class Derived : public Base // 正确
class Derived:public Base   // 错误, 冒号前后没有空格
 
MyClass::MyClass() : _member_var(0) // 正确
MyClass::MyClass():_member_var(0)   // 错误, 冒号前后没有空格
 
++i; !i; ~i; *i; &i;        // 正确
++ i; ! i; ~ i; * i; & i;   // 错误,一元运算符不应该加空格
 
a + b; a ~ b; a || b; a << b; a = b; a %= b;   // 正确
a+b; a~b; a||b; a<<b; a=b; a%=b;               // 错误, 二元运算符前后应该加空格
 
a ? b : c    // 正确
a?b:c        // 错误, 三元运算符前后应该加空格
 
a.b; a->b; a.*b; a->*b; a::b;             // 正确
a . b; a -> b; a .* b; a ->* b; a :: b;   // 错误, 成员访问或作用域运算符前后都不应该有空格
 
xx_comparator(a, b); xx_map["key"];        // 正确
xx_comparator (a, b); xx_map ["key"];      // 错误,圆括号运算符和方括号运算符前边不应该加空格
xx_comparator( a, b ); xx_map[ "key" ];    // 错误,圆括号运算符和方括号运算符内部不应该加空

for (int x : my_array)	// 正确
for (int x: my_array)	// 错误
for (int x:my_arrya)	// 错误

1.2.5 语句长度

强制 ①:一行代码不超过120个字符。


1.2.6 函数参数

强制 ①:对于不会进行修改的输入参数,必须使用const关键字修饰,推荐使用const引用。

示例:

// 如果函数实现中不会对入参进行修改,使用const方便上层const数据传入。
int predict(const std::vector<int>& input, std::vector<int>& output);

1.3 命名规范

1.3.1 一般命名规范

强制 ①:全局可见的,却又无法通过命名空间约束的标识符命名,必须以库名作为前缀,以避免冲突。

强制 ②:命名应当尽可能有描述性,不使用非通用缩写,不使用有歧义的缩写。

示例:

// 给变量起有描述性的名字, 不要过于在意它的长度,容易阅读更为重要
// 下面是好的例子
int num_errors;                  // Good.
int num_completed_connections;   // Good.
 
// 下面是很烂的
int n;              // Bad - 无意义
int nerr;           // Bad - 有歧义的缩写
 
// 缩写: 只能使用众所周知的缩写;
// 好的例子:
int num_dns_connections;  // 大家都知道"DNS"是什么东西
int price_count_reader;   // OK, price count. Makes sense.
 
// 错误的例子
int wgc_connections;      // wgc 是啥??


1.3.2 文件命名

强制 ①:C/C++头文件统一都使用.h后缀,纯 C 源码使用.c后缀,C++源码使用 .cpp 后缀。

强制 ②:文件名全部用小写字母, 中间用下划线_分隔。比如: my_awesome_file.cpp。


1.3.3 命名空间命名

强制 ①:使用下划线分隔的全小写命名法命名命名空间,比如:bd_flycv。


1.3.4 全局变量命名

强制 ①:尽可能不使用全局变量,如果必须使用,必须以g为前缀,而且必须足够长以避免名字冲突,

使用下划线分隔的全小写命名法命名。比如:g_flycv_instance。


1.3.5 局部变量命名

强制 ①:局部变量名使用下划线分隔的全小写命名法命名。变量命名选择需要语义明确,禁止使用有歧义的缩写,变量名长短不重要,是否易于理解最重要。


1.3.6 静态变量命名

强制 ①:全局静态变量使用 s_ 作为前缀。

解释 ①:s_ 中的 s 代表 static , 这样命名可以让阅读者很快知道这个变量所属类型。


1.3.7 类/结构体数据成员命名

强制 ①:类私有数据成员使用下划线作前缀表示,命名用小写单词中间下划线分割,如 MyClass::_my_attr。

强制 ②:结构体数据成员不使用下划线前缀,命名用小写单词中间下划线分割,如 MyStruct::my_attr。

解释 ②:结构体只用来做数据集合,要求成员都是public,因此不需要加下划线。


1.3.8 常量命名

强制 ①:const 常量与枚举常量都使用下划线分隔的全大写命名法命名。


1.3.9 函数命名

强制 ①:函数命名使用下划线分隔的全小写命名法。


1.3.10 用户自定义类型名命名

强制 ①:自定义类类型使用首字母大写的驼峰命名法命名。

强制 ②:枚举类型成员,使用全大写命名法,单词间用下划线分隔。

示例:

enum class Color {
    RED,
    BLUE,
    GREEN,
    COLOR_COUNT
};

struct Point {
    int x;
    int y;
};

1.4 注释

强制 ①:变量、函数声明处必须有注释。

强制 ②:单行注释用 //,多行注释用 /* ... */。

强制 ③:多行注释必须写在被注释代码上方,单行注释原则上写在被注释代码上方。例外:条件判断语句的注释可以写在右方,需要保证简短。

强制 ④:函数(包括成员函数)注释风格统一采用如下格式:

/**
 * @brief   描述函数实现的功能
 * @param[in]  		入参用in标记,并解释其含义和用法
 *   ...
 * @param[out] 		出参用out标记,并解释其含义和用法
 * @param[in&out]	既是入参也是出参需要都标记,并解释其含义和用法(不推荐既是入参也是出参)
 * @return          描述函数的返回状态值和对应含义
 */

1.5 变量声明

1.5.1 一般变量声明

强制 ①:一行内只应声明一个变量。

解释 ①:混合声明值变量、指针变量、引用变量难于理解,容易出错。


1.5.2 局部变量声明

强制 ①:局部变量应尽量延迟到第一次使用处声明。

强制 ②:局部变量必须在声明处赋初值。

解释 ①:变量作用域越小,出错的可能性就越小。

解释 ②:避免没有赋初值而处于非法状态的变量,尤其要避免野指针。


2.语法规范

2.1 面向对象编程

2.1.1 构造函数职责

强制 ①:必须使用构造函数初始化列表显式初始化直接基类与所有的基本类型数据成员。

强制 ②:禁止在构造函数中进行可能出错的复杂操作(比如申请资源),复杂操作用额外的 init() 函数实现。

解释 ①:显式初始化使代码更清晰,还能避免二次赋值造成的效率问题。

解释 ②:构造函数必须足够简单,避免执行产生异常,否则可能导致内存泄露等问题。

示例:

class Person {
public:
    // 单参数构造函数尽量加上explicit
    explicit Person(const std::string& name): _name(name) {}  // 尽可能使用初始化列表初始化数据成员
private:
    std::string _name;
};

2.1.2 默认构造函数

强制 ①:有默认值语意的类,必须显式定义默认构造函数。

解释 ①:编译器自动生成的默认构造函数与程序员期望可能会有不同,比如成员变量初始值可能不符合预期,因此最好显示定义并进行列表初始化。


2.1.3 拷贝构造函数和移动构造函数

强制 ①:如果类型可拷贝,一定要同时定义拷贝构造函数和重载赋值运算符; 如果类型可移动, 一定要同时定义移动构造函数和移动赋值函数。

强制 ②:如果需要使用默认的拷贝和移动操作, 请使用 = default 定义。

强制 ③:如果类型不需要拷贝/移动操作,请使用 = delete 手段禁用。

示例:

class MyClass {
public:
    MyClass(const MyClass& other) = delete; // 禁用拷贝操作
    MyClass& operator=(const MyClass& other) = delete;
    MyClass(MyClass&& other) = default; // 使用默认的移动操作
    MyClass& operator=(MyClass&& other) = default;
};

2.1.4 显式构造函数

强制 ①:只有一个参数的构造函数,除非是拷贝构造函数,或者希望提供隐式转换功能,否则必须声明为显式构造函数。

解释 ①:单参数构造函数如果不用explicit关键字修饰,则可能被编译器用来做隐式类型转换,隐式转换除了会带来效率低下问题,同时可能造成难以排出的bug。

示例:

class Person {
public:
    explicit Person(const std::string& name) : _name(name) {}
private:
    std::string _name;
};

// 如果不添加 explicit关键字,下面这段代码是合法的
Person person = "abc"; // 编译器会将 ”abc“ 隐式转换为Person对象,很可能与预期不一致。

// 添加explict关键字,则必须进行显示构造
Person person = Person("abc");

2.1.5 拷贝构造函数

强制 ①:有复制意义的类必须显式给出复制构造函数,并小心指定其行为(浅复制、深复制等)

解释 ①:

  • 托管了资源的类,往往是没有复制意义的。此时应当防止用户错误调用而导致资源泄漏、重复释放的后果。
  • 编译器默认生成的复制构造函数,对指针数据成员使用浅复制策略,但这种策略常常不是程序员希望的。

示例:

// 一个可以进行复制的类
class Point {
public:
    Point(int x, int y) : _x(x), _y(y) {}
    Point(const Point& other) :   // 复制构造函数
            _x(other._x), _y(other._y) {}
private:
    int _x;
    int _y;
};

// 一个不可以进行复制的类
class File {
public:
    explicit File(const char* file_name) {
        ...
    }
    ~File() {
    	_fs.close();
    }

private:
    fstream _fs;	// fstream对象不存在赋值函数
    File(const File&);
    File& operator=(const File&);
};

2.1.6 赋值运算符

强制 ①:有复制赋值意义的类必须显式定义复制赋值运算符,并小心指定其行为(浅复制赋值、深复制赋值等)

解释 ①:

  • 托管了资源的类,往往是没有赋值意义的。此时应当防止用户错误调用而导致资源泄漏、重复释放的后果。
  • 编译器默认生成的赋值运算符,对指针数据成员使用浅复制策略,但这种策略常常不是程序员希望的。

示例:

class Point {
public:
    Point(int x, int y) : _x(x), _y(y) {}
    Point(const Point& other) :
            _x(other._x), _y(other._y) {}
    Point& operator=(const Point& other) {   //复制赋值运算符
        _x = other._x;
        _y = other._y;
        return *this;
    }
private:
    int _x;
    int _y;
};

2.1.7 析构函数

强制 ①:若类定义了虚函数,必须定义虚析构函数。

强制 ②:若类设计为可被继承的,应该定义公开的虚析构函数。

强制 ③:含有指针成员,必须显式给出析构函数,并小心指定其行为(是否销毁指针,如何销毁等)。

解释 ②:若基类的析构函数没有声明为虚函数,则 delete pBase; 将不会调用子类析构函数,从而导致错误行为和内存泄漏。

解释 ③:编译器默认生成的析构函数不会析构指针成员指向的对象,更不会回收其内存。如果忘记处理的话会导致资源泄漏。


2.1.8 继承

强制 ①:使用公有继承,禁用保护继承和私有继承。

强制 ②:派生类不允许覆盖基类中的非虚函数。

解释 ②:非虚函数是静态绑定,覆盖非虚函数会导致多态性失效,引起难以发现的bug。

class Parent {
    int run() {
	printf("parent");
    }
};

class Child : public Parent {
    int run() {
	printf("child");
    }
};

Parent* ptr = new Child();
ptr->run();		// 输出 ”parent“,不能绑定实际对象类型的run方法,多态失效。

2.1.9 成员访问控制

强制 ①:所有非静态数据成员一律定义为private,通过setter/getter访问。

强制 ②:类可以定义 public 静态数据成员,但必须是 const 的。否则,应该通过静态 getter/setter 访问,并通过文档指定其是否线程安全。

解释 ①:当类成员通过 public/protected 的方式暴露给使用者/派生类后,就失去了通过重构优化代码的能力,因此,适当的访问控制是必须的。

解释 ②:类的静态成员除了面临封装性问题外,还须面临线程安全问题。因此,如果是常数定义为 const,如果非常数使用 getter/setter 保护可变的参数。


2.1.10 成员声明顺序

强制①:按照 public、protected、private 的顺序声明成员。

解释①:一致的顺序便于阅读,一般来说排在最前面的是用户最关心的部分。


2.1.11 函数返回值规范

强制 ①:如果使用 int 作为返回值, 必须遵循”负值表示失败, 非负值表示成功。

强制 ②:若调用了可能返回错误码/空指针的函数,必须检查其返回值。


2.1.12 强类型枚举

强制 ①:使用强类型枚举替代原来标准C++的枚举。

解释 ①:标准C++枚举类型实质上就是整型, 存在类型不安全隐患, 同时还存在名字空间冲突的问题,为此C++11引入了强类型枚举。

示例:

// 标准C++枚举
enum Side {
    LEFT,
    RIGHT,
};
 
enum Thing {
    WRONG,
    RIGHT, // RIGHT和Side中的RIGHT冲突
};
 
// C++11 强类型枚举
enum class Side {
    LEFT,
    RIGHT,
};
 
enum class Thing {
    WRONG,
    RIGHT, // RIGHT和Side中的RIGHT不会冲突
};

2.1.13 显示虚函数重载

强制 ①:被重载的虚函数通过override显示声明。

解释 ①:显示声明虚函数重载可以增强代码可读性, 同时还容易在编译期发现隐蔽的错误.

示例:

// 如果不使用override, 错误只有运行时才能发现
struct Base {
    virtual void some_func(float);
};
  
struct Derived : Base {
    virtual void some_func(int) override;   // 错误:Derive::some_func并没有override Base::some_func
    virtual void some_func(float) override; // OK
};
  
// final的例子
struct Base1 final { };
  
struct Derived1 : Base1 { }; // 错误:class Base1已声明为final
  
struct Base2 {
    virtual void f() final;
};
  
struct Derived2 : Base2 {
    void f(); // 错误:Base2::f已声明为final
};

2.1.14 const成员函数

强制 ①:对于不会修改类成员数据的函数,必须加上const修饰使之声明为const成员函数。

解释 ①:C++中const对象只能调用const成员函数,为了const对象也能访问一些类成员数据这样做是非常有必要的。


2.2 其他C++特性

2.2.1 命名空间

强制 ①:头文件中禁止在函数、方法和类作用域外的地方使用 using namespace 或者 using class。


2.2.2 内联函数

强制 ①:禁止内联虚函数。

强制 ②:不应该内联过大、过于复杂的函数(10行以外不允许内联)。

解释 ①:内联虚函数不能达到函数展开的目的,还会导致代码膨胀。


2.2.3 函数/方法重载

强制 ①:只有在参数类型不同,而表示意义相同时才使用函数重载。

强制 ②:特别地,避免整形数值与指针类型之间的重载。

解释 ②:在 C++ 里 NULL 只是定义为 0 的一个宏。如果重载整形数值与指针,当传入 NULL 时,就会调用错误版本的函数。


2.2.4 类型转换

强制 ①:必须使用 C++ 风格类型转换( static_cast、dynamic_cast、const_cast、reinterpret_cast ),但应该尽量避免类型转换。

解释 ①:

  • 类型转换是容易出错的地方,尽可能避免使用。
  • 容易出错的地方必须显眼,而且尽可能清晰明了。C++ 风格的类型转换可以明确指明转换的方式,还可以通过”_cast”来方便地找到。

示例:

使用 const_cast 来调用旧风格的程序库
// 正确的声明应该是void old_copy(void *dest, const void *src, size_t n);
// 但由于在另外一个库里,无法改写
void old_copy(void* dest, void* src, size_t n);

// 重新封装一个函数
void new_copy(void* dest, const void* src, size_t n) {
    old_copy(dest, const_cast<void*>(src), n);   // 把有const保护的强制转换为没有const保护的,是相当危险的,所以要明确指明。
}

2.2.5 自增++/自减––运算符

强制 ①:在不考虑返回值的前提下, 数值类型、指针、迭代器,统一使用前置自增 (自减)。

解释 ①:后置的++/--产生了额外的复制成本。如果迭代器使用了动态内存,还会造成额外动态内存分配。


2.3 C/C++ 共有特性

2.3.1 条件表达式

强制 ①:避免在条件表达式中赋值。

强制 ②:避免对浮点数进行相等或不等比较。

解释 ①:条件表达式中的赋值常常是因为手误造成的,应避免。

解释 ②:浮点数在运算中经常会产生少量的误差,这个误差会导致 == 或 != 运算返回与期望相反的结果。

示例:

// 浮点数避免使用 == 和 !=
const double EPSILON = 1e-9; // 比如说,接受1e-9以内的误差
if (fabs(my_value) < EPSILON) {
    ...
}

2.3.2 NULL, nullptr与0

强制 ①:在 C++11 的项目中,使用系统关键字 nullptr 代表空指针。


2.3.3 静态/全局变量/函数

强制 ①:尽可能避免使用全局变量,如有必要应使用单例模式代替。

强制 ②:内部使用的全局函数,必须声明为静态函数,不能在目标文件中出现外部可见的符号。

强制 ③:内部使用的全局变量,必须声明为静态变量,不能在目标文件中出现外部可见的符号。

解释 ②③:内部使用的静态变量和函数,声明为static可以避免错误链接。

Clone this wiki locally