C++中多态与对象模型及菱形继承

本文详细探讨了C++中的多态性,包括静态多态和动态多态,重点介绍了动态多态的实现机制——虚函数和虚表。文章通过分析不同继承模式下(单继承、多继承、菱形继承)的对象模型,阐述了虚函数如何实现多态,以及虚函数的作用和使用注意事项。同时,文章提到了虚继承在解决菱形继承问题中的作用及其底层实现原理。
摘要由CSDN通过智能技术生成

多态

多态就是多种形态,C++的多态分为静态多态和动态多态。
1. 静态多态就是重载,因为是在编译期决议确定,所以称为静态多态。
2. 动态多态就是通过继承重写基类的虚函数实现的多态,因为是在运⾏时决议确定,所以称为动态多态。

上篇博客讲到继承 关于菱形继承的对象模型该文也有出现
需要的可以了解 继承与菱形继承

构成多态的条件

1,虚函数重写
(1)虚函数
(2)子类与父类虚函数的函数名,参数,返回值相同。
2,父类的指针或引用指向对象。

我们先看一下虚函数

虚函数

虚函数——类的成员函数前⾯加virtual关键字,则这个成员函数称为虚函数。
虚函数重写–当在⼦类的定义了⼀个与⽗类完全相同的虚函数时,则称⼦类的这个函数重写(或覆盖)了⽗类的这个虚函数。

