C++多态浅析

 多态,字面意思就是多种形态,最初来源于希腊语,在C++中有着更加广泛的含义,是面向对象编程领域的核心概念。
 多态性可以简单地概括为“一个接口,多种方法。先来看看多态的分类:
 这里写图片描述
 
 来看个静态多态的例子:

int Add(int left, int right)
{
    return left + right;
}
char Add(char left, char right)
{
    return left + right;
}
void FunTest()
{
    cout << Add(1, 2) << endl;//调用int类型的Add函数
    cout << Add('1', '2') << endl;//调用char类型的Add的函数
}

静态多态是编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可以推断出要调用哪个函数,如果有对应的函数就调用该函数,否则出现编译错误。
我们主要来看看动态多态,实现动态多态需要两个条件:
1.在派生类中,必须对基类的虚函数进行重写
2.必须在继承的体系下,通过基类类型的引用或者指针调用虚函数
先来看一段代码:

class Base
{
public:
    void FunTest()
    {
        cout << "Base::FunTest()" << endl;
    }
private:
    int _b;
};
class Derived :public Base
{
public:
    void FunTest()
    {
        cout << "Derived::FunTest()" << endl;
    }
private:
    int _d;
};
int main()
{
    Derived d1;
    Base* pb = &d1;//隐式类型转换
    pb->FunTest();
    system("pause");
    return 0;
}

这里写图片描述
可以发现,首先我们定义了一个Derived类的对象d1,接着定义了一个指向Base类的指针变量pb,然后利用该变量调用pb->FunTest().估计很多人往往将这种情况和c++的多态性搞混,认为d1实际上是Derived类的对象,应该是调用Derived类的FunTest(),然而结果却不是.
c++编译器在编译的时候,要确定每个对象调用的函数(非虚函数)的地址,这称为早期绑定,当我们将Derived类的对象d1的地址赋给pb时,c++编译器进行了类型转换,此时c++编译器认为变量pb保存的就是Base对象的地址,当执行pb->FunTest(),调用的当然就是Base对象的FunTest函数。

将代码稍微改动一下,再来运行看看结果:

class Base
{
public:
    virtual void FunTest()
    {
        cout << "Base::FunTest()" << endl;
    }
private:
    int _b;
};
class Derived :public Base
{
public:
    void FunTest()
    {
        cout << "Derived::FunTest()" << endl;
    }
private:
    int _d;
};
int main()
{
    Derived d1;
    Base* b = &d1;
    d1.FunTest();
    system("pause");
    return 0;
}

这里写图片描述
我们发现结果是调用了Derived类的FunTest()函数,说明根据对象的类型调用了正确的函数。前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用动态绑定,动态绑定是在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
(注:当使用virtual关键字修饰类的成员函数时,指明该函数为虚函数,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。)
那么在把基类的函数声明为虚函数之后,发生了什么呢?
编译器在编译的时候,发现Father类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,在这个表中存放每个虚函数的地址。
下面我们对这个存放虚函数地址的虚表来剖析一下:

class Base
{
public:
    void FunTest()
    {
        cout << "Base::FunTest()" << endl;
    }
private:
    int _b;
};
class Base1
{
public:
    virtual void FunTest()
    {
        cout << "Base::FunTest()" << endl;
    }
private:
    int _b1;
};
int main()
{
    Base b;
    Base1 b1;
    cout << "Base类的大小为:" << sizeof(b) << endl;
    cout << "Base1类的大小为:" << sizeof(b1) << endl;
    system("pause");
    return 0;
}

这里写图片描述
根据程序及运行结果,我们可以看到Base1类比Base类多了四个字节,而Base类和Base1类唯一的区别就是Base1类中的成员函数为虚函数,根据下面的代码来看看这四个字节具体的内容

class Base1
{
public:
    Base1()
    {
        cout << "Base::FunTest()" << endl;
    }
    virtual ~Base1()
    {}
    int _b1;
};
int main()
{
    Base1 b1;
    b1._b1 = 1;
    system("pause");
    return 0;
}

这里写图片描述
我们可以看到,Base1类中的8个字节,前四个字节为虚表指针,后四个字节为成员变量_b1,虚表指针指向的地址中存储了虚函数~Base1()的地址。对于有虚函数的类,编译器都会维护一张虚表,对象的前四个字节就存放了指向虚表的指针。

