void Test_Polym_Virtual_Func()
{
cout << "Test_Polym_Virtual_Func" << endl;
// 测试虚函数
class A
{
public:
A(){ cout << "A Constructor." << endl; f(); g(); m_p1_ = new int; *m_p1_ = 1; }
virtual~A(){ cout << "A Destructor." << endl; f(); g(); delete m_p1_; m_p1_ = nullptr; }
// 非虚函数
void f(){ cout << "A f()" << endl; }
// 虚函数
virtual void g(){ cout << "A g()" << endl; }
private:
int m_i_ = 6;
int* m_p1_ = new int;
};
class B : public A
{
public:
B(){ cout << "B Constructor." << endl;
f(); g();
m_p2_ = new int; *m_p2_ = 2; }
virtual ~B(){ cout << "B Destructor." << endl; f(); g(); delete m_p2_; m_p2_ = nullptr; }
// 非虚函数
void f(){ cout << "B f()" << endl; }
// 虚函数
void g(){ cout << "B g()" << endl; } // 从A继承过来的虚函数默认也是虚函数
private:
int m_j_ = 7;
int* m_p2_;
};
{
cout << "*1*" << endl;
A& a = B(); // A类型引用指向B类型对象
a.f(); // 输出:A f()
a.g(); // 输出:B f()
/*说明
1.变量a地址连续排列地址含义
在a变量地址依次排列了一个虚函数表指针__vfptr、和A::m_i_、A::m_p1_指针地址、B::m_j_、B::m_p2_指针地址
在__vfptr中存储了B类型的析构函数和函数g()地址,虚函数表是一个数组
*/
}
{
cout << "*2*" << endl;
A* p_a = new B; // A类型指针指向B类型指针对象
p_a->f(); // 输出:A f()
p_a->g(); // 输出:B f()
delete p_a;
p_a = nullptr;
/*说明
1.p_a指向的堆内存地址连续排列地址含义
在p_a指向的堆内存地址依次排列了一个虚函数表指针__vfptr、和A::m_i_、A::m_p1_指针地址、B::m_j_、B::m_p2_指针地址
在__vfptr中存储了B类型的析构函数和函数g()地址,虚函数表是一个数组
*/
}
}
上面实验输出如下:
1.基类对象的指针指向派生类对象时,如果基类析构函数不是虚函数,派生类的析构函数不会调用 这种情况下,如果派生类存在堆分配内存成员变量时,本来期望在派生类析构函数中释放堆分配内存,但不能调用析构函数,就会内存泄漏,派生类其他的非堆分配内存成员变量资源都会被释放;
基类引用指向派生类对象时,派生类对象是栈上分配的内存对象,即使基类析构函数不是虚函数,也会调用派生类的析构函数,因为派生类对象本来就是栈上分配的内存对象,离开作用域时,自然被系统自动调用析构函数回收资源;
测试这个异常,可以将A类型析构函数不设置成virtual,那么输出结果如下图,
在析构a引用对象时,调用了B的析构函数,析构p_a指针对象时,确实没有调用B的析构函数
2.无论基类还是派生类构造函数或析构函数中调用虚函数,都不会实现多态,都是调用自己的函数,因为基类构造函数是在派生类之前执行的,所以在基类构造函数运行的时候派生类的数据成员还没有被初始化。如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成其实尚未被初始化,这样是及其不安全的,所以在基类的构造过程中,虚函数调用从不会被传递到派生类中。在对象的析构期间,存在与上面同样的逻辑。一旦一个派生类的析构函数运行起来,该对象的派生类数据成员就被假设为是未定义的值,这样以来,C++就把它们当做是不存在一样,如果基类的析构过程中,虚函数调用被传递到派生类中,这样也及其不安全。其实,一旦进入到基类的构造函数或析构函数中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。
4.虚函数表指针__vfptr属于类,不属于对象,在编译期已经生成,并且__vfptr值在对象首地址存储,也就是对象地址开始前面4个字节就是__vfptr(32位)
下图是a引用对象中虚标指针:0x0034eb94
下图是p_a指针对象中虚标指针:0x0034eb94,并且看不到B类型对象m_p2的指针成员变量,是因为现在表现为A类型对象,实际的函数调用会多态执行B类型的重写虚函数的;
5.构造函数不能是虚函数,否则编译报错;因为在调用构造函数构造对象时肯定已经知道了具体的类型,而虚函数行为是在运行期间确定实际类型的,不可能在构造派生类对象时,通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。