More Effective C++ 读书笔记
More Effective C++ 读书笔记
01 区分pointer和reference
使用reference的好处是:
- 没有null reference,这意味着不需要进行空校验。
- 可以避免歧义:比如operatr[]返回指针需要进行就地修改时,必须解引用,这个解引用就会造成歧义。
1
2vector<int> v(10);
*v[5] = 10; // 这看起来v好像是一个指针形成的vecotr
03 不要以多态处理数组
我们知道指针和引用可以表现动态多态,但如果是以值传递方式,将派生类数组赋值给一个基类数组,就会出现对象截断现象。
1 | class BST{...}; |
我们知道array是个指针,array[i]表示array+i,计算的是i*sizeof(对象)。你给编译器声明的数组元素是BST,实际给编译器的是BBST,这两者大小不一致时
将出现未定义行为。
同样地,如果你删除数组时,会出现下列调用
1 | void deleteBSTArray(BST array[]) |
04 非必要不提供默认构造函数
什么时候需要默认构造函数?
- 产生局部对象数组时
- 模板类中产生局部对象数组时
05 对定制的“类型转换函数”保持警觉
警惕隐式类型转换,这包括:
- 单自变量构造函数
- 类型转换操作符。
所谓类型转换操作符:关键词operator之后加上类型名称。取代类型转换操作符,替代以普通成员函数asFloat等。
单自变量构造函数可以通过添加关键字explicit避免隐式类型转换。
还有一个解决办法称之为:嵌套类
利用规则:编译器不能做两次及以上的类型转换。构造中间类,将构造函数的变量替换为中间类。
08 了解不同意义的new和delete
区分 new operator 和 operator new
new operator就是最常见的new操作符号,比如
1 | string *ps = new string("Memmory Managenmt"); |
它分为两步:
- 分配内存
- 调用类的构造函数
其中分配内存的函数名称为:operator new。你可以重载该函数,但第一个参数必须为size_t。1
void* operator new(size_t size);
new和delte要成对使用
1 | void* buffer = operator new(50*sizeof(char)); |
10 在构造函数内阻止资源泄露
C++只会析构已经构造完成的对象。
为什么呢?当构造进行到一半时,对象处于一种中间态,如果此时调用析构函数,将无法正确释放资源。
因此,构造期间抛出异常的对象,必须设计一种方法,使得它们抛出一场时能够自我清理。
效率
80-20 法则
使用lazy evaluation
- 引用计数
- 区分读写
- lazy fetching 缓式取出
- lazy expression evaluation 表达式缓评估
- 分期瘫还 over-eager evaluation
19 了解临时对象的来源
什么是临时对象?
1 | template<class T> |
以上示例代码中,tmp虽然在意义上是“临时对象“,但是在编译器眼中,它只是函数局部对象。
C++所谓临时对象是不可见的,它不会在你的源代码中出现。只要你产生了一个non-heap object而没有为它命名,便产生一个临时对象。
通常有两种情况:
- 隐式类型转换
- 函数返回对象时
C++禁止为non-const reference 对象产生临时对象,原因是non-const修改的是临时变量,修改不会起作用。
20 协助返回值优化
当有些函数硬是要返回对象时,采用 constructor arguments 取代对象,可以让编译器消除临时对象的成本。
1 | const Rational& operator*(cosnt Rational& lhs, const Rational& rhs) |
26 限制class能产生的对象数量
允许0个或1个对象
【单例模式】阻止class产出对象最简单的办法就是将其构造函数声明为private。
从语法上看,namespcae就像一个class,只不过没有public等访问控制符,里面都是public的。
static
(1)初始化顺序
- “class”拥有一个static对象,即使从未被使用,他也会被构造以及析构。
- 函数拥有一个static对象,此对象在函数内第一次调用时才产生。
cpp对于不同编译单元内的static对象的初始化顺序没有任何说明,因此使用该方法时需要特别注意。
(2)static 与 inline
1 | Printer& thePrinter() |
如果将此函数声明为inline,会发生什么?
如果是非成员函数,它可能有内部连接。而带有内部连接的函数,可能会在程序中被复制。所以你的程序可能带有多份local static对象的副本。
限定产生K个对象
- 构造函数私有,包括拷贝构造
- 添加static类成员计数、最大类数量常数
- 创建工厂函数,调用构造函数,构造函数对数量进行验证
自动模版
1 | template<class BeingCounted> |
模版使用:
- private继承Counted,模版参数为自己的类(奇异递归模版模式)
- 采用using声明符,使得private的Counted函数成为public
有了这个模版,Printer就无需自己计数,就好像有人专门为它服务似的。
1 | class Printer: private Counted<Printer>{ |
27 禁止对象产生于heap之中
参考:https://blog.desirer233.fun/2025/05/25/C++/约定对象创建限制/
30 代理类
实现二维数组
1 | Template<class T> |
由于我们无法重载两个方括号,因此我们采用代理类的设计方式,重载operator[],令其返回1维数组的代理,然后再重载代理类的operator[]。
1 | Template<calss T> |
Array2D 的用户不需要知道Array1D的存在,用户好像是在使用真正的二维数组。
区分operator[]的读写
遗憾的是,编译器无法告诉我们针对operator[]的动作是读还是写,因此我们必须悲观地假设 non-const opeartor[]均为写动作。
1 | // COW String类设计 |
但是,我们可以利用代理类的设计方式,为字符设计代理,从而解决这个问题。
1 | class String{ |
Const operator[]返回的是const proxy,而 const proxy并不能作为赋值的对象。所有的“写”动作都将移动到CharProxy的赋值操作符中。
1 | String::CahrProxy& String::CharProxy::operator=(const CharProxy& rhs) |
31 让函数根据一个以上的对象类型来决定虚化
参考:https://www.cnblogs.com/reasno/p/4872147.html
1 | class SpaceShip; // 前置声明 |
看起来有些像递归调用,其实并非这样,在该函数内部,*this实际上已经对应该函数的动态类型,因此otherObject调用的将不再是collide(GameObject& otherObject)
,而是collide(SpaceShip& otherObject)
。
这种方法不需要使用RTTI,但却有和RTTI一样的缺点:一旦有新的class假如,代码就必须修改——含入一个新的虚函数,这涉及到类定义得修改,然而修改类定义会引起包含这些类定义的文件的重新编译,在很多情况下成本相当大.
More Effective C++ 读书笔记
https://xyz.desirer233.fun/2025/07/13/C++/more effecitve C++ 读书笔记/