每天学习亿点点day 10, 14, 15:Effective C++快速总结

 2021.6.11 全部施工完毕

1. 尽量用const和inline而不用#define: 尽量用编译器而不用预处理

主要是因为错误难回溯, 定义的符号名根本就不会加入符号列表

 

2. 尽量用iostream而不是stdio.h

主要是用的printf和scanf不是类型安全的 (出错也不告诉你)

 

3. 尽量用new和delete而不用malloc和free

原因是malloc和free太简单,根本就不涉及调用析构函数,这里会造成严重问题

 

4. 尽量使用C++风格的注释

 

5. 对应的new和delete要采用相同的形式

简而言之就是你new了一个单体你就得delete一个单体,如果你new了一个数组,你delete的时候就得delete一个数组

 

6. 要在析构函数里对指针成员调用delete

 

7. 预先准备内存不够的情况

你在任何需要new新的内存的情况下都必须要考虑到内存不够的情况下你该如何处理,要么catch错误输出错误信息方便debug,要么在不够的情况下准备好能够及时调度内存的情况.

 

8. 写operator new和 operator delete的时候要遵循常规

自己重写的时候要提供和系统缺省的operator new一致的行为,要有正确返回值,出问题的及时Catch, 对一些奇怪的要求比如要求new 0字节的内容的时候要正确处理 (当一个字节来处理).

 

9. 避免因为运算符重载隐藏标准形式的new, 这样会使得标准new无法使用,避免这种情况发生就使用函数重载

 

10. 如果重写了new就要重写 delete

其中有个之前一直没注意到的知识点就是new一块内存的时候不仅会得到一块可用内存,而且会使用一小块空间存储该调用内存的大小, 这种小空间会造成内存浪费,所以使用内存池是一个好做法, 就一开始申请很大一片地,需要的时候就给一个引用,不需要的时候就收回.

内存泄漏实际上意味着指向某块内存的指针信息已经丢失了,已经无法轻易进行内存释放.

 

11. 为需要动态分配内存的类声明一个拷贝函数构造函数和一个赋值操作符

没有在类中定义拷贝构造函数,如果有人直接将两个实例进行赋值(会对对象实例里的指针进行逐位拷贝)或者进行拷贝构造(因为拷贝构造函数没有定义,所以系统会默认进行赋值操作),可能有内存泄漏,还会有生存空间的问题.

 

12. 尽量使用初始化而不要在构造函数里赋值

因为指针成员对象在进行拷贝和赋值的时候可能会引起指针混乱(M11)

const和引用数据成员只能使用初始化列表

其次就是效率高,一个对象实例在创建的时候第一步是数据成员初始化,然后才是执行构造函数。先初始化列表,然后才是构造函数。

如果使用构造函数,那么首先对象会被缺省构造一次, 然后使用重载的赋值符号进行赋值, 也就是进行了两次.

 

13. 初始化列表中成员列出顺序和它们在类中声明的顺序相同

template<class T>
class Array {
public:
Array(int lowBound, int highBound);
...
private:
vector<T> data; // 数组数据存储在 vector 对象中
// 关于 vector 模板参见条款 49
size_t size; // 数组中元素的数量
int lBound, hBound; // 下限,上限
};
template<class T>
Array<T>::Array(int lowBound, int highBound)
: size(highBound - lowBound + 1),
lBound(lowBound), hBound(highBound),
data(size)
初始化的时候C++会先看data,再看size所以你size还没准备好就初始化data准会出问题.

 

14. 确定基类有虚析构函数

通过基类的指针去删除派生类的对象的时候,而基类又没有虚析构函数的时候,结果不可预测.

你要做基类你就得做好析构函数为虚的准备,如果不打算继承,就不要包含虚析构函数,bad idea. 虚指针带来的额外空间存储不利于移植.

当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数.

纯虚析构函数必须要提供定义,因为派生类的对象进行析构的时候是必然会层层析构到抽象类的头上的,这时候定义就必须要给出了.

这里纯虚析构函数什么也不做也不太好,所以让它inline就可以避免函数调用的开销。但是inline函数的地址必须要进到vtb里面,可是inline函数的地址又不好找,所以需要找到地址.

 

15. 让operator=返回*this引用

有人喜欢返回void,但这样会妨碍链式操作.

有人喜欢const返回值,但这个时候你就无法给const对象赋值了. 又妨碍了链式赋值

const int&p 不能给int &m赋值. 因为会破坏权限. p是无法改变引用对象的值的,但是m又能改变了,这样很不安全.

结论是:定义自己的赋值运算符的时候,必须返回赋值运算符的左边参数的引用,*this. 如果不这么做就会导致不能连续赋值,或者导致调用时的隐式转换不能进行,或两种情况都会发生.

 

