条款1:尽量用const 和inline 而不用#define
首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const 外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:
const char * const authorName = "Scott Meyers";
另外,定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员:
class GamePlayer {
private:
static const int NUM_TURNS = 5; // constant declaration
// enum { NUM_TURNS = 5 };
int scores[NUM_TURNS]; // use of constant
};
还有一点,正如你看到的,上面的语句是NUM_TURNS 的声明,而不是定义,所以你还必须在类的实现代码文件中定义类的静态成员:
const int GamePlayer::NUM_TURNS;所以,为了弥补那些(不正确地)禁止类内进行整型类常量初始化的编译器的不足,可以采用称之为“借用enum”的方法来解决。这种技术很好地利用了当需要int 类型时可以使用枚举类型的原则.
条款2:尽量用<iostream>而不用<stdio.h>
如果编译器同时支持 <iostream>和<iostream.h> ,那头文件名的使用会很微妙。例如,如果使用了#include<iostream>, 得到的是置于名字空间std(见条款28)下的iostream 库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名字冲突的发生.
条款3:尽量用new 和delete 而不用malloc 和free
malloc 和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。,在C++程序里使用malloc 和free没有错,只要保证用malloc 得到的指针用free,或者用new 得到的指针最后用delete 来操作就可以了。千万别马虎地把new 和free 或malloc 和elete 混起来用,那只会自找麻烦。
条款4:尽量使用C++风格的注释
条款5:对应的new 和delete 要采用相同的形式:
如果你调用new时用了[],调用delete时也要用[]。如果调用new 时没有用[],那调用delete 时也不要用[]。为了避免混乱,最好杜绝对数组类型用typedefs。
条款6:析构函数里对指针成员调用delete
大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete 释放内存。
删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete 掉他们,而不用担心他们是不是被new 过。
条款7:预先准备好内存不够的情况
当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new 不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler 函数。
指定出错处理函数时要用到set_new_handler 函数,它在头文件<new>里大致是象下面这样定义的:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
可以看到,new_handler 是一个自定义的函数指针类型,它指向一个没有输入参数也没有返回值的函数。set_new_handler 则是一个输入并返回new_handler 类型的函数。set_new_handler 的输入参数是operator new 分配内存失败时要调用的出错处理函数的指针,返回值是set_new_handler 没调用之前就已经在起作用的旧的出错处理函数的指针。卸除new-handler。也就是传递空指针给set_new_handler。没有安装newhandler,operator new 分配内存不成功时就会抛出一个标准的std::bad_alloc类型的异常。
C++不支持专门针对于类的new-handler 函数,而且也不需要。你可以自己来实现它,只要在每个类中提供自己版本的set_new_handler 和operatornew。类的set_new_handler 可以为类指定new-handler( 就象标准的set_new_handler 指定全局new-handler 一样)。类的operator new 则保证为类的对象分配内存时用类的new-handler 取代全局new-handler。
不管是用“正规” ( 即抛出异常 ) 形式的 new 还是“无抛出”形式的 new ,重要的是你必须为内存分配失败做好准备。最简单的方法是使用set_new_handler ,因为它对两种形式都有用。
条款8. 写operator new 和operator delete 时要遵循常规
C++ 标准要求,即使在请求分配 0 字节内存时, operator new 也要返回一个合法指针。
为没有办法可以直接得到出错处理函数的指针,所以必须通过调用 set_new_handler 来找到。
,有关 operator new 和 operator delete( 以及他们的数组形式 ) 的规定不是那么麻烦,重要的是必须遵守它。只要内存分配程序支持 new-handler 函数并正确地处理了零内存请求,就差不多了;如果内存释放程序又处理了空指针,那就没其他什么要做的了。
条款 9. 避免隐藏标准形式的 new
条款 10. 如果写了 operator new 就要同时写 operator delete
条款 11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
用 delete 去删除一个已经被删除的指针,其结果是不可预测的。
只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。
可以只声明这些函数(声明为 private 成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。
条款 12: 尽量使用初始化而不要在构造函数里赋值
const 成员只能被初始化,不能被赋值。
要在构造函数的初始化列表里对引用进行初始化。还可以对名字同时声明 const 和引用,这样就生成了一个其名字成员在类外可以被修改而在内部是只读的对象。
当使用成员初始化列表时,只有一个 string 成员函数被调用。而在构造函数里赋值时,将有两个被调用。
对象的创建分两步:
1. 数据成员初始化。(参见条款 13 )
2. 执行被调用构造函数体内的动作。
养成尽可能使用成员初始化列表的习惯,不但可以满足 const 和引用成员初始化的要求,还可以大大减少低效地初始化数据成员的机会。
但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。
条款 13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。
只是非静态数据成员的初始化遵守以上规则。静态数据成员的行为有点象全局和名字空间对象,所以只会被初始化一次(详见条款 47 )。另外,基类数据成员总是在派生类数据成员之前被初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面。
如果使用多继承,基类被初始化的顺序和它们被派生类继承的顺序一致,它们在成员初始化列表中的顺序会被忽略。使用多继承有很多地方要考虑。
条款 14: 确定基类有虚析构函数
虚函数的目的是让派生类去定制自己的行为(见条款 36 ),所以几乎所有的基类都包含虚函数。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为 vptr(虚函数表指针)的指针。 vptr 指向的是一个称为 vtbl (虚函数表)的函数指针数组。每个有虚函数的类都附带有一个 vtbl 。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向 vtbl 的 vptr 在 vtbl 里找到相应的函数指针来确定的。
无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
条款 15: 让 operator=返回*this的引用
一般情况下几乎总要遵循 operator= 输入和返回的都是类对象的引用的原则,然而有时候需要重载 operator= 使它能够接受不同类型的参数。
C++ 程序员经常犯的一个错误是让 operator= 返回 void ,这好象没什么不合理的,但它妨碍了连续(链式)赋值操作,所以不要这样做。
另一个常犯的错误是让 operator= 返回一个 const 对象的引用
当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用, *this 。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。
条款 16: 在 operator=中对所有数据成员赋值
,一个正确的派生类的赋值运算符必须调用它的每个基类的的赋值运算符
一个赋值运算符必须首先释放掉一个对象的资源(去掉旧值),然后根据新值分配新的资源。
条款 17: 在 operator=中检查给自己赋值的情况
怎么着手设计高效的类呢?首先,必须清楚你面临的问题。实际上,设计每个类时都会遇到下面的问题,它的答案将影响到你的设计。
对象将如何被创建和摧毁?它将极大地影响构造函数和析构函数的设计,以及自定义的 operator new, operator new[], operator delete, 和 operator delete[] 。(条款 M8 描述了这些术语的区别)
对象初始化和对象赋值有什么不同?答案决定了构造函数和赋值运算符的行为以及它们之间的区别。
通过值来传递新类型的对象意味着什么?记住,拷贝函数负责对此做出回答。
新类型的合法值有什么限制?这些限制决定了成员函数(特别是构造函数和赋值运算符)内部的错误检查的种类。它可能还影响到函数抛出的例外的种类以及函数的例外规范(参见条款 M14 ),如果你使用它们的话。
新类型符合继承关系吗?如果是从已有的类继承而来,那么新类的设计就要受限于这些类,特别是受限于被继承的类是虚拟的还是非虚拟的。如果新类允许被别的类继承,这将影响到函数是否要声明为虚拟的。
允许哪种类型转换?如果允许类型 A 的对象隐式转换为类型 B 的对象,就要在类 A 中写一个类型转换函数,或者,在类 B 中写一个可以用单个参数来调用的非 explicit 构造函数。如果只允许显式转换,就要写函数来执行转换功能,但不用把它们写成类型转换运算符和或单参数的非 explicit 构造函数。(条款 M5讨论了用户自定义转换函数的优点和缺点)
什么运算符和函数对新类型有意义?答案决定了将要在类接口中声明什么函数。
哪些运算符和函数要被明确地禁止?它们需要被声明为 private 。
谁有权访问新类型的成员?这个问题有助于决定哪些成员是公有的,哪些是保护的,哪些私有的。它还有助于确定哪些类和 / 或函数必须是友元,以及将一个类嵌套到另一个类中是否有意义。
新类型的通用性如何?也许你实际上不是在定义一个新的类型,而是在定义一整套的类型。如果是这样,就不要定义一个新类,而要定义一个新的类模板。
条款 18: 争取使类的接口完整并且最小
无端地在接口里增加函数不是没有代价的,所以在增加一个新函数时要仔细考虑:它所带来的方便性(只有在接口完整的前提下才应
该考虑增加一个新函数以提供方便性)是否超过它所带来的额外代价,如复杂性,可读性,可维护性和编译时间等。
在最小的接口上增加一些函数有时是合理的。如果一个通用的功能用成员函数实现起来会更高效,这将是把它增加到接口中的好理由。(但,有时不会,参见条款 M16 )如果增加一个成员函数使得类易于使用,或者可以防止用户错误,也都是把它加入到接口中的有力依据。
函数不对它们所在的对象进行任何修改操作,而且为遵循“能用 const 就尽量用 const” 的原则
条款 19: 分清成员函数,非成员函数和友元函数
成员函数和非成员函数最大的区别在于成员函数可以是虚拟的而非成员函数不行。所以,如果有个函数必须进行动态绑定(见条款 38 ),就要采用虚拟函数,而虚拟函数必定是某个类的成员函数。
使 operator* 成为一个非成员函数,从而允许编译器对所有的参数执行隐式类型转换
只要能避免使用友元函数就要避免,因为,和现实生活中差不多,友元(朋友)带来的麻烦往往比它 (他 / 她)对你的帮助多。
这个函数一定要是非成员函数。这样,就别无选择了:需要访问非公有成员的非成员函数只能是类的友元函数。
虚函数必须是成员函数。如果 f 必须是虚函数,就让它成为 C 的成员函数。
operator>> 和 operator<< 决不能是成员函数。如果 f 是 operator>> 或operator<< ,让 f 成为非成员函数。如果 f 还需要访问 C 的非公有成员,让 f 成为 C 的友元函数。
只有非成员函数对最左边的参数进行类型转换。如果 f 需要对最左边的参数进行类型转换,让 f 成为非成员函数。如果 f 还需要访问 C 的非公有成员,让 f 成为 C 的友元函数。
其它情况下都声明为成员函数。如果以上情况都不是,让 f 成为 C 的成员函数。
条款 20: 避免 public接口出现数据成员
,在 public 接口里放上数据成员无异于自找麻烦,所以要把数据成员安全地隐藏在与功能分离的高墙后。如果现在就开始这么做,那我们
就可以无需任何代价地换来一致性和精确的访问控制。
条款 21: 尽可能使用 const
char *p = "Hello"; // 非 const 指针 , 非 const 数据
const char *p = "Hello"; // 非 const 指针 , const 数据
char * const p = "Hello"; // const 指针 , 非 const 数据
const char * const p = "Hello"; // const 指针 , const 数据
条款 22: 尽量用“传引用”而不用“传值”
没有构造函数或析构函数被调用,因为没有新的对象被创建。
通过引用来传递参数还有另外一个优点: 它避免了所谓的“切割问题( slicing problem )”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。
引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如 int—— 传值实际上会比传引用更高效。
条款 23: 必须返回一个对象时不要试图返回一个引用
引用只是一个名字,一个其它某个已经存在的对象的名字。
条款 24: 在函数重载和设定参数缺省值间慎重选择
第一,确实有那么一个值可以作为缺省吗?第二,要用到多少种算法?一般来说,如果可以选择一个合适的缺省值并且只是用到一种算法,就使用缺省参数(参见条款 38 )。否则,就使用函数重载。
对很多函数来说,会找不到合适的缺省值。 这种情况下就别无选择:必须重载函数
另一种必须使用重载函数的情况是:想完成一项特殊的任务,但算法取决于给定的输入值。
在重载函数中调用一个“为重载函数完成某些功能”的公共的底层函数——很值得牢记,因为它经常有用
条款 25: 避免对指针和数字类型重载
作为重载函数的设计者,归根结底最基本的一条是,只要有可能,就要避免对一个数字和一个指针类型重载。
条款 26: 当心潜在的二义性
多继承(见条款 43 )充满了潜在二义性的可能。最常发生的一种情况是当一个派生类从多个基类继承了相同的成员名时
条款 27: 如果不想使用隐式生成的函数就要显式地禁止它
方法是声明这个函数,并使之为 private 。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为 private ,就防止了别
人去调用它。
条款 28: 划分全局名字空间
用户于是可以通过三种方法来访问名字空间里的符号:将名字空间中的所有符号全部引入到某一用户空间;将部分符号引入到某一用户空间;或通过修饰符显式地一次性使用某个符号:
名字空间带来的最大的好处之一在于:潜在的二义不会造成错误(参见条款 26 )。
条款 29: 避免返回内部数据的句柄
或者使函数为非const ,或者重写函数,使之不返回句柄。
对于 const 成员函数来说,返回句柄是不明智的,因为它会破坏数据抽象。对于非 const 成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮( dangle )的。所以一定要象避免悬浮的指针那样,尽量避免悬浮的句柄。
同样不能对本条款绝对化。在一个大的程序中想消灭所有可能的悬浮指针是不现实的,想消灭所有可能的悬浮句柄也是不现实的。但是,只要不是万不得已,就要避免返回句柄,这样,不但程序会受益,用户也会更信赖你。
条款 30: 避免这样的成员函数:其返回值是指向成员的非 const 指针或引用,但成员的访问级比这个函数要低
条款 31: 千万不要返回局部对象的引用,也不要返回函数内部用 new 初始化的指针的引用
条款 32: 尽可能地推迟变量的定义
推迟变量定义可以提高程序的效率,增强程序的条理性,还可以减少对变量含义的注释。
条款 33: 明智地使用内联
一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。
内联函数中的静态对象常常表现出违反直觉的行为。所以,如果函数中包含静态对象,通常要避免将它声明为内联函数。
条款 34: 将文件间的编译依赖性降至最低
分离的关键在于, " 对类定义的依赖 " 被 " 对类声明的依赖 " 取代了。所以,为了降低编译依赖性,我们只要知道这么一条就足够了:只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。其它一切方法都源于这一简单的设计思想。
声明一个除纯虚函数外什么也不包含的类。这样的类叫协议类( Protocol class ),它为派生类仅提供函数接口,完全没有实现。
条款 35: 使公有继承体现 "是一个" 的含义
条款 36: 区分接口继承和实现继承
当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为 ---- 不管一个派生类有多特殊。
声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
条款 37: 决不要重新定义继承而来的非虚函数
条款 38: 决不要重新定义继承而来的缺省参数值
条款 39: 避免 "向下转换" 继承层次
条款 40: 通过分层来体现 "有一个" 或 "用... 来实现"
。 " 分层 " 这一术语有很多同义词,它也常被称为:构成 (composition) ,包含 (containment) 或嵌入 (embedding) 。
条款 41: 区分继承和模板
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
条款 42: 明智地使用私有继承
和公有继承相反,如果两个类之间的继承关系为私有,编译器一般不会将派生类对象转换成基类对象。第二个规则是,从私有基类继承而来的成员都成为了派生类的私有成员,即使它们在基类中是保护或公有成员。
私有继承意味着 " 用 ... 来实现 " 。 用条款 36 引入的术语来说,私有继承意味着只是继承实现,接口会被忽略。
条款 43: 明智地使用多继承
条款 44: 说你想说的;理解你所说的
共同的基类意味着共同的特性。如果类 D1 和类 D2 都把类 B 声明为基类,D1 和 D2 将从 B 继承共同的数据成员和 / 或共同的成员函数。见条款 43 。
公有继承意味着 " 是一个 " 。如果类 D 公有继承于类 B ,类型 D 的每一个对象也是一个类型 B 的对象,但反过来不成立。见条款 35 。
私有继承意味着 " 用 ... 来实现 " 。如果类 D 私有继承于类 B ,类型 D 的对象只不过是用类型 B 的对象来实现而已;类型 B 和类型 D 的对象之间不存在概念上的关系。见条款 42 。
分层意味着 " 有一个 " 或 " 用 ... 来实现 " 。如果类 A 包含一个类型 B 的数据成员,类型 A 的对象要么具有一个类型为 B 的部件,要么在实现中使用了类型B 的对象。见条款 40 。
纯虚函数意味着仅仅继承函数的接口。如果类 C 声明了一个纯虚函数 mf ,C 的子类必须继承 mf 的接口, C 的具体子类必须为之提供它们自己的实现。见条款 36 。
简单虚函数意味着继承函数的接口加上一个缺省实现。如果类 C 声明了一个简单(非纯)虚函数 mf , C 的子类必须继承 mf 的接口;如果需要的话,还可以继承一个缺省实现。见条款 36 。
非虚函数意味着继承函数的接口加上一个强制实现。如果类 C 声明了一个非虚函数 mf , C 的子类必须同时继承 mf 的接口和实现。实际上, mf 定义了 C 的 " 特殊性上的不变性 " 。见条款 36 。
条款 45: 弄清 C++在幕后为你所写、所调用的函数
一个空类什么时候不是空类? ---- 当 C++ 编译器通过它的时候。如果你没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。
换句话说,如果你这么写:
class Empty{};
和你这么写是一样的:
class Empty {
public:
Empty(); // 缺省构造函数
Empty(const Empty& rhs); // 拷贝构造函数
~Empty(); // 析构函数 ---- 是否 为虚函数看下文说明
Empty& operator=(const Empty& rhs); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const;
};
条款 46: 宁可编译和链接时出错,也不要运行时出错
条款 47: 确保非局部静态对象在使用前被初始化
条款 48: 重视编译器警告
条款 50: 提高对 C++的认识