文档章节

继承中构造函数和析构函数的调用顺序

Start-up
 Start-up
发布于 2012/05/17 00:18
字数 1896
阅读 1K
收藏 0

由架构中基类的设计想到的......

现在,有三个类,类的定义如下

#include <iostream>

using namespace std;

class CA
{
public:
    CA(){ cout << "CA constructor" << endl; }
    ~CA(){ cout << "CA destructor" << endl; }
};

class CB:public CA
{
public:
    CB(){ cout << "CB constructor" << endl; }
    ~CB(){ cout << "CB destructor" << endl; }
};
class CC:public CB
{
public:
    CC(){ cout << "CC constructor" << endl; }
    ~CC(){ cout << "CC destructor" << endl; }
};

int main(int argc, char* argv[])
{
    CC c;
    return 0;
}

CA是爷爷,CB是爸爸,CC是儿子。

那么任何一本C++的书都会讲,构造函数的调用顺序是CA CB CC,析构函数的调用顺序是CC,CB,CA,什么???你的书没讲,靠,扔了吧。

于是,这个程序运行结果是

CA constructor
CB constructor
CC constructor
CC destructor
CB destructor
CA destructor

靠,太简单了,一个鸡蛋飞过来了,:(

继续……………………

(2) 再做第二个试验之前,先做一点小小修改

//~CA(){ cout << "CA destructor" << endl; }
virtual ~CA(){ cout << "CA destructor" << endl; }

修改main 代码如下

int main(int argc, char* argv[])
{
    CA* pt = new CC;
    delete pt;

    return 0;
}

yeah
结果一模一样哦

CA constructor
CB constructor
CC constructor
CC destructor
CB destructor
CA destructor

但是如果把virtual  ~CA(){cout<<"CA destructor"<<endl;}virtual 去掉,

那么(2)中的运行结果为

CA constructor
CB constructor
CC constructor
CA destructor

只调了CA的析构函数哦,出问题了
这样的话,就会出现基类的构造函数调用了,但是派生类的构造函数没调用,对象的派生部分不会被销毁,这将导致资源泄漏!!

所以我们在设计一个类的时候,如果类至少拥有一个虚函数,或者说基类被设计用于多态,在这种情况下,一个派生类的对象可能通过一个基类指针来进行操作,然后进行销毁,如果这样的话,那么这个基类的析构函数要设置成虚拟的,有些类虽然是基类,但是不是用于多态的,没有虚函数,没有被设计成允许经由基类接口对派生类对象进行操作,那么也无需设成虚析构函数,毕竟增加了开销。

好了,解释清楚了,我们也知道怎么做了,继续试验。


(3)
保留CA中的虚析构函数

修改main 代码如下

int main(int argc, char* argv[])
{
    CB * pt = new CC;
    delete pt;

    return 0;
}

运行结果

CA constructor
CB constructor
CC constructor
CC destructor
CB destructor
CA destructor

取消CA中的虚析构函数,那么,CA,CB,CC中没有虚析构函数

class CA
{
public:
    CA(){ cout << "CA constructor" << endl; }
     ~CA(){ cout << "CA destructor" << endl; }
};

class CB:public CA
{
public:
    CB(){ cout << "CB constructor" << endl; }
    ~CB(){ cout << "CB destructor" << endl; }
};

那么 3 中代码运行结果如下

CA constructor
CB constructor
CC constructor
CB destructor
CA destructor

可以看到,只调到CB的析构哦(还有他的父类CA的析构)

继续试验,CACBCC中,只有CB是虚析构函数

class CA
{
public:
    CA(){ cout << "CA constructor" << endl; }
     ~CA(){ cout << "CA destructor" << endl; }
};

class CB:public CA
{
public:
    CB(){ cout << "CB constructor" << endl; }
    virtual ~CB(){ cout << "CB destructor" << endl; }
};

class CC:public CB
{
public:
    CC(){ cout << "CC constructor" << endl; }
    ~CC(){ cout << "CC destructor" << endl; }
};

int main(int argc, char* argv[])
{
    CB * pt = new CC;
    delete pt;

    return 0;
}

3 中代码运行如下

CA constructor
CB constructor
CC constructor
CC destructor
CB destructor
CA destructor


所以,如果是CB指向派生类,只要CB或者CB的基类中存在虚析构函数,那么也是所有的析构函数都调用的了

继续………………

(4)
修改main 代码如下

int main(int argc, char* argv[])
{
    CA * pt = new CC;
    delete pt;

    return 0;
}

如果A的析构函数是虚的,那么情况如2,不多说了

如果是CA的析构函数不是虚的,而CB或者CC的析构函数是虚拟的,即类似:

class CA
{
public:
    CA(){ cout << "CA constructor" << endl; }
     ~CA(){ cout << "CA destructor" << endl; }
};

class CB:public CA
{
public:
    CB(){ cout << "CB constructor" << endl; }
    virtual ~CB(){ cout << "CB destructor" << endl; }
};

class CC:public CB
{
public:
    CC(){ cout << "CC constructor" << endl; }
    ~CC(){ cout << "CC destructor" << endl; }
};

int main(int argc, char* argv[])
{
    CA * pt = new CC;
    delete pt;

    return 0;
}


那么在调用delete p;情况分为两种情况:

(一) VC2010会出现提示内存错误


Expression:_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)   
是在释放内存的时候出现这样的错误
上网查了一下,_BLOCK_TYPE_IS_VALID是用来检测内存有效性宏中的一个,这个错误说明指针使用出现了问题

