Effective C++(条款7到9)

条款七:为多态基类声明virtual析构函数

严格来说,多态分为编译时多态和运行时多态,编译时多态是函数的重载,而运行时多态则是迟绑定技术,即根据基类指针或引用的实际指向来决定采取何种行动,一般来说,多态特指运行时多态。下面我们来举有关C++多态的一个简单例子

class Shape
{
private:
         int color;
public:
         virtual void print() = 0;
};

class Circle: public Shape
{
private:
         double radius;
public:
         void print(){cout << "This is a Circle" << endl;}
};
class Square: public Shape
{
private:
         double length;
public:
         void print(){cout << "This is a Square" << endl;}
};

多态前提是继承,如上面这个例子,Shape是基类,它有一个所有形状都具有的属性:颜色。Circle和Square派生自Shape,它们都有颜色的属性,但同时Circle还有半径的属性,Square有边长的属性。这里print()函数是一个有多态行为的函数,比如:

Shape *p1 = new Square;
 p1->print(); // 这时的运行结果就是“This is a Square”
 Shape *p2 = new Circle;
p2->print(); // 这时的运行结果就是“This is a Circle”

但这里会有一个问题,如果接下来操作:
delete p1;
delete p2;
就会出现内存的泄露,为什么会这样?因为Shape没有显式定义析构函数,编译器为我们生成的析构函数不是虚的,这时候delete p1,编译器就简单地析构了基类的部分,派生类的部分就被架空了,造成了内存的泄露。解决方法是基类声明virtual析构函数:

class Shape
{
private:
         int color;
public:
         virtual void print() = 0;
         virtual ~Shape(){}
};

有了virtual关键字,析构时就会检查指针实际指向的对象,也就会先定位到相应的派生类,完成派生类的析构之后,再析构基类。那么是怎么“检查”指针实际指向的对象呢?C++为每个含有virtual关键字的类中提供了一个虚表指针vptr,它指向一个由函数指针构成的数组,称为虚表。程序在运行时会维护类对象中的vptr,比如:

Shape *p1 = new Square;

vptr就会指向Square的print()函数。

那是不是为了安全起见,将所有的析构函数都加上virtual呢?这样的确可以防止多态过程中析构造成的内存泄露,但同时也有问题,即一旦出现了virtual,编译器就会生成vptr,这个vptr本身是要占空间的,对于一个并不大的类而言,vptr所占空间(32位机上是4字节,64位机上是8字节)的百分比还是很可观的。所以virtual要相时而加,出现多态继承关系时一定要加上,而非基类或者不用于多态的基类(比如条款6说的Uncopyable)就不要加。另外,所有的STL,包括string,vector等等,在设计时就没把它们当作基类,所以它们的析构函数都不是虚函数,因此试图定义一个类去继承STL是非常危险的!
再看

class Shape
{
private:
         int color;
public:
         virtual void print() = 0;
         virtual ~Shape(){}
};

Shape obj; 就不能通过编译。使不想生成对象的类成为抽象类,这是一个很好的技术实现方案,但万一有时候难以找到一个纯虚函数,怎么办呢?自己强行加一个像print()一样的函数固然是可以的,但不自然,一个好的方法是把它的析构函数变成纯虚函数,像这样:

class Shape
{
private:
         int color;
public:
         virtual ~Shape() = 0;
};

问题算是解决一半了,还有一个问题,编译器会在Shape派生类的析构函数中创建一个对~Shape()的调用动作,如果不实现这个~Shape(),链接器会报错,一个解决方案就是简单地加上括号,像这样:

class Shape
{
private:
         int color;
public:
         virtual ~Shape() = 0{};
};

好了,这下Shape既成了抽象类(不能拥有对象),也能在继承链中妥善地处理析构的内存问题。

总结一下:

(1) 带有多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数;

(2) 若一个类不作为基类,或者不具备多态性(比如Uncopyable),就不该声明virtual析构函数。

条款八:别让异常逃离析构函数

class Widget
{
public:
         ~Widget(){…} // 析构函数
};

void DoSomething()
{
         vector<Widget> v;
}

STL的vector在析构时会逐一调用容器内每个元素的析构函数,这样问题就来了,万一在Widget的析构函数里出现了异常,又没有及时地在析构函数中处理这个异常,导致异常被抛出,因为vector中存在多个Widget元素,这样在析构时就会多次抛出未处理的异常,多个异常同时存在是非常危险的,它会导致不明确的行为。

最好的解决方案就是在析构函数里面把异常给解决掉,像这样:

~Widget()
{
         try
         {…}
         catch
         {
            记录出错的位置
            终止程序
         }
}

记录错误总是要做的,但关于要不要出现异常就终止程序,在书上也作了一番探讨,主要在于这个程序是否可以终止,还是必须要坚持运作下去,这取决于实际的需要。

