More effective c++ 总结

来源:互联网 发布:淘宝采集软件多少钱 编辑:程序博客网 时间:2024/06/03 22:46

Item M1:指针与引用的区别

引用必须被初始化,且不能改变它本身使其成为另一个变量的别名。

Item M2:尽量使用C++风格的类型转换
这样的类型转换不论是对人工还是对程序都很容易识别。它们允许编译器检测出原来不能发现的错误。
你能用dynamic_cast把指向基类的指针或引用转换成指向其派生类或其兄弟类的指针或引用,而且你能知道转换是否成功。失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。dynamic_cast只能在有继承关系的类之间进行转换。
reinterpret_cast的最普通的用途就是在函数指针类型之间进行转换。
reinterpret_cast<type>(expression) 这个expression必须是指针或引用!!!

Item M3:不要对数组使用多态
因为派生类的大小一般不与基类大小相同,而数组的存储方式在内存中是连续的。 所以一般Base[i]与Derive[i]并不是指向相同的位置。
语言规范中说通过一个基类指针来删除一个含有派生类对象的数组,结果将是不确定的。
多态和指针算法不能混合在一起来用,所以数组与多态也不能用在一起。

Item M4:避免无用的缺省构造函数
提供无意义的缺省构造函数也会影响类的工作效率。因为缺省构造函数一般不能保证所有的成员被正确的初始化,所以在其它成员函数里面都要对数据成员进行判断,以确保进行了有效的初始化。会使可执行程序变大。
但如果不使用缺省构造函数时:
建立对象数组的时候,会有问题。
它们无法在许多基于模板(template-based)的容器类里使用。因为实例化一个模板时,模板的类型参数应该提供一个缺省构造函数,这是一个常见的要求。

Item M5:谨慎定义类型转换函数
有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符。
class Rational {
public:
...
operator double() const; // 转换Rational类成double类型
};
Rational r(1, 2);
cout<<r;
如果没有重载<<运算符,也不会提示错误。因为r可以用operator double()转为浮点型。
单参构造函数造成的隐式转换的问题甚至比隐式类型转换运算符的问题都大。
不声明运算符(operator)的方法可以克服隐式类型转换运算符的缺点。通过给单参构造函数加explicit关键字,可以解决隐式类型转换问题。

Item M6:自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别
后缀自增自减都返回const对象,如果不是const对象,则会允许i++++;的语句出现。不允许这样做!!!

Item M7:不要重载“&&”,“||”, 或“,”
C++使用布尔表达式短路求值法. 如if (p!=null && p++) p为null时后面的p++就不会计算
如果用operator &&重载了&&,就会用函数调用法替代了短路求值法
if (expression1 && expression2) 就会变为:
if (expression1.operator&&(expression2)) //operaotr &&为成员函数时
if (operator&&(expression1, expression2)) //operaotr &&为全局函数时
它会运算它所有的参数,而且运算的顺序不能确定。
一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。
重载逗号运算符也没办法保证计算次序

Item M8:理解各种不同含义的new和delete
new操作符(new operator)与new操作(operator new)的区别
string *ps = new string("Memory Management");
你使用的new是new操作符。这个操作符就象sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。
delete操作符(delete operator)与new操作符相反,先调用析构函数,然后再删除内存。
new操作符为分配内存所调用函数的名字是operator new。所以可以通过重载new操作(operator new)来更改new操作符申请内存的行为。
void * operator new(size_t size);
使用:
void *buffer = operator new(50);    //50为size大小
operator delete(buffer);

placement new (一种特殊的operator new)
用来为一些已经分配,但是尚未处理的(raw)内存中构造对象。
定义:
void * operator new(size_t, void *location)
{
    return location;
}
使用:
new (buffer) Widget(widgetSize);  //buffer是已经申请好的内存空间地址
placement new是标准C++库的一部分,为了使用placement new,你必须使用语句#include <new>。
operator new[]和operator delete[] 也可以被重载,用来操作对象数组。

调用相应的new,就要使用相对应的delete,不要混用。