我们再来看看不同情况虚表的变化:
1. 没有覆盖(单继承)
我们通过打印虚表的方式来看看

class Base
{
public:
    Base()
    {
        _b = 1;
    }
    virtual void FunTest1()
    {
        cout << "Base::FunTest1()" << endl;
    }
    virtual void FunTest2()
    {
        cout << "Base::FunTest2()" << endl;
    }
    int _b;
};
class Derived :public Base
{
public:
    virtual void FunTest3()
    {
        cout << "Derived::FunTest3()" << endl;
    }
    virtual void FunTest4()
    {
        cout << "Derived::FunTest4()" << endl;
    }
};
typedef void(*V_FUNC)();//重命名一个函数指针
void PrintVTable(int vtable)//打印虚表
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    Base b;
    Derived d;
    PrintVTable(*((int*)&b));//取对象b的地址强转为int*,然后解引用得到虚表中第一个地址
    PrintVTable(*((int*)&d));//同上
    system("pause");
    return 0;
}

这里写图片描述
通过这张图可以看出,派生类对象的对象模型如下图:
这里写图片描述
跟我们之前分析的一样,前四个字节存储的是虚表指针,后四个字节为Derived类的成员变量。根据打印出的虚表可以看出,派生类的虚表先继承了基类的虚函数,然后是自己的虚函数。而且继承下来的虚函数地址还是和基类虚函数的地址相同,这就是没有覆盖的情况。
2. 有覆盖(单继承)
直接上测试代码:

class Base
{
public:
    Base()
    {
        _b = 1;
    }
    virtual void FunTest1()
    {
        cout << "Base::FunTest1()" << endl;
    }
    virtual void FunTest2()
    {
        cout << "Base::FunTest2()" << endl;
    }
    int _b;
};
class Derived :public Base
{
public:
    virtual void FunTest1()//重写了基类的虚函数FunTest1()
    {
        cout << "Derived::FunTest1()" << endl;
    }
    virtual void FunTest3()
    {
        cout << "Derived::FunTest3()" << endl;
    }
    virtual void FunTest4()
    {
        cout << "Derived::FunTest4()" << endl;
    }
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    Base b;
    Derived d;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&d));
    system("pause");
    return 0;
}

这里写图片描述
这段代码中,我们在派生类中重写了基类的虚函数FunTest1(),随之打印出来的派生类的虚表就发生了变化
这里写图片描述
派生类重写的FunTest1()调用了派生类的,而没有重写的按照原样继承下来,再把派生类自己的虚函数加在后面。
根据这两个例子可以总结一下单继承下,派生类虚表的生成过程:
1、把基类的虚函数拷贝一份
2、检测派生类中是否对基类的虚函数进行了重写,用派生类中重写的虚函数覆盖相同偏移量位置的基类的虚函数
3、在最后添加派生类自己的虚函数
(注:通过基类的引用或指针调用虚函数时,是根据运行时引用或指针实际引用或指向的对象类型来确定调用的是基类还是派生类的虚函数,而调用非虚函数时,无论基类指向的是什么类型,都调用的是基类的函数。)

3.多继承(没有覆盖)

class B
{
public:
    B()
    {
        _b = 1;
    }
    virtual void FunTest1()
    {
        cout << "B::FunTest1()" << endl;
    }
    int _b;
};
class C
{
public:
    C()
    {
        _c = 2;
    }
    virtual void FunTest2()
    {
        cout << "C::FunTest2()" << endl;
    }
    int _c;
};
class D :public B,public C
{
public:
    virtual void FunTest3()
    {
        cout << "D::FunTest3()" << endl;
    }
    virtual void FunTest4()
    {
        cout << "D::FunTest4()" << endl;
    }
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    C c;
    D d;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&c));
    PrintVTable(*((int*)&d));
    PrintVTable(*((int*)&d+2));//由剖析的对象模型可以知道d中有两个虚表在这里分别打印
    system("pause");
    return 0;
}

这里写图片描述
那根据内存窗口来看看派生类对象在内存中是如何存储的?
这里写图片描述
可以看到在派生类对象中有两个虚表指针,再来看看这两个虚表中的虚函数分别是什么,并与我们打印出来的虚表进行对比
这里写图片描述
我们会发现,第一个虚表指针打印出来的虚表不但有B类的虚函数还有派生类的虚函数,下来再看看有覆盖的情况
4.多继承(有覆盖)

