应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止,以此避免构造(和析构)非必要对象,还可以避免无意义的default构造行为
对于循环操作,在循环前还是中进行构造,取决于赋值操作与构造+析构操作的成本对比。循环前构造会将变量的作用域扩大,除非知道该变量的赋值成本比“构造+析构”成本低,或者对这段程序的效率要求非常高,否则建议使用循环中构造
const_cast:通常被用来将对象的常量性转除
dynamic_cast:主要用来执行“安全向下转型”,要用于基类和派生类之间的类型转换,具有类型检查的功能,比static_cast安全。运行时转型,但开销大,会执行对继承体系的检查
reinterpret_cast:意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植
static_cast:用来强迫隐式转换,编译时进行类型转换,主要用于基本数据类型的转换、隐式转换的显式化和向上转型
尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。
如果有个设计需要转型动作,通常两种做法,一:使用容器,并在其中存储直接指向derived class对象的指针(通常是智能指针),这样就避免了上述需求。二:在base class内提供virtual函数做你想对各个派生类想做的事情。这样可以使得你通过base class 接口处理“所有可能之各种派生类”。
如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内
宁可使用C++新式转型,也不用用C的旧式,因为新式的更容易被注意到,而且各自用途专一
reference、指针、迭代器系统都是所谓的handles。函数返回一个handle,随之而来的便是“降低对象封装性”的风险。它也可能导致:虽调用const成员函数却造成对象状态被更改的风险。
Point& UpperLeft() const { return pData->ulhc; }
Point& LowerRight() const { return pData->lrhc; }
成员变量可以在外部被修改,违反 logical constness 的原则的。绝对不应该令成员函数返回一个指针指向“访问级别较低”的成员函数。
改成返回常引用可以避免对成员变量的修改
const Point& UpperLeft() const { return pData->ulhc; }
const Point& LowerRight() const { return pData->lrhc; }
但是依然会带来一个称作 dangling handles(空悬句柄) 的问题,当对象不复存在时,你将无法通过引用获取到返回的数据
最保守的做法,返回一个成员变量的副本
Point UpperLeft() const { return pData->ulhc; }
Point LowerRight() const { return pData->lrhc; }
异常安全函数(Exception-safe functions)提供以下三个保证之一:
int DoSomething() noexcept;
当异常抛出时,1.不泄漏任何资源 2.不允许数据败坏;
void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
unlock(&mutex);
}
若在new Image函数中抛出异常,mutex会发生资源泄漏,未执行unlock,bgImage和imageChanges也会发生数据败坏,bgImage指向已删除对象
为做到强烈保证:
void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
Lock m1(&mutex);
bgImage.reset(std::make_shared<Image>(imgSrc));
++imageChanges;
}
删除动作只发生在新图像创建成功后,只有make_shared成功,reset才调用,delete也只在reset函数内使用
另一个常用于提供强烈保证的方法是copy and swap,为打算修改的对象做出一份副本,对副本执行修改,并在所有修改都成功执行后,用一个不会抛出异常的swap方法将原件和副本交换
void PrettyMenu::ChangeBackground(std::vector<uint8_t>& imgSrc) {
Lock m1(&mutex);
auto pNew = std::make_shared<PMImpl>(*pImpl); // 获取副本
pNew->bgImage.reset(std::make_shared<Image>(imgSrc));
++pNew->imageChanges;
std::swap(pImpl, pNew);
}
强烈保证并非对所有函数都可实现或具备现实意义
异常安全保证具有木桶效应
inlining函数可以免除函数调用成本的开销
inlining两种实现方式:一种是为其显式指定inline关键字,另一种是直接将成员函数的定义式写在类中
inline函数通常一定被置于头文件内,因为inlining大部分情况下都是编译期行为;template通常也被置于头文件内,因为大部分建置环境都是在编译期完成具现化动作;如果一个template具现出来的函数都应该inlined,则将此template声明为inline,否则应避免此声明
inline只是对编译器的一个申请,不是强制命令。大多数编译器如无法将要求的函数inline化,会给出一个警告信息
将大多数inlining限制在小型、被频繁调用的函数身上,以便于日后的调试和二进制升级
编译器通常不对“通过函数指针而进行的调用”实施inlining
构造函数和析构函数并不适合用于inlining,往往会引起代码的膨胀(即不要随便将构造函数和析构函数的定义体放在类声明中)
inline函数代码如发生改变,所有用到该inline函数的程序都必须重新编译
inline修饰符用于解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题
在现在的 C++ 标准中,inline作为优化建议的含义已经被完全抛弃,取而代之的是“允许函数在不同编译单元中多重定义”,使得可以在头文件中直接给出函数的实现(每个.cpp的编译都是独立的,对于每个cpp来说,都是包含了Func的声明和实现,所以在链接时不清楚到底是链接哪一个同名函数)
头文件里面定义全局变量或实现了具体的函数会报错“fatal error LNK1169: 找到一个或多个多重定义的符号”,类、结构体、函数声明等抽象的东西不会
为了增加编译速度,应该减少类文件之间的相互依存性(include),但是类内又常常使用到其他类,不得不相互依存
解决方案是:将类的声明和定义分开(不同的头文件),声明相互依存,而定义不相依存,这样当定义需要变更时,编译时不需要再因为依赖而全部编译。即尽可能让头文件自我满足
两种方法:
Handle classes/句柄类:一个声明类,一个imp实现类,声明类中不涉及具体的定义,只有接口声明,在定义类中include声明类,而不是继承。handle类的成员函数必须通过imp指针取得对象数据,每次访问会增加一层间接性会增加开销。且imp指针必须初始化在handle类的构造函数内,指向一个动态分配得来的imp对象,指针大小和动态分配内存也会带来开销
Interface classes/接口类:在接口类中提供纯虚函数,作为一个抽象基类,定义类作为其子类来实现具体的定义。增加存储虚表指针和实现虚函数跳转带来的开销