后来想了一想,应该是因为继承类中出现了虚函数,所以多了一个指向虚函数表的指针,而基类中一个虚函数都没有,所以也没有这个指针啦
所以在delete的时候就出现了内存错,事实证明,这个猜想应该是站得住脚的,在CA中添加一个虚函数,即使的空的虚函数,也不会出现内存  。关于这个问题,我想在下次继续讨论吧,这里不深入进去了。

回到正题,在CA中添加一个空的,任意的虚拟函数以后,运行正确了,
运行结果是
CA constructor
CB constructor
CC constructor

CA desstructor

这与(2)中的情况是一样的,只要CA的析构函数不是虚拟的,就只能调用CA的析构了

(二)GCC 只会调用CA的析构函数


最后来看一种非常 Bt 的做法
(5)
CA CB CC
中的析构函数,谁是虚拟的,无所谓,随便

修改main 代码如下

int main(int argc, char* argv[])
{
    void * pt = new CC;
    delete pt;

    return 0;
}

运行结果
CA constructor
CB constructor
CC constructor

下面呢???下面没有了,……………………

这种情况,构造了一个cc的对象,然后呢,居然没有调析构函数,直接把申请的内存释放了最好不要这样用咯


好了,最后,上面,基本上把所有的,我能想到的情况都整理了一下,

 

构造函数顺序:

class A
{
public:
 A()
 {
  cout<<"A()"<<endl;
 }
};

class B:public A
{
public:
 B()
 {
  cout<<"B()"<<endl;
 }
};

class C
{
public:
 C()
 {
  cout<<"C()"<<endl;
 }
 B  a;

};
int main(void) 
{

 C c;
 return 0; 
}

如上代码输出:

A()
B()
C()

表明:成员对象最先构造,接着是基类构造,再是自身构造.析构时正好相反。


总结一下:


C1 * p = new C2();
delete p;

这样的代码

这里,C1C2的基类,C1可能是C2的爸爸,可能是爷爷,可能是爸爸的爷爷,可能是爷爷的爷爷…………………………

那么首先,调用的构造函数是 C2的第一个祖先一直到C2………………。和C1是什么没关系

delete p的时候,那么有以下几种情况:
1) C1
或者C1的祖先(基类)中,含有虚析构函数,那么调用的析构函数的顺序是从C2一直到C2的第一个祖先(#add最开始的祖先)
2
)如果C1或者C1的祖先中,没有一个是类是含有虚析构函数的,那么调用的是从C1一直到C1的(也是C2)第一个祖先的(#add表述有待细思)
3
)如果C1void,那就什么析构都不调用了。

