C++规范笔记
一、代码原则
(1)清晰易读第一 (2)力求简洁,当为简单需求时,使用最简单的代码,减小出错几率。 (3)保持与代码原风格一致,保证阅读统一性。
1.常量
- 使用const常量代替宏,因为宏仅仅是文本替换,未进行类型检查,以及没有作用域限制。
- 当需要同一类型常量来标识,定义为枚举变量,枚举会检查类型。枚举内部有重复值时,优先使用内部已定义枚举来赋值。
- 善用const修饰,当变量不能修改时,或类成员函数不修改成员变量,应该使用const修饰
函数参数:如何不修改入参时,形参应声明为const
void fun(const int num);
成员函数,不修改类内部成员变量时,多为get类型,用const修饰
void getfun() const;
2.初始化和类型转换
- 禁止用memcpy、memset初始化非基础类型,禁止对class对象使用上述函数,例如使用memset(this,0,sizeof(*trhis))进行操作的话会情况类的虚函数表,在调用函数是导致宕机。
- 在代码中使用变量应遵循作用域最小与就近声明,应使用初始化代替声明再赋值。
string username(“xiaoming”)
- 初始化列表要严格按照成员声明的顺序来初始化成员变量,避免出现成员变量之间相互依赖,导致初始化错误
//不好的例子:初始化顺序与声明顺序不一致
class Employee
{
public:
Employee(const char* firstName, const char* l astName) : firstName_(firstName),lastName_(lastName) , email_(firstName_ + "." + lastName_ + "@huawei.com") {};
private: string email_, firstName_, lastName_;
};
类型转换
- 上行转换:派生类的指针或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。
- 下行转换:基类的指针或引用转换成派生类的指针或引用)时,由于没有动态类型检查,所以不安全的
- dynamic_cast:主要用于下行转换,类似于虚函数,dynamic_cast需要知道转换后的类型,如前后转换类型不对,容易出错。
- static_cast:类似于C语言的强制类型转换,但是从派生类转换为基类,不会出现安全问题。
static_cast<tuint32>(CPFTime(ftFrame).GetTime());
3.函数
内联函数:Inline
作用:函数频繁调用时,加快运行速度,可以对参数进行类型检查。
函数参数原则
1.使用const表明入参是否可以修改,并且尽量使用引用替代指针。
2.使用派生和继承替代函数指针
问题
:当函数不满足当前使用需求时,是否可以使用缺省参数。可能当前函数使用的地方较多,难以修改,相比于增加一个相同函数,增加缺省参数似乎是一个更优的选择。
4.类
类的设计原则:职责单一,接口清晰、少而完备,类间低耦合、类内高内聚
1.封装 向调用者隐藏函数实现细节,尽量使用函数名和文档向调用者说明函数功能,类似于黑盒。
- 成员变量设为私有,并提供获取和设置的接口。
- 不能暴露成员的指针和引用,避免成员变量被意外修改,操作。
- 运行时多态,将内部实现(派生类提供)与对外接口(基类提供)分离。
- 使用PIMPL模式,确保私有成员真正不可见
2.构造、赋值、析构
- 当类中有成员变量时,必须定义构造函数用于初始化成员变量,避免出现使用未初始化的变量,导致出现bug。
- 资源管理类需要定义拷贝构造函数、赋值操作符和析构函数,避免出现浅拷贝情况,导致资源重复释放和内存泄露。
注意:
创建子类对象时,先调用基类构造函数->子类构造函数。基类构造函数负责初始化继承的数据成员,子类构造函数负责新增成员的初始化
析构相反,子类析构->基类析构
浅拷贝只是增加了一个指针指向已存在的内存地址,仅仅是指向被复制的内存地址。
深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lnNBFpXI-1668416251120)(en-resource://database/739:1)]
- 避免在构造函数和析构函数中调用虚函数,在构造函数和析构函数中调用虚函数,会导致未定义的行为。
class BaseA //基类BaseA
{
public:
BaseA();
virtual void log() const=0;
//不同的派生类调用不同的日志文件
};
BaseA::BaseA()
//基类构造函数
{
log();
//调用虚函数log
}
class DeriveB:public BaseA //派生类
{
public:
virtual void log() const;
};
DeriveB B;
当执行DeriveB B时,先跑BaseA的构造函数,log()是BaseA中的,与设想的结果不一致。
3.继承、组合
- public继承是一种 is-a 的关系,组合是一种 has-a 的关系;一般优先使用对象组合,而不是类继承;继承允许根据基类的实现来定义派生类的实现,在继承方式中,基类的内部细节对子类可见。继承一定程度上破坏了基类的封装,基类的改变,对派生类有很大的影响,派生类与基类的依赖关系很强,耦合度很高;
- 继承分为:实现继承和接口继承 1.实现继承: 优点:简单直观。 缺点:不够灵活编译时定义,暴露基类实现细节,派生类和基类耦合性强。 2.接口继承: 优点:可以同时继承接口和缺省实现,又能够覆盖继承的实现。
- 虚函数绝不使用缺省参数值,因为虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。
- 避免派生类中定义与基类同名但参数类型不同的函数
4.重载 规则:
- 仅在输入参数类型不同、功能相同时重载函数。 说明:使用重载,导致在特定调用处很难确定到底调用的是哪个函数;当派生类只重载函数的部分变 量,会对继承语义产生困惑,造成不必要的费解
- C/C++混用时,避免重载接口函数
- 使用重载以避免隐式类型转换 说明:隐式转换常常创建临时变量;如果提供类型精确匹配的重载函数,不会导致转换。
5.作用域,模板
作用域规则:
- 尽可能不使用局部类 说明:定义在函数体内的类称为局部类。局部类只在定义它的局部域内可见。局部类的成员函数必须被定义在类定义中,且不能超过15行代码。否则,代码将变得很难理解。
- 使用静态成员函数或名字空间内的非成员函数,避免使用全局函数 说明:非成员函数放在名字空间内可避免污染全局作用域。或使用static关键字 (如 static int Foo() {...}) 限定其作用域
- 避免class类型的全局变量,尽量用单件模式 说明:静态生存周期的对象, 包括全局变量, 静态变量, 静态类成员变量, 以及函数静态变量, 都必须是原生数据类型 (POD : Plain Old Data)。静态变量的构造函数, 析构函数以及初始化操作的调用顺序在C++标准中未明确定义,从而导致难以发现的 bug。
模板规则:
- 模板的每一次实例化都会产生一份新的源代码,容易造成代码规模的过度膨胀。
- 模板类型应该使用引用或指针 说明:实例化和参数传递复杂类型(结构体,对象),传值的代价很高;引用和指针可以提高效率。
- 禁止extern "C"内部使用#include包含其他头文件,避免嵌套过深。 说明:函数、变量以及函数类型这三种对象放置于extern
- 使用sizeof(变量)而不是sizeof(类型) 说明:使用 sizeof(varname),当代码中变量类型改变时会自动更新。
6.资源分配和释放
1.内存申请与释放一般原则
- 对象在退出其作用域时,就应该立即被释放,而且要做到:谁申请,谁释放。
- 函数内分配的内存, 函数退出之前要释放,避免跨函数释放;
- 类中数据成员的内存,在析构函数中确认并释放;
- 全局变量、静态变量的内存空间则在进程退出时,或相应的共享库被卸载时,由操作系统回收;
2.new行为和检查策略 说明:当perator new无法满足内存分配需求时,返回值会异常,需要对返回值判断非空处理。
- 释放内存后,要立即将指针设置为NULL,防止产生野指针
- 单个对象释放使用delete,数组对象释放使用delete [] 调用delete所包含的动作:若是对象调用相应的析构函数;将内存归还系统。 调用delete[]所包含的动作:从new[]将找出的n值;调用n次相应的析构函数;将内存归还给系统。
- 释放结构(类)指针时,首先释放其成员指针的内存空间
struct STORE_BUF_S
{
ULONG ulLen;
UCHAR *pcData;
}STORE_BUF_T;
void func()
{
STORE_BUF_T *pstStorageBuff = NULL;
//申请结构内存….
//程序处理…
//错误
free(pstStorageBuff);
//正确
free (pstStorageBuff->pcData);
free(pstStorageBuff);
return;
}
- 释放指针数组时,首先释放数组每个元素指针的内存
- 禁止返回局部对象指针
- 不能强制关闭线程
- 使用new, delete的封装方式来分配与释放内存
- 避免在不同的模块中分配和释放内存 说明:在一个模块中分配内存,却在另一个模块中释放它,会使这两个模块之间产生远距离的依赖,使程序变得脆弱。
7 异常与错误处理
异常 说明:异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
- try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块 }catch( ExceptionName e2 )
{
优点
:
- 异常可以集中捕捉,错误检测与算法处理相分离,算法逻辑更清晰;而返回错误在每个返回点都要进行检测与错误处理,代码逻辑分散。
- 异常的约束更强,用户不能忽略抛出的异常,否则程序默认会被终止,而返回错误则可能被忽略。
缺点
:
- 必须检查所有调用点是否可能抛出异常,在抛出后必须正确处理状态和资源变量等,否则可能导 致对象状态不正确或者资源泄露等。
- 必须清楚可能抛出的所有异常,并在合适的地方捕捉。
- 使用异常很难评估程序的控制流,代码很难调试。
注意点
:
- 构造和析构函数不能抛出异常,可能会导致对象初始化不成功或者是资源未释放。
- 独立编译模块或子系统的外部接口禁止抛异常
错误
- 建立合理的错误处理策略 说明:这里所说的错误指运行时错误,并非模块内部的编程和设计错误。模块内部的编程和设计错误 应该通过断言标记。
- 离错误最近的地方处理错误或转换错误
- 错误发生时,至少确保符合基本保证;对于事务处理,至少符合强保证;对于原子操作,符合无错误保证 说明:基本保证是指访问对象时的状态都是正确的;强保证是对基本保证的增强,不仅要状态正确,而且当失败时状态要回滚到操作前的状态,要么成功要么什么都不做;无错误保证是不能出现失败。编码中严格遵循此原则,会极大提升程序的健壮性。
8 标准库
规则:
- 避免使用auto_ptr
- 不要保存string::c_str()指针 说明:在C++标准中并未规定string::c_str()指针持久有效,因此特定stl实现完全可以在调用string::c_str()时返回一个临时存储区并很快释放。
- 使用容器时要评估大量插入删除是否会生成大量内存碎片 说明:不同的操作系统和运行时库分配内存的策略各不相同,由于容器内存多是动态分配而来,对于反复大量插入删除的操作,有可能会造成大量的内存碎片或者内存无法收回。
- 使用string代替char。
9 程序效率
- FREE:性能开销很小,甚至有优化,可以放心使用;
- CHEAP:性能开销有一定程度,多数情况下可以使用,在性能关键地方需要注意;
- EXPENSIVE:性能开销较大,需要按照情况使用,在性能关键地方需要慎重使用。
C++语言特性
性能分级
备注
封装
FREE
class和C的struct在使用空间上是相同的,class中的成员函数的时间开销也和C等效代码是一致的。
多态
FREE
含有虚函数的class在空间上需要增加虚表指针(4字节),在虚函数的执行上需要间接寻址的开销。虽然有微量开销,但等同于C等效代码。
命名空间
FREE
Namespace会带来符号名字符串长度的增加,但C等效代码也需要增加前缀字符串(比如模块名)。
内联
FREE
没有函数调用的开销,没有指令跳转的顺序执行能让编译器进行更好的优化,但会增加程序大小。
重载
FREE
等效类成员函数的开销
构造和析构
FREE
等效类成员函数的开销
引用
FREE
等效或优于指针的使用,引用可以避免指针的间接寻址开销
模板
FREE~EXPENSIVE
模板具有静态多态的优点,部分逻辑提前到编译阶段以提升性能;但会导致代码膨胀,程序尺寸增大。
RTTI
CHEAP~EXPENSIVE
执行时间有一定耗时,gcc/VC中dynamic_cast的开销小于10倍函数调用,程序尺寸增大。
异常
EXPENSIVE
异常捕捉很耗时,其包括栈展开等操作,gcc/VC中异常处理开销大于300倍函数调用。
STL
CHEAP~EXPENSIVE
STL提供线性(list)、对数级(map)和常量级(hash_map)不同性能的容器,建议根据应用实际需求选用。
- 在构造函数中用初始化代替赋值 说明:通过成员初始化列表来进行初始化总是合法的,效率也高于在构造函数体内赋值。
- 当心空的构造函数或析构函数的开销 说明:空构造函数的开销不一定是0,空构造函数也包括基类构造、类内部成员对象的构造等。如果对象的构造函数或析构函数有相当的开销,建议避免临时对象的使用,并在性能关键路径上考虑避免非临时对象的构造和析构,比如Lazy/Eager/Partial Acquisition设计模式
- 对象参数尽量传递引用(优先)或指针而不是传值 说明:对于数值类型的int、char等传值既安全又简单;但对于自定义的class、struct、union等对象来说,传引用效率更高:
- 不需要拷贝。class等对象的尺寸一般都大于引用,尤其可能包含隐式的数据成员,虚函数指针等,所以传值的拷贝的代价远远大于引用。
- 不需要构造和析构。如果传值,传入是调用拷贝构造函数,函数退出时还要析构。
- 尽量减少临时对象
- 优先采用前置自增/自减
- 简单访问方法尽量采用内联函数
- 避免在函数内部的小块内存分配
10 并发
- 多线程、进程并行访问共享资源时,一定要加锁保护 说明:共享资源包括全局变量,静态变量,共享内存,文件等。
- 锁的职责单一 说明:每个锁只锁一个唯一共享资源;这样,才能保证锁应用的单一,也能更好的确保加锁的范围尽量小
- 锁范围尽量小,只锁对应资源操作代码 说明:使用锁时,尽量减少锁的使用范围,在函数内部靠近资源操作的地方加锁。
- 进程间通讯,使用自己保证互斥的数据库系统、共享内存,或socket消息机制;尽量避免使用文件等进程无法管理的资源 说明:由于文件在不同进程间访问,无法保证互斥。当然,可以在进程间加进程锁,但只受限于我们能加锁的进程,对于第三方进程等无法保证。
- 可重入函数尽量只使用局部变量和函数参数,少用全局变量、静态变量
11 风格
- 类命名以大写字母开头,中间单词也以大写开头
- 类的声明按照一定的次序进行,关键字不缩进
- 构造函数初始化列表在同一行或按4格缩进并排几行
- 使用‘//’注释方式,而不是
/* */
- 整个项目需要的公共头文件应放在一个单独的目录下
12 可移植性
32位和64位CPU架构之间的移植,关键问题如下:
- 指针截断
- 数据类型字节对齐
- 对内存地址的错误假设
- 对复合数据类型成员地址的错误假设
- 大小端,网络字节序问题
- 重定义基本数据类型
typedef int32_t int;
typedef int64_t long long;
- 注意数据类型对齐问题 调整结构体对齐的方案: gcc 中可使用__attribute__((packed)),MSVC 提供了#pragma pack()和__declspec(align())。
- 在涉及网络字节序处理时,要注意进行网络字节序与本地字节序的转换 说明:小端法(Little-Endian) 低位字节排放在内存的低地址端即起始地址,高位字节排放在内存的高地址端。 大端法(Big-Endian) 高位字节排放在内存的低地址端即起始地址,低位字节排放在内存的高地址端。
X86、AMD64平台使用小端法、而HP-IA, IBM AIX的CPU采用的是大端法。网络字节序是大端法。
- 避免无符号数与有符号数的转换
13 全球化
- 使用正确的数据类型和类处理多语言字符和字符串
- 对字符进行处理时,需分配足够的内存空间并确保字符完整 说明: 1.使用char类型存储UTF-8多语言字符串时,注意分配足够的内存空间。UTF-8的每个字符占用空间长 度在1-4字节之间,世界常用语言的字符长度在1-3个字节中,如英文字母长度为一个字节;阿拉伯语 中字符长度为两个字节;汉字长度则为三个字节; 2.使用指针操作在char数组中存储的UTF8字符串时,需要根据存储的数据正确的增加和减少,确保指针始终指向字符的开始位置。
- 使用标准库函数判断字符属性,使应用能够支持多区域
- 对字符串进行本地化排序、比较和查找时使用有区域参数的locale::collate<>函数
- 保持资源的语义完整性,不要拼接字符串资源
- 避免使用本地时间直接进行时间计算, 应当基于UTC时间来计算避免夏令时跳变 带来的影响。同样,不同时区的本地时间之间的计算,同样也必须转换为UTC时间后才可进行。