每天学习亿点点day 6.21- 7.2 More effective C++ 总结

1. 要区分指针和引用

引用不能为空

2. 多使用C++ style的cast

可读性好

static_cast 无法转换基础类型到struct 也不能移除constness

const_cast 去除const或者volatileness

dynamic_cast 专门用来转换类的层级,失败了就返回null, 引导层级

不能用于缺乏虚函数的类

reinterpret_cast 最常用的是函数指针之间的转换, 慎重使用

3. 永远不要多态地处理数组

最好不要再用实体类继承另一个实体类

 

数组多态不要搞的原因是 数组的取值运算是在基础运算符上加sizeof(class 数组元素类型)*i的,如果你把派生类给它,很明显就 移动的距离依旧是按照旧的方案,就很怪.... 

4. 避免白送的默认构造函数

placement new

new (ptr)A(); ptr就是A()执行完初始化之后将对象放置的空间的首地址

 

这种方法需要先调用析构函数后调用delete[] 来收回raw memory. 要用delete删除new出来的指针,这个也是要配对的

没有默认构造函数的类会和很多template不兼容

 

虚基类如果没有默认的构造函数的话,会在之后子类里每次初始化都要明白并且指定参数很麻烦

 

5. 小心用户定义的强转重载函数

 

单参数构造函数

Call conversion unintended

一个做法是把强转重载符号改为等价的函数即可

one single parameter 的构造函数会把基础类型隐式转换到一个类的实例

解决one parameter 的问题方法: explicit 不准隐式调用

 

隐式转换不会超过一段,利用这个特性。int-》sizeofClass-》class instance

6. 区分++p和p++

为啥要返回一个const object,不让你连++++

不连++的原因是不符合内置类型的规范,实际上原始object也只加了一次

前置++的效率更高

 

7. 永远不要重载 &&    ||     ,

逗号运算符 ,

求解过程是:先求解表达式 1,再求解表达式 2。整个逗号表达式的值是表达式 2 的值。

最右边的那个表达式的值将作为整个逗号表达式的值,其他表达式的值会被丢弃。

例如:

var = (count=19, incr=10, count+1);

在这里,首先把 count 赋值为 19,把 incr 赋值为 10,然后把 count 加 1,最后,把最右边表达式 count+1 的计算结果 20 赋给 var。上面表达式中的括号是必需的,因为逗号运算符的优先级低于赋值操作符。

 

所有参数都不得不算

不要重载因为会破坏很好的一些特性

 

 

 

8. 理解不同的new和delete的意义

operator new 只知道分配空间并不知道 constructor

replacement new

就是在已经分配好的地址释放object

new (buffer) Widget(widgetSize)

new operator 分配空间和调用constructor

delete operator

 

而operator new 只分配空间

operator delete

 

place new 先显式析构再回收内存. 直接delete只会把object的内存回收而整个内存并不一定会被回收

 

无 new [] 符号支持的编译器里还是不要弄自定义内存管理,因为全局new只有一个,你重写了就不和任何其他库兼容了

 

 

9. 使用析构函数来防止资源泄露

异常抛出会略过后面的所有代码,所以delete操作也略过造成内存泄露.

智能指针

把资源封存在object里面 可以帮助你避免资源泄露

 

10. 使用构造函数防止资源泄露

构造函数没调用完怎么办?用智能指针呗

C++ 保证删除空指针是okay的

C++只会销毁那些 已经调用完构造函数的object

智能指针在异常处理时候的内存泄漏问题里起到了很好的作用

 

11. 不要让异常抛出离开析构函数

从析构函数里不要抛出异常,因为你在析构里抛出的时候,本身这个析构函数在调用的时候就有可能正在进行异常处理这个时候程序会被terminate

避免terminate,让析构函数做完它该做的事情

 

12. 理解清楚抛出异常和传递参数以及调用虚函数的区别

function 会将控制转到调用处

但是throw 不会回到调用处

 

throw永远都是 传一个拷贝,保证原始数据不会被改

 

拷贝总是基于你的静态类型

 

throw是只扔出接收到的异常,不会改变异常的类型

而throw w必然是要拷贝一个东西

 

1.总会被至少复制一次

2.严格按照类型来,不会进行类型转换

3.catch是按照出现顺序一个一个检验的。只要catch到了就会跳过之后所有的代码

基类可以捕捉派生类

 

13. 使用引用Catch异常

把派生类对象传值给一个基类参数会导致切割问题,即就算是把派生类对象传给基类,也会把派生类的特征切除掉

 

pointer传参 会出现指针指向一个已经被删除的对象的时候

 

14. 明智地使用异常说明

未识别的异常类型通常会导致terminate, terminate会导致栈上内容无法被清除

 

unexpected exception 如果你在调用的函数中直接throw,实际上是会被bad_exception调用

 

1. 如果违反specification 会出现 unexpected error 就会terminate

2. 而且会部分被编译器检查,会出现一些作者不想要的行为

 

