本章内容旨在解决以下几个问题:

  1. 什么是 C++ 多态, C++ 多态的实现原理是什么
  2. 什么是虚函数,虚函数的实现原理是什么
  3. 什么是虚表,虚表的内存结构布局如何,虚表的第一项(或第二项)是什么
  4. 菱形继承(类 D 同时继承 B 和 C,B 和 C 又继承自 A)体系下,虚表在各个类中的布局如何,如果类 B 和类 C 同时有一个成员变了 m,m 如何在 D 对象的内存地址上分布的,是否会相互覆盖
  5. 存在虚函数的类对象size计算

什么是 C++ 多态, C++ 多态的实现原理是什么

在 C++ 程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中,一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法);也就是说,每个对象可以用自己的方式去响应共同的消息所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。换言之,可以用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。在C++中主要分为静态多态和动态多态两种,在程序运行前就完成联编的称为静态多态,主要通过函数重载和模板实现,动态多态在程序运行时才完成联编,主要通过虚函数实现。

函数重载示例:

void print_hello(string name){
    cout << "hello " << name << endl;;
}

void print_hello(string pre_, string name){
    cout << "hello, " << pre_ << " " << name << endl;
}

int main(){
    print_hello("fan");
    print_hello("Mr.", "fan");
    return 0;
}
// out
/*
hello fan
hello, Mr. fan
*/

函数模板示例:

template <class T>
T add_two_num(T a, T b){
    return a + b;
}

int main(){
    cout << add_two_num<int>(1, 2) << endl;
    cout << add_two_num<float>(1.0, 2.0) << endl;
    cout << add_two_num(1.0, 2.0) << endl; // 编译器自动推断
    // cout << add_two_num(1, 2.0) << endl; // error
    int va = 10;
    cout << add_two_num<decltype(va)>(va, 2.0) << endl;
    return 0;
}
// out
/*
3
3
3
12
*/

虚函数示例:

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

class B:public A{
public:
    virtual void fun(){
        cout << "hello B" << endl;
    }
};

int main(){
    A *a = new A();
    a->fun();
    a = new B();
    a->fun();
    return 0;
}
// out
/*
hello A
hello B
*/

运行期多态实现原理

对于一个继承体系来说,如果在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。运行期多态就是通过虚函数和虚函数表实现的。一个含有虚函数的类中至少都有一个虚函数表指针,且有一个虚表,虚函数指针指向虚函数表。虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

什么是虚函数,虚函数的实现原理是什么

直观上来说,虚函数就是类中使用 virtual 关键字描述的函数。虚函数的作用主要是实现了多态的机制,基类定义虚函数,子类可以重写该函数;在派生类中对基类定义的虚函数进行重写时,需要在派生类中声明该方法为虚方法,否则将会形成覆盖。虚函数的底层实现机制基于虚函数表+虚表指针。
编译器处理虚函数的方法是:为每个类对象添加一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,称为虚表指针(vptr),这种数组成为虚函数表(virtual function table, vtbl),即,每个类使用一个虚函数表,每个类对象用一个虚表指针。如果派生类重写了基类的虚方法,该派生类虚函数表将保存重写的虚函数的地址,而不是基类的虚函数地址。如果基类中的虚方法没有在派生类中重写,那么派生类将继承基类中的虚方法,而且派生类中虚函数表将保存基类中未被重写的虚函数的地址。注意,如果派生类中定义了新的虚方法,则该虚函数的地址也将被添加到派生类虚函数表中,虚函数无论多少个都只需要在对象中添加一个虚函数表的地址。调用虚函数时,程序将查看存储在对象中的虚函数表地址,转向相应的虚函数表,使用类声明中定义的第几个虚函数,程序就使用数组的第几个函数地址,并执行该函数。

详细请参考

什么是虚表,虚表的内存结构布局如何,虚表的第一项(或第二项)是什么

对于每个存在虚函数的类来说,其都含有一个虚函数表与至少一个虚指针。虚函数表指针(vfptr)指向虚函数表(vftbl)的某一项,虚函数表中按照对象继承的顺序排列对象的虚函数地址,虚基类表中按照对象继承的顺序排列对象的直接虚继承类到虚基类的偏移。

对于虚基类来说,虚表中按声明顺序依次保存所有虚函数地址。