书上还举了一个例子,就是对析构函数做的事情模块化成一个函数,将这个函数设置成public,交由用户自行调用,若用户忘了做这件事情,再交给析构函数做。我觉得这样太麻烦,把程序弄的冗长了,所以这里就不介绍了。

下面总结一下:

(1) 析构函数绝对不要吐出异常,应该在内部“消化”掉这些异常;

(2) 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数,而非在析构函数中执行该操作。

条款九:绝不在构造和析构函数中调用virtual函数

class Base
{
public:
         Base()
         {
                   VirtualFunction();
         }

         virtual void VirtualFunction()
         {
                   cout << "In the Base Class" << endl;
         };
};



class Derived: public Base
{
public:
         void VirtualFunction()
         {
                   cout << "In the Derived Class" << endl;
         }
};


int main()
{
         Derived d;
}

定义派生类对象时,会先构造基类,调用基类的构造函数,在基类的构造函数中调用了虚函数,如果按照多态的思路,行为的执行者应该是派生类的VirtualFunction(),也就是输出的是In the Derived Class,然而实际跑一下这个程序,运行结果却是In the Base Class,为什么会这样呢?

一种很好的理解方法就是,派生类部分必须在基类部分构造完全之后才会去构造,因此在虚表中尚未注册派生类的VirtualFunction(),这时只能调用基类的VirtualFunction()。对于析构函数,同样是如此,派生类部分先析构,这时基类中的虚函数将无法定位到派生类,只能调用基类自身的函数。书上指出“在base class构造期间,virtual函数不是virtual函数”。

这样的结果会使读者感到困惑,与多态法则的效果不一致,所以本书作者强调“绝不在构造和析构函数中调用virtual函数”。

当实例化一个派生类对象时,首先进行基类部分的构造,然后再进行派生类部分的构造。即创建Derive对象时,会先调用Base的构造函数,再调用Derive的构造函数。
 当在构造基类部分时,派生类还没被完全创建,从某种意义上讲此时它只是个基类对象。即当Base::Base()执行时Derive对象还没被完全创建,此时它被当成一个Base对象,而不是Derive对象,因此Foo绑定的是Base的Foo。

在构造函数中,所有基类构造函数总是在继承类构造函数执行之前被调用,而继承类对象将在构造函数执行完之后被完整创建。如果在构造函数中调用一个虚函数,则会不会发生动态绑定呢?答案是调用的是这个虚函数的本地版本。在构造函数中,基类构造函数已经被执行完成,如果在这个时候通过基类引用或指针调用一个虚函数,会把对象视为base class对象,因为这个时候,继承类对象还尚未被初始化完成,所以面对它们,最安全的做法就是视它们不存在。

所有的构造函数被调用后才会有最后派生的VTABLE,如果在构造函数中进行虚函数调用,应该使用早绑定,因为它们知道,晚绑定只会对本地函数产生调用。在构造函数中调用虚函数往往得到的结果并不是我们想要的,一般好的设计并不在构造函数中调用虚函数。在《Effective C++》中有条款9:绝不在构造函数和析构过程中调用virtual函数。

