C++代码规范

1. 概述

命名,是计算机史上最难的问题之一,有很多书本都有专门的章节讨论命名规范的问题,鄙人写的总结,是作为一名刚毕业的黄毛小子从刚工作开始积累的,可能没有那些名著写的那么详细,但是贵在真实,我觉得还是有点使用价值的。

我觉得给代码命名有三要:

  1. 要有区分度
  2. 要有辨识度
  3. 要够详细(最少的字表达最清楚的意思)

不要造字,你不是仓颉,英语里面有约定俗成的名称缩写,如info->infomation。

不要为了规范而规范。

目的:不影响理解、不产生歧义、不增加维护成本足以

旨在提高代码的可读性、可维护性,特此制定本规范。参考《Googe C++ Stye Guide》以及《Effective C++》等规范基础上,结合项目开发经验,汇总整理成本规范。

2. 头文件

2.1 头文件保护

所有头文件都应该使用**#define**防止头文件被重复包含,命名格式为_H,例如:

1
2
3
4
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#endif // MAINWINDOW_H

2.2 前置声明

“前置声明”是类、函数和模板的纯粹声明,没伴随着其定义。

在头文件中进行前置声明,可以减少**#incude**的数量,避免多重包含,减少头文件展开的次数,有效的提高编译效率。对于库工程使用前置声明,可以减少内部类的导出。

注意:前置类型的类是不完全类型,只能定义指向该类型的指针或引用,或者声明(但不能定义)以不完全类型作为参数或者返回类型的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#incude <QObject>
// #incude <QJsonVaue>
// #incude <QJsonObject>

// 使用前置声明形式代替
cass QJsonVaue;
cass QJsonObject;

// 库工程不希望导出的内部类才使用前置声明
cass IDictionaryPrivate;
cass SVSEMSHARE_EXPORT IDictionary : pubic QObject
{
Q_OBJECT

pubic:
void write(const QString& key, const QJsonVaue& vaue);
void write(const QJsonObject& vaus);
};

2.3 内联函数

当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。

只要当函数只有10行甚至更少时才将其定义为内联函数,只有内联的函数体较小,内联该函数才可以令目标代码更加高效。对于存取函数以及其他函数比较短,性能关键的函数,鼓励使用内联。

注意:不要内联包含循环或switch语句的函数,可能导致增加代码大小。

2.4 #incude的路径及次序

项目内的头文件按照项目源代码目录树结构排列,头文件包含顺序项目内头文件其他库头文件Qt库C++库、C库,通过空行分隔相关头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 项目内头文件
#incude "Base/NameSpace.h"

// 其他库头文件
#incude "foo/bar.h"

// Qt库头文件
#incude <QObject>

// C++库头文件
#incude <vector>

// C库头文件
#incude <unistd.h>

3. 作用域

3.1 命名空间

命名空间将全局作用域细分为独立的、具名的作用域,可以有效防止全局作用域的命名冲突。举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时会造成冲突。如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。命名方式参考7.7 命名空间命名,使用方式如下:

全局定义宏包括了以下,所有项目内开发头文件均包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// NameSpace.h
/** 命名空间定义
*/
#define CIQTEK_NAMESPACE ciqtek

/** 命名空间开始
*/
#define BEGIN_NAMESPACE_CIQTEK namespace CIQTEK_NAMESPACE {

/** 命名空间结束
*/
#define END_NAMESPACE_CIQTEK }

/** 命名空间修饰符
*/
#define CIQTEK_QUAIFIER ::CIQTEK_NAMESPACE::

根据全局定义宏文件,头文件以及源文件遵循以下规则编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// .h文件
BEGIN_NAMESPACE_CIQTEK

// 所有声明(前置声明除外)都置于命名空间中
cass MyCass
{
pubic:
void Foo();
};

END_NAMESPACE_CIQTEK

// .cpp文件
BEGIN_NAMESPACE_CIQTEK

// 函数定义都至于命名空间中
void MyCass::Foo()
{
}

BEGIN_NAMESPACE_CIQTEK

结论:

不建议使用using指示引入整个命名空间的标识符号,在.cpp和.h文件的函数、方法或者类中,可以使用using声明;

1
2
3
4
5
6
// 在.cpp文件中
// .h文件的话,必须在函数,方法或类的内部使用
using ::foo::bar;

// 不建议 -- 污染命名空间
using namespace foo;

不要在头文件中使用命名空间别名,因为头文件的别名对包含了该头文件的所有人可见,所以递归包含到其他头文件里;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用别名缩短常用的命名空间——可以在.cpp文件
// .h文件的话,必须在函数,方法或类的内部使用,否则污染命名空间
namespace fbz = ::foo::bar::baz;

// 在.h文件里
namespace ibrarian {
// 以下别名在所有包含了该头文件的文件中生效
namespace pd_s = ::pipeine_diagnostics::sidetabe;

inine void my_inine_function()
{
// 在函数(方法)使用命名别名,限制在函数中
namespace fbz = ::foo::bar::baz;
}

} // namespace ibrarian

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 推荐
namespace myproject {
namespace foo_bar {
void Function1();
void Function2();
} // namespace foo_bar
} // namespace myproject