16. 在operator=中对所有数据成员赋值

因为派生类的赋值运算符也必须处理基类成员的赋值

 

17. 在operator=中检查给自己赋值的情况

主要是为了防止别名出现

1.高效率节省大量时间资源,可以理解返回

2.赋值运算符必须首先释放掉一个对象的资源,然后根据新值分配新的资源

还有确认两个对象是一样的方法有:

确定值是不是一样,或者确定内存空间位置是不是一样,或使用唯一标识符

 

18. 争取使类的接口完整并且最小

少,独立,全面,精炼

太多接口会打击用户学习的积极性

不同名却功能一样会很烦人

难以维护

 

19. 分清成员函数,非成员函数和友元函数

成员函数和非成员函数最大的区别就在于成员函数可以是虚拟的,而非成员函数不行

explicit构造函数不能用于隐式转换

能避免使用友元就避免

>> 和<<决不能是成员函数,因为成员函数是固定了调用的顺序的.

 

20. 避免public接口出现数据成员

功能分离

别干扰我的函数功能.

 

21. 尽可能使用const

返回常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。

const成员函数的目的是为了指明那个成员函数可以在const对象上被调用.

bitwise constness 数据意义上的const,不允许修改数据

conceptual constness 概念上的const,可以修改数据只要用户不发现

mutable关键字可以让const失效

也可以使用const_cast来把一个const T *const p转化为T* const p

 

你在强转const to 非const的时候要确定,前面的const指的对象真的不是const,不然会出问题的.

 

22. 尽量用传引用而不是传值

防止切割问题:

比如说你派生类的对象给基类赋值,基类实例只能得到基类对象的功能和数据.

 

在返回对象的size很小的时候比如一个char,bool的时候没必要用引用,因为引用本身也是指针实现的,所以还是用传值划得来

 

23. 必须要返回一个对象的时候不要试图返回一个引用

当需要返回引用和返回对象间做决定的时候,选能正确完成功能的.

 

24. 在函数重载和设定参数缺省值间慎重选择

如果可以选择一个合适的缺省值并且只是用到一种算法,就是用缺省参数,否则就使用函数重载.

在重载函数中调用一个公共底层功能函数很好用。

 

25. 避免对指针和数字类型重载

因为你

F(int);

F(String*);

二义性炸了

 

26. 当心潜在的二义性

当一个需要A类型参数的函数被给予B类型实参的时候,要么就构造类A的构造函数使用B为参数构建一个,要么就调用B的转换运算符,把B转换成一个A对象,这里就会存在二义性,到底是用哪个呢?

改变一个类成员的访问权限不应该改变程序的含义

 

27. 如果不想使用隐式生成的函数就要显示地禁止它

比如讨厌的赋值函数,如果你不写系统帮你自动赋值,所以你不想用的时候就把它声明一下,然后放在private里面,不去实现它因为玩意友元函数调用了也麻烦

这个条例适合所有编译自动帮你生成的函数

拷贝构造函数!说的就是你!

 

28. 划分全局名字空间

namespace sdm

{

/////////////

}

不要和别人撞车

当然也可以用struct代替,虽然效果没那么好就是了

 

29. 避免返回内部数据的句柄

内部数据生命周期短啊,你返回了句柄可用却不能用的情况很多的

 

30. 避免这样的成员函数: 其返回值是指向成员的非const指针或引用, 但是成员的访问级比这个函数低

你又把自己的内部东西暴露给别人了!!!!

你硬是要返回一个访问级低的成员的话,就返回一个const T &;

 

31. 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用

生命周期的问题

还有就是你函数内部new了再又要delete就很烦人

 

32. 尽可能地推迟变量的定义

如果是一个很复杂的类的实例,价格太昂贵了不自然没必要

 

推迟变量定义可以提⾼程序的效率,增强程序的条理性,还可以减少对变
量含义的注释。看来是该和那些开放式模块的变量定义吻别了。

 

 

33. 明智地使用内联

增加体积

代码膨胀会导致不合理的页面调度行为,慢的一笔

降低指令高速缓存的命中率

读取指令的速度降低

未被内联的内联函数将会被编译器声明为static

程序库的设计者必须预先估计到声明内联函数带来的负⾯影响。因为想对
程序库中的内联函数进⾏⼆进制代码升级是不可能的。

内联函数中的静态对象常常表现出违反直觉的⾏为。所以,如果函数中包
含静态对象,通常要避免将它声明为内联函数。

 

34. 将文件间的编译依赖性降至最低

一般是在类的私有部分再增加一个指针指向数据部分这样就可以减少编译的依赖性

只要有可能,尽量让
头⽂件不要依赖于别的⽂件;如果不可能,就借助于类的声明,不要依靠类的
定义。其它⼀切⽅法都源于这⼀简单的设计思想