class B
{
public:
    B()
    {
        _b = 1;
    }
    virtual void FunTest0()
    {
        cout << "B::FunTest0()" << endl;
    }
    virtual void FunTest1()
    {
        cout << "B::FunTest1()" << endl;
    }
    int _b;
};
class C
{
public:
    C()
    {
        _c = 2;
    }
    virtual void FunTest2()
    {
        cout << "C::FunTest2()" << endl;
    }
    virtual void FunTest3()
    {
        cout << "C::FunTest3()" << endl;
    }
    int _c;
};
class D :public B,public C
{
public:
    D()
    {
        _d = 3;
    }
    virtual void FunTest0()//重写B类的FunTest0()函数
    {
        cout << "D::FunTest0()" << endl;
    }
    virtual void FunTest3()//重写C类的FunTest3()函数
    {
        cout << "D::FunTest3()" << endl;
    }
    virtual void FunTest4()//派生类D自己的虚函数
    {
        cout << "D::FunTest4()" << endl;
    }
    int _d;
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    C c;
    D d;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&c));
    PrintVTable(*((int*)&d));
    PrintVTable(*((int*)&d+2));//由剖析的对象模型可以知道d中有两个虚表在这里分别打印
    system("pause");
    return 0;
}

这里写图片描述
观察代码,我们可以发现在派生类D中,我们对FunTest0()和FunTest3()进行了重写,打印出来的虚表就发生了变化。
综上,可以得出多继承下,派生类虚表的生成过程
1.拷贝基类的虚表
2.检测派生类中是否对基类的虚函数进行重写,用重写的虚函数替换基类的虚函数
3.先继承的基类虚表在前,将派生类自己的虚函数放在第一张虚表后面
菱形继承其实是一种特殊的多继承,所以菱形继承有覆盖和无覆盖的情况我们在这里不做剖析,参考多继承虚表生成过程即可

5.菱形虚拟继承(无覆盖)

class B
{
public:
    B()
    {
        _b = 1;
    }
    virtual void FunTest0()
    {
        cout << "B::FunTest0()" << endl;
    }
    int _b;
};
class C1:virtual public B
{
public:
    C1()
    {
        _c1 = 2;
    }
    virtual void FunTest1()
    {
        cout << "C1::FunTest1()" << endl;
    }
    int _c1;
};
class C2:virtual public B
{
public:
    C2()
    {
        _c2 = 2;
    }
    virtual void FunTest2()
    {
        cout << "C2::FunTest2()" << endl;
    }
    int _c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        _d = 3;
    }
    virtual void FunTest3()
    {
        cout << "D::FunTest3()" << endl;
    }
    int _d;
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    C1 c1;
    C2 c2;
    D d;
    cout << "派生类D的大小:" << sizeof(D) << endl;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&c1));
    PrintVTable(*((int*)&c2));
    PrintVTable(*((int*)&d));//由剖析的对象模型可以知道d中有三个虚表在这里分别打印
    PrintVTable(*((int*)&d+3));
    PrintVTable(*((int*)&d+7));
    system("pause");
    return 0;
}

在程序中我们先看了一下派生类D的大小,发现为36个字节,来看看这36个字节在内存中的存储的是什么?
这里写图片描述
观察内存窗口我们可以发现,在C1类和C2类中,都有两个类似于地址的东西,根据之前剖析菱形虚拟继承的对象模型我们可以知道,这两个地址一个是虚表指针,一个是偏移量表格。我们可以在内存窗口中看看这两个地址的内容并和打印出的虚表进行对比,如下图,就可以得知上面的为虚表指针,下面的是偏移量表格(这里只对C1的进行分析,C2的与之类似)
这里写图片描述

6.菱形虚拟继承(有覆盖)