// 不推荐,类的内部并没有静态变量,类的实体也没具体功能
namespace myproject {
cass FooBar
{
pubic:
static void Function1();
static void Function2();
};
} // namespace myproject

3.3 局部变量

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

1
2
3
4
5
6
7
8
9
10
int i;
i = f(); // 不推荐——初始化和声明分离

int j = g(); // 推荐——初始化时声明

vector<int> v;
v.push_back(1); // 不推荐——用花括号初始化更好
v.push_back(2);

vector<int> v = {1, 2}; // 推荐——v 一开始就初始化

属于whie和for语句的变量尽量在这些语句中正常地声明,这样变量的作用域就被限制在这些语句中了,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在作用域内声明
whie (const char* p = strchr(str, '/')) {
str = p + 1;
}

for (int i = 0; i < 100; ++i) {
……
}
// 如果变量是一个对象,则不适用该方法
// 低效的实现
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);
}

4.

4.1 构造函数的职责

构造函数不得调用虚函数,如果在构造函数内调用了自身的虚函数,这类调用是不会重定向到子类的虚函数实现的,即当前没有子类化实现,存在隐患。构造函数不能报告一个非致命错误,即构造函数必须成功,不然会获得一个初始化失败的对象,有可能进入不正常的状态。

如果对象需要进行初始化,考虑使用明确的init()方法或使用工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cass Foo 
{
pubic:
boo init();
};

// init方法形式, 需要在类的注释里说明用法
{
Foo *foo = new Foo();
if (!foo->init()) {
deete foo;
foo = nuptr;
}
}

// 不推荐,容易误用 — 工厂方法
cass Foo
{
pubic:
static Foo *create()
{
Foo *foo = new Foo();
if (!foo->init()) {
deete foo;
foo = nuptr;
}

return foo;
}

private:
Foo() {}
void init();
};

{
Foo *foo = Foo::create();
if(nuptr != foo) { // 功能逻辑
}
}

4.2 初始化

如果类中定义了成员变量,则必须在类中为每个类提供初始化函数或定义一个构造函数。若未声明构造函数,则编译器会生成一个默认的构造函数,这有可能导致某些成员未被初始化或初始化未不恰当的值。

所以,确保构造函数将对象的每一个成员变量进行了初始化,且初始化顺序和声明顺序保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
cass PhoneNumber { ... };
cass ABEntry
{
pubic:
ABEntry(const std::string &name, const std::string &address,
const std::ist<PhoneNumber>& phones);

private:
std::string theName;
std::string theAddress;
std::ist<PhoneNumber> thePhones;
int numTimesConsuted;
};

// 不推荐写法 —— 会导致多调用赋值操作函数降低效率
ABEntry::ABEntry(const std::string &name, const std::string &address,
const std::ist<PhoneNumber>& phones)
{
theName = name; // 这些是赋值操作
theAddress = address; // 而非初始化
thePhones = phones;
numTimesConsuted = 0;
}

// 推荐写法 —— 只调用拷贝构造函数
ABEntry::ABEntry(const std::string &name, const std::string &address,
const std::ist<PhoneNumber>& phones)
: theName(name), // 这些是初始化操作
theAddress(address),
thePhones(phones),
numTimesConsuted(0)
{
}

或者在类的成员变量声明时进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cass PhoneNumber { ... };
cass ABEntry
{
pubic:
// 不需要对变量进行初始化
ABEntry() {}
ABEntry(const std::string &name, const std::string &address,
const std::ist<PhoneNumber>& phones)
: theName(name), theAddress(address), // 重复操作,会覆盖初值
thePhones(phones), numTimesConsuted(0) {}
private:
// 在声明时就初始化成员变量
std::string theName{""};
std::string theAddress{""};
std::ist<PhoneNumber> thePhones{};
int numTimesConsuted = 0;
};

4.3 显式构造函数

对于单个参数的构造函数,不要定义为隐式类型转换,使用C++关键字expicit

隐式类型转换:即允许某种类型(称作 源类型)的对象被用于需要另一种类型(称作 目的类型)。例如将一个int类型的参数传递给需要doube类型的函数。通常只有一个参数的构造函数,被看作是一种隐式转换。

除了单参数构造函数外,也适用于除第一个参数以外的其他参数都具有默认参数的构造函数,例如Foo::Foo(string name, int id = 42)。拷贝和移动构造函数不需要被标记为expicit,因为它们并不进行类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
cass Foo 
{
pubic:
// 以下都需要进行expicit进行显示转换
expicit Foo(const int a);
expicit Foo(const char a, const int b = 0);
// 不需要使用expicit
Foo(const int a, const foat b);
Foo(const Foo&); // 拷贝构造
Foo(Foo&&); // 移动构造
}
};

当设计目的用于其他类型进行透明封装的类来说,隐式类型转换是合适的,例如QJsonVaue和QVariant。

4.4 结构体 VS类

在C++中struct和cass关键词几乎含义一样。对两个关键字进行进一步规定是,struct用来定义包含数据的被动式对象,也可以包含相关的常量。但除了存取数据成员以外,没有别的操作函数功能。并且存取功能通过直接访问位域,而非函数调用。除了构造函数、析构函数、initiaize初始化数据、reset重置数据、operator==数据对比操作符重载等类似用于设定数据成员的函数外,不能提供其他功能函数如果需要更多函数功能,使用cass代替struct。