Item M9:使用析构函数防止资源泄漏
应该对操纵局部资源的指针说再见。让智能指针来代替。(使用指针指针简洁,对产生异常的情况也适用)

Item M10:在构造函数中防止资源泄漏
当在构造函数执行的时候有异常发生,则已经分配的内存空间无法释放。因为这时候不会去执行析构函数。
可以用异常处理,在构造函数里面捕获到异常之后,在catch块里面对内存进行释放。
成员指针变量用智能指针auto_ptr来代替。当对象消失的时候,会被自动调用。

Item M11:禁止异常信息(exceptions)传递到析构函数外
禁止异常传递到析构函数外有两个原因,第一能够在异常转递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。 (如果异常从析构函数中传递到外面的话,terminate会被自动调用,彻底终止你的程序)
一般通过在析构函数中用try catch来捕获它本身产生的任何异常。
(堆栈辗转开解:也即堆栈退栈。如果某个函数中发生了异常,并执行了throw来抛出异常对象,则压在栈中的对象都会被自动析构,直到碰到catch。)

Item M12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
传递函数参数与异常的途径均可以是传值、传递引用或传递指针,这是相同的。当你传递参数和异常时,系统所要完成的操作过程则是完全不同的。产生这个差异的原因是:你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。
在参数中传引用的时候,引用指向实际的对象。而异常传递的时候,不管是通过传值还是传引用,都会进行对象的拷贝操作。(因为产生异常后,会使原来的异常对象离开生存空间,其析构函数也会被调用。)即使异常对象是静态的,不会被释放,也会进行拷贝。
因为异常总是会复制对象,所以抛出异常运行速度稍慢一些。
对象拷贝的时候,会使用静态类型拷贝构造函数,而不是动态类型的。
如:
class Base { ... };
class Derive: public Base { ... };
Derive d;
Base &b = d;
在throw b的时候,只会调用Base的拷贝构造。
另一个区别是:在函数调用中不允许转递一个临时对象到一个非const引用类型的参数里,但是在异常中却被允许。
通过指针来捕获异常的话,异常对象必须是全局或堆中的!因为指针传递异常时,只有指针的拷贝被传递。
函数调用时,可能有隐式的类型转换。但在捕获异常的时候,不会有类型转换。 如catch(double d),并不会捕获产生的int型异常。(不过有继承类与基类间可能转换。如捕获基类的异常可以捕获派生类类型的异常。 还有,允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void* 指针的catch子句能捕获任何类型的指针类型异常)
catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的catch将被用来执行。当一个对象调用一个虚拟函数时,被选择的函数位于与对象类型匹配最佳的类里。

Item M13:通过引用(reference)捕获异常
四个标准的异常:
bad_alloc: 当operator new不能分配足够的内存时,被抛出
bad_cast:  当dynamic_cast针对一个引用(reference)操作失败时,被抛出
bad_typeid: 当dynamic_cast对空指针进行操作时,被抛出
bad_exception: 用于unexpected异常
通过指针方式捕获异常,处理完异常之后,无法确定是否要删除这个异常对象。(因为 如果是全局对象,则不用删除,如果是堆对象,则要删除。)
通过传值捕获异常,当它们被抛出时系统将对异常对象拷贝两次。而且会有slicing problem,即即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。
通过引用捕获异常,不会有对象删除的问题。没有slicing problem (可以通过父类引用调用虚函数,实现多态)。异常对象只被拷贝一次。(所以尽量选用“引用捕获异常”)
(引用是除指针外另一个可以产生多态效果的手段。)

Item M14:审慎使用异常规格(exception specifications)
如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数unexpected将被自动地调用。unexpected缺省的行为是调用函数terminate。
void f1(); //没有声明异常规格,可以抛出任意的异常
void f2() throw(int); //抛出int型的异常
void f2() throw(int)
{
   f1();   //f1抛出的异常不是int,但是合法
}
但是,其抛出的异常与发出调用的函数的异常规格不一致,并且这样的调用可能导致你的程序执行被终止

Item M15:了解异常处理的系统开销
为了在运行时处理异常,程序要记录大量的信息。