```
class A//基类(父类)
{
public:
virtual void Fun()
{}
public :
 int _a ; 
};
//B为派生类(子类),public为继承关系
class B : public A
{
public:
virtual void Fun()
{}
public :
 int _b;
};
void Test()
{
B b;
b.Fun();
}

如上代码当我们调用b.Fun时,系统会调用子类的Fun()函数

虚函数的调用原则:
1,构成多态,跟对象有关。
2,不构成多态,与类型有关。

C++中虚函数的作用:实现多态

总结:
1. 派⽣类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
2. 基类中定义了虚函数,在派⽣类中该函数始终保持虚函数的特性。
3. 只有类的成员函数才能定义为虚函数。
4. 静态成员函数不能定义为虚函数。
5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
6. 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使⽤时容易引起混淆。
7. 不要在构造函数和析构函数⾥⾯调⽤虚函数,在构造函数和析构函数中,对象是不完整的,可能会发⽣未定义的⾏为。
8. 最好把基类的析构函数声明为虚函数。(why?另外析构函数⽐较特殊,因为派⽣类的析构函数跟基类的析构函数名称不⼀样,但是构成覆盖,这⾥是因为编译器做了特殊处理

虚表

虚表存放虚函数,作用:为了实现多态

class A
{
public:
    virtual void Func1()
    {
        cout <<" Func1" << endl;
    }
    virtual void Func2()
    {
        cout << "Func2" << endl;
    }
public:
    int _a;
};

房贷首付是

多态的对象模型——单继承

class A
{
public:
    virtual void Func1()
    {
        cout <<"A:: Func1()" << endl;
    }

public:
    int _a;
};
class B:public A
{
public:
    virtual void Func1()
    {
        cout <<"B::Func1()" << endl;
    }
public:
    int _b;
};


int main()
{

    A a;
    B b;
    A* p = &a;
    p->Func1();

    p = &b;
    p->Func1();
    system("pause");
    return 0;
}

我们可以看到输出结果为:
古典风格
Func1()为虚函数,但是只有有虚函数重写并且父类的指针或引用指向对象才能构成多态

在构成多态的情况下,看对象。
在没有构成多态的情况下,看类型。

#include<iostream>
#include<windows.h>
using namespace std;
class A
{
public:
    virtual void Func1()
    {
        cout <<"A:: Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "A:: Func2()" << endl;
    }

public:
    int _a;
};
class B:public A
{
public:
    virtual void Func1()
    {
        cout <<"B::Func1()" << endl;
    }
    virtual void Func3()
    {
        cout << "B::Func3()" << endl;
    }
    virtual void Func4()
    {
        cout << "B::Func4()" << endl;
    }
public:
    int _b;
};

//实现虚表打印地址
typedef void(*V_FUNC)();
void PrintVTable(int* vtable)
{
    printf("vtable:0x%p\n", vtable);
    int** ppVtable = (int**)vtable;
    for (size_t i = 0; ppVtable[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, ppVtable[i]);
        V_FUNC f = (V_FUNC)ppVtable[i];
        f();
    }
    cout << "----------------------------------" << endl;
}

int main()
{

    A a;
    B b;
    int* VTable1 = (int*)(*(int*)&a);
    int* VTable2 = (int*)(*(int*)&b);
    PrintVTable(VTable1);
    PrintVTable(VTable2);
    system("pause");
    return 0;
}

方式

我们打开监视窗口看到的是A的虚表存放的Func1(),Func2()
可以看到派⽣类B::Func1重写基类A::Func1,覆盖了相应虚表位置上的函数。
可以看到这⾥没有看到派⽣类B中的Func3和Func4,这两个函数就放在Func2的后⾯,这里看不到其他函数是因为编译器进行了优化,我们需要自己写虚表打印地址的函数才可以完整的看到
官方给对方

郭德纲的

多态的对象模型——多继承

#include<iostream>
#include<windows.h>
using namespace std;
class A
{
public:
    virtual void Func1()
    {
        cout <<"A:: Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "A::Func2()" << endl;
    }

public:
    int _a;
};
class B
{
public:
    virtual void Func1()
    {
        cout <<"B::Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "B::Func2()" << endl;
    }

public:
    int _b;
};
class C :public A, public B
{
public:
    virtual void Func1()
    {
        cout << "C::Func1()" << endl;
    }
    virtual void Func3()
    {
        cout << "C::Func3()" << endl;
    }


public:
    int _c;

};

typedef void(*V_FUNC)();
void PrintVTable(int* vtable)
{
    printf("vtable:0x%p\n", vtable);
    int** ppVtable = (int**)vtable;
    for (size_t i = 0; ppVtable[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, ppVtable[i]);
        V_FUNC f = (V_FUNC)ppVtable[i];
        f();
    }
    cout << "---------------------------------------------------" << endl;
}

int main()
{
    C d1;
    int* VTable = (int*)(*(int*)&d1);
    PrintVTable(VTable);
    // B虚函数表在对象A后面
    VTable = (int *)(*((int*)&d1 + sizeof (A) / 4));
    PrintVTable(VTable);

    system("pause");
    return 0;
}

方式发到

在子类C中出现了Func3(),那么大家可以看出来这个函数在A 的虚表中,这个跟继承的先后有关。

多态的对象模型——菱形继承

#include<iostream>
#include<windows.h>
using namespace std;
class A
{
public:
    virtual void f1()
    {
        cout <<"A:: f1()" << endl;
    }
    virtual void Func2()
    {
        cout << "A::f2()" << endl;
    }

public:
    int _a;
};
class B :public A
{
public:
    virtual void f1()
    {
        cout <<"B::f1()" << endl;
    }
    virtual void f3()
    {
        cout << "B::f3()" << endl;
    }
public:
    int _b;
};
class C :public A
{
public:
    virtual void f1()
    {
        cout << "C::f1()" << endl;
    }
    virtual void f4()
    {
        cout << "C::f4()" << endl;
    }



public:
    int _c;

};
class D :public B,public C
{
public:
    virtual void f1()
    {
        cout << "D::f1()" << endl;
    }

    virtual void f5()
    {
        cout << "D::f5()" << endl;
    }


public:
    int _d;

};
typedef void(*V_FUNC)();
void PrintVTable(int* vtable)
{
    printf("vtable:0x%p\n", vtable);
    int** ppVtable = (int**)vtable;
    for (size_t i = 0; ppVtable[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, ppVtable[i]);
        V_FUNC f = (V_FUNC)ppVtable[i];
        f();
    }
    cout << "---------------------------------------------------" << endl;
}

int main()
{
    D d;
    d.B::_a = 1;
    d._b = 2;
    d.C::_a = 3;
    d._c = 4;
    d._d = 5;
    cout << sizeof(B) << endl;
    PrintVTable(*((int**)&d));
    PrintVTable(*((int**)((char*)&d + sizeof(B))));
    system("pause");
    return 0;
}

发到

多态的对象模型——菱形虚拟继承

#include<iostream>
#include<windows.h>
using namespace std;
class A
{
public:
    virtual void Func1()
    {
        cout <<"A:: Func1()" << endl;
    }
    virtual void Func2()
    {
        cout << "A::Func2()" << endl;
    }

public:
    int _a;
};
class B :virtual public A
{
public:
    virtual void Func1()
    {
        cout <<"B::Func1()" << endl;
    }
    virtual void Func3()
    {
        cout << "B::Func3()" << endl;
    }
public:
    int _b;
};
class C :virtual public A
{
public:
    virtual void Func1()
    {
        cout << "C::Func1()" << endl;
    }
    virtual void Func4()
    {
        cout << "C::Func4()" << endl;
    }



public:
    int _c;

};
class D :public B,public C
{
public:
    virtual void Func1()
    {
        cout << "D::Func1()" << endl;
    }

    virtual void Func5()
    {
        cout << "D::Func5()" << endl;
    }


public:
    int _d;

};
typedef void(*V_FUNC)();
void PrintVTable(int* vtable)
{
    printf("vtable:0x%p\n", vtable);
    int** ppVtable = (int**)vtable;
    for (size_t i = 0; ppVtable[i] != 0; ++i)
    {
        printf("vtable[%d]:0x%p->", i, ppVtable[i]);
        V_FUNC f = (V_FUNC)ppVtable[i];
        f();
    }
    cout << "--------------------------------" << endl;
}

int main()
{
    D d;
    d.B::_a = 1;
    d._b = 2;
    d.C::_a = 3;
    d._c = 4;
    d._d = 5;
    cout << sizeof(B) << endl;
    //B
    PrintVTable(*((int**)&d));
    // C  
    PrintVTable(*((int**)((char*)&d + sizeof(B)-sizeof(A))));
    // A  
    PrintVTable(*((int**)((char*)&d + sizeof(D)-sizeof(A))));
    system("pause");
    return 0;
}

发

我们可以看到B,C中都存在虚基表用来解决菱形继承的二义性和数据冗余性。

而B,C,D中都有虚表用来存放虚函数。

练习题

下面是继承与多态的重要练习题

1)简述C++虚函数作⽤及底层实现原理
要点是要答出虚函数表和虚函数表指针的作用。
C++中虚函数使用虚函数表和 虚函数表指针实现,虚函数表是一个类的虚函数的地址表,用于索引类本身以及父类的虚函数的地 址,假如子类的虚函数重写了父类的虚函数,则对应在虚函数表中会把对应的虚函数替换为子类的 虚函数的地址;虚函数表指针存在于每个对象中(通常出于效率考虑,会放在对象的开始地址处), 它指向对象所在类的虚函数表的地址;在多继承环境下,会存在多个虚函数表指针,分别指向对应不同基类的虚函数表。

2)⼀个对象访问普通成员函数和虚函数哪个更快?
不一定,如果虚函数构成多态的条件,那么普通成员函数快
否则的话是一样的。

3)在什么情况下,析构函数需要是虚函数?
用父类指向new出的子类对象,要定义为虚函数。
最好把父类的析构函数定义为虚函数

A*p1=new AA*p1=new B;//若A,B不为虚函数,则都清理A,会造成内存泄漏

delete A;
delete B;

析构函数之所以可以写成虚函数是因为在编译器中析构函数名相同,并不是自己写的。

4)内联函数、构造函数、静态成员函数可以是虚函数吗?

都不可以。
内联函数需要在编译阶段展开,而虚函数是运行时动态绑定的,编译时无法展开;并且内联函数没有函数地址,不能是虚函数。

构造函数在进行调用时还不存在父类和子类的概念,父类只会调用父类的构造函数,子类调用子类 的,因此不存在动态绑定的概念。

虚函数是需要虚表的,在构造函数时对象是不完整的,虚表未完成,所以不能是虚函数。

静态成员函数是以类为单位的函数,与具体对象无关(静态成员函数可以不通过对象调用),虚函数是 与对象动态绑定的,所以不能是虚函数。

5)构造函数中可以调⽤虚函数吗?
不可以,当我们构造一个子类的对象时,先调用基类的构造函数,构造子类中基类部分,子类还没有构造,还没有初始化,如果在基类的构造中调用虚函数,如果可以的话就是调用一个还没有被初始化的对象,那是很危险的,所以C++中是不可以在构造父类对象部分的时候调用子类的虚函数实现。

6)简述C++中虚继承的作⽤及底层实现原理
虚继承用于解决多继承条件下的菱形继承的二义性和数据冗余问题。
底层实现原理与编译器相关,一般通过虚基类指针实现,即各对象中只保存一份父类的对象,多继承时通过虚基类指针引用该公共对象,从而避 免菱形继承中的二义性问题。

7)析构函数中可以调用虚函数吗?
不可以,在析构的时候会首先调用子类的析构函数,析构子类,然后在调用父类的析构函数析构父类,如果在父类的析构函数里面调用虚函数,会导致其调用已经析构了的子类对象里面的函数,这是非常危险的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值