Effective C++ 读书笔记

URL: http://my.opera.com/Maple2005/blog/show.dml/21742

#从C转向C++

条款1:尽量用const和inline(编译器)而不用#define(预处理)
理由:
a.#define(预处理)进入编译器之前预处理程序会将符号去掉,代以常量,在编译的时候报错信息指向常量而难以理解,定义一个const常量能很好的解决这个问题;
b. 用#define来实现那些看起来象函数而又不会导致函数调用的宏(eg: #define max(a,b) ((a) > (b) ? (a) : (b))),会发生奇怪而易错的事情,而使用内联函数安全而同样高效(eg: inline int max(int a, int b) { return a > b ? a : b; })。
注意:
1、const定义指针常量时会,除了指针的指向类型要定义成const外,指针也常要定义成const(eg: const char * const authorName = "Scott Meyers";);
2、定义某个类的常量首先要使它成为类的静态成员(eg:
class GamePlayer {
private:
static const int NUM_TURNS = 5; // constant eclaration
int scores[NUM_TURNS];// use of constant
...
),这样还要在类的实现代码中定义类的静态成员(之前只是声明)(eg: const int GamePlayer::NUM_TURNS;);
3、使用模版可以解决内联函数处理类型的限制(eg: template<class T>
inline const T& max(const T& a, const T& b)
{ return a > b ? a : b; },只要a、b可以转换成同种类型即可比较);
4、在使用95年之前的部分编译器时,也许会在定义某个类的常量时还会遇到部分问题,详见原书。

条款2:尽量用“iostream”而不用“stdio.h”
理由:
a.scanf/printf系列函数不是类型安全的,而且没有扩展性;
b.scanf/printf系列函数把要读写的变量和控制读写格式的信息分开来,而在使用">>和<<"时编译器可以根据不同的变量类型选择操作符的不同形式(在使用非标准用法时需要operator重载);
c.在传递读和写的对象时>>和<<采用的语法形式相同,所以不必像scanf那样死记一些规定,在这里Scott说:"编译器没别的什么事好做的,而你却不一样。"
注意:
1、在形如"friend ostream& operator<<(ostream& s, const Rational& );"的重载时operator<<不是成员函数,而传递给operator<<的不是Rational对象,而是定义为 const的对象的引用;
2、在一些特殊的实现内,iostream的操作实现起来比相应的C stream效率要低;
3、对于iostream库,不同的厂商遵循标准的程度也不同;
4、iostream库的类有构造函数而<stdio.h>里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准C库会更简单实用;
5、 <iostream.h>已经简化为<iostream>,当编译器同时支持 <iostream>和<iostream.h>时,如果使用了#include <iostream> 得到的是置于名字空间std(见条款28)下的iostream库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名字冲突的发生。在这个地方Scott补充说:"打字时<iostream>比<iostream.h>少两个字,这也是很多人用它的原因。:)"。

条款3:尽量用new和delete而不用malloc和free
理由:
malloc 和free(及其变体)不知道构造函数和析构函数,当使用malloc申请内存空间的时候,在内存中实际上并没有创建这些对象,而即使已经创建这些对象,在释放的时候由于对象并不会调用析构函数,这些内存将丢失,而new和delete可以有效地与构造函数和析构函数交互。
注意:
new/delete和malloc/free不兼容,混用在一起回导致不可预测的结果,特别要注意一些函数内部到底使用malloc还是new分配内存的。

条款4:尽量使用C++风格的注释
理由:
在使用/**/嵌套注释的时候有可能会将注释提前结束。

#内存管理

条款5:对应的new和delete要采用相同的形式
理由:
否则会导致不可预见的结果
注意:
1、必须自己告诉delete要被删除的指针指向的是单个对象呢还是对象数组,后者需要使用"[]"(eg: delete [] stringptr2;),在删除单个对象时使用"[]"或者在删除数组对象的时候没有使用"[]"都将导致不可预见的结果。解决方法Scott总结为"如果你调用new时用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。";
2、在使用typedef的时候,如果用new创建一个typedef定义的类型的对象后,该用什么形式的delete来删除。

条款6:析构函数里对指针成员调用delete
理由:
内存泄露的不断增长最终会导致程序夭折,因此在每增加一个指针成员到类里的时候要在析构函数里面delete。

条款7:预先准备好内存不够的情况
理由:
Scott在论述这个问题时说:"处理内存不够所产生的异常真可以算得上是个道德上的行为,但实际做起来又会象刀架在脖子上那样痛苦。所以,你有时会不去管它,也许一直没去管它。但你心里一定还是深深地隐藏着一种罪恶感:万一new真的产生了异常怎么办?"
注意:
1、当内存分配请求不能满足时,调用你预先指定的一个出错处理函数比简单地让系统内核产生错误信息来结束程序要好,因为后者无法对每种形式的异常进行处理;
2、 C++不支持专门针对于类的new-handler函数,需要自己实现--在每个类中提供自己版本的set_new_handler(保存传给它的任何指针,并返回在调用它之前所保存的任何指针)和operator new(保证为类的对象分配内存时用类的new-handler取代全局new-handler);
3、创建实现类的new-handler功能的基类(让所有的子类可以继承set_new_handler和operator new功能)以及设计模版使每个子类有不同的currenthandler数据成员,可设计出可重用代码(原书中给出了较为详尽的例子)。

条款8: 写operator new和operator delete时要遵循常规
理由:
函数的行为要和系统缺省的operator new一致,这包括了正确的返回值、可用内存不够时要调用出错处理函数等。
注意:
1、处理零字节请求的技巧在于把它作为请求一个字节来处理,operator new返回一个合法指针;
2、使用一个无限循环使得new-handler必须获得更多的内存、抛出异常或者返回失败这三件事中的一件;
3、如果想控制基于类的数组的内存分配,必须实现operator new[]。

条款9: 避免隐藏标准形式的new
理由:
在类里定义了一个称为"operator new"的函数后,会不经意地阻止了对标准new的访问
注意:
1、解决方法之一是在类里写一个支持标准new调用方式的operator new,它和标准new做同样的事;
2、解决方法之二是为每一个增加到operator new的参数提供缺省值。

条款10: 如果写了operator new就要同时写operator delete
理由:
1、当调用缺省operator new来分配对象时,得到的内存由于额外的数据信息可能要比存储这个指针(或一对指针)所需要的要多,使用自己定义版本的operator new将会得到更少的内存和更快的速度;
2、自行定义的operator new返回了一个不带头信息的内存的指针,而缺省的operator delete却假设传给它的内存包含头信息,两者不同时写将导致前后的不匹配。
注意:
1、在类的声明时表头指针声明为静态成员使得整个类只有一个自由链表而不是每个对象都有;
2、如果要删除的对象是从一个没有虚析构函数的类继承而来的,那传给operator delete的size值有可能不正确,因此必须保证基类必须要有虚析构函数;
3、在以上设计的operator new和operator delete中没有内存泄露,因为每一小块内存都放在自由链表中,所有的内存块要不被对象使用(由客户来负责避免内存泄露),要不就在自由链表上;
4、定义一个专门的类,使他的每个对象均是某类对象的内存分配器可提高代码的可重用性。

#构造函数,析构函数和赋值操作符

条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
理由:
使用缺省的拷贝和复制会产生一系列不良结果--例如在进行字符串的拷贝时,被拷贝指针曾指向的内存永远不会被删除而产生内存泄露;或者两个指针中任何一个调用析构函数都将导致另一指针指向的那块内存被删除等。
注意:
1、用delete去删除一个已经被删除的指针,其结果是不可预测的,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数;
2、在不需要实现拷贝构造函数和赋值操作符的时候,可以只声明这些函数(声明为private成员)而不去定义它们,这就防止了会有人去调用它们。

条款12: 尽量使用初始化而不要在构造函数里赋值
a.一些情况如const和引用数据成员只能用初始化,不能被赋值;
b.成员初始化列表还是比在构造函数里赋值效率要高。
注意:
1、如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化;
2、当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候对类的数据成员用赋值比用初始化更合理;
3、静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去"初始化"它们没有任何意义。

条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
理由:
类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系--如果这种情况发生,编译器就要为每一个对象跟踪其成员初始化的顺序,以保证它们的析构函数以正确的顺序被调用--这将带来昂贵的开销。
注意:
初始化列表中成员列出的顺序和成员在类内声明的顺序保持一致。

条款14: 确定基类有虚析构函数
理由:
当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。Scott在这里说:"这意味着编译器生成的代码将会做任何它喜欢的事:重新格式化你的硬盘,给你的老板发电子邮件,把你的程序源代码传真给你的对手,无论什么事都可能发生。"∶P
注意:
1、使基类有虚构函数virtual,让派生类去定制自己的行为;
2、当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意,因为包含虚函数将使对象的体积翻番,而且降低代码的可移植性下降;
3、在定义抽象类的时候,定义纯虚构函数(eg: awov::~awov() {});

条款15: 让operator=返回*this的引用
理由:
采用缺省形式定义的赋值运算符里,对象返回值有两个很明显的候选者:赋值语句左边的对象(被this指针指向的对象)和赋值语句右边的对象(参数表中被命名的对象),在自定义的operator=中返回右边对象的版本往往不能通过
注意:
1、不要让operator=返回void--它妨碍了连续(链式)赋值操作;
2、不要让operator=返回const对象的引用;
3、当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用。

条款16: 在operator=中对所有数据成员赋值
理由:
只要想对赋值过程的某一个部分进行控制,就必须负责做赋值过程中所有的事。
注意:
1、当类里增加新的数据成员时,也要记住更新赋值运算符函数;
2、派生类的赋值运算符也必须处理它的基类成员的赋值。

条款17: 在operator=中检查给自己赋值的情况
理由:
自己给自己赋值的情况有可能发生在对同一对象不同名字的赋值,这样delete会将该对象删除,从而导致不可预见的结果发生。
注意:
1、可能发生的自己给自己赋值的情况先进行检查,如果该情况发生就立即返回;
2、一个方法是对相同内容的对象认为是同一对象(用operator==实现);
3、另一个确定对象身份是否相同的方法是用内存地址。

#类和函数:设计和声明

条款18: 争取使类的接口完整并且最小
理由:
a.接口中函数越多,以后的潜在用户就越难理解(也容易产生混淆),他们越难理解,就越不愿意去学该怎么用;
b.大的类接口难以维护;
c.长的类定义会导致长的头文件,浪费编译时间。

条款19: 分清成员函数,非成员函数和友元函数
注意:
1、成员函数和非成员函数最大的区别在于成员函数可以是虚拟的而非成员函数不行;
2、explicit构造函数不能用于隐式转换;
3、如果需要的话,编译器会对每个函数的每个参数执行隐式类型转换——但它只对函数参数表中列出的参数进行转换,决不会对成员函数所在的对象(即,成员函数中的*this指针所对应的对象)进行转换;
4、只要能避免使用友元函数就要避免;(在这个地方Scott说:“和现实生活中差不多,友元(朋友)带来的麻烦往往比它(他/她)对你的帮助多。”)
5、很多情况下,不是成员的函数从概念上说也可能是类接口的一部分,它们需要访问类的非公有成员的情况也不少。
6、最后Scott帮助我们总结该条款得出的结论——假设f是想正确声明的函数,c是和它相关的类:
•虚函数必须是成员函数。如果f必须是虚函数,就让它成为c的成员函数。
•operator>>和operator<<决不能是成员函数。如果f是operator>>或operator<<,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
•只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。如果f还需要访问c的非公有成员,让f成为c的友元函数。
•其它情况下都声明为成员函数。如果以上情况都不是,让f成为c的成员函数。

条款20: 避免public接口出现数据成员
理由:
a.如果public接口里都是函数,用户每次访问类的成员时就用不着“抓脑袋”去想是否要使用括号;
b.如果使数据成员为public,每个人都可以对它读写,如果用函数来获取或设定它的值,就可以实现禁止访问、只读访问和读写访问等多种控制;
c.如果用函数来实现对数据成员的访问,以后就有可能用一段计算来取代这个数据成员,而使用这个类的用户却一无所知。

条款21: 尽可能使用const
理由:
通知编译器某种对象不能修改。
注意:
1、在判断到底指定指针本身为const,还是指定指针所指的数据为const,或二者同时指定为const……这诸多情况时,Scott告诉我们一种简单的方法——“画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。”;
2、在指针所指为常量的情况下,有些程序员喜欢把const放在类型名之前,有些程序员则喜欢把const放在类型名之后、星号之前,这两种情况相同;
3、对函数返回值使用const有可能提高一个函数的安全性和效率;
4、mutable是C++标准组织为解决部分有关const问题的一个方案,他们可以在任何地方被修改,即使在const成员函数里;
5、在成员函数中尝试“消除const”往往会导致不可确定的后果。

未完待续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值