Item M16:牢记80-20准则(80-20 rule)
大约20%的代码使用了80%的程序资源。
尽量使用工具来找出系统的瓶径,如profiler,而不要人为观察或凭直觉判断。

 

 

Item M17:考虑使用lazy evaluation(懒惰计算法)

如:String s1 = "Hello";

String s2 = s1;

或许s2在程序中根本不会用到,所以在开始的时候不要调用s2的赋值构造函数来初始化,到后面要用,或者要改写s2的值的时候再调用,再进行赋值。

Item M18:分期摊还期望的计算

比如要计算某组数据的平均值。 我们可以在每一个新数值加入到程序里面的时候就去计算新的平均值。这样,当我们要取这个值的时候,可以立刻取出来。

Item M19:理解临时对象的来源

1、传值参数(pass-by-value parameter) 如:void fun(string s); 在用string对象传参数的时候就会产生临时对象。

2、返回值(return value)

3、隐式类型转换(Implicitly Typecast)

4、++ 和 --

解决方法:

1、避免使用传值参数,尽量使用传指针或传引用。

2、隐式类型转换的问题是定义了针对某个类型的构造函数,但是没有定义针对该类型的operator=所导致的,所以,构造函数和operator=必须成对出现。

3、尽量使用++i代替i++

4、利用编译器的优化

Item M20:协助完成返回值优化

(返回值优化)

一些函数(operator*也在其中)必须要返回对象。返回constructor argument而不是直接返回对象,能让编译器消除临时对象的开销。

Item M21:通过重载避免隐式类型转换

没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。

因为隐式类型转换可能导致额外的开销。 如,

Cls b;

Cls a = 1 + b;

当用1转为Cls类型时,就会调用Cls的隐式转换函数。这样就会有开销。

Item M22:考虑用运算符的赋值形式(op=)取代其单独形式(op)

op=的时候,不会产生临时对象,它将结果写到左边的参数里面。

Item M23:考虑变更程序库

如,有时候可以用stdio,特点是小,且速度快

有时候可用iostream,特点是类型安全,且可扩展

Item M24:理解虚拟函数、多继承、虚基类和RTTI所需的代价

Item M25:将构造函数和非成员函数虚拟化

虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。虚拟拷贝构造函数。

利用特性:被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。

虚拟构造函数并不是与类名相同的函数,而是单独定义的一个函数,这个函数返回本类类型的对象指针,这个指针是个父类指针。在子类里面实现的时候,这个返回的指针是子类类型的指针。

就象构造函数不能真的成为虚拟函数一样,非成员函数也不能成为真正的虚拟函数。

它的实现方法:将某个成员函数声明为虚函数,然后在非成员函数里面通过父类调用这个虚函数,这样就可以根据传入的参数不同,而产生不同的行为了。

Item M26:限制某个类所能产生的对象数量

如使用单件模式,则可以限制类实例化个数为1。

将构造函数声明为private,可以阻止类建立对象。可以加入一个友元函数,来进行建立对象,以返回类对象指针,或类对象引用。

可以在类中加入静态的计数器,在构造函数中加1,析构函数中减1,则可以灵活的将把个类的实例限制在固定数目之内。

或可以单独创建一个计数器类,要计数的类都从这个类里继承。

Item M27:要求或禁止在堆中产生对象

 要求在堆中建立对象: 可以通过将构造或析构函数声明为private来实现。一般将析构设为private或protected,这样,就无法自动释放对象,栈上的对象就会出错。

 判断一个对象是否在堆中: 可以通过重载operator new操作符,在里面加上标识,如果调用了operator new,则将标志置位。不过这种方法不适合于创建数组,创建数组的时候只调用一次operator new (不过如果编译器支持重载operator new[]的话,也可以重载这个函数)。  或可以通过堆是向上生长,栈是向下生长。通过比较两个局部变量等,可以得知目前是在栈上还是堆上。

不过因为存在静态变量,所以无法区分是在堆上还是在静态变量区。

静态变量包括static声明的,在全局域定义的,在命名空间定义的。