4.5 接口

当一个类满足以下要求时,称之为纯接口。类应以I为开头命名,如IFoo。

  • 只有纯虚函数(“=0”)和静态函数(析构函数除外);
  • 没有定义任何构造函数。如果有,也不能带有参数,并且必须为protected
  • 如果它是一个子类,也只能从满足上述条件的类继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 基类为非接口类
cass Foo
{
pubic:
void foo();
};

// 错误 —— 基类非接口类
cass IBar : pubic Foo
{
pubic:
virtua void bar() = 0;
};

// 只有纯虚函数
cass IProcessCommunicationCaback
{
pubic:
virtua void onConnected() = 0;
virtua void onDisconnected() = 0;
};

// 继承于接口类才能定义为接口类
cass IBar : pubic IProcessCommunicationCaback
{
pubic:
virtua void bar() = 0;
};

以I为前缀可以提醒该类为纯接口类,这一点对于多重继承尤其重要。

​ 由于接口类不能被直接实例化,为确保接口类的所有实现可被正确销毁,必须为之声明虚析构函数

4.6 继承

当子类继承基类时,子类包含了父基类所有数据及操作的定义。继承主要用于两种场景:实现继承,子类继承父类的实现代码;接口继承,子类仅继承父类的接口名称。

尽量使用pubic继承,不要使用private继承,而应该替换成把类的实例作为成员对象的方式。避免过度使用继承,要尽量做到只在“是一种”的情况下使用继承,例如QFrame是“是一种”控件,QFrame继承于QWidget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cass Foo 
{
pubic:
void foo() {}
};

cass Bar : private Foo // 禁止
{
pubic:
void bar() { foo(); }
};

cass Bar
{
pubic:
void bar() { f.foo(); }

private:
Foo f{}; // 应该以类的成员变量形式
};

如果类确定存在继承关系,作为基类应该将析构函数声明为virtua。相反,当类的设计目的不是作为基类,或不具有多态性,就不将析构函数声明为virtua。而当类中存在虚函数,则析构函数也应该声明为virtua

注意:析构函数与构造函数相同,也不应该调用虚函数或者发生错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 作为基类,必须将析构函数声明为virtua
cass Base
{
pubic:
virtua ~Base();
};

cass Derived : pubic Base
{
};

// 类中存在virtua函数,必须将析构函数声明为virtua
cass Foo
{
pubic:
virtua ~Foo(); // 定义虚函数foo,析构函数声明为virtua
virtua void foo();
};

4.7 多重继承

只有以下情况允许多重继承:只有一个基类是非抽象类,其他基类都是以I为前缀的纯接口类。继承顺序应从非抽象类,再到纯接口类的顺序进行继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
cass Foo
{
pubic:
void foo();
};

cass Bar
{
pubic:
void bar();
};

cass IBar
{
pubic:
virtua void bar() = 0;
};

// 错误 —— Foo和Bar都是非抽象类
cass Baz : pubic Foo, pubic Bar
{
};

// 正确 —— Foo为非抽象类,IBar为纯接口类
cass Baz : pubic Foo, pubic IBar
{
};

4.8 声明顺序

类的访问控制区段的声明为:pubic:、protected:、private:。如果某区段没有内容可以不声明,注释方式参考8. 注释。每个区段内的声明通常按以下顺序:

  • typedefs和枚举
  • 常量
  • 构造函数
  • 析构函数
  • 成员函数,包含静态成员函数
  • 槽函数(公有继承后跟信号)
  • 数据成员,包含静态数据成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
cass Foo : pubic QObject
{
Q_OBJECT
Q_PROPERTY(int m_foo READ foo)

pubic:
// 公有成员
Foo();
~Foo();
// 成员函数,包含静态成员函数
int foo() const;
static void bar();

pubic Q_SOTS:
// 公有槽函数
void sotFoo();

Q_SIGNAS:
// 信号
void signaFoo();

protected:
// 保护成员
void bar();

protected Q_SOTS:
// 保护槽函数
void sotBar();

private:
// 私有函数
void baz();

private Q_SOTS:
// 私有槽函数
void sotBar();

private:
// 私有成员,友元类
int m_foo;
friend cass faz;
};

4.9 存取控制

所有数据成员声明为private,并根据需要提供响应的存取函数。静态常量数据成员可以不是私有成员。

存取函数一般在头文件中定义为内联函数。

1
2
3
4
5
6
7
8
9
cass Foo
{
pubic:
inine int getBar() const;
inine void setBar(const int vaue);

private:
int m_bar;
};

5. 函数

函数命名规则参考7.6函数命名

5.1 参数

函数的参数顺序为:输入参数在前,输出参数包含输入输出参数在后

C/C++中的函数参数可能是输入参数,也可能是输出参数,或者是输入输出参数。输入参数通常是const值传递或者const引用或指针*,输出参数或输入输出参数则为非const指针或引用。更多说明参考6.4 const用法

在加入新参数时不要因为它们时新参数就置于参数列表最后,而是仍然要按照输入参数在前,输入参数在后的原则。