15. 理解异常处理的开销

 

try block的入口和出口

然后还有相关联的catch句子以及捕捉类型

还有必要的比较抛出的异常和我们的specification是否符合

specification 也生成代码

3个数量级慢

了解这个cost但不要把数字看的太重

 

16. 记住2-8定律

 

1.80的计算资源给20代码用

2.80的内存给20代码用

3.80的硬盘access给20的代码用

4.80的维护成本给20的代码用

要用最多的数据来测,不然你的优化没啥代表性

 

17. 考虑使用惰性评估

 

不到需要结果的时候我就是不算

不到你确实需要一份拷贝的时候就不要去拷贝

延迟判断是否是读取还是写入

 

1.减少不必要的copy

2.区分读和写

3.减少不必要数据库读取

4.避免不必要数学计算

 

 

18. 摊销预期计算的开销

new 操作要调用系统的函数很慢

欲计算

cache

vector增长两倍 为了少调用operator new

 

19. 理解临时对象的来源

分清local variable 和temporary:未命名 object

来源:隐式形转

return object

 

引用const变量

返回object都会产生temporary object

 

20. 促进返回值优化

返回值优化实际上就是把意图 更简化处理

 

21. 重载以避免隐式类型转换

多重载来避免隐式转换会减少很多负担

 

22. 考虑使用op=而不是单独的op的使用

好写性能好

不容易带来construct和destruct的问题

 

23. 考虑备选库

 

每个库都有自己侧重的好处,要学会甄别并挑选

 

24. 了解虚函数的成本,多重继承,虚基类,和RTTI

先从基类虚函数中覆盖,然后才是本类的虚函数

每个object文件里留一份vtbl,linker把多余的剥除

vtbl放在第一个定义了非内联,非纯虚函数的文件里

不要把虚函数声明为内联,这样的话就增大了每个文件里存一个备份的可能,也就白白浪费了太多空间

虚函数表指针放在哪里是由compiler确定的,放在哪里并不一定,这也是一笔开销

动态绑定的机制:

1.根据指向的object的vptr找到vtbl

2.找到对应名字的调用函数的指针

3.调用函数

 

RTTI就是在运行时告诉我们object和类的信息的,其实就是反射机制

存在type_info的object里面,type_info使用typeid operator

在拥有virtual函数的类中寻找

RTTI就是关于vtbl而设计的:

直接在vtbl里面放一个type_info

object

 

 

总结:

1.线性继承自一个虚基类的时候虚表指针一直都是白嫖虚基类的,每次继承都会让当前类的虚函数和虚基类的虚函数进行名字对比,有相同的就覆盖。直到形成一个完整的表

2.当同时继承两个及以上的虚基类的时候,每个虚基类都依次进行1所说的线性继承,所有的虚基类都排列在此派生类的内部,所以当有共同起点的类进行派生时,会有函数重复的问题.即虚基类有两份,很不划算。

3.当有虚继承的时候,唯一的不同就是会新增一个虚基指针,在虚基里保留虚表指针,这里的继承方法和1中的线性方法一样合并加覆盖,只不过是因为有同一个虚基类,所以合并相同虚基使得虚基永远只有一份

4.有一点需要记住的是,继承下来并覆盖的虚函数其实只是某个名字被覆盖了,而不是本身那个函数不在了,那个函数永远都在,你甚至可以在之后用BASE::f()调用他,只不过这个时候它有点被埋没的感觉.

25. 虚拟化构造函数和非成员函数

简而言之这里虚构造函数更多的是意义上的,也就是依据输入来创建不同类型的object

最近才采用的一个规则就是,重写基类的虚函数的时候不需要再返回同样的type了,比如基类返回基类型的指针时,派生类可以返回派生类的指针

 

思想就是 写一个 代表性功能函数,参数是基类指针或者引用,然后利用类的虚函数的动态绑定的特性实现一个非成员函数也能做到的动态绑定意义上的功能函数

 

26. 限制一个类的实例个数

 

定义在函数里静态比在类里好,不用的时候永远都不需要创建

 

而作为静态成员在类里存在会导致绝对的定义开销

 

而且定义顺序上没有函数那么明确

 

inline不要使用,因为会导致多份static object

 

内部链接讲道理就是 只在一个文件里可见的东西

 

独立个体,

作为基类,

作为成员变量。

为了解决这个计数不方便的问题可以把构造函数私有化

 

如果你想在类内部定义初始化的值,可以试一下匿名enum

 

在派生类中使用using语句可以改变基类部分函数的accessibility

如果编译器不支持namespace也可以不加using这句

 

instance counting最好还是写一个类比较好,然后之后所有的object私有继承即可。

只允许一个实例:函数静态局部变量或者类的静态变量

 

27. 请求或禁止堆上的实例对象

限制构造函数和析构函数的权限确实可以防止创建非堆上object,但是也会阻碍继承和封装

dynamic_cast<const void*> 还可以将指针转换到指向的物体的起始位置

 