class B
{
public:
    B()
    {
        _b = 1;
    }
    virtual void FunTest0()
    {
        cout << "B::FunTest0()" << endl;
    }
    int _b;
};
class C1:virtual public B
{
public:
    C1()
    {
        _c1 = 2;
    }
    virtual void FunTest1()
    {
        cout << "C1::FunTest1()" << endl;
    }
    virtual void FunTest2()
    {
        cout << "C1::FunTest2()" << endl;
    }
    int _c1;
};
class C2:virtual public B
{
public:
    C2()
    {
        _c2 = 2;
    }
    virtual void FunTest3()
    {
        cout << "C2::FunTest3()" << endl;
    }
    virtual void FunTest4()
    {
        cout << "C1::FunTest4()" << endl;
    }
    int _c2;
};
class D :public C1, public C2
{
public:
    D()
    {
        _d = 3;
    }
    virtual void FunTest1()//重写C1类的FunTest1()函数
    {
        cout << "D::FunTest1()" << endl;
    }
    virtual void FunTest4()//重写C2类的FunTest4()函数
    {
        cout << "D::FunTest4()" << endl;
    }
    virtual void FunTest5()
    {
        cout << "D::FunTest5()" << endl;
    }
    int _d;
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    C1 c1;
    C2 c2;
    D d;
    cout << "派生类D的大小:" << sizeof(D) << endl;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&c1));
    PrintVTable(*((int*)&c2));
    PrintVTable(*((int*)&d));//由剖析的对象模型可以知道d中有三个虚表在这里分别打印
    PrintVTable(*((int*)&d+3));
    PrintVTable(*((int*)&d+7));
    system("pause");
    return 0;
}

这里写图片描述
和之前类似,有覆盖时打印出的虚表,派生类重写的虚函数将会覆盖基类的虚函数
可以发现,菱形虚拟继承和多继承虚表形成不一样的地方就是多了一个偏移量表格

7.虚继承
C++使用虚继承,解决了从不同路径继承来的相同基类的数据成员在内存中有不同的拷贝造成数据不一致的问题,将共同基类设置为虚基类,这时从不同路径继承的虚基类在内存就只有一个映射。

class B
{
public:
    virtual void FunTest0()
    {
        cout << "B::FunTest0()" << endl;
    }
    int _b;
};
class D :virtual public B
{
public:
    virtual void FunTest0()
    {
        cout << "D::FunTest0()" << endl;
    }
    virtual void FunTest1()
    {
        cout << "D::FunTest1()" << endl;
    }
    int _d;
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    b._b = 1;
    D d;
    d._d = 2;
    cout << "派生类D的大小:" << sizeof(D) << endl;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&d));
    PrintVTable(*((int*)&d+3));
    system("pause");
    return 0;
}

这里写图片描述
根据监视窗口,可以发现派生类的对象模型好像是基类在前,后面跟着派生类自己的虚表和成员变量,那我们去内存中实际看一下
这里写图片描述
从内存窗口我们可以看到D的对象模型并不是之前想的那样,而是把基类放在了派生类下面,而且我们可以发现此时派生类的大小是20。
对代码稍微做出改动再来看看结果:

class B
{
public:
    virtual void FunTest0()
    {
        cout << "B::FunTest0()" << endl;
    }
    int _b;
};
class D :virtual public B
{
public:
    D()
    {}
    virtual void FunTest0()
    {
        cout << "D::FunTest0()" << endl;
    }
    virtual void FunTest1()
    {
        cout << "D::FunTest1()" << endl;
    }
    int _d;
};
typedef void(*V_FUNC)();
void PrintVTable(int vtable)
{
    int* vf_array = (int *)vtable;
    printf("vtable:0x%p\n", vtable);
    for (size_t i = 0; vf_array[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, vf_array[i]);
        V_FUNC f = (V_FUNC)vf_array[i];
        f();
    }
    printf("---------------------------------------\n");
}
int main()
{
    B b;
    D d;
    d._b = 1;
    d._d = 2;
    cout << "派生类D的大小:" << sizeof(D) << endl;
    PrintVTable(*((int*)&b));
    PrintVTable(*((int*)&d));
    PrintVTable(*((int*)&d+4));
    system("pause");
    return 0;
}

这里写图片描述
我们在派生类中加入了构造函数,发现派生类的大小多了四个字节,在内存窗口中看看多的是什么
这里写图片描述
跟之前的对比,我们发现多出的四个字节存放了0,而且分隔了派生类和基类。读者可以自己根据反汇编查看,就会发现这个0并没有做什么工作,所以就认为它起分隔作用。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值