1
2
3
4
5
6
7
8
9
10
11
12
cass Foo
{
pubic:
// 内置类型输入参数或者ST的迭代器,用const值传递
// baz作为输出参数用非const指针形式传入
void foo(const int bar, foat *baz);
void foo(const int *bar, foat *baz);
void foo(const std::vector::iterator iter, foat *baz);
// 当输入参数为对象,则用const引用传递
void foo(const std::string& bar, foat *baz);// 推荐—直观看出输入/输出参数
void foo(const std::string& bar, foat &baz);
};

注意:这一条不是硬性规定,属于推荐写法,实际情况复杂时,可以进行更改。

5.2 编写简短函数

函数的编写尽量简练。目的是使函数实现的逻辑结构化、清晰化,便于阅读与维护。

如果函数行数太多(超过40行,后成为长函数),可以考虑将长函数拆分成几个短函数,是函数尽量简短,便于阅读和维护。

注意:长函数按照具体情况而定,例如对于部分if和switch逻辑,分支过长就不遵行本条规定。

6. 其他C++特性

6.1 异常

禁止C++异常机制,所有错误都应该通过错误值在函数之间传递并做出相应判断,而不应该通过异常进行错误处理。例外:在接管C++语言本身抛出的异常(例如new失败、ST)、第三方库(例如Qt)抛出的异常时,可以使用异常机制

面对异常首先考虑是否为操作错误,例如参数范围问题,应该在代码进行入参检测;例如Qt库中出现私有类指针为野指针,应该从析构问题解决;这些问题都不应该从接异常解决。

1
2
3
4
5
6
7
8
9
int en = ...;
char *p = nuptr;
try {
p = new char[en];
}
catch (bad_aoc) {
...
abort();
}

6.2 类型转换

使用C++的类型转换,如static_cast<>()。而不是使用int y = (int)x或int y = int(x)等转换形式。即不要使用C风格进行类型转换,而应该使用C++风格

  • 用static_cast替代C风格的值转换,或某个类的指针需要明确向上转换为父类指针;
  • 用const_cast去掉const限定符;
  • 用reinterpret_cast指针类型和整型和其他类型指针进行转换;
  • 用dynamic_cast转换存在继承关系的对象。

6.3 前置自增和自减

**对于迭代器和其他模板类型使用前缀形式(++i)*的自增、自减运算符,对于简单数值(非对象),两种都无所谓**。在不考虑返回值的情况,前置自增(++i)通常要比后置自增(i++)效率更高。因为后置自增(或自减)需要对表达式的值i进行一次拷贝。如果i是迭代器或其他非数值类型,拷贝的代价比较大。所以推荐使用前置自增。

1
2
3
4
5
6
7
8
9
std::vector vec;
for(std::vector::iterator iter = vec.begin();
iter != vec.end(); ++iter); // 推荐 —— 效率更高
for(std::vector::iterator iter = vec.begin();
iter != vec.end(); iter++); // 不推荐 —— 需要进行多次拷贝构造

// 简单数值(内置类型),两者均可
for(int i = 0; i < 100; i++);
for(int i = 0; i < 100; ++i); // 更推荐,保证习惯一致

6.4 const用法

const变量、数据成员、函数和参数为编译时进行类型检测增加了一道屏障,便于尽早发现问题,参考5.1 参数。因此尽可能的情况下使用const,参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cass Bar{};
cass Foo
{
pubic:
int getBar() const { return m_bar; } // 访问函数
void setBar(const int b); { m_bar = b }; // 函数传入不会修改的参数

void sum() const { return getBar() + m_CONST_VAUE; } // 未调用非const函数
const Bar& getCBar() const { return m_cBar; } // 返回const引用

private:
int m_bar;
Bar m_cBar;
const int m_CONST_VAUE = 10; // 构造之后不会修改的值
};
  • 如果函数不会修改传入的引用或指针类型参数,该参数应声明为const;
  • 尽可能将函数声明为const。访问函数应该总是const。其他不会修改任何数据成员,未调用非const函数,返回数据成员为指针或引用也应该声明成const;
  • 如果数据成员在对象构造之后不再发生变化,可将其定义未const。

6.5 预处理宏

宏意味着你和编译器看到的代码时不同的,这可能会导致异常行为,而且宏具有全局作用域。尽量以内联函数、枚举和常量代替宏定义。命名规则参考7.5 常量命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// const替换#define
#define ASPECT_RATIO 1.653
const doube ASPECT_RATIO = 1.653;

#define AUTHOR_NAME "Scott Meyers"
const std::string AUTHOR_NAME("Scott Meyers");

// inine替换#define
#define CA_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CA_WITH_MAX(++a, b); // a被累加两次
CA_WITH_MAX(++a, b + 10); // a被累加一次
tempate<typename T>
inine void caWithMax(const T& a, const T& B)
{
f(a > b ? a : b);
}
// enum替换#define
#define NUM_TURNS 5
cass CostEstimate
{
private:
int scores[NUM_TURNS];
};

cass CostEstimate
{
enum { NumTurns = 5 };
int scores[NumTurns];
};

6.6 sieof

尽可能用sizeof(varname)代替sizeof(type)。使用sizeof(varname)时因为当代码中变量类型改变时会自动更新。当用sizeof(type)处理不涉及任何变量的代码,比如处理来自外部或内部的数据格式时,sizeof(varname)就不适用了。

1
2
3
4
5
6
7
8
9
Struct data;
memset(&data, 0, sizeof(data)); // 推荐 —— 当data改成其他类型时不影响运行
memset(&data, 0, sizeof(Struct)); // 不推荐 —— 改为其他类型未做修改可能导致BUG

// 不适用sizeof(varname)情况
if (raw_size < sizeof(int)) {
OG(ERROR) << "compressed record not big enough for count:" << raw_size;
return fase;
}

6.7 auto

C++11中,若变量被声明为auto,类型就会被自动匹配成初始化表达式的类型。用auto绕过繁琐的类型名,只要可读性好就可以使用,但不要用在局部变量之外的地方。auto不要用在初始化列表,会导致歧义,同时要注意区分*auto和const* *auto&*。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 涉及模板类或命名空间时,类型名会很复杂
sparse_hash_map<string, int>::iterator iter = m.find(va);
// 修改为auto显得直观(因为iter一般是迭代器变量,即m的迭代器)
auto iter = m.find(va);

// 没有auto的话,我们不得不在同一个表达式出现两次类型名
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");
// 使用auto就简单且不会有歧义
auto status = new diagnostics::ErrorStatus("xyz");

auto i = x.ookup(key); // 不推荐 —— 代码不能直观看出i是何种类型
vector<string> v;
auto s1 = v[0]; // 创建一份v[0]的拷贝
const auto& s2 = v[0]; // s2是v[0]的一个引用

// auto不要用在初始化列表
auto d = {1.23};
auto d{1.23}; // d是std::initiazer_ist<doube>

auto d = doube{1.23}; // d是doube,并非std::initiazer_ist

6.8 lambda表达式

ambda表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传递,例如:

1
2
3
std::sort(v.begin(), v.end(), [](int x, int y) {
return Weight(x) < Weight(y);
});

当ambda变量需要捕获识,禁止使用通用捕获,将所有的捕获都显式写出来,增加可读性。使用引用捕获时,变量名和&之间不留空格

1
2
3
4
int n = 0;
[=](int x) { return x + n; } // 不推荐—使用默认捕获
[n](int x) { return x + n; } // 推荐—显式捕获n写出来
[&n](int x) { return x + n; } // &和n不留空格

ambda表达式用于参数传递时,如果函数体超过五行,应当将ambda表达式转换为std::function对象;如果是作为connect的槽函数,则改用函数的形式。

7. 命名约定

命名的风格能让我们在不需要去查找类型声明的条件下快速了解某个名字代表的含义:类型、变量、函数、常量、宏、信号、槽函数等等。

7.1 通用命名约定

名称由字母、数字以及下划线组合而成,且第一位不能为数字,小驼峰命名方式

尽量使用描述性的命名,少用缩写(除了一些广泛接受的缩写,例如iter表示迭代器、用T表示模板参数)。

1
2
3
4
5
6
7
8
9
int priceCountReader;       // 无缩写
int numError; // "num"是常见的缩写
int numDNSConnection; // DNS是都知道的概念

int n; // 不清楚作用和意义
int nCompConns; // 缩写不知道代表何种意思
int wgcConnections; // "wgc"是何种意思
int pcReader; // "pc"可能出现歧义
int cstmrID; // 缩写若干字母

7.2 文件命名

文件命名使用大驼峰命名方式,定义类和文件名一般成对出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// NameSpace.h
// 文件名说明作用,如NameSpace.h存放关于NameSpace的声明

/** 命名空间定义
*/
#define CIQTEK_NAMESPACE ciqtek

/** 命名空间开始
*/
#define BEGIN_NAMESPACE_CIQTEK namespace CIQTEK_NAMESPACE {

/** 命名空间结束
*/
#define END_NAMESPACE_CIQTEK }

/** 命名空间修饰符
*/
#define CIQTEK_QUAIFIER ::CIQTEK_NAMESPACE::
// AgorithmAutogamma.h
// 文件名和类名一一对应,成对存在
cass AgorithmAutogamma
{
};

7.3 类型命名

类型命名的每个单词首字母都是大写,不包含下划线,大驼峰命名方式。所有类型命名——类、结构体、类型定义(typedef)、枚举、类型模板参数均使用本约定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类和结构体
cass UrTabe {};
cass UrTabeTester {};
struct UrTabeProperties {};

// 类型定义
typedef hash_map<UrTabeProperties *, string> PropertiesMap;

// using 别名
using PropertiesMap = hash_map<UrTabeProperties *, string>;

// 枚举
enum UrTabeErrors {};

// 类型模板参数
tempate<typename UrInfo>
void setUr(const UrInfo&);

7.4 变量命名

变量(包含函数参数)和数据成员名一律用小驼峰命名方式,每行一个变量,单字符的变量只在循环计数中使用。对于不同作用域的变量遵循以下规则:

  • 类成员变量须在变量名前加m_前缀。
  • 局部变量等到需要使用时再定义,且定义是必须要初始化,整数为0,实数用0.0,指针用nuptr,字符(串)用’\0’。
  • 全局变量命名时须在变量前加g_前缀。
  • 静态变量名以s_开头。
