您的位置:首页 > 编程语言 > C语言/C++

More effective C++读书心得

2008-01-08 20:10 309 查看
今天学习了More effective C++的基础议题部分,运算符部分,异常部分。
学习总结如下:
基础议题部分:
3.1 M1:指针与引用的区别
首先,要认识到在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。
(不存在指向空值的引用意味着使用引用的代码效率比使用指针的要高)
其次,指针可以被重新赋值以指向另一个不同的对象。
什么情况下应该使用指针?
1存在不指向任何对象的可能。
2能够在不同时刻指向不同对象

什么情况下应该使用引用?
1总是指向一个对象并且不会改变指向
2重载某个操作符时

3.2 M2 尽量使用C++风格的类型转换
为什么不使用C风格?
1过于粗鲁,允许在任何类型之间进行转换
2C风格的类型转换在程序语句中难以识别

C++新引进的风格
1 static_cast 在功能上基本上与C风格的类型转换一样强大,也有功能上的限制。但是不能从表达式中去除const属性
2 const_cast 用于类型转换掉表达式的const或者volatileness属性。最普通的用途是转换掉对象的const属性
3 dynamic_cast 用于安全的沿着类的继承关系向下进行类型转换。即把指向基类的指针或引用转换成指向其他派生类或者其兄弟类的指针或引用。失败的转换将返回空指针或者抛出异常
4 reinterpret_cast 转换结果都是执行期定义,最普通的用途就是在函数指针类型之间进行转换。转换函数指针的代码是不可移植的

3.3 M3 不要对数组使用多态
通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的,指针和多态不能混在一起用,数组和多态也不能混在一起用。

3.4 M4 避免无用的缺省构造函数
在三种情况下,没有缺省构造函数是不能工作的:
1建立数组时 A a【10】//错,没有缺省构造函数可供调用
2无法在许多基于模板的容器类里使用,因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数
3虚基类一般需要提供缺省构造函数

运算符部分:
4.1 M5 谨慎定义类型转换函数
两种函数允许编译器进行隐式转换:单参数构造函数和隐式类型转换运算符。
什么是单参数构造函数?
单参数构造函数可以是只定义了一个参数,也可以是虽然定义了多个参数但第一个参数以后的所有参数都有缺省值。
隐式类型转换运算符的格式如下:operator关键字 +类型符号 例如
operator double()const
隐式类型可以转换为显式转换函数,虽然不方便,可以避免错误。即通过不声明运算符operator的方法。例如:转换成:double asDouble() const
对于单参数构造函数,可以利用最新编译器的特性,explicit关键字。构造函数如果用explicit声明,编译器会拒绝为了隐式类型转换而调用构造函数

4.2M6自增自减操作符前缀与后缀形式的区别
1C++规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0作为int参数的值
2这些操作符前缀与后缀返回值类型不同。前缀形式返回一个引用,后缀形式返回一个const类型。

4.3 M7不要重载”&&”,”|”| “,”

4.4 M8理解各种不同含义的new和delete
共有三种不同形式的new
1 new 操作符 new operator string *ps = new string(“Memory Management”)
第一 分配足够的内存以便容纳所需类型的对象
第二 调用构造函数初始化内存中的对象

2 new操作 operator new 是new操作符为分配内存所调用函数的名字
所以operator函数只是用来分配内存,而且对构造函数一无所知。
void *operator new (size_t size)
For example:void *ramMemory = operator new(sizeof(string));

