C++ 实用编程规范与建议
概述
本文档主要分享实用的编程规范与建议,不讨论 if () 前后是否要有空格,以及 if else 的大括号是与 if 同一列
if (condition)
{
// do something
}
else
{
// do something
}
还是依照 Qt 的编码约定 (Coding Convention) 等。
if (condition) {
// do something
} else {
// do something
}
I - 通用约定
使用科学的编码规范,可以将测试期间或运行期间出现的软件缺陷,提前到编译期间解决,节省人力和时间成本。
此外,较好的编程习惯,也可以提高程序的性能和执行效率。
1.1 - 命名规则
通常为了使文件名/函数名/变量名/类名/宏等更加明确,采用简单明了和意义明确的英文单词。建议文件名/类名命名使用大驼峰的方式,变量名使用小驼峰。
代码的一致性可以减少开发人员之间维护的成本和减少代码阅读时的不识感。
大驼峰 (PascalCase)
小驼峰 (camelCase)
大驼峰举例文件名,首字母大写,且每个单词首字母使用大写TcpServer.h
,变量名举例 testTime
。
1.2 - 头文件
为了防止某个头文件被重复包含,会通过定义特定的宏。在一个大型的项目中,需要注意避免文件名的重复和宏定义的重复。
一般有两种方式
1、pragma 预编译指令
// 1 - pragma
#pragma once
2、 header guard 头文件守卫
// 2 - header guard
#ifndef _SOLUTION_PROJECT_HEADER_H_
#define _SOLUTION_PROJECT_HEADER_H_
// content...
#endif // _SOLUTION_PROJECT_HEADER_H_
第 2 种方式,尤其在一个大型项目中,为了避免宏定义的重复,宏定义需要使用 _解决方案名称_项目名称_头文件名称_H_
的方式。若出现宏定义重复,可能无法正确引入头文件内容的问题。
II - 类相关
2.1 - 不明确的函数访问权限声明为私有
新增类成员函数,在不确定其使用范围时,使用 private 限定。
- 降低其他开发人员查找外部接口时的负担
- 保证类的分层和封闭性
- 未使用的私有函数可使用工具检测出来,方便后续精简结构时去除无用代码。
2.2 - 虚函数重写需要添加 override 关键字
在派生类中重写基类的虚函数,若重写的函数没有添加 override
关键字,在函数名称或参数书写错误的情况下,编译时会创建一个新的虚函数,编译期间不报错,运行时不能正确地实现动态多态,且在代码量大时,难以定位且难以察觉。
派生类的虚函数声明处,添加 override
关键字能够借助编译器对虚函数声明进行一致性检查,确保其父类中一定包含函数签名一致的虚函数,在不一致时会编译报错。
class Base
{
//...
protected:
virtual bool ConnectToRemote();
};
class Derivated : public Base
{
//...
protected:
virtual bool ConnectToRemote() override;
}
另外,所有派生类的虚函数都添加 override
关键字后,基类虚函数定义更新,编译时会列出所有需要修改的位置,避免疏漏和遗忘。
2.3 - 类成员变量在声明处设置默认值
类成员变量未定义,或因存在多种构造函数,在某个构造函数函数体内忘记初始化某个类成员变量,尤其是指针变量,很容易引起错误。
为避免此种疏漏,在类声明的头文件中,类成员变量声明处均需进行默认值的赋值。
赋值建议使用花括号 { }
,在有类型不匹配或者值溢出时,编译器也会报错。
class RemoteManager
{
//...
private:
Remote * m_remoteClient {
nullptr };
bool m_firstConnected {
false };
int m_remoteNumber {
0.0 }; // 编译期间报错
int m_remoteTime = 0.0; // 编译期间不报错 , 建议使用前一种
};
注:类类型等有默认构造函数的成员变量可以不设置默认值,如
std::string, std::vector<int>, std::map<std::string, int> //...
等,有其他需求的除外。
2.4 - 析构函数释放动态分配的内存
为避免内存泄漏,程序运行越久占用内存越大的问题,在类析构时需要释放其动态分配的内存,即 delete 指针类型成员变量,指向同一片内存的指针除外,需要在其管理的类中释放。
struct
除默认访问权限和默认继承权限不同,以及关键字用处不同外,其他与 class
等价。也需要做同样的释放。
class RemoteManager
{
//...
~RemoteManager()
{
if (!m_pRemoteClient)
{
delete m_pRemoteClient;
m_pRemoteClient = nullptr;
}
//...
}
//...
private:
Remote * m_pRemoteClient {
nullptr };
//...
};
struct RemoteCoordinate
{
//...
~RemoteCoordinate()
{
if (!m_longitude)
{
delete m_longitude;
m_longitude = nullptr;
}
if (!m_latitude)
{
delete m_latitude;
m_latitude = nullptr;
}
//...
}
//...
private:
double * m_longitude {
nullptr };
double * m_latitude {
nullptr };
};
2.5 - 含动态分配成员变量需实现/禁用拷贝赋值
当类中存在需要动态分配内存的类成员变量时,需要手动实现其拷贝构造函数和赋值操作符重载,或将此两种构造函数禁用。此两种构造函数在不声明时,编译器均会自动生成默认,容易引起浅拷贝和双重释放的运行时异常退出或者软件崩溃等问题。
class RemoteManager
{
RemoteManager();
//...
private:
Remote * m_remoteClient {
nullptr };
bool m_firstConnected {
false };
//...
};
RemoteManager::RemoteManager():
//...
, m_remoteClient{
new Remote() }
, m_firstConnected{
false }
//...
{
//...
}
需实现拷贝构造和赋值操作符重载的接口
class RemoteManager
{
RemoteManager();
RemoteManager(const RemoteManager & rhs);
RemoteManager & operator = (const RemoteManager & rhs);
//...
private:
Remote * m_remoteClient {
nullptr };
bool m_firstConnected {
false };
//...
};
//...
RemoteManager::RemoteManager()
{
//...
}
// copy constructor
RemoteManager::RemoteManager(const RemoteManager & rhs)
{
//...
// Remote has implemented Remote(const Remote * rmtp) deep-copy
this->m_remoteClient = new Remote(rhs.m_remoteClient);
//...
}
// assignment operator
RemoteManager & operator = (const RemoteManager & rhs)
{
//...
this->m_remoteClient = new Remote(rhs.m_remoteClient);
//...
}
或者禁用拷贝构造和赋值操作符重载
class RemoteManager
{
RemoteManager();
//...
private:
Remote * m_remoteClient {
nullptr };
bool m_firstConnected {
false };
//...
private:
// prevent shallow-copy, disable copy constructor
RemoteManager(const RemoteManager & rhs) = delete;
// idem, disable assignment operator
RemoteManager & operator = (const RemoteManager & rhs) = delete;
};
III - 语法细节
3.1 - 条件判断时常值置于双等号左侧
条件判断双等号 ==
在输入时可能由于键盘问题或者其他不可控疏忽,导致漏掉一个等号字符,变成了恒成立的赋值语句。此种语句通常可正常编译,运行时错误难排查,尤其对于大型项目。
因此要求将常值、常量放置在双等号左侧,即便因为不可控的因素漏掉一个等号符,编译期间也会报错,因常值无法赋值修改。
const int remoteNum = 1;
// ...
if (remoteNum == ivalue)
{
// ...
}
// ...
if ("RemoteParameter" = strvalue)
{
//...
}
注:此处若使用 std::string 的 compare 方法,记得与 0 值比较,或者取反,这是一个新手易错的地方。
3.2 - 分支和循环体的单行语句置于花括号中
if
, else
, for
, while
等分支和循环语句后,对于紧跟的单行语句的使用,语法上可将花括号省去,但可能因意想不到的宏函数展开导致执行错误,或由于后续代码维护增加内容以及操作不当引起其他错误。
#define REPORT(cmd) \
PrintConsole(cmd);\
SendToMaintenance(cmd);
//...
if (!Remote::Check())
{
REPORT("Error: Remote is not well functionning.");
}
//...
如代码中所示,若省去花括号则会引起运行时的错误。
if (!Remote::Check())
PrintConsole("Error: Remote is not well functionning.");
SendToMaintenance("Error: Remote is not well functionning.");
程序的执行将不是预期结果。
3.3 - 使用 nullptr 替代 NULL
关键字 NULL
的定义在 C 语言中为 ((void *) 0)
,而在 C++ 中为 0
,nullptr
类型为 std::nullptr_t
,可隐式转换为所有的原生指针类型。
#define NULL 0
在多种函数重载时容易出现问题,例:
int CheckRemote(Remote * rmt); // 1
int CheckRemote(int serialNum); // 2
CheckRemote(nullptr); // call 1
CheckRemote(NULL); // call 2
关键字 NULL
在代码中容易识别为整型的 0 ,会出现函数重载匹配错误。
3.4 - 复合类型参数传递尽量使用常量引用
类类型和复合类型的参数传递,若未修改则应尽量使用常量引用,此种方式优势:
- 1 - 省去函数体中的拷贝构造和析构,提高执行效率
- 2 - 在值被代码意外修改时,会编译报错
const Type &
可以匹配左值和右值, 如对于 std::string
类型,可以匹配变量,也可以匹配字符串字面值。
std::string Convert(const std::string & str);
// local object
std::string serialNum {
"XXX-XXX-XXX"};
std::string validString = Convert(serialNum);
// lvalue
std::string validStringA = Convert("XXX-XXX-XXX");
注:基础类型如 int
, float
等,不建议使用引用传递,会导致效率更低。此外, STL 迭代器和函数对象也建议使用值传递。
3.5 - 指针类型的入参声明为常量
由于布尔类型和指针在 C++ 中可隐式转换,在有多种类型函数重载的情况下,问题尤为明显。
如下
DataStruct(char * str); // 1
DataStruct(bool bvalue); // 2
DataStruct(std::string svalue); // 3
DataStruct dt("a string value"); // call 2
C 语言中字符串字面值的类型为 char *
而 C++ 中字符串字面值的类型为 const char *
,编译时编译器进行类型推断, 会首先匹配最接近的类型,即 const char *
,在此类型缺席的情况下会顺位匹配布尔类型,其次才为 std::string
类型。
所以建议所有传递指针的地方,若函数体内不修改则尽量使用常量指针。
DataStruct(const char * str);
int CheckRemote(const Remote * rmt);