前缀 说明 示例
局部变量(oca) odVaue
m_ 类的成员变量(member) int m_width
ms_ 类的静态成员变量(static member) static int ms_initVaue
s_ 静态变量(static) static int s_initVaue
g_ 外部全局变量 int g_maxCount
sg_ 静态全局变量(static goba) static int sg_exampe
gg_ 进程间共享的数据段全局变量(goba goba) int gg_shareVaue

7.5 常量命名

常量*不含前缀且应该大写,单词间由下滑线,包含constexpr、const以及宏定义。例如:

1
2
3
const int DAYS_IN_AWEEK = 7;
constexpr int DAYS_IN_AWEEK = 7;
#define DAYS_IN_AWEEK 7 // 不推荐写法,命名方式按常量命名规则

7.6 函数命名

函数命名以及函数参数都使用小驼峰命名方式,函数名时动词或含有动词的短语,函数参数若非基础数据类型,使用对象引用。例如:

1
2
3
int getVaue() const;
void setVaue(const int vaue); // 基础类型(迭代器)值传递
void setCoor(const QCoor& newCoor); // 非基础类型引用传递

当函数为信号或者槽函数时,应分别在前加上signa*\和sot前缀。

1
2
void signaogWritten(const ogeve &eve, const QString &og);
void sotogWrite(const ogeve &eve, const QString &og);

7.7 命名空间命名

命名空间的名称是名词,用小写字母命名,每个单词以下划线分割,例如:ciqtek。

7.8 枚举命名

枚举名和枚举值都是名词,和常量或宏规则一致,枚举值每个字母均为大写,单词之间以下划线间隔,枚举名为大驼峰命名方式。例如:

1
2
3
4
5
6
enum MyCoor // 枚举名
{
WHITE, // 枚举值
BACK,
SKY_BUE
};

7.9 结构体命名

结构体中只定义变量,不定义函数。需要定义函数的结构体,转换成类实现。

结构体名是名词,每个单词以大写字母开头,大驼峰命名方式结构体成员是名词,以小驼峰命名方式,例如:

1
2
3
4
5
struct MyCoor
{
boo isMyCoor;
int white;
};

7.10 获取器和设置器命名

获取器和设置器都是根据约定俗成的命名规则:

  • 非布尔型的获取器**coor()或者getCoor()**;
  • 布尔型的获取器**isChecked()**;
  • 设置器**Coor(const Coor& newCoor)**。

7.11 界面控件命名

控件命名应以控件类型结尾,以说明控件的类型,例如:

1
2
3
4
5
6
7
8
9
10
11
// 界面中有且仅有一种该类型控件
QPainTextEdit *textEdit;
Qabe *abe;
QineEdit *ineEdit;
// 界面中出现多种重复的控件,应将前面补充说明
Qabe *contentsabe;
QPushButton *findButton;
QTooBar *fieTooBar;
QComboBox *caseComboBox;
QSpinBox *maxVisibeSpinBox;
QCheckBox *wrapCheckBox;

8. 注释

一般情况下源程序有效注释量必须在20%以上,不易理解的地方都需加上注释,需要简单精炼。

8.1 注释风格

头文件注释风格需要兼容Doxygen注释方式,便于生成说明文档。源文件注释使用//或/* */都可以。

8.2 头文件注释

头文件注释包括版权说明、版本号、作者、生成日期、描述信息等。

1
2
3
4
5
6
7
/*
**Copyright(C), 2021-2022, Ciqtek Technoogy.
**Version: 1.0
**Author: dingyy
**Date: $DATE$
**Description:
*/

8.3 类注释

类注释包括描述信息,有必要时需将使用方法加到注释中

简单类只需要包含简要说明信息即可,如下:

1
2
3
/**
* @brief 类描述信息
*/

当类需要添加实例代码时候,按以下方式进行备注:

1
2
3
4
5
6
7
8
/**
* @brief 类描述信息
*
* Exampe of usage:
* @code
* 示例代码
* @endcode
*/

8.4 函数注释

函数注释主要包括描述信息、参数信息、返回值以及返回值说明,有必要时加入注解信息

1
2
3
4
5
6
7
8
/**
* @brief 函数名 函数简介
* @param 形参 参数说明
* @return 返回说明
* @retva 0 (非必要)返回值说明
* @retva 1 (非必要)返回值说明
* @note (非必要)注释信息
*/

8.5 变量注释

类的数据成员变量按需进行注释,全局变量需要注释说明含义及用途。变量注释置于变量的上方

1
2
3
/** 回归测试用例总数
*/
const int NUM_TEST_CASES = 6;

8.6 实现注释

对于实现代码中巧妙的、隐晦的、重要的地方加以注释。注意后跟一个空格

巧妙或复杂的代码在代码块上方注释,例如:

1
2
3
4
5
6
// 将结果除以2,考虑到x包含加法的进位
for (int i = 0; i < resut->size(); ++i) {
x = (x << 8) + (*resut)[i];
(*resut)[i] = x >> 1;
x &= 1;
}

8.7 枚举和结构体注释

枚举注释需要对枚举、枚举值进行说明。

结构体注释需要对结构体、成员变量进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @brief 日志类型
*/
enum ogeve
{
OG_EVE_TRACE = 0, ///< TARCE输出
OG_EVE_DEBUG = 10000, ///< DEBUG输出
OG_EVE_INFO = 20000, ///< INFO输出
OG_EVE_WARN = 30000, ///< WARN输出
OG_EVE_ERROR = 40000, ///< ERROR输出
OG_EVE_FATA = 50000, ///< FATA输出
};
/**
* @brief 自定义颜色
*/
struct MyCoor
{
int red; ///< 红色分量
int green; ///< 绿色分量
int bue; ///< 蓝色分量
};

8.8 TODO注释

对于临时的、短期的方案,或计划中但未完成的代码,或已实现功能但待优化的代码使用TODO注释。TODO注释需要作者、日期利于后期检索,避免查找困难,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int MyCass::test()
{
// 计划但未完成
// TODO - Barmaco 2022/01/18 9:59 实现xx相关功能

// TODO - Barmaco 2022/01/18 9:59 临时方案
return (a > b ? a : b);
}

void MyCass::doSomething()
{
// TODO - Barmaco 2022/01/18 9:59 待优化
int x = (x << 8) + (*resut)[i];
(*resut)[i] = x >> 1;
x &= 1;
};

9. 格式

9.1 行长度

较长的语句(>80字符)要分成多行书写,长表达式要在较低优先级操作符处划分新行,操作符放在新行之首,逗号放在一行的结束,划分出的新行要进行适当的缩进,使排版整齐,语句可读,例如:

1
2
3
4
5
if ((taskOne < taskNumber) && (taskTwo < taskNumber) 
&& (taskThree < taskNumber) &&(taskFour < taskNumber)) {
} // 操作符放在新行之首
boo retva = doSomething(averyveryveryveryveryveryongargument1,
argument2, argument3); // 后一行与第一个实参对齐

9.2 文件编码

为了统一文件编码,避免开发过程中文件编码混乱问题,文件保存过程中,统一使用UTF-8无签名编码。编码转换时,默认在UTF-8中转换。

下载VS插件Force UTF-8(No BOM),选择第一个点击下载→关闭并重启VS安装插件。

image-20250317195151183

9.3 缩进

使用4个空格进行代码缩进,禁止使用制表符。

9.4 大括号

大括号的总结来说有两种使用情况,分别是{ 跟在语句后面空一格或者 { 独占一行,} 独占一行。
{ 必须独占一行情况,主要是定义的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 类定义
class Foo
{
};

// 结构体定义
struct Bar
{
};
// 枚举定义
enum Baz
{
};

// 函数定义
void Foo::foo()
{
}

{ 即可独占一行,或者空一格形式,主要是代码逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// if语句
if (condition) {
}

// for循环语句
for (int i = 0; i < 100; ++i) {
}

// while和do-while循环语句
while (condition) {
}
do {
} while(0);

// switch-case
switch (value) {
case 0: {
break;
}
}

9.5 函数调用

函数调用要么在一行写完调用,要么在圆括号里对参数分行,要么参数另起一行且缩进四格。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 正常情况
bool retval = doSomething(argument1, argument2, argument3);

// 同一行放不下,换新行且和第一个实参对齐,左圆括号后和右圆括号前不留空格
bool retval = doSomething(averyveryveryverylongargument1,
argument2, argument3);
// 模拟层次多的情况
if (...) {
if (...) {
doSomething(
argument1, argument2, // 4 空格缩进
argument3, argument4);
}
}

如果一些参数本身就是略复杂的表达式,那么可以直接创建临时变量描述该表达式,并传递给函数。也可以将某个参数独立成行,添加注释,增加可读性。

1
2
3
4
5
int my_heuristic = scores[x] * y + bases[x];
bool retval = doSomething(my_heuristic, x, y, z);

bool retval = doSomething(scores[x] * y + bases[x], // Score heuristic.
x, y, z);

如果一系列的参数本身就有一定的结构,可以酌情地按其结构来决定参数格式。

1
2
3
4
// 通过 3x3 矩阵转换 widget.
my_widget.Transform(x1, x2, x3,
y1, y2, y3,
z1, z2, z3);

9.6 函数声明与定义

返回类型和函数名放在同一行,参数在放不下时,对形参分行,{ }分别单独占一行。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 返回值、函数名、形参在同一行
ReturnType ClassName::FunctionName(Type par_name1, Type par_name2)
{ // { 独占一行
doSomething();
} // } 独占一行

// 形参换行
ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
Type par_name3)
{
doSomething();
}

// 第一个参数都放不下,换新行4空格缩进
ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
Type par_name1, // 4空格缩进
Type par_name2,
Type par_name3)
{
doSomething();
}

未被使用参数,或者根据上下文很容易看出用途参数,可以省略参数名。但是尽量不要省略参数名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 继承于Qt的事件的函数,事件参数未被使用,可以省略参数名
// 这种情况大多是因为继承,重写事件而省略参数操作
void showEvent(QShowEvent *);
void hideEvent(QHideEvent *);
// 需要移除拷贝构造函数或者是赋值操作函数,可以省略参数名
Foo(const Foo&) = delete;
Foo& operator=(const Foo &) = delete;

// 接口中形参恒有命名
class Shape
{
public:
virtual void Rotate(double radians) = 0;
};

class Circle : public Shape
{
public:
void Rotate(double radians) override;
};

void Circle::Rotate(double) {} // 不推荐 —— 省略会不清楚变量的作用
void Circle::Rotate(double /*radians*/) {} // 推荐 —— 说明变量作用并消除警告

9.7 条件语句

对基本条件语句不在圆括号内使用空格,if后面空一格,{ 前空一格或换行,} 独立一行,即不跟else或者else if。例如:

1
2
3
4
5
6
if (condition) { 
... // 缩进四格
}
else { // else或else if另起一行
...
}

如果能增强可读性,简短的条件语句允许写在同一行。只有当语句简单并且没有使用else子句时使用:

1
2
3
4
5
6
if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

// 不允许 - 当有 ELSE 分支时 IF 块却写在同一行
if (x) DoThis();
else DoThat();

如果语句中某个if-else语句使用大括号的话,其他分支也必须要使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 不可以这样子 - IF 有大括号 ELSE 却没有
if (condition) {
foo;
}
else
bar;


// 不可以这样子 - ELSE 有大括号 IF 却没有
if (condition)
foo;
else {
bar;
}

// 只要其中一个分支用了大括号, 两个分支都要用上大括号
if (condition) {
foo;
}
else {
bar;
}

9.8 布尔表达式

如果一个布尔表达式过长,断行方式要统一为逻辑操作符放在新行的开头。但逻辑复杂时考虑增加圆括号,增加可读性。

1
2
3
4
5
if (this_one_thing > this_other_thing 
&& (a_third_thing == a_fourth_thing) // 加入圆括号增加可读性
&& yet_another && last_one) // 操作符放在开头对齐第一个参数
{
}

9.9 switch-case语句

循环和switch-case语句的 { 可以与关键词在同一行,也可以另起单独一行与 } 对齐。后续代码不作展示说明。
switch语句中的case块必须要大括号进行分段,以表明case之间不是连在一起的。switch应该总是包含一个default匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
switch (var) { // 可以在同一行,也可以另起单独一行与}对齐
case 0: { // 空格缩进四格
... // 空格缩进八格
break;
}
case 1: { // 可以在同一行,也可以另起单独一行与}对齐
...
break;
}
default: {
assert(false); // 如果不可能到default可以尝试添加assert语句
}
}

9.10 指针和引用表达式

**句点或箭头前后不要有空格;指针/地址操作符(*,&)之后不能有空格。**例如:

1
2
3
4
x = *p;
p = &x;
x = r.y;
x = r->y;

在声明指针变量或参数时,星号与类型或变量名紧挨都可以:

1
2
3
4
5
6
7
8
9
10
11
// 推荐——空格前置
char *c;
const string &str;

// 推荐——空格后置
char* c;
const string& str;

int x, *y; // 不推荐——在多重声明中不能使用 & 或 *
char * c; // 不推荐—— * 两边都有空格
const string & str; // 不推荐—— & 两边都有空格

9.11 预处理指令

预处理指令不要缩进, 从行首开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 推荐 - 指令从行首开始
if (lopsided_score) {
#if DISASTER_PENDING // 正确 - 从行首开始
DropEverything();
#endif
BackToNormal();
}

// 不推荐 - 指令缩进
if (lopsided_score) {
#if DISASTER_PENDING // 差 - "#if" 应该放在行开头
DropEverything();
#endif // 差 - "#endif" 不要缩进
BackToNormal();
}

9.12 构造函数初始值列表

构造函数初始化列表放在同一行或按四格缩进并排多行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 如果所有变量能放在同一行:
MyClass::MyClass(const int var) : m_someVar(var)
{
doSomething();
}

// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(const int var)
: m_someVar(var), m_someOtherVar(var + 1)
{
doSomething();
}

// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(const int var)
: m_someVar(var), // 缩进 4 个空格
m_someOtherVar(var + 1)
{
doSomething();
}

9.13 类格式

访问控制块关键词的声明不需要缩进。
类声明得基本格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass : public OtherClass 
{
public: // 注意没有缩进
MyClass(); // 标准的空格缩进四格
explicit MyClass(const int var);
~MyClass() {}

void someFunction();
void someFunctionThatDoesNothing() {}

void setSomeVar(const int var) { m_someVar = var; }
int someVar() const { return m_someVar; }

private:
bool someInternalFunction();

int m_someVar;
int m_someOtherVar;
};

注意事项:

  1. 所有基类名应在 80 列限制下尽量与子类名放在同一行;
  2. 访问控制块关键词没有缩进,关键词后不要保留空行;
  3. 除第一个关键词 外,其他关键词前要空一行;
  4. 关于声明顺序的规则请参考 4.8 声明顺序 一节

10 完结撒花

正确性 > 稳定性 > 可测试性 > 可读性 > 全局效率 > 局部效率 > 个人习惯。

  1. 正确性,指程序要实现设计 要求的功能;
  2. 稳定性、安全性,指程序稳定、可靠、安全;
  3. 可测试性,指程序要具有良好的可测试性;
  4. 规范/可读性,指程序书写风格、命名规则等要符合规范;
  5. 全局效率,指软件系统的整体效率;
  6. 局部效率,指某个模块/子模块/函数的本身效率;
  7. 个人表达方式/个人方便性,指个人编程习惯。