class Base {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
int main(){
    Base *b = new Base();
}

虚表示例:

C++多态-LMLPHP

表中最后的一个点表示虚函数结束标志

一般继承的情况下,虚函数按照其声明顺序放于表中,且父类的虚函数在子类的虚函数前面。

class Derive: public Base{
public:
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
    virtual void h1() { cout << "Base::h" << endl; }
};

C++多态-LMLPHP

一般继承且存在虚函数覆盖的情况,覆盖的虚函数将被放到虚表中原来父类虚函数的位置,没有被覆盖的函数按之前的顺序保存,最后在表中添加子类新加的虚函数地址。

class Derive: public Base{
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
    virtual void h1() { cout << "Base::h" << endl; }
};

C++多态-LMLPHP

多重继承(无虚函数覆盖)时,每个父类都有自己的虚表,且子类的成员函数被放到了第一个父类的表中。

class Base1 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Base2 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Base3 {
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g() { cout << "Base::g" << endl; }
    virtual void h() { cout << "Base::h" << endl; }
};
class Derive: public Base1, public Base2, public Base3{
public:
    virtual void f1() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
};

C++多态-LMLPHP

多重继承(有虚函数覆盖)时,父类虚表中对应的虚函数地址将被子类的虚函数地址覆盖,子类新加的虚函数地址将被添加到第一个父类的虚函数表之后。

class Derive: public Base1, public Base2, public Base3{
public:
    virtual void f() { cout << "Base::f" << endl; }
    virtual void g1() { cout << "Base::g" << endl; }
};

C++多态-LMLPHP

综上,虚表的第一项(或第二项)是父类或者子类声明的第一(二)个虚函数地址。

菱形继承(类 D 同时继承 B 和 C,B 和 C 又继承自 A)体系下,虚表在各个类中的布局如何,如果类 B 和类 C 同时有一个成员变了 m,m 如何在 D 对象的内存地址上分布的,是否会相互覆盖

虚表会同时存在两个A,内存分布与多继承一致,即存在两个虚指针指向两个父类虚表(B, C),B和C的虚表中又同时存在A的虚表。虚表内存模型如下:

class Base
{
public:
    Base (int a = 1):base(a){}
    virtual void fun0(){cout << base << endl;}
    int base;
};
class Base1:public Base
{
public:
    Base1 (int a = 2):base1(a){}
    virtual void fun1(){cout << base1 << endl;}
    int base1;
};
class Base2:public Base
{
public:
    Base2 (int a = 3):base2(a){}
    virtual void fun2(){cout << base2 << endl;}
    int base2;
};
class Derive: public Base1, public Base2
{
public:
    Derive (int value = 4):derive (value){}
    virtual void fun3(){cout << derive << endl;}
    int derive;
};

C++多态-LMLPHP

首先给出结论,不会互相覆盖,不过直接使用 m 的话将会造成二义性问题,这是可以使用类名+引用的方式进行调用

class A{
};

class B:public A{
public:
    int n;
    B(){
        A();
        n = 2;
    }
};

class C: public A{
public:
    int n;
    C(){
        A();
        n = 3;
    }
};

class D: public B, public C{
public:
    void fun(){
        cout << B::n << endl;
        cout << C::n << endl;
        cout << sizeof(D);
    }
};

int main(){
    D d;
    d.fun();
    return 0;
}
// out
/*
2
3
8
*/

内存分布为

存在虚函数的类对象size计算

空类的大小为1,因为在C++中任何对象都需要有一个地址,最小为1。对于存在虚函数的类来说,至少存在一个虚函数指针,指针大小与机器相关(int),在64位的机器上,应为8字节,在32位的机器上为4字节。在进行计算的时候还要注意1. 不同的数据类型会进行对齐 2.对于多重继承,多重继承几个基类就有几个虚指针。

class A{
    int n;
};

class B{
    int n;
    double m;
};

class C{
    int n;
    int l;
    double m;
};

class D {
    int n;
    double m;
    int l;
};

int main(){
    A a;
    B b;
    C c;
    D d;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d);
    return 0;
}
// out
/*
4
16 // int和double对齐 (4->8)
16
24 // n向m对齐,然后l和m对齐
*/
class A {
    virtual void fun() {

    }
};

class B {
    virtual void fun() {

    }
};

class C : A, B {
    virtual void fun() {

    }
};

class D : A {
    virtual void fun() {

    }
};

class E : C {
    virtual void fun() {

    }
};

int main() {
    A a;
    B b;
    C c;
    D d;
    E e;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d) << " " << sizeof(e) << endl;
}
// out
/*
4
4
8
4
8
*/

一个含有虚函数的类中含有的虚函数表指针个数

一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中。当存在多重继承时,多重继承了几个基类,子类将含有几个虚指针,并且此指针具有传递性。

class A {
    virtual void fun() {

    }
};

class B {
    virtual void fun() {

    }
};

class C : A, B {
    virtual void fun() {

    }
};

class D : A {
    virtual void fun() {

    }
};

class E : C {
    virtual void fun() {

    }
};

int main() {
    A a;
    B b;
    C c;
    D d;
    E e;
    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << " " << sizeof(d) << " " << sizeof(e) << endl;
}
// out
/*
4
4
8
4
8
*/

参考链接

后台开发:核心技术与应用实践 -- C++

C++之多态性

C++函数模板

C++虚函数和虚函数表原理

C++ | 虚函数表内存布局

虚函数实现原理

c++中虚基类表和虚函数表的布局

c++继承汇总(单继承、多继承、虚继承、菱形继承)

C++继承内存布局 - 多继承(无虚继承)

05-18 07:05