C++编码规范
遵循编码规范,可减少低级问题,提升项目的稳定性
提交代码审查前,养成自审代码的习惯
1 命名
1.1 文件命名
- 文件名全部使用小写字母
示例: kctlinechartview.h
1.2 类命名
- 类的命名使用前缀+驼峰命名法
- 当一个类需要暴露给其它工程使用时,应加上工程约定的前缀,前缀全部使用大写字母
- 类命名尽量使用完整英文单词,除非是常见的缩写
1
KCTLineChartView
- Com接口命名在普通前缀前加 I 作为标志
示例: IKCTLineChartView
1.3 其他类型命名
- struct、typedef 与enum 的命名原则与类相同
- enum成员采用大写字母开头的驼峰写法,且应在命名中体现enum名称,具体位置不做强制要求,但同一个enum内的命名规则要保持一致。
1
2
3
4
5
6enum ActionType
{
InsertActionType,
DeleteActionType,
MoveActionType
};
1.4 变量命名
- 变量命名使用小写字母开头的驼峰写法,建议加类型信息前缀,命名应使用能表达变量涵义的完整英文单词。
示例:1
2
3int nIndex = 0; //整数类型n
KCTLineChartView *pLineChartView = nullptr; //普通指针类型p
std::unique_ptr<CSomeObject> spObject = new CSomeObject(); //智能指针类型sp - 仅允许在for单层循环中的 i 和表示坐标的x、y、z上使用单字母变量,其他情况下不允许使用
- 非static 的类成员变量前加上小写字母 m_ 前缀,static 的类成员变量前加上小写字母 s_ 前缀,这两种情况下变量名的第一个单词首字母小写,后面单词首字母大写。
1
2
3
4
5
6class GoodClass
{
private:
int m_dataMember;
static int s_staticDataMember;
};
1.5 函数命名
- 除构造函数和析构函数外,类成员函数使用小写字母开头的驼峰写法
- 非类成员函数使用大写字母开头的驼峰写法
示例:1
2lineChartView->lineCount();
GetObjectCountInDocument(); - 名词属性的类成员函数命名分两种情况
- 若欲获取的信息当成返回值返回,则命名方式为 名词或名词+修饰语 的样式
- 若欲获取的信息通过某个参数返回,则命名方式为get+名词(+修饰语)的样式
示例 :1
2
3
4QString title = lineChartView->getTitle();
lineChartView->title();
lineChartView->subViewAtIndex(index);
lineChartView->getSubViews(&outPointer);
- 动词属性的类成员函数命名方式为动词+宾语,若宾语是this, 应省略
示例:1
2
3lineChartView->removeAllSubViews();
lineChartView->setTitle(L"Hello World");
lineChartView->initialize(); - 形容词属性的类成员函数命名方式为 is/has等修饰语+形容词/名词
示例:1
2lineChartView->isVisible();
lineChartView->hasTitle(); - 返回对象指针的函数若内部分配了内存,应使用 copy, create 等关键字把该信息反映在函数名上
示例:1
2lineChartView->subViews(); //未重新分配内存
lineChartView->copySubViews(); //重新分配了内存
1.6 宏命名
- 原则上宏的命名全部大写,如有必要单词间用下划线分隔。如无必要,不把一段代码定义成宏
示例:1
1.7 全局变量
- 关于使用全局变量的注意事项见后文
- 全局变量前加小写字母 g_ 前缀
- static全局变量前加小写字母 gs_ 前缀
示例:1
2bool g_inFileOpening = false;
static bool gs_staticInFileOpening = true;
1.8 名称空间命名
- 名称空间使用简短的全小写英文单词命名
1
2
3
4namespace chart
{
....
}
1.9 不允许使用My或自己的姓名作为以上命名的前缀
- 禁止下列写法
1
2
3
4
5class MyStringProject
{
public:
...
};
2 头文件
2.1 头文件包含保护
- 在所有头文件中均使用 #ifndef + #define + #endif 来避免该文件被重复包含
- 宏的命名方式为 PROJECTNAME_FILENAME_H
示例
- KProject 项目有头文件名为 kheader.h,则文件头部加上
1
2 - 末尾加上
1
2.2 前置声明
- 当使用一两个前置声明就能编译通过时,不要在头文件里包含另一个头文件
2.3 头文件包含顺序
- 在代码文件里包含多个头文件时,应按照如下顺序将头文件分组,每组之间使用空行隔开:
- 预编译头文件,通常是stdafx.h
- 与代码文件同名的头文件
- 系统头文件
- 其他工程头文件
- 本工程其他头文件
示例
- KProject 工程中用到如下头文件
- kclass.cpp实现了声明在kclass.h内的类
- C++标准库中的vector容器
- ExternalProject 工程中的externalclass.h和externalutils.h
- KProject工程中的kfriendclass.h、kfileiohelper.h、kstringutils.h
1
2
3
4
5
6
7
8
9
10
11
12
13
// 其他工程的头文件
// 本工程其他头文件
3 类
3.1 初始化
- 类的所有成员变量必须初始化,只有成员变量没有函数的类,必须定义默认构造函数
- 除非有特殊需求,在类的构造函数中仅进行不涉及具体功能的初始化操作,例如为成员变量赋零
- 较复杂的初始化操作,应放在一个单独的 init() 方法中,由类实例的创建者负责调用
解释:
构造函数内难以报告错误,且构造函数中对虚函数的调用不会派发给子类,所以较复杂的初始化操作应在构造完成之后进行 - 若没有明确的将单参构造函数用于隐式类型转换的需求,应使用 explicit 关键字
3.2 拷贝构造函数、赋值运算符
- 若没有明确的对类进行拷贝的需求,应在private 段中声明拷贝构造函数和赋值运算符
3.3 继承
- 只使用 public 继承
- 在语义明确的时候从父类继承具体实现,其他时候从抽象接口继承
- 若类有虚函数,则析构函数也定义为虚函数
- 父类中声明为 virtual 的函数,子类声明中要明确标明为 virtual 以及 override
3.4 运算符重载
- 尽量避免运算符重载,除非是为了在容器类中使用而必须实现的代码
3.5 访问权限声明
- 类声明中按
public, protected, private的顺序声明函数和变量 - 每个关键字仅占用一段,每一段中的声明顺序为:
typedef, enumQ_OBJECT及类似声明- 嵌套类
- 常量
- 构造函数、析构函数、成员函数、数据成员
3.6 友元
- 友元的定义和友类的定义应放在同一个文件中
4 函数与实现
4.1 参数
- 参数排列顺序为输入参数在前,输出参数在后
- 引用作为输入参数时应配合
const使用,不使用缺省参数 - 不允许一个参数既作为传入参数,又作为传出参数
- 复杂类型参数,通过加引用避免内存拷贝
4.2 内联函数
- 内联函数不超过10行,推荐仅对1行的函数进行
inline - 不使用循环或
switch...case,不进行递归 - 在类定义体内实现的函数,不要加
inline关键字
4.3 异常处理
- 尽量避免使用异常(
try...catch),优先考虑基于返回错误码处理异常
4.4 类型编程
- 不使用运行时类型信息
- 明确使用
static_cast,const_cast进行类型转换,避免使用reinterpret_cast,小心使用dynamic_cast
4.5 宏
- 尽量使用
const、enum、inline替代#define
4.6 类数据成员的引用
- 避免成员函数返回指向类成员的指针或引用
5 注释
5.1 文件头
文件头注释应包括文件名、创建者、创建时间、功能描述和版权信息
1 | /* --------------------------- |
5.2 代码注释
- 注释使用
//风格,应简洁清晰 - 非必要,不写注释
- 需要写注释的场景:
- 故意违背编码规范之处
- 分成多步完成的任务
- 较复杂的逻辑
- 和常理不符的代码
- 比较重要,需要引起注意的地方
5.3 注释语言
- 默认注释语言为英文
- 注释中的工程师姓名
- 只在仅两种情形下把姓名加入注释中:
- 文件头中的创建者信息
- 做
TODO注释时1
// TODO(tommy_zhang): 删掉这里的全局变量!
5.4 预处理宏注释
- 中间代码段较长的
#else与#endif之后用注释标明宏的名字1
2
3
4
5
// ...
// ...
6 格式
6.1 对齐与缩进
- 使用
Tab进行对齐 - 花括号上下对齐,不允许将左花括号放在
if等语句的末尾1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21void KFunction()
{
if (...)
{
// ...
}
else
{
// ...
}
do
{
// ...
} while (...);
for (int i = 0; i < 10; ++i)
{
// ...
}
} - 构造函数初始化列表和多个基类的每一行逗号放前面,和冒号对齐
1
2
3
4
5
6
7
8
9
10KGroupGrid::KGroupGrid(QWidget *pParent)
: KWidget(pParent)
, m_scrollBar(Qt::Vertical)
, m_nHeaderHeight(0)
, m_itemSize(80, 64)
, m_showToolTip(false)
, m_showSeparatorLine(true)
, m_scrollBarPolicy(Qt::ScrollBarAsNeeded)
{
} - 只有一行代码的
if语句不加花括号1
2
3
4if (inputType == badType)
doSomethingBad();
else
doSomethingElse(); if语句中某一个分支有一行以上代码时,所有if/else分支都要加花括号1
2
3
4
5
6
7
8
9if (inputType == badType)
{
doSomethingBad();
}
else
{
doSomethingElse1();
doSomethingElse2();
}- 推荐一行代码不超过120个字符
- 函数参数表过长时,应换行,返回值类型、左括号应保持与函数名同一行,具体换行方案有两种:
1
2
3
4
5
6HRESULT KClass::method(ParameterOne param1,
ParameterTwo param2,
ParameterThree param3)
HRESULT KClass::method(ParameterOne param1,
ParameterTwo param2, ParameterThree param3) - 函数类型为
const时,const关键字与最后一个参数同行1
2
3HRESULT KClass::constMethod(ParameterOne param1,
ParameterTwo param2,
ParameterThree param3) const - 所有预编译宏全顶格对齐
1
2
3
4
5
6
7
8
9
10
11
12
13void KClass::methodWithMacros()
{
static int integer1 = 0;
integer1++;
if (integer >= 10)
{
integer1 = 1;
integer1 = 2;
}
} - 分为多行的布尔表达式中
&&与||置于行首 - 换行应在运算符优先级最低的地方进行,尽可能避免把配对的括号分成两行
- 当条件比较复杂时,应将高优先级的运算用括号明确标识出来
1
2if ((valueGood >= somethingGood || otherCondition)
&&(valueBad + 1) <= somethingReallyBad) - 命名空间
namespace内容不缩进
6.2 空格
- 函数调用的括号前后不加空格,参数之间逗号后加空格
1
void KFunction(ParameterOne para1, ParameterTwo para2)
if、while、for等关键字与括号间加一个空格,括号后不加空格1
2
3if (nullptr == thePointer)
for (int index = 0; index < maxIntValue; index++)- 使用
Tab键无法刚好对齐时,用空格补齐 - 没有参数的函数括号内不加空格
- 二元运算符前后各加一个空格
- 自增减运算符与分号,变量间都不加空格
1
2
3if (inputType1 == goodType && inputType2 == badType)
for (int index = 0; index < MAX_INT; index++) - 引用符号(
. ->)前后不加空格1
spA->value();
- 地址、引用运算符(
*, &)后不加空格1
*pA = 1;
- 声明指针类型的
*号前加一空格,后边不加空格1
KCircle *pCircle = nullptr;
6.3 换行
- 禁止滥用换行;禁止重复换行
- 建议使用换行的场景:
- 文件级:
include分组间、命名空间边界 - 类级:访问权限段间、成员类型间
- 函数间:每个函数实现之间
- 函数内:独立功能逻辑步骤之间
- 特殊块:预编译宏、异常处理前后
- 文件级:
7 作用域
7.1 嵌套类
- 不需要暴露嵌套类作为接口的时候,将嵌套类声明为
private
7.2 局部变量
- 局部变量应在将要使用时进行声明,声明的同时初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void SomeMagicalFunction()
{
const int magicNumber = 1;
int integer1 = magicNumber;
integer1++;
if (integer1 <= 0)
DoSomethingWeird();
float floatingNumber1 = 1.0f;
float *floatingPointer = &floatingNumber1;
if (floatingPointer == NULL)
HowCouldThisHappen();
}
void hello()
{
}
7.3 全局变量
- 除非特殊情况,尽量不使用
Class类型的全局变量,不使用Class类型的static类数据成员 - 可以用单例模式替代
Class类型的全局变量 - 全局的字符串变量使用 C 风格的
char,不使用各种字符串类
7.4 名称空间(namespace)
- 允许在
cpp文件中使用匿名名称空间进行保护,不允许在头文件中使用匿名名称空间 - 避免使用
using namespace将一个名称空间中的所有名称全部导入
8 模板
8.1 模板声明
- 使用
typename关键字声明模板,不使用class关键字
8.2 模板适用范围
- 一般情况下仅使用模板来实现容器类或通用算法,如果需要用作其他用途,请增加注释说明必要性
8.3 模板中的嵌套类
- 模板中使用嵌套类时,加上
typename关键字声明1
2
3
4
5
6template<typename T>
void someFunction(const T& container)
{
typename T::Iterator iter(container.begin());
iter.next();
}
8.4 模板继承
- 在模板派生类中调用模板基类函数时,明确在函数调用前声明该函数属于基类
1
2
3
4
5
6
7
8
9template<typename T>
class DerivedClass : public BaseClass<T>
{
public:
void doAction()
{
BaseClass<T>::doBaseAction();
}
};
9 内存
9.1 优先使用静态内存
- 编译期能够确定的常量数组,应声明为
static1
2
3
4
5void func(int type)
{
static const int someInt[] = {80, 40, 40, 50, ... 94, 94, 47};
// ...
}
9.2 其次为栈内存
- 编译期能够确定长度的非常量数据,应使用栈内存
1
2
3
4
5
6
7
8
9
10
11void func(int type)
{
const int length = 1024;
unsigned char buffer[length + 1];
DWORD dwReadSize = length;
while (::ReadFile(hFile, buffer, length, &dwReadSize, NULL))
{
// ...
dwReadSize = length;
}
}
9.3 使用智能指针和容器管理堆内存
- 单个堆对象的生命周期用
std::unique_ptr来管理 - 连续的内存空间用
std::vector或QVector来分配和管理,不允许在代码中出现new[]和delete[]的显式调用1
2
3
4
5
6
7
8void func(int type)
{
std::unique_ptr<CSomeObject> spObject(new CSomeObject());
int bufferLength = getBufferLength();
std::vector<SomeType> vecBuffer(bufferLength);
readBuffer(&vecBuffer[0], bufferLength);
} - 字符串用
std::basic_string<T>或QString来管理 delete不应该出现在析构函数或用于清理内存的函数之外- 禁止使用
malloc/free来分配和释放内存
10 跨平台
10.1 路径分隔符
- 禁止代码里拼路径时写死路径分隔符,依赖Qt的模块可使用
QDir::separator()
10.2 数据类型
- 禁止使用平台不一致的数据类型,如
Windows平台long为固定的4字节,而Unix则通常32位下为4字节,64位下为8字节。
10.3 系统 API
- 优先使用跨平台的 API,如QT;尽量避免使用Windows系统的API,如需要必须加平台宏进行隔离
10.4 字符串
- 优先使用跨平台的 WCHAR 字符串
11 其它规范
- 常量定义优先使用
constexpr,而不是const - 非函数内部的常量定义优先考虑大写
- 禁止出现重复代码(重复2次以上),重复代码要抽取复用
- 尽量避免函数过于臃肿(超过500行)
- 多线程读写的变量加
atomic或其他原子修饰符 - 禁止使用高危函数
- 指针变量赋值为空使用nullptr,不要用NULL
- 禁止代码中出现中文硬编码,注释除外
- 设置大小,位置等UI参数时,要考虑DPI缩放处理
- 禁止出现无用的变量或代码