“判断是否能够删除一个指针”比“判断一个指针指向的事物是否在堆上”要容易。C++使用一种抽象mixin基类满足了我们的需要。它通过将所有new返回的指针放入list中来实现。

const void *rawAddress = dynamic_cast<const void*>(this); //dynamic_cast可以将this指针变为一个指向当前对象起始地址的指针。在多继承或虚继承的时候,这个是非常有用的。

禁止堆对象: 将operator new与operator delete声明为private的。

Item M28:灵巧(smart)指针

Item M29: 引用计数

有时候多个对象的值完全相同,这时候就可以让它们共享一份数据,以节约空间。

如几个string对象指向同一个"hello"

当某个string被改写时,要将该对象与其它对象分离开来了。

写时拷贝:与其它对象共享一个值直到写操作时才拥有自己的拷贝。

也可以使用带引用计数的基类。

Item M30:代理类

扮演其它对象的对象通常被称为代理类.(如通过代理类来实现多维数组)

如stl中的vector<bool>特化版本,就使用了代理类。

因为vector<bool>作了优化,每个元素占用一个bit,所以对某个元素赋值true或false时,无法直接对它赋值,这时候就要使用代理类,先把值赋给代理类,然后代理类再操作具体的bit位。

Item M31:让函数根据一个以上的对象来决定怎么虚拟

比如a、b、c、d几个元素(类型为a_t)相互结合会产生不同的结果。如果用虚函数的话,则要在虚函数中对类的实际类型进行判断,然后根据不同的判断来调用不同的代码,而且当添加一个新类的时候,要更改大量代码,全部重新编译,而且完全不具备可维护性。

一个比较有效的解决办法就是使用非成员的碰撞处理函数。

即:map<pair<a_t, a_t>, fun>

当添加新的元素检测时,就在全局map表中添加。 维护起来相对比较容易,且可以动态的进行操作。

Item M32:在未来时态下开发程序

适应变化。

尽量用C++语言自己来表达设计上的约束条件,而不是用注释或文档。

处理每个类的赋值和拷贝构造函数,即使“从没人这样做过”。他们现在没有这么做并不意味着他们以后不这么做。

最小惊讶法则:努力提供这样的类,它们的操作和函数有自然的语法和直观的语义。和内建数据类型的行为保持一致:拿不定主意时,仿照int来做。

只要是能被人做的,就有人这么做。只有程序有可能出错,就一定会出错。(莫菲法则)

努力于可移植的代码。只有在性能极其重要时采用不可移植的结构才是可取的。

将你的代码设计得当需要变化时,影响是局部的。尽可能地封装;将实现细节申明为私有

避免导致虚基类的设计,因为这种类需要每个派生类都直接初始化它--即使是那些间接派生类。

未来时态考虑的约束:

·提供完备的类,即使某些部分现在还没有被使用。如果有了新的需求,你不用回过头去改它们。

·将你的接口设计得便于常见操作并防止常见错误。使得类容易正确使用而不易用错。

·如果没有限制你不能通用化你的代码,那么通用化它。

Item M33:将非尾端类设计为抽象类

将基类的析构函数设为纯虚析构函数是很常见的。 要想使基类不能被实例化,类中必须要包含纯虚函数,将析构函数设为纯虚的,是非常常用的方法。但是此时,必须要给纯虚析构添加实现,因为它们在派生类析构函数被调用时也将被调用。

如果基类没有数据成员,则将它设计为抽象类,因为没有数据的实体类是没有用处的。

不管怎么说啦,需要通过公有继承将两个实体类联系起来,通常表示需要一个新的抽象类。 即,如果从c1继承出c2,这时候应该这样考虑:

设计一个abstractC,然后使c1和c2都从里面继承出来。

在处理外来的类库时,你可能需要违背这个规则;但对于你能控制的代码,遵守它可以提高程序的可靠性、健壮性、可读性、可扩展性。

Item M34:如何在同一程序中混合使用C++和C

Item M35:让自己习惯使用标准C++语言

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/ciahi/archive/2009/07/05/4322704.aspx