虚函数那些小事儿(一)

本文详细解析了虚函数的概念及其在C++中的应用,通过代码实例展示了虚函数表、析构函数调用顺序、成员变量对齐等关键特性。深入剖析了虚函数如何在多态实现中影响内存布局与对象生命周期。
转载注明出处,原文地址:http://blog.csdn.net/powerwoo25/article/details/47429545

虚函数那些小事儿(一)


        本文不准备纠结于虚函数、虚函数表的概念,倒是想说说几个虚函数比较经典的具有迷惑性的问题。秉承废话少说,放码过来(Talk is cheap, show me the code)的原则先上代码(32位/g++编译

转载注明出处,原文地址:http://blog.csdn.net/powerwoo25/article/details/47429545
#include <iostream>
#include <cstdio>

using namespace std;

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

    virtual void func1()
    {
        cout << "In A::func1" << endl;
    };
    virtual ~A()
    {
        cout << "In A destructor" << endl;
    }
    virtual void func2()
    {
        cout << "In A::func2" << endl;
    }
    virtual void func3()
    {        cout << "In A::func3" << endl;
    }
    void func4()
    {
        cout << "In A::func4" << endl;
    }
    void func5()
    {
        cout << "In A::func5" << endl;
    }

public:
    int m;
};

class B: public A
{
public:
    B()
    {
        cout << "In B constructor" << endl;
    }
    ~B()
    {
        cout << "In B destructor" << endl;
    }
    virtual void func1()
    {
        cout << "In B::func1" << endl;
    }
    virtual void func2()
    {
        cout << "In B::func2" << endl;
    }
    void func5()
    {
        cout << "In B::func5" << endl;
    }


public:
    char n[2];
    int l;
};

typedef void (*Func)(void);

int main()
{
    A *aa = new B;
    cout << (int*)aa << endl;
    aa->func2();
    delete aa;
    cout << endl;

    A a;
    B b;
    Func pFunc = NULL;
    cout << "-------------------------------Object structure-------------------------------" << endl;
    cout << "a address: " << (long*)&a << endl;
    cout << "vptr address: " << (int*)(&a) << endl;
    cout << "---- vtbl address: " << (int*)*(int*)(&a) << endl;
    cout << "---- ---- vtbl[0] address: " << (int*)*(int*)(&a) << endl;
    cout << "---- ---- vtbl[1] address: " << (int*)*(int*)(&a) + 1 << endl;
    cout << "---- ---- vtbl[2] address: " << (int*)*(int*)(&a) + 2 << endl;
    cout << "---- ---- vtbl[3] address: " << (int*)*(int*)(&a) + 3 << endl;
    cout << "---- ---- vtbl[4] address: " << (int*)*(int*)(&a) + 4 << endl;
    cout << "---- ---- ---- a::func1 address: " << (int*)*((int*)*(int*)(&a)) << endl;
    cout << "---- ---- ---- a::~A() address: " << (int*)*((int*)*(int*)(&a) + 1) << endl;
    cout << "---- ---- ---- a::~A() address: " << (int*)*((int*)*(int*)(&a) + 2) << endl;
    cout << "---- ---- ---- a::func2 address: " <<  (int*)*((int*)*(int*)(&a) + 3)<< endl;
    cout << "---- ---- ---- a::func3 address: " <<  (int*)*((int*)*(int*)(&a) + 4)<< endl;
    printf("a::func4 address: %#x\n", &A::func4);
    printf("b::func5 address: %#x\n", &A::func5);
    cout << "a.m address: " << (int*)&(a.m) << endl;
    cout << "a size: " << sizeof(a) << endl;
    cout << "----------------------------------------------------------------------------" << endl;

    pFunc = (Func)*((int*)*(int*)(&a));
    pFunc();
//    pFunc = (Func)*((int*)*(int*)(&a) + 1);
//    pFunc();
//    pFunc = (Func)*((int*)*(int*)(&a) + 2);
//    pFunc();
    pFunc = (Func)*((int*)*(int*)(&a) + 3);
    pFunc();
    pFunc = (Func)*((int*)*(int*)(&a) + 4);
    pFunc();
    cout << endl;

    cout << "-------------------------------Object structure-------------------------------"<< endl;
    cout << "b address: " << (int*)&b << endl;
    cout << "vptr address: " << (int*)(&b) << endl;
    cout << "---- vtbl address: " << (int*)*(int*)(&b) << endl;
    cout << "---- ---- vtbl[0] address: " << (int*)*(int*)(&b) << endl;
    cout << "---- ---- vtbl[1] address: " << (int*)*(int*)(&b) + 1 << endl;
    cout << "---- ---- vtbl[2] address: " << (int*)*(int*)(&b) + 2 << endl;
    cout << "---- ---- vtbl[3] address: " << (int*)*(int*)(&b) + 3 << endl;
    cout << "---- ---- vtbl[4] address: " << (int*)*(int*)(&b) + 4 << endl;
    cout << "---- ---- ---- b::func1 address: " << (int*)*((int*)*(int*)(&b)) << endl;
    cout << "---- ---- ---- b::~B() address: " << (int*)*((int*)*(int*)(&b) + 1) << endl;
    cout << "---- ---- ---- b::~B() address " << (int*)*((int*)*(int*)(&b) + 2) << endl;
    cout << "---- ---- ---- b::func2 address: " << (int*)*((int*)*(int*)(&b) + 3) << endl;
    cout << "---- ---- ---- b::func3 address: " <<  (int*)*((int*)*(int*)(&b) + 4)<< endl;
    printf("b::func4 address: %#x\n", &B::func4);
    printf("b::func5 address: %#x\n", &B::func5);
    cout << "b.m address: " << (int*)&(b.m) << endl;
    cout << "b.n[0] address: " << (int*)&(b.n[0]) << endl;
    cout << "b.n[1] address: " << (int*)&(b.n[1]) << endl;
    cout << "b.l address: " << (int*)&(b.l) << endl;
    cout << "b size: " << sizeof(b) << endl;
    cout << "----------------------------------------------------------------------------" << endl;
//
    pFunc = (Func)*((int*)*(int*)(&b));
    pFunc();
//    pFunc = (Func)*((int*)*(int*)(&b) + 1);
//    pFunc();
//    pFunc = (Func)*((int*)*(int*)(&b) + 2);
//    pFunc();
    pFunc = (Func)*((int*)*(int*)(&b) + 3);
    pFunc();
    pFunc = (Func)*((int*)*(int*)(&b) + 4);
    pFunc();
    cout << endl;
}

         上述代码不难理解。主要是分两部分。第一部分(主函数前五行)意在体现Effective C++里面提及的“多态实现中,基类的析构函数不声明为virtual的情况下,对基类指针指向的派生类进行delete时所发生的析构不完全这种情况。