3 placement new
特殊的operator new,,在已经被分配但是尚未处理的内存构造一个对象
Placement new返回指向该内存的指针,因为operator new 的目的是为对象分配内存然后返回指向该内存的指针。
Void *operatore new(size_t size,void *location)
{
Return location.`
}
new(buffer) Widget(widgetSize)

如何选取使用这三种不同类型的new
1 如果想在堆上建立一个对象,选用new操作符。既分配内存又为对象调用构造函数
2 如果仅仅分配内存,就调用operator new函数,不会调用构造函数
3 如果在已经获得指针的内存里建立一个对象,应该用placement new

Delete:operator delete与delete operator
Operator new 与new operator
Delete ps:调用析构函数并释放对象占有的内存
Operator delete(buffer)仅仅释放资源到内存

异常部分:
5.1 M9使用析构函数防止资源泄漏
使用auto_ptr,每一个auto_ptr类的构造函数里面,让一个指针指向一个堆对象,并且在析构函数里删除这个对象。
隐藏在auto_ptr后的思想是:用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。
template <class T>
class auto_prt{
public:
auto_ptr(T *p=0): ptr(p) {}
~auto_ptr(){delete ptr;}
private:
T *ptr;
};

5.2M10在构造函数中防止资源泄漏
C++只能删除被完全构造的对象,只有一个对象的构造函数完全运行完毕,这个对象才能被完全的构造
如果类中含有指针的话,把指针类型改称为auto_ptr类型可以避免问题,因为这个时候对象已经初始化完毕,如果资源分配异常,即可调用析构函数。
在对象构造函数中,处理各种抛出异常的可能,采用auto_ptr,可以化繁为简
Auto_ptr特点:指向动态分配的对象的指针,当指针消失的时候,对象也应该被删除

5.3M11禁止异常信息传递到析构函数
原因如下
第一, 能够在异常转递的堆栈辗转开解(stack-unwinding)过程中,防止terminate被调用。
第二, 能确保析构函数总能完成我们希望它做的事情

5.4 M12 理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
相同点:可以传值,传递引用或者传递指针
不同点:1函数调用,程序的控制权最终还会返回到函数的调用处
抛出异常,控制权永远不会回到抛出异常的地方。
2抛出异常的速度比参数传递慢。因为不论通过传值或传递引用,都将进行强制异常对象copy操作。当异常对象被copy时,拷贝操作是对象的静态类型所对应的拷贝构造函数。而对象作为参数传递给函数时不一定需要copy.
3异常抛出比参数传递转换类型要少,仅支持两种类型:1继承类与基类的转换 2允许从一个类型化指针转变成无类型指针.
4 catch子句进行异常类型匹配的顺序即为他们在代码中出现的顺序,第一个类型匹配成功的catch将用来被执行

5.5 Item13通过引用(reference)捕获异常
因为通过使用引用捕获异常,可以避开是否删除异常对象而烦恼,能够避开slicing异常对象,能够捕获标准异常类型,减少异常对象需要被拷贝的数目。

5.6Item 14 谨慎使用异常规格(exception specification
异常规格明确地描述一个函数可以抛出什么样的异常。For example: void f2() throw(int)
5.7Item15了解异常处理的开销

6效率
6.1 Item M16 牢记80-20准则
80─20准则说的是大约20%的代码使用了80%的程序资源,即软件整体的性能取决于代码组成中的一小部分。使用profiler来确定程序中的那20%,关注那些局部效率能够被极大提高的地方。
6.2 Item M17 考虑使用LAZY EVALUATION(懒惰计算法)
采用此种方法的类将推迟计算工作直到系统需要这些计算的结果。
引用计数:除非确实需要,不去为任何东西制作拷贝。
区分对待读取与写入:operator[]因为读取很容易,然而写入这个string则需要在写入前做一个copy
可以用代理类来实现
懒惰存取:Lazy Fetch 每次不从磁盘上读取所有数据。
懒惰表达式计算
6.3 Item M18 分期摊还期望的计算
过度热情计算法(over-eager evaluation),如果一个计算需要频繁进行,可以设计一个数据结构高效的处理这些计算需求。
6.4 Item M19 理解临时对象的来源
这里的临时对象是看不见的,不出现在源代码中。通常两种条件下会产生:
1为了使函数成功调用而进行的隐式类型转换
2 函数返回对象时
在任何时候只要见到常量引用(reference to const)参数,就存在建立临时对象而绑定在参数上的可能性。
在任何时候只要见到函数返回对象,就会有一个临时对象被建立
6.5 Item M20 协助完成返回值优化
消除传值返回的对象的努力不会获得胜利,所以我们只能减小对象开销
1直接返回constructor argument而不出现局部对象
2通过声明函数为inline来消除operator调用开销
6.6 Item M21通过重载避免隐式类型转换
因为编译器完成这种隐式转换是有开销的,所以希望避免这种隐式的转换
6.7 Item M22 考虑用运算符的赋值形式(op=)取代其单独形式(op
这是因为赋值形式的效率要高:
1operator的赋值形式比单独形式效率更高,因为单独形式要返回一个新对象,存在构造和释放的开销
2提供operator赋值形式的同时也要提供其标准形式,允许类的客户端选择
3不能在operator+里使用返回值优化
6.8 Item M23 考虑变更程序库
具有相同功能的不同程序库在性能上采取不同的权衡措施,所以一旦找到瓶颈,可考虑通过变更程序库提高效率
6.9 Item M24 理解虚拟函数,多继承,虚基类和RTTI所需的代价
虚拟函数,大多数编译器的实现是通过virtual table和virtual table pointers,分别被称为vtbl和vptr
vtbl是一个函数指针数组,程序中的每个类只要声明了虚函数或继承了虚函数,就有自己的vtbl,vtbl的项目是指向虚函数实现体的指针
vtpr:每个声明了虚函数的对象都带有vptr,它是一个看不见的数据成员,指向对应类的virtual table
所以虚拟函数有两个代价:
1必须为每个包含虚函数的类的virtual table留出空间
2在每个包含虚函数的对象里,必须为额外的指针付出代价
3放弃使用了内联函数
多继承:
单个对象里有多个vptr(每个基类对应一个),但在多继承里,为寻找vptr而进行的计算更为复杂
RTTI:因为运行时找到对象和类的相关信息,所以type_info对象存储了这些,通过typeid操作符访问类的type_info对象

7 技巧

7.1 Item M25将构造函数和非成员函数虚拟化
一般来说,构造函数是不能虚拟的,这是因为对象此时还没有生成不能确定对象类型,不可能进行动态绑定。
这里所指的虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象,然而实际上它并不是真正的构造函数。
类的虚拟拷贝构造函数只是调用它们真正的构造函数。
虚拟非成员函数的实现,是通过编写一个虚拟函数完成功能。然后写一个非虚拟函数什么也不做,只调用这个虚拟函数。
7.2 Item M26 限制某个类所能产生的对象数量
单个对象情况:将构造函数声明为private,再声明一个静态函数,并含有一个函数的静态成员。我们不把静态成员放在类里有两个原因:
1类中的静态对象总被构造和释放,即使不不用该对象。函数中的静态对象只有在第一次执行函数时才被执行
2类中的静态成员初始化时间不一样
缺点:带有private构造函数的类不能作为基类使用,也不能嵌入到其他对象中
限制个对象的情况:使用具有对象计数功能的基类

限制个数的对象:
可以在类中声明一个静态的变量 static size_t numObjects来记录该类的实例个数。如果大于一定的个数,运行构造函数返回失败。

最好的方法引入具有计数功能的类

7.3 Item M27 要求或禁止在堆中产生对象
要求在堆中建立对象:(因为非堆对象在定义它的地方自动构造,在生存时间结束时自动被释放,因此禁止使用隐式的构造函数和析构函数就可以实现)
构造函数设为public,析构函数设为protected。再加一个伪析构函数
For example: void destroy () const {delete this;},每次删除的时候显式调用。
判断一个对象是否在堆中:由此我们可以想到的是程序的地址空间作为线性地址管理,程序的栈从地址空间的顶部向下扩展,堆则从底部向上扩展。但我们如果根据地址的高低来判断是否对象在堆中,是不行的,因为静态对象(包括static对象,包含在全局和命名空间里的对象)处于堆的底部,所以我们无法区分是静态对象还是堆对象。
由此问题可以转化为可以转化为判断是否能够删除一个指针,将new中得到的地址加入list中,通过查找可判断是否能删除指针。
A *a = new A (on the heap)
A b (not on the heap)
把一个指针dynamic_cast成void*类型(或const void*或volatile void*等),生成的指针将指向“原指针指向对象内存”的开始处。但是dynamic_cast只能用于“指向至少具有一个虚拟函数的对象”的指针上。
禁止堆对象 可以将operator new声明为私有成员函数,这样 new就不能被调用,但是这样做导致了,派生类对象的基类实例化就会失败。
所以最终解决方案:判断如果在堆中,则抛出一个异常
7.4 Item M28 灵巧指针
灵巧指针从模板中生成,因为要与内建指针类似,必须是strongly type(强类型)的。
当auto_ptr被拷贝和赋值时,对象所有权随之被传递,所以要使用传引用而不是传值的方法
Deference实现 * -> 返回类型必须是引用,如果返回了一个基类对象而不是一个派生类对象的引用,会导致slicing问题
测试灵巧指针是否为NULL:共2种方法:
1 提供隐式类型操作符,用于这种目的类型转换的是void *
2 在灵巧指针中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true.

除非有让人信服的原因,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。
灵巧指针和继承类到基类的类型转换
使用成员函数模板来实现dumb指针到灵巧指针的类型转换。
operator SmartPtr<newType>()
{
return SmartPtr<newType>(pointee);
}
7.5 Item M29 引用计数
引用计数是基于对象通常共享相同的值假设的优化技巧。
写时copy:与其他对象共享一个值直到写操作时才拥有自己的copy.Lazy原则特例
使用带有引用计数的基类

7.6 Item M30 代理类
代理类通常扮演其它对象的对象,通过代理类可以实现
1二维数组,Array1D扮演一维数组,是代理类
2区分通过operator[]进行的是读操作还是写操作。通过修改operator[]让他返回一个proxy对象而不是字符本身
如果对左值来说,编译器努力的寻找一个隐式类型转换
如果对右值来说,赋值操作被调用
3限制隐式类型转换 by class ArraySize
7.7 Item M31 让函数根据一个以上的对象来决定怎么虚拟

8杂项
8.1 Item M32 在未来时态下开发程序
提供完备的类,即使某些部分现在还没有被使用
将接口设计得便于常见操作并防止常见错误(E46),阻止拷贝构造和赋值操作如果类不需要(E27),防止部分赋值(M33)
如果没有限制不能通用化代码,就通用它

8.2Item M33 将非尾端类设计为抽象类
处理外来的类库时,可能需要违背这个规则;但对能够控制的代码,遵守它可以提啊高程序的可靠性,健壮性,可读性,可扩展性
8.3Item M34 如何在同一程序中混合使用C++和C
名变换:C++编译器给每个函数换一个独一无二的名字,因为C++引入了函数重载,但是C并不需要函数重载,如果要在C++里使用C函数,就要禁止名变换,使用C++的extern “C”指示。
Extern “C”可以对一组函数生效
静态初始化:在main执行前后都有大量的代码被执行,尤其是静态的类对象和定义在全局的、命名空间中的或文件中的类对象的构造函数通常在main被执行前调用(E47)。对应的析构函数在main结束运行之后。如果main不是C++写的,那么这些对象从来没对初始化和析构。
动态内存分配:总用delete释放new分配的内存,总用free释放malloc分配的内存
数据结构的兼容性:在C++和C之间相互传递数据结构是安全的,C++和C提供同样的编译。C++除了增加非虚成员函数不影响兼容性外,都影响兼容。

8.4 Item M35 让自己习惯使用标准的C++语言
标准C++中STL中有三个概念:1包容器(Container) 2选择子(iterator)3算法(algorithm)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: