本文章属于专栏《业界Cpp进阶建议整理》
继续上一篇《More Effective C++》- 极精简版 11-20条。本章我会继续讲解我对21-30条的极精简的理解。
- 21、利用重载技术避免隐式转换
- const A operator+(const A& a1, const A& a2) ,且有构造函数 A(int){}
- 那 A a3 = 2 + a2;时,会先将2隐式转换为A类型的临时变量,然后调用函数。
- 这里可以通过增加 const A operator+(int a1, const A& a2)来防止隐式转换产生临时变量
- 但是注意,重载操作符,至少获取一个用户定制类型,不能是const A operator+(int a1, int a2)
- 个人见解:在业务代码中,尽量使用explicit不要让隐氏转换发生。
- 22、使用操作符,考虑用复合形式(op=)替换独身形式(op)
- 如 +=是在自身对象上操作,没有临时对象的构造和析构。所以尽量用+=
- 当然有时候为了代码的可读性,使用 A a1 = a2 + a3 + a4 + a5;也是可以的,在于这段代码的使用频率,是不是性能优化的重点
- 另外为了,代码逻辑方便维护,实际加法逻辑只有一份, operator+ 调用operator+=是一个很有意义的方法。
- 23、考虑使用其他程序库
- 个人见解:在用三方库时,不受惯性思想,搜一下当前时间点,性能最好,最稳定的库
- 24、了解virtual functions、multiple inheritance、virtual base classes、runtime type identification成本
- 虚函数成本:
- 虚函数表:每个包含虚函数的类都有一个虚函数表,表中存的是指向虚函数的地址,虚函数表的大小和虚函数个数相关。
- 即使是多继承、或者virtual继承,虚函数本身的开销不会更大,反而是它不能inline关系更大
- 虚函数放弃了inline(两个语义本身就冲突,虚函数运行时调用,inline编译期用函数体替换)
- 虚函数调用就是,通过指针或者引用,找到对象的虚函数表指针,然后找到需要调用的虚函数表中的虚函数指针,执行函数。
- 虚函数指针:每个对象都有一个虚函数指针
- RTTI成本:
- 作用:运行时,获取objects和classes的信息。
- 原理:存放在每个对象的type_info结构中,这个结构由编译器生成 。一个class只需要一份RTTI,这个结构在很多编译器的实现中,放到了虚函数表中
- 虚函数成本:
- 25、虚拟化构造函数和非成员函数
- 将std::vector<Base*>的内容复制到新的vector(这里是复制内容,不是指针),不能直接调用拷贝构造函数,因为不知道具体是哪个class,这里需要一个clone函数。如基类是Base* clone() {return *this} 派生类Derive* clone() {return *this}
- 非成员函数虚拟化,是为了多态
- 26、限制class能产生的数量
- 生产0个:将构造函数放到private
- 生产1个:写一个返回对象的static函数(有static成员的函数不能用inline,每个调用函数的地方,都可能有自己的static成员变量),函数中有一个static成员
- 生产多个:class有自己的计数器,有自己的工厂函数,有一个自己的static成员,进行计数。
- 用一个template基类来做(注意一定要用模板基类,static计数,被所有派生类公用)
- 27、要求、禁止对象产生于heap中
- 要求对象必须产生在heap中,将构造函数和析构函数放到protect(private中,会影响继承)
- 但是这样的话,派生类可以生成在栈上,其基类部分自然也在栈上
- 想要派生类也保证只在heap上声明,利用的是operator delete,让其只删除通过operator new出来的东西,覆盖operator new(让其只从堆申请内存,并存放到类的list结构中),而delete时,去判断delete的内容是否来自于堆。
- 这样的做法,在编译期间没有办法发现,需要在运行时候,通过异常发现(由delete抛出)
- 个人见解:这样的需求,对于业务代码来说,有些纸上谈兵,实际中要求对象必须生产在堆中或者不在堆中,由开发者来保证是十分容易的。这样的设计模式增加的维护代价,收益却不明显。
- 28、智能指针
- 构造、析构和赋值容易,注意赋值需要判断是否是自己。operator()和operator->一个返回实际对象,一个返回实际指针
- 判断存的内容是否为nullptr,调用智能指针的.get(),
- 与继承的关系,
- shared_ptr<Base> 和shared_ptr<Derived>不是同一类型,也不能自动转换,所以当函数参数要<Base>时,传<Derived>是不能通过编译的。
- 对于自己写的函数,可以考虑template,这样会对不同类型的智能指针,产生实例
- 对于历史的,没有办法改变的函数,考虑做一个拷贝 <Base> b = d,然后传入
- shared_ptr<Base> 和shared_ptr<Derived>不是同一类型,也不能自动转换,所以当函数参数要<Base>时,传<Derived>是不能通过编译的。
- 与const的关系
- 虽然const与非const不是一个类型,但是shared_ptr<Base> b; shared_ptr<const Base> cb = b;是可以的,shared_ptr已经内部实现了
- 个人补充:多线程对同一个智能指针对象操作是不安全的,但是智能指针对象通过复制进入各个线程,则是线程安全的
- 29、引用计数
- 什么时候使用引用计数
- 引用技术是有成本的,当管理的对象越小,其相对来说代价越大。只有当引用技术带来的构造和析构的成本小时,不成为性能瓶颈,且控制数据的申请释放比较困难,我们才更愿意使用(在有限的范围内,数量不是巨大的时候,引用技术是很有意义的)
- 什么时候使用引用计数
- 30、代理类
- 通过代理类+类型转换,区分operator[]的读和写。以在读和写时进行不同的操作。如智能指针。下面是一个简单的区分原理代码
#include <iostream>// 原始数据类
class Data {
private:int value;public:Data(int val) : value(val) {}// 代理类class Proxy {private:Data& data;public:Proxy(Data& d) : data(d) {}// 读操作operator int() const {std::cout << "Reading value: " << data.value << std::endl;return data.value;}// 写操作Proxy& operator=(int newVal) {std::cout << "Writing value: " << newVal << std::endl;data.value = newVal;return *this;}};// 返回代理类对象Proxy operator[](int index) {// 可以在这里添加索引范围检查等逻辑return Proxy(*this);}
};int main() {Data dataObj(42);// 通过代理类进行读操作int readValue = dataObj[0];std::cout << "Read result: " << readValue << std::endl;// 通过代理类进行写操作dataObj[0] = 100;return 0;
}//运行结果
//Reading value: 42
//Read result: 42
//Writing value: 100