结果如下(第一张为声明基类析构函数virtual下的delete,第二张为基类析构函数non-virtual下的delete)可见,析构不完全的根本原因就是基类的析构函数获得执行,但派生类的析构函数没有获得执行。




第二部分(Object structure)将与基类对象及派生类对象所关联的所有结构地址给打印出来。所关联的部分有虚函数表指针、类成员函数、类成员变量。
结果如下





从结果来看,直观上可以获得如下几个结论:
1、对象首地址就是虚函数表指针的地址,说明对象的头部存储了4Bytes的虚函数表指针。
2、对象所属的类的成员函数很多,但是不占对象内存空间。

但是具体分析相关参数的地址可以获得很多信息:
1、在执行相应虚函数的时候,先从虚函数表指针处获得虚函数表的地址,再查阅相应的表项获得相应的函数入口地址执行该函数,其中虚函数在虚函数表的位置跟虚函数声明顺序有关。
2、对象数据成员、类成员函数、虚函数表分别在不同的地方存储,要调用时实际上都是通过地址进行相应的寻找。
3、能重复用的函数绝不进行多余拷贝,直接继承的成分都沿用原始地址,经过重写的函数则会进行额外开辟空间放置。
调用关系、继承关系、代码段函数存储顺序图示如下:



4、对象成员变量存储时进行了对齐, b中的实际参数大小综合为4(vptr)+4(m)+2(char n[2])+4(l) = 14 Bytes,但是编译器将char n[2]后面的两个Bytes也视作占用,强行将对象大小扩充为int的整数倍进行对齐,所以最终b size是16。

关于第一部分的问题分析:
        首先承认一个事实,就是派生类的析构函数静态绑定了基类析构函数的调用,也就是说,只要派生类的析构函数进行调用了,就必定会首先调用基类的析构函数。不管基类析构函数上有没有virtual。当基类的析构函数声明为non-virtual时,aa本质是指向一个派生类对象,但是却被声明为A类型,在使用析构函数的时候就会按照类型(就是A)来调用对应的A析构函数,结果基类部分被析构。这没什么问题。
        当基类的析构函数声明为virtual时,aa本质还是指向一个派生类对象,还是被声明成了A类型,在使用析构函数的时候还是会按照类型(就是A)来调用对应的A析构函数。这还是没有什么问题,但为什么跟前面不同,这次能够全部成分析构完整呢?
        问题在于对应A析构函数是如何被找到的。在non-virtual版本里,在寻找A析构函数时,在虚函数表里面没有析构函数,所以直接找到了A析构函数的函数入口调用之。而在virtual版本里,A析构函数在虚函数表里存储其地址,当delete时进行析构函数的寻找就会找到虚函数表的某个偏移位置,重点就在于本质为派生类对象的aa所拥有的vptr指向的虚函数表是派生类版本的虚函数表,在本应是存储有A析构函数(入口地址)的位置早已经被B析构函数(入口地址)所覆盖,所以在”调用A析构函数“的时候实际上调用了B析构函数,完成了完整的析构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值