logo资料库

EffectiveC++中文版.pdf

第1页 / 共304页
第2页 / 共304页
第3页 / 共304页
第4页 / 共304页
第5页 / 共304页
第6页 / 共304页
第7页 / 共304页
第8页 / 共304页
资料共304页,剩余部分请下载后查看
从C转向C++
条款1:尽量用const和inline而不用#define
条款2:尽量用而不用
条款3:尽量用new和delete而不用malloc和free
内存管理
条款4:尽量使用C++风格的注释
条款5:对应的new和delete要采用相同的形式
条款6:析构函数里对指针成员调用delete
条款7:预先准备好内存不够的情况
条款8. 写operator new 和operator delete 时要遵循常规
条款9. 避免隐藏标准形式的new
条款10. 如果写了operator new就要同时写operator delete
条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
条款12: 尽量使用初始化而不要在构造函数里赋值
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
条款14: 确定基类有虚析构函数
条款15: 让operator=返回*this的引用
条款16: 在operator=中对所有数据成员赋值
条款17: 在operator=中检查给自己赋值的情况
类和函数:设计与声明
条款18: 争取使类的接口完整并且最小
条款19: 分清成员函数,非成员函数和友元函数
条款20: 避免public 接口出现数据成员
条款21: 尽可能使用const
条款22: 尽量用“传引用”而不用“传值”
条款23: 必须返回一个对象时不要试图返回一个引用
条款24: 在函数重载和设定参数缺省值间慎重选择
条款25: 避免对指针和数字类型重载
条款26: 当心潜在的二义性
条款27: 如果不想使用隐式生成的函数就要显式地禁止它
条款28: 划分全局名字空间
类和函数: 实现
条款29: 避免返回内部数据的句柄
条款30: 避免这样的成员函数:其返回值是指向成员的非const 指针或引用,但成员的访问级比这个函数要低
条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new 初始化的指针的引用
条款32: 尽可能地推迟变量的定义
条款33: 明智地使用内联
条款34: 将文件间的编译依赖性降至最低
继承和面向对象设计
条款35: 使公有继承体现"是一个" 的含义
条款36: 区分接口继承和实现继承
条款37: 决不要重新定义继承而来的非虚函数
条款38: 决不要重新定义继承而来的缺省参数值
条款39: 避免"向下转换" 继承层次
条款40: 通过分层来体现"有一个" 或"用...来实现"
条款41: 区分继承和模板
条款42: 明智地使用私有继承
条款43: 明智地使用多继承
条款44: 说你想说的;理解你所说的
杂项
条款45: 弄清C++在幕后为你所写、所调用的函数
条款46: 宁可编译和链接时出错,也不要运行时出错
条款47: 确保非局部静态对象在使用前被初始化
条款48: 重视编译器警告
条款49: 熟悉标准库
条款50: 提高对C++的认识
从 C 转向 C++ 对每个人来说,习惯 C++需要一些时间,对于已经熟悉 C 的程序员来说, 这个过程尤其令人苦恼。因为 C 是 C++的子集,所有的 C 的技术都可以继续使 用,但很多用起来又不太合适。例如,C++程序员会认为指针的指针看起来很 古怪,他们会问:为什么不用指针的引用来代替呢? C 是一种简单的语言。它真正提供的只有有宏、指针、结构、数组和函数。 不管什么问题,C 都靠宏、指针、结构、数组和函数来解决。而 C++不是这样。 宏、指针、结构、数组和函数当然还存在,此外还有私有和保护型成员、函数 重载、缺省参数、构造和析构函数、自定义操作符、内联函数、引用、友元、 模板、异常、名字空间,等等。用 C++比用 C 具有更宽广的空间,因为设计时 有更多的选择可以考虑。 在面对这么多的选择时,许多 C 程序员墨守成规,坚持他们的老习惯。一 般来说,这也不是什么很大的罪过。但某些 C 的习惯有悖于 C++的精神本质, 他们都在下面的条款进行了阐述。 条款 1:尽量用 const 和 inline 而不用#define 这个条款最好称为:“尽量用编译器而不用预处理”,因为#define 经常被认 为好象不是语言本身的一部分。这是问题之一。再看下面的语句: #define ASPECT_RATIO 1.653 编译器会永远也看不到 ASPECT_RATIO 这个符号名,因为在源码进入编译器 之前,它会被预处理程序去掉,于是 ASPECT_RATIO 不会加入到符号列表中。 如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指 的是 1.653,而不是 ASPECT_RATIO。如果 ASPECT_RATIO 不是在你自己写
的头文件中定义的,你就会奇怪 1.653 是从哪里来的,甚至会花时间跟踪下去。 这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在 符号列表中。 解决这个问题的方案很简单:不用预处理宏,定义一个常量: const double ASPECT_RATIO = 1.653; 这种方法很有效。但有两个特殊情况要注意。 首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许 多源文件会包含它),除了指针所指的类型要定义成const 外,重要的是指针也 经常要定义成 const。例如,要在头文件中定义一个基于 char*的字符串常量, 你要写两次 const: const char * const authorName = "Scott Meyers"; 关于 const 的含义和用法,特别是和指针相关联的问题,参见条款 21。 另外,定义某个类(class)的常量一般也很方便,只有一点点不同。要把常 量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝, 还要把它定义为静态成员: class GamePlayer { private: static const int NUM_TURNS = 5; // constant declaration int scores[NUM_TURNS]; // use of constant ... }; 还有一点,正如你看到的,上面的语句是 NUM_TURNS 的声明,而不是定义, 所以你还必须在类的实现代码文件中定义类的静态成员: const int GamePlayer::NUM_TURNS; // mandatory definition; // goes in class impl. file
你不必过于担心这种小事。如果你忘了定义,链接器会提醒你。 旧一点的编译器会不接受这种语法,因为它认为类的静态成员在声明时定 义初始值是非法的;而且,类内只允许初始化整数类型(如:int, bool, char 等), 还只能是常量。 在上面的语法不能使用的情况下,可以在定义时赋初值: class EngineeringConstants { // this goes in the class private: // header file static const double FUDGE_FACTOR; ... }; // this goes in the class implementation file const double EngineeringConstants::FUDGE_FACTOR = 1.35; 大多数情况下你只要做这么多。唯一例外的是当你的类在编译时需要用到 这个类的常量的情况,例如上面 GamePlayer::scores 数组的声明(编译过程中 编译器一定要知道数组的大小)。所以,为了弥补那些(不正确地)禁止类内进行 整型类常量初始化的编译器的不足,可以采用称之为“借用 enum”的方法来解 决。这种技术很好地利用了当需要 int 类型时可以使用枚举类型的原则,所以 GamePlayer 也可以象这样来定义: class GamePlayer { private: enum { NUM_TURNS = 5 }; // "the enum hack" — makes // NUM_TURNS a symbolic name // for 5
int scores[NUM_TURNS]; // fine ... }; 除非你正在用老的编译器(即写于 1995 年之前),你不必借用 enum。当然, 知道有这种方法还是值得的,因为这种可以追溯到很久以前的时代的代码可是 不常见的哟。 回到预处理的话题上来。另一个普遍的#define 指令的用法是用它来实现那 些看起来象函数而又不会导致函数调用的宏。典型的例子是计算两个对象的最 大值: #define max(a,b) ((a) > (b) ? (a) : (b)) 这个语句有很多缺陷,光想想都让人头疼,甚至比在高峰时间到高速公路去开 车还让人痛苦。 无论什么时候你写了象这样的宏,你必须记住在写宏体时对每个参数都要 加上括号;否则,别人调用你的宏时如果用了表达式就会造成很大的麻烦。但 是即使你象这样做了,还会有象下面这样奇怪的事发生: int a = 5, b = 0; max(++a, b); // a 的值增加了 2 次 max(++a, b+10); // a 的值只增加了 1 次 这种情况下,max 内部发生些什么取决于它比较的是什么值! 幸运的是你不必再忍受这样愚笨的语句了。你可以用普通函数实现宏的效
率,再加上可预计的行为和类型安全,这就是内联函数(见条款 33): inline int max(int a, int b) { return a > b ? a : b; } 不过这和上面的宏不大一样,因为这个版本的 max 只能处理 int 类型。但模板 可以很轻巧地解决这个问题: template inline const T& max(const T& a, const T& b) { return a > b ? a : b; } 这个模板产生了一整套函数,每个函数拿两个可以转换成同种类型的对象 进行比较然后返回较大的(常量)对象的引用。因为不知道 T 的类型,返回时传 递引用可以提高效率(见条款 22)。 顺便说一句,在你打算用模板写象 max 这样有用的通用函数时,先检查一 下标准库(见条款 49),看看他们是不是已经存在。比如说上面说的 max,你会 惊喜地发现你可以后人乘凉:max 是 C++标准库的一部分。 有了 const 和 inline ,你对预处理的需要减少了,但也不能完全没有它。抛弃 #include 的日子还很远,#ifdef/#ifndef 在控制编译的过程中还扮演重要角色。 预处理还不能退休,但你一定要计划给它经常放长假。 条款 2:尽量用而不用 是的,scanf 和 printf 很轻巧,很高效,你也早就知道怎么用它们,这我承 认。但尽管他们很有用,事实上 scanf 和 printf 及其系列还可以做些改进。尤其 是,他们不是类型安全的,而且没有扩展性。因为类型安全和扩展性是 C++的 基石,所以你也要服从这一点。另外,scanf/printf 系列函数把要读写的变量和 控制读写格式的信息分开来,就象古老的 FORTRAN 那样。是该向五十年代说 诀别的时候了! 不必惊奇,scanf/printf 的这些弱点正是操作符>>和<<的强项: int i;
Rational r; // r 是个有理数 ... cin >> i >> r; cout << i << r; 上面的代码要通过编译,>>和<<必须是可以处理 Rational 类型对象的重载 函数(可能要通过隐式类型转换)。如果没有实现这样的函数,就会出错(处理 int 不用这样做,因为它是标准用法)。另外,编译器自己可以根据不同的变量类型 选择操作符的不同形式,所以不必劳你去指定第一个要读写的对象是 int 而第二 个是 Rational。 另外,在传递读和写的对象时采用的语法形式相同,所以不必象 scanf 那 样死记一些规定,比如如果没有得到指针,必须加上地址符,而如果已经得到 了指针,又要确定不要加上地址符。这些完全可以交给 C++编译器去做。编译 器没别的什么事好做的,而你却不一样。最后要注意的是,象 int 这样的固定类 型和象 Rational这样的自定义类型在读写时方式是一样的。而你用sacnf 和 printf 试试看! 你所写的表示有理数的类的代码可能象下面这样: class Rational { public: Rational(int numerator = 0, int denominator = 1); ... private: int n, d; // 分子,分母 friend ostream& operator<<(ostream& s, const Rational& r);
}; ostream& operator<<(ostream& s, const Rational& r) { s << r.n << '/' << r.d; return s; } 上面的代码涉及到 operator<<的一些微妙(但很重要)的用法,这在本书其 他地方详细讨论。例如:上面的 operator<<不是成员函数(条款 19 解释了为什 么),而且,传递给 operator<<的不是 Rational 对象,而是定义为 const 的对象 的引用(参见条款 22)。operator>>的声明和实现也类似。 尽管我不大愿意承认,可有些情况下回到那些经过证明而且正确的老路上 去还是很有意义的。第一,有些 iostream 的操作实现起来比相应的 C stream 效率要低,所以不同的选择会给你的程序有可能(虽然不一定,参见条款 M16) 带来很大的不同。但请牢记,这不是对所有的 iostream 而言,只是一些特殊的 实现;参见条款 M23。第二,在标准化的过程中,iostream 库在底层做了很多 修改(参见条款 49),所以对那些要求最大可移植性的应用程序来说,会发现不 同的厂商遵循标准的程度也不同。第三,iostream 库的类有构造函数而 里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会 带来隐患,用标准 C 库会更简单实用。 iostream 库的类和函数所提供的类型安全和可扩展性的价值远远超过你当 初的想象,所以不要仅仅因为你用惯了而舍弃它。毕竟,转换到iostream 后,你也不会忘掉。 顺便说一句,本条款的标题没有打印错;我确实说的是而非 。从技术上说,其实没有 这样的东西——标准化委
员会在简化非 C 标准头文件时用取代了它。他们这样做的原因在条 款 49 进行了解释。还必须知道的是,如果编译器同时支持 , 那 头 文 件 名 的 使 用 会 很 微 妙 。 例 如 , 如 果 使 用 了 #include , 得到的是置于名字空间 std(见条款 28)下的 iostream 库的元素; 如果使用#include ,得到的是置于全局空间的同样的元素。在全 局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名 字冲突的发生。还有,打字时少两个字,这也是很 多人用它的原因。:) 条款 3:尽量用 new 和 delete 而不用 malloc 和 free malloc 和 free(及其变体)会产生问题的原因在于它们太简单:他们不知道 构造函数和析构函数。 假设用两种方法给一个包含 10 个 string 对象的数组分配空间,一个用 malloc,另一个用 new:     string *stringArray1 =     static_cast(malloc(10 * sizeof(string)));     string *stringArray2 = new string[10]; 其结果是,stringArray1 确实指向的是可以容纳 10 个 string 对象的足够空 间,但内存里并没有创建这些对象。而且,如果你不从这种晦涩的语法怪圈(详 见条款 M4 和 M8 的描述)里跳出来的话,你没有办法来初始化数组里的对象。 换句话说,stringArray1 其实一点用也没有。相反,stringArray2 指向的是一个 包含 10 个完全构造好的 string 对象的数组,每个对象可以在任何读取 string 的 操作里安全使用。
分享到:
收藏