一、派生类指针指向父类时父类的虚构函数必须设置为虚函数
看下面的代码,其中基类的析构函数并没有设置为虚函数
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data构造" << endl;
}
~Data()
{
cout << "~Data析构" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base构造" << endl;
}
~Base()
{
cout << "~Base析构" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_data(data)
{
cout << "Derived构造" << endl;
}
~Derived()
{
cout << "~Derived析构" << endl;
}
private:
Data m_data;
};
int main()
{
Base *p = new Derived(10);
delete p;
system("pause");
return 0;
}
输出结果
发现并没有调用派生类Derived的析构函数,也没有对派生类的成员变量进行析构。这就是基类析构不设置成虚函数引发的问题。
解释:虽然基类Base的指针指向的是派生类的对象,但是由于函数不是虚函数(没有在虚函数表里面)所以调用的时候会直接找基类的成员函数来执行。
把析构函数变成虚函数后:
class Base
{
public:
Base()
{
cout << "Base构造" << endl;
}
virtual ~Base() // 虚析构防止基类指向派生类的指针释放时无法进行派生类的析构函数
{
cout << "~Base析构" << endl;
}
};
结果如下
说明成功的调用了派生类的析构函数了,并且也调用了成员变量的析构函数。调试过程中的虚函数表信息如下:
二、成员变量为指针和普通数据成员在析构时的不同
查看下面的代码,新代码块中多加了一个类型为指针的成员变量m_pData,指向pData类型。
class pData
{
public:
pData(int data)
{
a = data;
cout << "pData构造" << endl;
}
~pData()
{
cout << "~pData析构" << endl;
}
private:
int a;
};
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data构造" << endl;
}
~Data()
{
cout << "~Data析构" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base构造" << endl;
}
virtual ~Base() // 虚析构防止基类指向派生类的指针释放时无法进行派生类的析构函数
{
cout << "~Base析构" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_pData(NULL),m_data(data)
{
cout << "Derived构造" << endl;
m_pData = new pData(data);
}
~Derived()
{
cout << "~Derived析构" << endl;
}
private:
pData* m_pData;
Data m_data;
};
int main()
{
Base *p = new Derived(10);
delete p;
system("pause");
return 0;
}
打印输出为:
在输出中发现,对指针pData进行了构造,却没有进行析构,这是为啥呢?
我理解m_data和m_pData在对象中的内存结构是不同的,内存结构好像是下图的样子:
所以说,在释放Derived对象的时候
(1)由于m_data有着完整的对象信息,就会直接调用其析构函数来对自己进行释放。
(2)其实m_pData也是释放了的,只是变成了一个不知道指向哪里的指针,原来所指向的地址并没有释放,这种情况下也就造成了内存泄露。
下面我们改写一下程序代码验证我们的想法:
class pData
{
public:
pData(int data)
{
a = data;
cout << "pData构造" << endl;
}
int getA() { return a; }
~pData()
{
cout << "~pData析构" << endl;
}
private:
int a;
};
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data构造" << endl;
}
~Data()
{
cout << "~Data析构" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base构造" << endl;
}
virtual pData* getPdata() { return NULL; }
virtual ~Base() // 虚析构防止基类指向派生类的指针释放时无法进行派生类的析构函数
{
cout << "~Base析构" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_pData(NULL),m_data(data)
{
cout << "Derived构造" << endl;
m_pData = new pData(data);
}
pData* getPdata()
{
return m_pData;
}
~Derived()
{
cout << "~Derived析构" << endl;
}
private:
pData* m_pData;
Data m_data;
};
int main()
{
Base *p = new Derived(8);
pData* pdata = p->getPdata(); // 保存指针指向的实际地址
cout << (*pdata).getA() << endl;
delete p;
cout << (*pdata).getA() << endl;
system("pause");
return 0;
}
在delete前后都打印pdata指向的内存块里面的值,我们发现就算是delete掉了也是可以打印出来的
调试的时候,也可以发现derived对象中的m_pdata确实是一个不知道指向哪里的野指针了
三、总结
1. 基类的析构函数最好声明为虚函数,虽然会在虚函数表中多占一个指针空间,但是会有效的防止不能调用派生类析构的情况。
2. 如果一个类的成员变量中有指针,在析构函数中一定要手动释放掉。而且要考虑写好拷贝构造和赋值运算符重载函数(具体原因参考之前写的博客:有指针时拷贝构造和赋值运算符的不同和带来的问题https://blog.csdn.net/struggle6688/article/details/105606661)。这个地方是经常出错的,一定要小心小心再小心。。。
涉及到内存结构的知识掌握的也不深,这些都是在VS2017下实验得出的结论,如果有问题请告诉我。