条款1:尽量用const和inline而不用#define
1.为方便调试,最好使用常量。
注意:常量定义一般放在头文件中,可将指针和指针所指的类型都定义成const,如const char * const authorName = “Scott Meyers”;
类中常量通常定义为静态成员, 而且需要先声明后定义。可以在声明时或定义时赋值,也可使用借用enum的方法。如enum{Num = 5};
2.#define语句造成的问题。
如#define max(a, b) ((a) > (b) ? (a) : (b))
在下面情况下:
Int a= 5, b = 0;
max(++ a, b);
max(++ a, b + 10);
max内部发生些什么取决于它比较的是什么值。解决方法是使用inline函数,可以使用template来产生一个函数集。
条款2:尽量用而不用
用>> 和<<使得编译器自己可以根据不同的变量类型选择操作符的不同形式,而采取的语法形式相同。
条款3:尽量用new和delete而不用malloc和free
使用malloc和free的时候不会自己调用构造函数和析构函数,因此如果对象自己分配了内存的话,那么这些内存会全部丢失。另外,将new和malloc混用会导致不可预测的后果。
条款4:尽量使用C++风格的注释
C++的注释可以在注释里还有注释,所以注释掉一个代码块不用删除这段代码的注释。C则不行。
条款5:对应的new和delete要采用相同的形式
调 用new时用了[],调用delete时也要用 []。如果调用new时没有用[],那调用delete时也不要用[]。对于typedef来说,用new创建了一个typedef定义的类型的对象后, delete时必须根据typedef定义的类型来删除。因此,为了避免混乱,最好杜绝数组类型用typedef。
条款6:析构函数里对指针成员调用delete
删除空指针是安全的,因此在析构函数里可以简单的delete类的指针成员,而不用担心他们是否被new过。
条款7:预先准备好内存不足的情况
1.用try-cache来捕获抛出的异常。
2. 当内存分配请求不能满足时,调用预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数—一般称之为new-handler函数。还可以创建一个混合风格的基类—这种基 类允许子类继承它某一特定的功能(即函数)。
条款8:写operator new和operator delete时要遵循常规
内存分配程序支持new-handler函数并正确地处理了零内存请求,并且内存释放程序处理了空指针。此外还必须要有正确的返回值。
条款9:避免隐藏标准形式的new
在 类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new的访问(到底如何隐藏的???)。一个办法是在类里写一个支持标准new调用方式的operator new,它和标准new做同样的事,这可以用一个高效的内联函数来封装实现。另一种方法是为每一个增加到operator new的参数提供缺省值。
条款10:如果写了operator new就要同时写operator delete
operator new和operator delete需要同时工作,如果写了operator new,就一定要写operator delete。对于为大量的小对象分配内存的情况,可以考虑使用内存池,以牺牲灵活性来换取高效率。
条款11:为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
如果没有自定已拷贝构造函数和赋值操作符,C++会生成并调用缺省的拷贝构造函数和赋值操作符,它们对对象里的指针进行逐位拷贝,这会导致内存泄漏和指针重复删除。因此,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值运算符函数。
条款12:尽量使用初始化而不要在构造函数里赋值
尽量使用成员初始化列表,一方面对于成员来说只需承担一次拷贝构造函数的代价,而非构造函数里赋值时的一次(缺省)构造函数和一次赋值函数的代价;另一方面const和引用成员只能被初始化而不能被赋值。
条款13:初始化列表中的成员列出的顺序和它们在类中声明的顺序相同
类的成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没有关系。
条款14:确定基类有虚析构函数
通过基类的指针去删除派生类的对象,而基类有没有虚析构函数时,结果将是不可确定的。因此必须将基类的析构函数声明为virtual。但是,无故的声明虚析构函数和永远不去声明一样是错误的,声明虚函数将影响效率。
条款15:让operator=返回*this的引用
当 定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行(隐 式类型转换时要用到临时对象,而临时对象是const的),或两种情况同时发生。对于没有声明相应参数为const的函数来说,传递一个const对象是 非法的。
条款16:在operator=中对所有数据成员赋值
当类里增加新的数据成员时,要记住更新赋值运算符函数。对基类的私有成员赋值时,可以显示调用基类的operator=函数。派生类的拷贝构造函数中必须调用基类的拷贝构造函数而不是缺省构造函数,否则基类的数据成员将不能初始化。
条款17:在operator=中检查给自己赋值的情况
显 示的自己给自己赋值不常见,但是程序中可能存在隐式的自我赋值:一个对象的两个不同名字(引用)互相赋值。首先,如果检查到自己给自己赋值就立即返回,可 以节省大量的工作;其次,一个赋值运算符必须首先释放掉一个对象的资源,然后根据新值分配新的资源,在自己给自己的情况下,释放旧的资源将是灾难性的。
条款18:争取使类的接口完整并且最小
必要的函数是拷贝构造函数,赋值运算符函数,然后在此基础上选择必要的、方便的函数功能进行添加。
条款19:分清成员函数,非成员函数和友元函数
■虚函数必须是成员函数。如果f必须是虚函数,就让它称为类c的成员函数。
■ioerator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f称为非成员函数。如果f还需要 访问c的非公有成员,让f称为c的友元。
■其它情况下都声明为成员函数。如果以上情况都不是,让f称为c的成员函数。
Result = onehalf * 2;能通过编译的原因:调用重载*操作符的成员函数,对参数2进行隐式类型转换。
Result = 2 * onehalf;不能通过编译的原因:不能对成员函数所在对象(即成员函数中this指针指向的对象)进行转换。
条款20:避免public接口出现数据成员
访问一致性,public接口里都是函数。
精确的访问控制,可以精确设置数据成员的读写权限。
功能分离,可以用一段计算来取代一个数据成员。举例:计算汽车行驶的平均速度。
条款21:尽量使用const
如果const出现在*号左边,指针指向的数据为常量;如果const出现在*号右边,则指针本身为常量;如果const在两边都出现,二者都是常量。
将operator的返回结果声明为const,以防止对返回结果赋值,这样不合常规。
c+ +中的const:成员函数不修改对象中的任何数据成员时,即不修改对象中的任何一个比特时,这个成员函数才是const的。造成的问题是可以修改指针指 向的值,而且不能修改对象中的一些必要修改的值。解决方案是将必要修改的成员运用mutable关键字。另一种方法是使用const_cast初始化一个 局部变量指针,使之指向this所指的同一个对象来间接实现。还有一种有用又安全的方法:在知道参数不会在函数内部被修改的情况下,将一个const对象 传递到一个取非const参数的函数中。
条款22:尽量用“传引用”而不用“传值”
传值将导致昂贵的对象开销,而传引用则非常高效。
传引用避免了“切割问题”,即当一个派生类的对象作为基类对象被传递是,派生类的对象的作为派生类所具有的所有行为特性会被“切割”掉,从而变成了一个简单的基类对象。
条款23:必须返回一个对象时不要试图返回一个引用缩写
典型情况:操作符重载。
常见的错误:
返回引用,返回的是局部对象的引用。
堆中构造,使用new分配内存,但是无人负责delete的调用,从而造成内存泄漏。
返回静态对象,导致调用同一函数比较时总是相等。
正确的方法是直接在堆栈中创建对象并返回。
条款24:在函数重载和设定参数缺省值间慎重选择
如果可以选择一个合适的缺省参数,否则就使用函数重载。
有一些情况必须使用重载:函数的结果取决于传入参数的个数;需要完成一项特殊的任务。
条款25:避免对指针和数字类型重载
对于f(0):0代表int还是null。编译器认为是int,这和人们的想象不一样。解决办法是使用成员模板,构造一个可以产生null指针对象的类。最重要的是,只要有可能,就要避免对一个数字和一个指针类型重载。
条款26:当心潜在二义性
情形1:可以通过构造函数和转换运算符产生另一个类的对象,这时编译器将拒绝对其中的一种方法进行选择。
情形2:f(int);f(char);对于f(double)时产生二义。
情形3:多继承时,两个基类有同名的成员。此时必须指定基类方可调用,而不考虑访问控制权限和返回值。
条款27:如果不想使用隐式生成的函数就要显式地禁止它
方法是声明该函数,并使之为private。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为private,就防止了别人去调用它。为了防止成员函数和友元函数的调用,只声明而不定义这个函数。
条款28:划分全局名字空间
使用名字空间,以防止不同库的名字冲突。对于不支持名字空间的编译器,可以使用struct来模拟名字空间,但是此时运算符只能通过函数调用来使用。
条款29:避免返回内部数据的句柄
对于const成员函数来说,返回句柄可能会破坏数据抽象。如果返回的不是指向const数据的句柄,数据可能被修改。对非const成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮的。
条款30:避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
如果获得了私有或保护成员(包括成员函数)的地址(指针或引用),那么就可以象对待公有成员一样进行访问。如果不得不返回其引用或指针,可以通过返回指向const对象的指针或引用来达到两全其美的效果。
条款31:千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
如 果返回局部对象的引用,那个局部对象其实已经在函数调用者使用它之前就被销毁了。而返回废弃指针的问题是必须要有人负责调用delete,而且对于 product=one*two*three*four;的情况,将产生内存泄漏。因此,写一个返回废弃指针的函数无异于坐等内存泄漏的来临。
条款32:尽可能地推迟变量的定义
不 仅要强变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数位置。这样做,不仅可以避免对不必要的对象进行构造和析构,还可以避 免无意义的对缺省构造函数的调用。而且,在对变量初始化的场合下,变量本身的用途不言自明,在这里定义变量有益于表明变量的含义。
条款33:明智使用内联
内联函数的本质是将每个函数调用以它的代码体来替换。
大多数编译器拒绝“复杂”的内联函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。
若编译器不进行内联,则将内联函数当作一般的“外联”函数来处理。这称为“被外联的内联”。
找出重要的函数,将它内联。同时要注意代码膨胀带来的问题,并监视编译器的警告信息,看看是否有内联函数没有被编译器内联。
条款34:将文件间的编译依赖性降至最低
■如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明,定义此类型的对象则需要类型定义的参与。
■尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类。
■不要在头文件中再包含其它头文件,除非缺少了它们就不能编译。相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己去包含其它的头文件。
■最后一点,句柄类和协议类都不大会使用类联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。
条款35:使公有继承体现“是一个”的含义
如果类D从类B公有继承时,类型D的每一个对象也是类型B的一个对象,但反之不成立。任何可以使用类型B的对象的地方,类型D的对象也可以使用。
特别注意一般理解中的“是一个”,比如企鹅是鸟,并不严密。如果涉及到飞这个动作,二者之间不适合使用公有继承。
条款36:区分接口继承课实现继承
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。也可以为纯虚函数提供一种缺省实现。
声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
条款37:绝不要重新定义继承而来的非虚函数
如果重新定义继承而来的非虚函数,将导致对象对函数的调用结果由指向其的指针决定,而不是由对象本身的类型来决定。另外,也是类的设计产生矛盾,因为公有继承的含义是“是一个”,改变继承而来的方法显然是不合理的。
条款38:绝不要重新定义继承而来的缺省参数值
虚函数动态绑定,而缺省参数是静态绑定。因此重新定义继承而来的缺省参数值可能造成调用的是定义在派生类,但使用了基类中缺省参数值的虚函数。
条款39:避免“向下转换”继承层次
采用向下转换时,将不利于对代码进行维护,可以采用虚函数的方法来解决。
不得不进行向下转换时,采用安全的向下转换:dynamic_cast运算符。dynamic_cast运算符先尝试转换,若转换成功就返回新类型的合法指针,若失败则返回空指针。
条款40:通过分层来体现“有一个”或“用...来实现”
公有继承的含义是“是一个”。对应地,分层的含义是“有一个”或“用...来实现”。例如,要实现set类,因为list中可以包含重复元素,因此set不是一个list。set可以用list来实现,即在set中包含一个list。
条款41:区分继承和模板
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
条款42:明智地使用私有继承
关于私有继承的两个规则:和公有继承相反,如果两个类之间的继承关系为私有,编译器一般 不会将派生类对象转换为基类对象;从私有基类继承而来的成员都称为了派生类的私有成员,即使它们在基类中是保护或公有成员。
私有继承意味这“用...”来实现,但是应尽可能使用分层,必须时才使用私有继承。
条款43:明智地使用多继承
多 继承后果:二义性,如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的,你必须明确说出你所指的是哪个成员。这可能导致虚函数的 失效,并且不能对多继承而来的几个相同名称虚函数同时进行重定义。钻石型继承,此时向虚基类传递构造函数参数时要在继承结构中最底层派生类的成员初始化列 表中指定。同时还要仔细想想虚函数的优先度。
然而在适当时候还是可以使用多继承,例如将接口的公有继承和实现的私有继承结合起来的情况。
以增加中间类的代价来消除多继承有时侯是值得的。一般应该避免使用多继承以减少继承结构的复杂性。
条款44:说你想说的,理解你所说的
理解不同的面向对象构件在C++中的含义:
· 共同的基类意味着共同的特性。
· 公有继承意味着 "是一个"。
· 私有继承意味着 "用...来实现"。
· 分层意味着 "有一个" 或 "用...来实现"。
下面的对应关系只适用于公有继承的情况:
· 纯虚函数意味着仅仅继承函数的接口。
· 简单虚函数意味着继承函数的接口加上一个缺省实现。
· 非虚函数意味着继承函数的接口加上一个强制实现。
条款45:弄清C++在幕后为你所写、所调用的函数
如 果没有声明下列函数,编译器会声明它自己的版本:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符和一个缺省构造函数。对于拷贝构造函数 和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行“以成员为单位的”逐一拷贝构造(赋值)。
特别要注意由于编译器自动生成的函数造成的编译错误。
条款46:宁可编译和链接是出错,也不要运行时出错
通常,对设计做一点小小的改动,就可以在编译期间消除可能产生的运行时错误。这常常涉及到在程序中增加新的数据类型。例如对于需要类型检查的Month,可以将其设为一个Month类:构造函数私有,产生对象使用静态成员函数,每个Month对象为const。
条款47:确保非局部静态对象在使用前被初始化
如果在某个被编译单元中,一个对象的初始化要依赖于另一个被编译单元中的另一个对象的值,并且这第二个对象本身也需要初始化,就有可能造成混乱。
虽 然关于 "非局部" 静态对象什么时候被初始化,C++几乎没有做过说明;但对于函数中的静态对象(即,"局部" 静态对象)什么时候被初始化,C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。如果不对非局部静态对象直接访问,而用返回局部静态 对象引用的函数调用来代替,就能保证从函数得到的引用指向的是被初始化了的对象。
条款48:重视编译器警告
重视编译器产生的每一条警告信息。在忽略一个警告之前,一定要准确理解它想告诉你的含义。
条款49:熟悉标准库
对 于C++头文件,最大的挑战是把字符串头文件理清楚:是旧的C头文件,对应的是基于char*的字符串处理函数; 是包装了std的C++头文件,对应的是新的string类(看下文);是对应于旧C头文件 的std版本。如果能掌握这些,其余的也就容易了。
库中的一切都是模板。
条款50:提高对C++的认识
C++的设计目标:和C兼容,效率,和传统开发工具及环境的兼容性,解决真实问题的可应用性。
参考C++标准,理解C++的设计过程。
文章来源:
http://my.donews.com/robinchow/2007/01/10/ofzrlddyftbhlvscqmkjicnypymamyhaehrq/
条款1:指针与引用的区别
二者之间的区别是:在任何情况下都不能用指向空值的引用,而指针则可以;指针可以被重新赋值以指向另一个不同的对象,但是引用则总是指向在初始化时被指定的对象,以后不能改变。
在以下情况下使用指针:一是存在不指向任何对象的可能性;二是需要能够在不同的时刻指向不同的对象。
在以下情况使用引用:总是指向一个对象且一旦指向一个对象之后就不会改变指向;重载某个操作符时,使用指针会造成语义误解。
条款2:尽量使用C++风格的类型转换
static_cast:功能上基本上与C风格的类型转换一样强大,含义也一样。但是不能把struct转换成int类型或者把double类型转换成指针类型。另外,它不能从表达式中去除const属性。
const_cast:用于类型转换掉表达式的const或volatileness属性。但是不能用它来完成修改这两个属性之外的事情。
dynamic_cast:用于安全地沿着类的继承关系向下类型转换。失败的转换将返回空指针或者抛出异常。
reinterpret_cast:这个操作符被用于的类型转换的转换结果时实现时定义。因此,使用它的代码很难移植。最普通的用途就是在函数指针之间进行转换。
条款3:不要使用多态性数组
多态和指针算法不能混合在一起使用,所以数组和多态也不能用在一起。
数组中各元素的内存地址是数组的起始地址加上之前各个元素的大小得到的,如果各元素大小不一,那么编译器将不能正确地定位元素,从而产生错误。
条款4:避免无用的缺省构造函数
没有缺省构造函数造成的问题:通常不可能建立对象数组,对于使用非堆数组,可以在定义时提供必要的参数。另一种方法是使用指针数组,但是必须删除数组里的每个指针指向的对象,而且还增加了内存分配量。
提供无意义的缺省构造函数会影响类的工作效率,成员函数必须测试所有的部分是否都被正确的初始化。
条款5:谨慎定义类型转换函数
缺省的隐式转换将带来出乎意料的结果,因此应该尽量消除,使用显式转换函数。通过不声明运算符的方法,可以克服隐式类型转换运算符的缺点,通过使用explicit关键字和代理类的方法可以消除单参数构造函数造成的隐式转换。
条款6:自增和自减操作符前缀形式与后缀形式的区别
后缀式有一个int类型参数,当函数被调用时,编译器传递一个0作为int参数的值传递给该函数。可以在定义时省略掉不想使用的参数名称,以避免警告信息。
后缀式返回const对象,原因是 :使该类的行为和int一致,而int不允许连续两次自增后缀运算;连续两次运算实际只增一次,和直觉不符。
前缀比后缀效率更高,因为后缀要返回对象,而前缀只返回引用。另外,可以用前缀来实现后缀,以方便维护。
条款7:不要重载&&,||,或者“,”
对 于以上操作符来说,计算的顺序是从左到右,返回最右边表达式的值。如果重载的话,不能保证其计算顺序和基本类型想同。操作符重载的目的是使程序更容易阅 读,书写和理解,而不是来迷惑其他人。如果没有一个好理由重载操作符,就不要重载。而对于&&,||和“,”,很难找到一个好理由。
条款8:理解各种不同含义的new和delete
new操作符完成的功能分两部分:第一部分是分配足够的内存以便容纳所需类型的对象;第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事,我们不能以任何方式改变它的行为。
我们能改变的是如何为对象分配内存。new操作符通过调用operator new来完成必需的内存分配,可以重写或重载这个函数来改变它的行为。可以显式调用operator来分配原始内存。
如果已经分配了内存,需要以此内存来构造对象,可以使用placement new,其调用形式为new(void* buffer)class(int size)。
对于delete来说,应该和new保持一致,怎样分配内存,就应该采用相应的办法释放内存。
operator new[]与operator delete[]和new与delete相类似。
条款9:使用析构函数防止资源泄漏
使用指针时,如果在delete指针之前产生异常,将会导致不能删除指针,从而产生资源泄漏。
解决办法:使用对象封装资源,如使用auto_ptr,使得资源能够自动被释放。
条款10:在构造函数中防止资源泄漏
类中存在指针时,在构造函数中需要考虑出现异常的情况:异常将导致以前初始化的其它指针成员不能删除,从而产生资源泄漏。解决办法是在构造函数中考虑异常处理,产生异常时释放已分配的资源。最好的方法是使用对象封装资源。
条款11:禁止异常信息传递到析构函数外
禁止异常传递到析构函数外的两个原因:第一能够在异常传递的堆栈辗转开解的过程中,防止terminate被调用;第二它能帮助确保析构函数总能完成我们希望它做的所有事情。
解决方法是在析构函数中使用try-catch块屏蔽所有异常。
条款12:理解“抛出一个异常”与“传递一个参数”或“调用一个虚函数”间的差异
有 三个主要区别:第一,异常对象在传递时总被进行拷贝。当通过传值方式捕获时,异常对象被拷贝了两次。对象作为参数传递给函数时不需要被拷贝;第二,对象作 为异常被抛出与作为参数传递给函数相比,前者类型转换比后者少(前者只有两种转换形式:继承类与基类的转换,类型化指针到无类型指针的转换);最后一点, catch子句进行异常类型匹配的顺序是它们在源代码中出现的顺序,第一个类型匹配成功的擦他处将被用来执行。当一个对象调用一个虚函数时,被选择的函数 位于与对象类型匹配最佳的类里,急事该类不是在源代码的最前头。
条款13:通过引用捕获异常
有三个选择可以捕获异常:第一、指 针,建立在堆中的对象必需删除,而对于不是建立在堆中的对象,删除它会造成不可预测的后果,因此将面临一个难题:对象建立在堆中还是不在堆中;第二、传 值,异常对象被抛出时系统将对异常对象拷贝两次,而且它会产生“对象切割”,即派生类的异常对象被作为基类异常对象捕获时,它的派生类行为就被切割调了。 这样产生的对象实际上是基类对象;第三、引用,完美解决以上问题。
条款14:审慎使用异常规格
避免调用unexpected函数 的办法:第一、避免在带有类型参数的模板内使用异常规格。因为我们没有办法知道某种模板类型参数抛出什么样的异常,所以不可能为一个模板提供一个有意义的 异常规格;第二、如果在一个函数内调用其它没有异常规格的函数时应该去除这个函数的异常规格;第三、处理系统本身抛出的异常。可以将所有的 unexpected异常都被替换为自定义的异常对象,或者替换unexpected函数,使其重新抛出当前异常,这样异常将被替换为 bad_exception,从而代替原来的异常继续传递。
很容易写出违反异常规格的代码,所以应该审慎使用异常规格。
条款15:了解异常处理的系统开销
三 个方面:第一、需要空间建立数据结构来跟踪对象是否被完全构造,还需要系统时间保持这些数据结构不断更新;第二、try块。无论何时使用它,都得为此付出 代价。编译器为异常规格生成的代码与它们为try块生成的代码一样多,所以一个异常规格一般花掉与try块一样多的系统开销。第三、抛出异常的开销。因为 异常很少见,所以这样的事件不会对整个程序的性能造成太大的影响。
条款16:牢记80─20准则
80─20准则说的是大约20%的代码使用了80%的程序资源,即软件整体的性能取决于代码组成中的一小部分。使用profiler来确定程序中的那20%,关注那些局部效率能够被极大提高的地方。
条款17:考虑使用懒惰计算法
懒惰计算法的含义是拖延计算的时间,等到需要时才进行计算。其作用为:能避免不需要的对象拷贝,通过使用operator[]区分出读写操作,避免不需要的数据库读取操作,避免不需要的数字操作。但是,如果计算都是重要的,懒惰计算法可能会减慢速度并增加内存的使用。
条款18:分期摊还期望的计算
核心是使用过度热情算法,有两种方法:缓存那些已经被计算出来而以后还有可能需要的值;预提取,做比当前需要做的更多事情。
当必须支持某些操作而不总需要其结果时,可以使用懒惰计算法提高程序运行效率;当必须支持某些操作而其结果几乎总是被需要或不止一次地需要时,可以使用过度热情算法提高程序运行效率。
条款19:理解临时对象的来源
临时对象产生的两种条件:为了是函数成功调用而进行隐式类型转换和函数返回对象时。
临时对象是有开销的,因此要尽可能去消除它们,然而更重要的是训练自己寻找可能建立临时对象的地方。在任何时候只要见到常量引用参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)。
条款20:协助完成返回值优化
应当返回一个对象时不要试图返回一个指针或引用。
C+ +规则允许编译器优化不出现的临时对象,所有最佳的办法莫过于:retrun Ratinal(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator())。这种优化是通过使用函数的retuan location(或者用在一个函数调用位置的对象来替代),来消除局部临时对象,这种优化还有一个名字:返回值优化。
条款21:通过重载避免隐式类型转换
隐式类型转换将产生临时对象,从而带来额外的系统开销。
解决办法是使用重载,以避免隐式类型转换。要注意的一点是在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型的参数(这条规定是有道理的,如果没有的话,程序员将能改变预定义的操作,这样做肯定吧程序引入混乱的境地)。
另外,牢记80─20规则,没有必要实现大量的重载函数,除非有理由确信程序使用重载函数后整体效率会有显著提高。
条款22:考虑用运算符的赋值形式取代其单独形式
运算符的赋值形式不需要产生临时对象,因此应该尽量使用。对运算符的单独形式的最佳实现方法是return Rational(lhs) += rhs;这种方法将返回值优化和运算符的赋值形式结合起来,即高效,又方便。
条款23:考虑变更程序库
程序库必须在效率和功能等各个方面有各自的权衡,因此在具体实现时应该考虑利用程序库的优点。例如程序存在I/O瓶颈,就可以考虑用stdio替代iostream。
条款24:理解虚拟函数、多继承、虚基类和RTTI所需的代价
虚函数所需的代价:必须为每个包含虚函数的类的virtual table留出空间;每个包含虚函数的类的对象里,必须为额外的指针付出代价;实际上放弃了使用内联函数。
多继承时,在单个对象里有多个vptr(一个基类对应一个)。它和虚基类一样,会增加对象体积的大小。
RTTI能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息,让我们查询。这些信息被存储在类型为type_info的对象里,可以通过typeid操作符访问到一个类的typeid对象。通常,RTTI被设计为在类的vbtl上实现。
条款25:将构造函数和非成员函数虚拟化
构 造函数的虚拟化看似无意义,但是在实际当中有一定的用处。例如,在类中构建一个虚拟函数,其功能仅仅是实现构造函数,就可以对外界提供一组派生类的公共构 造接口。虚拟拷贝构造函数也是可以实现的,但是要利用到最近才被采纳的较宽松的虚拟函数返回值类型规则。被派生类重定义的虚拟函数不用必须与基类的虚拟函 数具有一样的返回类型。
具有虚拟行为的非成员函数很简单。首先编写一个虚拟函数完成工作,然后再写衣一个非虚拟函数,它什么也不做只是调用这个函数,可以使用内联来避免函数调用的开销。
条款26:限制某个类所能产生的对象数量
只 有一个对象:使用单一模式,将类的构造函数声明为private,再声明一个静态函数,该函数中有一个类的静态对象。不将该静态对象放在类中原因是放在函 数中时,执行函数时才建立对象,并且对象初始化时间确定的,即第一次执行该函数时。另外,该函数不能声明为内联,如果内联可能造成程序的静态对象拷贝超过 一个。
限制对象个数:建立一个基类,构造函数中计数加一,若超过最大值则抛出异常;析构函数中计数减一。
编程点滴:
●将模板类的定义和实现放在一个文件中,否则将造成引用未定义错误(血的教训);
●静态数据成员需要先声明再初始化;
●用常量值作初始化的有序类型的const静态数据成员是一个常量表达式(可以作为数组定义的维数);
●构造函数中抛出异常,将导致静态数组成员重新初始化。
条款27:要求或禁止在堆中产生对象
在堆中的对象不一定是用new分配的对象,例如成员对象,虽然不是用new分配的但是仍然在堆中。
要 求在堆中建立对象可以将析构函数声明未private,再建立一个虚拟析构函数进行对象析构。此时如果建立非堆对象将导致析构函数不能通过编译。当然也可 以将构造函数声明为private,但是这样将导致必须声明n个构造函数(缺省,拷贝等等)。为了解决继承问题,可以将其声明为protected,解决 包容问题则只能将其声明为指针。
没有办法不能判断一个对象是否在堆中,但是可以判断一个对象是否可以安全用delete删除,只需在operator new中将其指针加入一个列表,然后根据此列表进行判断。
把一个指针dynamic_cast成void*类型(或const void*或volatile void*等),生成的指针将指向“原指针指向对象内存”的开始处。但是dynamic_cast只能用于“指向至少具有一个虚拟函数的对象”的指针上。
禁止建立堆对象可以简单的将operator new声明为private,但是仍然不能判断其是否在堆中。
条款28:灵巧(smart)指针
灵巧指针的用处是可以对操作进行封装,同一用户接口。
灵巧指针从模板生成,因为要与内建指针类似,必须是强类型的;模板参数确定指向对象的类型。
灵巧指针的拷贝和赋值,采取的方案是“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”。此时,通过传值方式传递灵巧指针对象将导致不确定的后果,应该使用引用。
记住当返回类型是基类而返回对象实际上派生类对象时,不能传递对象,应该传递引用或指针,否则将产生对象切割。
测试灵巧指针是否为NULL有两种方案:一种是使用类型转换,将其转换为void*,但是这样将导致类型不安全,因为不同类型的灵巧指针之间将能够互相比较;另一种是重载operator!,这种方案只能使用!ptr这种方式检测。
最好不要提供转换到内建指针的隐式类型转换操作符,直接提供内建指针将破坏灵巧指针的“灵巧”特性。
灵巧指针的继承类到基类的类型转换的一个最佳解决方案是使用模板成员函数,这将使得内建指针所有可以转换的类型也可以在灵巧指针中进行转换。但是对于间接继承的情况,必须用dynamic_cast指定其要转换的类型是直接基类还是间接基类。
为了实现const灵巧指针,可以新建一个类,该类从非const灵巧指针继承。这样的化,const灵巧指针能做的,非const灵巧指针也能做,从而与标准形式相同。
条款29:引用计数
使用引用计数后,对象自己拥有自己,当没有人再使用它时,它自己自动销毁自己。因此,引用计数是个简单的垃圾回收体系。
在基类中调用delete this将导致派生类的对象被销毁。
写时拷贝:与其它对象共享一个值直到写操作时才拥有自己的拷贝。它是Lazy原则的特例。
精彩的类层次结构:
RCObject类提供计数操作;StringValue包含指向数据的指针并继承RCObject的计数操作;RCPtr是一个灵巧指针,封装了本属于String的一些计数操作。
条款30:代理类
可以用两个类来实现二维数组:Array1D是一个一维数组,而Array2D则是一个Array1D的一维数组。Array1D的实例扮演的是一个在概念上不存在的一维数组,它是一个代理类。
代 理类最神奇的功能是区分通过operator[]进行的是读操作还是写操作,它的思想是对于operator[]操作,返回的不是真正的对象,而是一个 proxy类,这个代理类记录了对象的信息,将它作为赋值操作的目标时,proxy类扮演的是左值,用其它方式使用它,proxy类扮演的是右值。用赋值 操作符来实现左值操作,用隐式类型转换来实现右值操作。
用proxy类区分operator[]作左值还是右值的局限性:要实现proxy类和原类型的无缝替代,必须申明原类型的一整套操作符;另外,使用proxy类还有隐式类型转换的所有缺点。
编程点滴:不能将临时对象绑定为非const的引用的行参。
条款31:让函数根据一个以上的对象来决定怎么虚拟
有 三种方式:用虚函数加RTTI,在派生类的重载虚函数中使用if-else对传进的不同类型参数执行不同的操作,这样做几乎放弃了封装,每增加一个新的类 型时,必须更新每一个基于RTTI的if-else链以处理这个新的类型,因此程序本质上是没有可维护性的;只使用虚函数,通过几次单独的虚函数调用,第 一次决定第一个对象的动态类型,第二次决定第二个对象动态类型,如此这般。然而,这种方法的缺陷仍然是:每个类必须知道它的所有同胞类,增加新类时,所有 代码必须更新;模拟虚函数表,在类外建立一张模拟虚函数表,该表是类型和函数指针的映射,加入新类型是不须改动其它类代码,只需在类外增加一个处理函数即 可。
条款32:在未来时态开发程序
未来时态的考虑只是简单地增加了一些额外约束:
●提供完备的类,即使某些部分现在还没有被使用。
●将接口设计得便于常见操作并防止常见错误。使得类容易正确使用而不易用错。
●如果没有限制不能通用化代码,那么通用化它。
条款33:将非尾端类设计为抽象类
如果有一个实体类公有继承自另一个实体类,应该将两个类的继承层次改为三个类的继承层次,通过创造一个新的抽象类并将其它两个实体类都从它继承。因此,设计类层次的一般规则是:非尾端类应该是抽象类。在处理外来的类库,可能不得不违反这个规则。
编程点滴:抽象类的派生类不能是抽象类;实现纯虚函数一般不常见,但对纯虚析构函数,它必须实现。
条款34:如何在同一程序中混合使用C++和C
混合编程的指导原则:
●确保C++和C编译器产生兼容的obj文件。
●将在两种语言下都使用的函数申明为extern ‘C’。
●只要可能,用C++写main()。
●总用delete释放new分配的内存;总用free释放malloc分配的内存。
●将在两种语言间传递的东西限制在用C编译的数据结构的范围内;这些结构的C++版本可以包含非虚成员函数。
条款35:让自己习惯使用标准C++语言
STL 基于三个基本概念:包容器(container)、选择子(iterator)和算法(algorithms)。包容器是被包容的对象的封装;选择子是类 指针的对象,让你能如同使用指针操作内建类型的数组一样操作STL的包容器;算法是对包容器进行处理的函数,并使用选择子来实现。
文章来源:
http://my.donews.com/robinchow/2007/01/11/qimyhmnmnudhngtapaqrnsghpptjuutkcfjj/