如果一个类,作为多态的基类,那么尽量把析构函数声明成虚拟的,不然………………

好了,就此打住吧,关于4中的内存错误,下次再谈论吧

另外,可见虚函数是用来实现多态的,但是虚析构函数是一种特殊的虚函数,它的目的主要是为了在多态中防止资源泄露。只要C1或者C1的祖先中有一个含有虚析构函数,那么该调到的析构函数都会调用到。如果C1C1的祖先中没有一个含有虚析构函数,那么只会按照C1类型处理,不关心具体指到的类型。

 

参考书籍      Effective C++ 

 

© 著作权归作者所有

Start-up
粉丝 47
博文 292
码字总数 579191
作品 0
海淀
高级程序员
私信 提问
加载中

评论(0)

C++ 类(继承中的构造和析构)

文章概述 类型(赋值)兼容性原则; 类型(赋值)兼容性原则可以分为2个情况; 继承中的对象模型(内存模型); 继承中的构造析构调用原则; 继承和组合混搭下的构造和析构; 继承中同名的成员函数和成员...

下忍
03/31
0
0
【C++温故】(2) 类的继承(一)

派生类继承基类中除构造函数和析构函数外的所有成员(is-a) 三种派生方式 包括公有继承、保护继承和私有继承3种,权限逐级降低 默认为私有继承 公有继承 基类的 public 和 protected 成员在...

zizi7
03/31
0
0
类的二三事

类 类的基本思想就是数据抽象和封装。数据抽象依赖于接口和实现。接口指类外成员对象可使用的函数接口。实现指类的成员函数和成员变量。封装实现了接口和实现的分离。 类本身就是一个作用域。...

sdoyuxuan
2017/04/07
0
0
复合&继承关系下的构造和析构

继承关系下的构造和析构 看一下测试代码: 代码描述了这样一种关系,如下: 继承表现为 is-a 的关系,举个例子人是哺乳动物,那么人具有哺乳动物的特征,所以将 Base 放在里面。 运行结果如下...

Tanswer_
2018/01/31
0
0
C++中继承及virtual小结

一、继承基础知识 C++中的继承 1.1继承的基本概念 类与类之间的关系 继承的特点 1.2继承中的访问控制 三种继承方式对子类访问的影响 父类如何设置子类的访问 1.3继承中的类型兼容性原则 继承...

sinat_27652257
03/31
0
0

没有更多内容

加载失败,请刷新页面

加载更多

展示如何在checkout里使用quote,quote item, address, shopping cart

展示如何更改并且在定制化的时候高效应用这些模块。 以下实体继承 \Magento\Framework\Model\AbstractExtensibleModel ,所以你可以使用第4章中讨论的可扩展属性。 Quote Quotes 是客户购物车...

忙碌的小蜜蜂
31分钟前
8
0
面向对象思想设计原则及常见设计模式

1、面向对象思想设计原则 在实际的开发中,我们要想更深入的了解面向对象思想,就必须熟悉前人总结过的面向对象的思想的设计原则 1.1、单一职责原则 高内聚,低耦合 每个类应该只有一个职责,...

庭前云落
39分钟前
25
0
fastadmin对接支付宝支付,遇到的问题之一二

一开始也没做过支付宝支付相关的东西 本来用的fastadmin的epay插件来配置支付宝的,本来以为会so easy,但是实际上还是遇到了一些问题,花了几天时间,把沙箱环境配置起来了... 算是一个良好的开...

老bia同学
40分钟前
5
0
记录一题生产者消费者问题

//有一个容器,能存储一定的产品,有put和get方法,有两个生产者,8个消费者的线程阻塞 import java.util.LinkedList; import java.util.concurrent.TimeUnit; public class Test3<T> { Lin...

南桥北木
50分钟前
13
0
线程池源码解读——回归基础

线程池源码解读——回归基础 线程池源码解读——回归基础 线程池的好处: JDK提供的创建线程池: java 中创建线程的方式: 线程池源码解读: 记录的知识点: 线程池的好处: 降低资源的开销 ...

lihua20103181
52分钟前
100
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部