C++虚析构函数的介绍与解析

原文地址:http://blog.csdn.net/xunyn/article/details/7439276 


当使用基类指针释放派生类的对象时,需要将基类的析构函数声明为virtual,这是C++的一条规范,今天花了些时间探讨了下。

先构建两个类:father类 和son类,简单定义下:

class Father
{
public:
 Father(void);
public:
 virtual ~Father(void);
 void description();
private:
 int m_i_gen;
};

子类

class Son: public Father
{
public:
 Son(void);
public: 
  ~Son(void);

 void description();

private:
 int m_i_num;

};

问题一:虚析构函数与普通虚函数有什么区别

    首先,在C++中,每个类都只有一个析构函数,比如在子类Son中,便不能再添加父类father的虚函数virtual ~Father(void)的实现版本,这与普通虚函数是不同,普通虚函数是要在派生类中,重新给出父类中虚函数的实现,并且函数名相同。

问题二:虚析构函数的实现机制

   虚析构函数,也是通过vftable来实现的,实际上当父类的析构函数声明为虚函数时,子类的析构函数也变成了虚函数(虽然两者函数名不同),即告诉编译器,我和我的派生类都需要动态(运行时)完成析构函数执行。

     下面通过代码来进行验证

Father::Father(void)
{
 m_i_gen=1;
 CString str;
 str.Format("This is Father's constructor call");
 std::cout<<str<<"\n";

}

Father::~Father(void)
{
 std::cout<<"Father destroy"<<"\n";
}
void Father::description()
{
 std::cout<<"I am a father"<<"\n";
}
子类

Son::Son(void)
{
 m_i_num=1;
 CString str;
 str.Format("This is Son's constructor call");
 std::cout<<str<<"\n";
}

Son::~Son(void)
{
 std::cout<<"Son destroy"<<"\n";
}

void Son::description()
{
 std::cout<<"I am a son"<<"\n";
}
void  Son:: test()
{
 Father fath;
 fath.description();
}

主函数

int _tmain(int argc, _TCHAR* argv[]){

  Father fath;
 Son son;
 Father *f;
 Son *s;
 f = new Son();
 delete f;
 return 0;
}

依旧是执行之后追踪变量s

-  s 0x0040ad22 {m_i_num=-1259292477 } Son *
-  Father {m_i_gen=1213847799 } Father
-  __vfptr 0xc01bd8f7 *
  [0] CXX0030: 错误: 无法计算表达式的值 
  m_i_gen 1213847799 int
  m_i_num -1259292477 int
出现了小插曲,vfptr的值竟然没发追踪到,可能是因为对象是在堆中申请的内存空间,所以进入反汇编

确定在004057C4 这条语句,调用了4198885 (十进制)的函数,即类Son的析构函数~Son();

 

对于栈上的对象son,其变量追踪为

-  son {m_i_num=1 } Son
-  Father {m_i_gen=1 } Father
-  __vfptr 0x00434738 const Son::`vftable' *
  [0] 0x004011e5 Son::`vector deleting destructor'(unsigned int) *
  m_i_gen 1 int
  m_i_num 1 int
其中将vfptr中[0]内的0x004011e5换算成十进制,刚好就4198885,也就是说虽然没有直接追踪到对象s的vfptr[0]的值,但其实际调用的是地址为4198885的函数,这也就是栈上的对象son的vfptr[0]的值,即s对象是通过虚析构函数,在若不声明虚析构函数的情况下,会调用Father类的析构函数,编程了通过vfptr(0)来调用了Son类的析构函数。

这与直接son对象和father对象直接调用析构函数是完全不一样的:

004057E7  mov         dword ptr [ebp-3Ch],0 
004057EE  mov         byte ptr [ebp-4],0 
004057F2  lea         ecx,[ebp-20h] 
004057F5  call        Son::~Son (401217h) 
004057FA  mov         dword ptr [ebp-4],0FFFFFFFFh 
00405801  lea         ecx,[ebp-28h] 
00405804  call        Father::~Father (4012E4h) 
00405809  mov         eax,dword ptr [ebp-3Ch]

这两个析构函数的调用,在编译时,已确定传入地址。

 

总的来说,使用基类指针,在堆上释放派生类对象,就必须将基类指针声明为虚函数,后续的工作C++会帮忙完成。C++使用“按照绝对位置查表”的方式实现多态,确实保证好较好的效率,但是相较于按“函数名称查表”的实现机制,更不够能体现面向对象模型语言的特点。



C++:构造函数和析构函数能否为虚函数?

简单回答是:构造函数不能为虚函数,而析构函数可以且常常是虚函数。

(1) 构造函数不能为虚函数

让我们来看看大牛C++之父 Bjarne Stroustrup 在《The C++ Programming Language》里是怎么说的:

To construct an object, a constructor needs the exact type of the object it is to create. Consequently,a constructor cannot be virtual. Furthermore, a constructor is not quite an ordinary function, In particular, it interacts with memory management in ways ordinary member functions don't. Consequently, you cannot have a ponter to a constructor.