总结一下:

判断是不是heap上的object

1.利用operator new和构造函数里面再用个bool的标志位来控制,直接从contruct进来创建对象是不成功的,而用new则可行

2. 利用地址比较。因为heap上的物体是从低地址长到高地址的,所以一般都是要比stack上的object地址要低很多,但是static这种object会导致错误

3.利用地址簿记录每次new出来的地址,之后看是不是onheap只需要从里面查找即可

new符号和delete符号都是会继承的,也就是说你基类如果new delete都私有化了,你new派生而来的类的时候就会调用基类的私有化的new和delete

 

同时如果把基类当成员变量的话,就不会这样了.

 

28. 智能指针

自动化构建和析构

复制和赋值

解引用慵懒策略

 

用object来进行开始和结束登录比函数要好用,因为自动调用自动清楚使用的内存,非常好

智能指针会被modified在赋值或者复制的时候

隐式转换智能指针很傻慎用

 

1.类函数模板有个弊端就是不太好移植

2.需要很深刻的理解

 

无法像普通指针那样实现基于继承层级的类型转换

 

29. 引用计数

referrence ability移到基类

封装到智能指针里面

智能指针就可以自动操作引用计数

 

如果是已有的class则创建一个类似于string value的中间类内含指针指向widget类

 

使用引用计数的时候:

1. 单个资源被很多object使用

2. 这个资源创建销毁很昂贵,比如说D3d里面的指向渲染设备资源的类实例指针

 

 

30. 代理类

 

一些功能类里需要代理类来完成功能,而这些代理类通常又是不对外的

代理类可以帮你:

1. 多元数组

2.左右值使用区分 (重载赋值符号作为左值,重载隐式转换作为右值)

3.抑制隐式转换

 

劣势:消耗代价

会改变本身的这个使用代理类的类的语义

 

31. 使函数对多个对象均为虚函数

 

Typeid对于基指针指向派生类之后处理有不同:

虚基类指针指向派生类对象之后其Typeid(*Base)指明的又是派生类,但是基类指针不是虚基的话,指明的还是基类

 

multiple dispatch实现方法思路:

1.利用RTTI来分辨类型

2.模仿虚函数表来把所有可能的函数指针组织成一个map

3.用名字来区分函数而不要用虚函数

4.最好是把这种函数功能从class里面剥离出来

 

 

32. 用将来时编程

 

适应新平台

提前应对未来的需求

 

1.尽可能把类写全比如拷贝构造函数

2.设计接口使得常用方法变快,并防止常见错误

3.泛化你的代码

 

 

33. 使非叶子类抽象

一个使用abstrac class的好时机就是你发现一个concrete类要继承另一个concrete类的时候

除非是在使用外部库否则还是最好遵守把叶子类做成abstract类的觉悟吧带来的好处有:

可靠性,鲁棒性,可读性,可扩展性

 

34. 理解如何结合C++和C在相同的程序里

 

1. 确保编译器产出可兼容的实例文件

2. 把两种语言都要使用的函数宣成extern "C"

C++因为有一个重载的机制,所以在定义完成函数之后,编译的时候实际上会把某些函数的名字改掉,所以打个比方如果你在C++里写了一个函数,然后在C里面调用,就会出现linker不匹配,因为C的linker根本找不到那个函数.

要用extern "c" 关键字.

_cplusplus实际山是一个整型值来确定当前是在被哪个版本的C++编译

 

3. 把main写在C++里面

C++ 有很多东西是可以在main之前和之后运行的,所以如果你的main程序是写在C里面,C的程序所调用的C++里的静态object可能并没有初始化完成,调用会造成麻烦的后果.

4. new delete,free和malloc一定要成对使用

5. 两种语言之间传递的数据结构最好还是依照C编译环境下的,除了非虚函数能兼容,其他的都会让struct不太方便传递

 

35.  要自己多熟悉语言标准

 

新特征: RTTI, 命名空间,mutable和explicit关键字,重载enum的运算符,类定义的时候初始化constant static整形成员变量

 

类成员函数模板已被允许,记号性可强制模板实例化,无类型参数可用于函数模板,类模板本身也可以用作模板参数

 

模板声明现在检查更严格了,unexpected函数可以抛出bad_exception object

 

operator new[] 和 operator delete[] 已被添加,而且他们也会抛出异常,并且有返回0类型的new 符号可供选择.

 

新增了各种cast

 

在定义虚函数不需要返回值匹配了,临时对象的生命周期已经被精确定义了

 

standard library:

支持C标准库

支持内置string

支持本地化

支持I/O

支持数值型计算

支持泛型容器和算法,全是模板

 重载->运算符的时候要么返回一个也已经重载过->的对象或者引用,要么返回一个指针,因为->重载之后会在符号调用的原位置继续进行->操作,所以你不用担心返回一个指针接下来应该怎么做.

posted @ 2021-06-21 10:43  Tonarinototoro  阅读(51)  评论(0编辑  收藏  举报