最后总结一下:

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至派生类。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Effective C++(编程的50个细节) Effective C++(编程的50个细节)着重讲解了编写C++程序应该注意的50个细节问题,书中的每一条准则描述了一个编写出更好的C++的方式,每一个条款的背后都有具体范例支持,书中讲的都是C++的编程技巧和注意事项,很多都是自己平时不太注意但又很重要的内容,绝对经典,作者Scott Meyers是全世界最知名的C++软件开发专家之一。 电子书PDF格式下载:http://www.yanyulin.info/pages/2013/11/effective.html 1、从C转向C++ 条款1:尽量用CONST和INLINE而不用#DEFINE 条款2:尽量用而不用 条款3:尽量用NEW和DELETE而不用MALLOC和FREE 条款4:尽量使用C++风格的注释 2、内存管理 条款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=中检查给自已赋值的情况 3、类和函数:设计与声明 条款18:争取使类的接口完整并且最小 条款19:分清成员函数,非成员函数和友元函数 条款20:避免PUBLIC接口出现数据成员 条款21:尽可能使用CONST 条款22:尽量用传引用而不用传值 条款23:必须返回一个对象时不要试图返回一个引用 条款24:在函数重载与设定参数默认值间慎重选择 条款25:避免对指针与数字类型的重载 条款26:当心潜在的二义性 条款27:如果不想使用隐式生成的函数要显示的禁止它 条款28:划分全局名字空间 4、类和函数:实现 条款29:避免返回内部数据的句柄 条款30:避免这样的成员函数,其返回值是指向成员的非CONST指针或引用 条款31:千万不要返回局部对象的引用,也不要返回函数内部用NEW初始化的指针 条款32:尽可能推迟变量的定义 条款33:明智的使用INLINE 条款34:将文件间的编译依赖性阡至最低 5、继承与面向对象设计 条款35:使公有继承体现是一个的函义 条款36:区分接口继承与实现继承 条款37:绝不要重新定义继承而来的非虚函数 条款38:绝不要重新定义继承而来的缺省参数值 条款39:避免向下转换继承层次 条款40:通过分层来体现有一个和用...来实现 条款41:区分继承和模板 条款42:明智的使用私有继承 条款43:明智的使用多继承 条款44:说你想说的,理解你说的 6、杂项 条款45:弄清C++在幕后为你所写、所调用的函数 条款46:宁可编译与链接时出错,也不要运行时出错 条款47:确保非局部静态对象在使用前被初始化 条款48:重视编译器警告 条款49:熟悉标准库 条款50:提高对C++的认识
内容简介: 有人说C++程序员可以分成两类,读过Effective C++的和没读过的。世界顶级C++大师Scott Meyers成名之作的第三版的确当得起这样的评价。当您读过《Effective C++中文版(第3版改善程序与设计的55个具体做法)》后,就获得了迅速提升自己C++功力的一个契机。   在国际上,本书所引起的反响,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍的推荐名单上,本书都会位于前三名。作者高超的技术把握力、独特的视角﹑诙谐轻松的写作风格﹑独具匠心的内容组织,都受到极大的推崇和仿效。这种奇特的现象,只能解释为人们对这本书衷心的赞美和推崇。   《Effective C++中文版(第3版改善程序与设计的55个具体做法)》不是读完一遍就可以束之高阁的快餐读物,也不是用以解决手边问题的参考手册,而是需要您去反复阅读体会的,C++是真正程序员的语言,背后有着精深的思想与无与伦比的表达能力,这使得它具有类似宗教般的魅力。希望这本书能够帮助您跨越C++的重重险阻,领略高处才有的壮美风光,做一个成功而快乐的C++程序员。 Effective C++中文版(第3版改善程序与设计的55个具体做法)》一共组织55个准则,每一条准则描述一个编写出更好的C++的方式。每一个条款的背后都有具体范例支撑。第三版有一半以上的篇幅是崭新内容,包括讨论资源管理和模板(templates)运用的两个新章。为反映出现代设计考虑,对第二版论题做了广泛的修订,包括异常(exceptions)、设计模式(design patterns)和多线程(multithreading)。   《Effective C++中文版(第3版改善程序与设计的55个具体做法)》的重要特征包括:   ·高效的 classes、functions、templates 和inheritance hierarchies(继承体系)方面的专家级指导。   ·崭新的 "TR1" 标准程序库功能应用,以及与既有标准程序库组件的比较。   ·洞察 C++和其他语言(例如Java、C#、C)之间的不同。此举有助于那些来自其他语言阵营的开发人员消化吸收 C++ 式的各种解法。 目录: 译序 中英简繁术语对照 目录 序言 致谢 导读 1.让自己习惯C++ 条款01:视C++为一个语言联邦 条款02:尽量以const,enum,inline替换#define 条款03:尽可能使用const 条款04:确定对象被使用前已先被初始化 2.构造/析构/赋值运算 条款05:了解C++默默编写并调用哪些函数 条款06:若不想使用编译器自动成生的函数,就该明确拒绝 条款07:为多态基类声明Virtual析构函数 条款08:别让异常逃离析构函数 条款09:绝不在构造和析构过程中调用Virtual函数 条款10:令Operator=返回一个referenceto this 条款11:在Operator=中处理“自我赋值” 条款12:复制对象时勿忘其每一个成分 3.资源管理 条款13:以对象管理资源 条款14:在资源管理类中小心Coping行为 条款15:在资源管理类中提供对原始资源的访问 条款16:成对使用new和delete对象置入智能指针 条款17:以独立语句将newed对象置入智能指针 4.设计与声明 条款18:让接口容易被正确使用,不易被误用 条款19:设计class犹如设计type 条款20:宁以pass-by-reference-to-const替换Pass-by-value 条款21:必须返回对象时,别妄想返回其reference 条款22: 将成员变量声明为private 条款23: 宁以non-member、non-friend替换member函数 条款24:若有所参数皆需类型转换,请为此采用non-member函数 条款25:考虑写出一个不抛异常的swap函数 5.实现 条款26:尽可能延后变量定义式的出现时间 条款27:尽量少做转型动作 条款28:避免返回handles指向对象内部成分 条款29:为“异常安全”而努力是值得的 条款30:透彻了解inlining的里里外外 条款31:将文件间的编译依存关系降至最低 6.继承与面向对象设计 条款32:确定你的public继承塑模出is-a关系 条款33:避免遮掩继承而来的名称 条款34:区分接口继承和实现继承 条款35:考虚virtual函数以外的其他选择 条款36:绝不重新定义继承而来的non-virtual函数 条款37:绝不重新定义继承而来的缺省参数值 条款38:通过复合塑模出has-a或“根据某物实现出” 条款39:明智而审慎地使用private继承 条款40:明智而审慎地使用private继承 7.模板与泛型编程 8.定制new和delete 9.杂项讨论 A 本书之外 B 新旧版条款对映 索引

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值