--- From 《The C++ Progamming Language》15.6.2

然而大牛就是大牛,这段话对一般人来说太难理解了。那下面就试着解释一下为什么:

这就要涉及到C++对象的构造问题了,C++对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable来调用,但此时面对一块 raw memeory,到哪里去找 vtable呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。

 

(2)析构函数可以是虚函数,且常常如此

这个就好理解了,因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。

 

 

困惑我们的是我们却经常看到“虚构造函数”这样的说法,这就要归咎于不负责任或者说误人子弟的媒体了(包括书、技术文章等等)。因为他们说的是类似下面这样的做法:

class Expr {

public:

     Expr();

     Expr(const Expr&);

     virtual Expr* new_expr() { return new Expr(); }

     virtual Expr* clone() { return new Expr(*this); }

};

 

问题一:虚析构函数与普通虚函数有什么区别

    首先,在C++中,每个类都只有一个析构函数,比如在子类Son中,便不能再添加父类father的虚函数virtual ~Father(void)的实现版本,这与普通虚函数是不同,普通虚函数是要在派生类中,重新给出父类中虚函数的实现,并且函数名相同(编译器实现的同名覆盖,会把所有析构函数解析为一个名字,如Destructor,故在子类中不会再添加父类的析构函数,因为子类有自己的析构函数,此时,若父类析构函数为虚函数,用子类对象的父类指针去访问父类析构函数其实就是多态,访问的为子类析构函数)。

问题二:虚析构函数的实现机制

   虚析构函数,也是通过vftable来实现的,实际上当父类的析构函数声明为虚函数时,子类的析构函数也变成了虚函数(虽然两者函数名不同),即告诉编译器,我和我的派生类都需要动态(运行时)完成析构函数执行。

 


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是实现要求的代码: ``` #include <iostream> #include <fstream> #include <vector> using namespace std; class Graph { private: int** m_AdjMat; // 邻接矩阵 vector<vector<int>> m_AdjList; // 邻接表 int m_NumVertex; // 顶点数 int m_NumEdge; // 边数 public: Graph() { m_NumVertex = 0; m_NumEdge = 0; m_AdjMat = nullptr; m_AdjList.clear(); } ~Graph() { if (m_AdjMat != nullptr) { for (int i = 0; i < m_NumVertex; i++) { delete [] m_AdjMat[i]; } delete [] m_AdjMat; } m_AdjList.clear(); } Graph(string filepath) { ifstream fin(filepath); if (!fin) { cout << "File not found." << endl; exit(1); } fin >> m_NumVertex >> m_NumEdge; // 读入顶点数和边数 m_AdjMat = new int*[m_NumVertex]; for (int i = 0; i < m_NumVertex; i++) { m_AdjMat[i] = new int[m_NumVertex]; for (int j = 0; j < m_NumVertex; j++) { m_AdjMat[i][j] = 0; // 初始化邻接矩阵 } } m_AdjList.resize(m_NumVertex); // 初始化邻接表 int u, v; for (int i = 0; i < m_NumEdge; i++) { // 读入边 fin >> u >> v; m_AdjMat[u][v] = m_AdjMat[v][u] = 1; // 更新邻接矩阵 m_AdjList[u].push_back(v); // 更新邻接表 m_AdjList[v].push_back(u); } fin.close(); } bool isConnected() { // 判断连通图 vector<bool> visited(m_NumVertex, false); dfs(0, visited); for (bool v : visited) { if (!v) { return false; } } return true; } bool isEulerian() { // 判断欧拉图 if (!isConnected()) { return false; } for (int i = 0; i < m_NumVertex; i++) { if (m_AdjList[i].size() % 2 != 0) { return false; } } return true; } bool isHamiltonian() { // 判断哈密顿图 vector<int> path; vector<bool> visited(m_NumVertex, false); for (int i = 0; i < m_NumVertex; i++) { path.clear(); visited.clear(); visited.resize(m_NumVertex, false); if (dfs_path(i, i, visited, path)) { return true; } } return false; } private: void dfs(int u, vector<bool>& visited) { // 深度优先搜索 visited[u] = true; for (int v = 0; v < m_NumVertex; v++) { if (m_AdjMat[u][v] && !visited[v]) { dfs(v, visited); } } } bool dfs_path(int u, int start, vector<bool>& visited, vector<int>& path) { // 查找哈密顿路径 visited[u] = true; path.push_back(u); if (path.size() == m_NumVertex) { if (m_AdjMat[u][start]) { return true; } path.pop_back(); visited[u] = false; return false; } for (int v : m_AdjList[u]) { if (!visited[v]) { if (dfs_path(v, start, visited, path)) { return true; } } } path.pop_back(); visited[u] = false; return false; } }; ``` 使用方法: - 默认构造函数:`Graph g;` - 从文件构造:`Graph g("filename.txt");` - 判断连通图:`bool connected = g.isConnected();` - 判断欧拉图:`bool eulerian = g.isEulerian();` - 判断哈密顿图:`bool hamiltonian = g.isHamiltonian();`

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值