不要在头⽂件中再(通过#include 指令)包含其它头⽂件,除⾮缺少了它
们就不能编译。相反,要⼀个⼀个地声明所需要的类,让使⽤这个头⽂件的⽤
户⾃⼰(通过#include 指令)去包含其它的头⽂件,以使⽤户代码最终得以通
过编译。

 

Person 类仅仅⽤⼀个指针来指向某个不确定的实现,这样的类常常被称为
句柄类(Handle class)或信封类(Envelope class)。

协议类(Protocol class) 。根据定义,协议类没有实现;它存在的⽬的是为派
⽣类确定⼀个接⼝(参⻅条款 36) 。所以,它⼀般没有数据成员,没有构造函
数;有⼀个虚析构函数 (⻅条款 14) , 还有⼀套纯虚函数, ⽤于制定接⼝。

35. 使公有继承体现"是一个"的含义

D继承自B,D是一个B而B不见得是一个D

 

36. 区分接口继承和实现继承

纯虚函数只继承接口

虚函数继承接口和缺省实现

普通函数接口和强制实现

 

37. 绝不要重新定义继承而来的非虚函数

非虚函数是静态绑定的,也就是原先是什么类型就只能调用该类型的非虚函数

 

38. 绝不要重新定义继承而来的缺省参数值

因为,如上所述,虚函数是动态绑定的,但缺省参数是静态绑定
的。这意味着你最终可能调⽤的是⼀个定义在派⽣类,但使⽤了基类中的缺省
参数值的虚函数

 

39. 避免"向下转换"继承层次,不要把父类指针强转为子类指针

正如上⾯已经看到的,"向下转换" 可以通过⼏种⽅法来消除。最好的⽅法
是将这种转换⽤虚函数调⽤来代替,同时,它可能对有些类不适⽤,所以要使
这些类的每个虚函数成为⼀个空操作。第⼆个⽅法是加强类型约束,使得指针
的声明类型和你所知道的真的指针类型之间没有出⼊。为了消除向下转换,⽆
论费多⼤⼯夫都是值得的,因为向下转换难看、容易导致错误,⽽且使得代码
难于理解、升级和维护

 

40. 通过分层来体现"有一个"或"用...来实现"

使某个类的对象成为另⼀个类的数据成员,从⽽实现将⼀个类构筑在另⼀
个类之上,这⼀过程称为 "分层"(Layering)

 

41. 区分继承和模板

当对象的类型不影响类中函数的⾏为时,就要使⽤模板来⽣成这样⼀组类
当对象的类型影响类中函数的⾏为时,就要使⽤继承来得到这样⼀组类

 

42. 明智地使用私有继承

私有继承在软件 "设计" 过程中毫⽆意义,只是在软件 "
实现" 时才有⽤。

尽可能地使⽤分层,必须时才使⽤私有继承。

意义是用什么实现什么

 

43. 明智地使用多继承

共同的基类意味着共同的特性。如果类 D1 和类 D2 都把类 B 声明为基类,
D1 和 D2 将从 B 继承共同的数据成员和/或共同的成员函数。⻅条款 43。
公有继承意味着 "是⼀个"。如果类 D 公有继承于类 B,类型 D 的每⼀个对
象也是⼀个类型 B 的对象,但反过来不成⽴。⻅条款 35。
私有继承意味着 "⽤...来实现"。如果类 D 私有继承于类 B,类型 D 的对象
只不过是⽤类型 B 的对象来实现⽽已;类型 B 和类型 D 的对象之间不存在概念
上的关系。⻅条款 42。
分层意味着 "有⼀个" 或 "⽤...来实现"。如果类 A 包含⼀个类型 B 的数据
成员,类型 A 的对象要么具有⼀个类型为 B 的部件,要么在实现中使⽤了类型
B 的对象。

 

44. 说你想说的,理解你所说的

 

45. 弄清C++在幕后为你所写,所调用的函数

有声明下列函数,体贴的编译器会声明它⾃⼰的版本。这些函数是:⼀个拷⻉
构造函数,⼀个赋值运算符,⼀个析构函数,⼀对取址运算符。另外,如果你
没有声明任何构造函数,它也将为你声明⼀个缺省构造函数。所有这些函数都
是公有的。

只是能用来创建和销毁对象

 

46. 宁可编译和链接时出错,也不要运行时出错

C++不太管运行时错误

 

47. 确保非局部静态对象在使用前被初始化

用函数内部局部静态变量返回,一是如果不调用就不会初始化也就不会有开销。

减少初始化依赖性

 

48. 重视编译器警告

 

49. 熟悉标准库

 

50. 提高对C++的认识

 

 

 

 

posted @ 2021-05-29 18:56  Tonarinototoro  阅读(81)  评论(0编辑  收藏  举报