c++虚函数及其内存布局、静态绑定与动态绑定与多态

本文详细探讨了C++中虚函数的内存布局,包括虚函数表的创建、虚指针的存在以及静态绑定与动态绑定的区别。还介绍了多态的概念,如何在继承结构中实现函数重载和虚函数的覆盖。
摘要由CSDN通过智能技术生成

虚函数

普通类的内存布局

在说虚函数之前,我们首先来看一个普通类的内存布局,我们仍然以上述代码为例:


class Base
{
public:
    Base(int data=10):m_a(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};
 
class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

在测试代码里,我们分别定义父类对象和子类对象

void test()
{
    Base b;
    Derive d;
}

1.编译代码并启动GDB进行调试

2.给程序打断点之后运行

3.分别查看基类对象b和子类对象d的内存情况显示如下

也就是下图分布情况

完整的gdb调试信息如下:

GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...done.
(gdb) l
17          Derive(int data=20):Base(data),m_b(data){}
18          void show(){cout<<"Derive:show()"<<endl;}
19      private:
20          int m_b;
21      };
22
23      void test()
24      {
25          Base b;
26          Derive d;
(gdb) b 27
Breakpoint 1 at 0x7e3: file main.cpp, line 27.
(gdb) r
Starting program: /home/pcl/cProject/unifile/main

Breakpoint 1, test () at main.cpp:27
27      }
(gdb) p b
$1 = {m_a = 10}
(gdb) p d
$2 = {<Base> = {m_a = 20}, m_b = 20}
(gdb)

这与我们预想的一样。

有虚函数的类的内存布局

接下来,我们对Base类的代码做一点小小的改动

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

在Base类的成员函数前加一个virtual关键字,此时Base的成员函数就被称之为虚函数

测试代码

void test()
{
    Base b;
}

1.重新编译代码并启动GDB进行调试

g++ -o main main.cpp -g
gdb main

可以看到,Base类型的对象b的内存布局里,在加上虚函数之后,多了一个虚函数指针_vptr,也就是下图

并且可以看到,这个虚函数指针的值为

0x555555755d50

我们先记住这个值,接下来我们使用GDB查看一下虚函数表的信息,其gdb命令为

info vtbl b

查看结果

(gdb) info vtbl b
vtable for 'Base' @ 0x555555755d50 (subobject @ 0x7fffffffe4d0):
[0]: 0x555555554bae <Base::show()>
[1]: 0x555555554be6 <Base::show(int)>

由结果可以看到,虚函数表的内存地址(红色框)与刚才我们打印的虚函数指针所保存的地址完全相同,除此以外,我们在这个虚函数表里也看到了我们在Base类里定义的两个虚函数地址(蓝色框)

也就是说,

如果一个类里有虚函数,那么该类所生成的对象内存布局里会多一个虚函数指针,其指针值就是虚函数表的地址,而这个虚函数表里保存着虚函数的地址信息

接下来,我们在测试代码里再加一个Base类对象,并重新编译调试

void test()
{
    Base b;
    Base b1;
}

查看调试结果 

可以看到,对象b和对象b1的虚函数指针所保存的虚函数表的地址完全相同,如图所示

也就是说

对于具有虚函数的类,只有一个虚函数表,每个实例化出来的对象都有一个虚函数指针,都指向这唯一的虚函数表

有虚函数的类的内存占用大小

我们再来看有虚函数的类的内存占用大小

void test()
{
    Base b;
    Base b1;
    cout<<"Base size:"<<sizeof(Base)<<endl;
}

再次观察上述修改后的代码,在测试函数中查看运行结果

可以看到,Base的大小是16B,而不再是4B,其原因就在于,Base类中除了有一个int类型的成员变量外还有一个占8B的vfptr,又根据内存对齐原则,故而Base类的大小就是8(vfptr)+4(int)+4(内存对齐)=16B 

总结

一个类里如果定义了虚函数,那么

  • 编译期间,编译器就会为该类产生一个唯一的vftable虚函数表
  • 该vftable虚函数表中主要存储的内容就是RTTI指针和虚函数的地址
  • 当程序运行时,每一张虚函数表都被加载到rodata区.(只读)

注:RTTI(run-time type infomation),即运行时的类型信息

以上Base类的虚函数表(vftable)为

除此以外,如果一个类里定义了虚函数,那么

  • 该类所定义的对象,其运行时,内存中的开始部分,会多存储一个vfptr虚函数指针,指向该类的虚函数表(存储该虚函数表的首地址)
  • 该类所定义的每个对象,都会有一个vfptr指针,但虚函数表只有一张

 

因此,一个类里虚函数的个数,不影响内存的大小(对象内存中只有一个虚函数指针vfptr),影响的是虚函数表的大小 

此外,如果派生类中的某个成员函数和基类中某个成员函数完全相同(包括函数名、函数类型),只有函数体的实现不同,那么该成员函数也将自动被处理为虚函数。

覆盖

子类不重写虚函数的内存布局

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};
 
class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    // void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

测试代码

void test()
{
    Base b;
    Derive d;
}

编译并启动GDB调试查看

观察结果可以看到

  • 子类与父类各自维护一张虚函数表
  • 子类不重写虚函数的情况下,子类的虚函数表与父类相同

子类重写虚函数的内存布局

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};
 
class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

测试代码

void test()
{
    Base b;
    Derive d;
}

同理,重新编译并启动GDB调试,观察结果

可以看到,当子类重新虚函数之后,子类的虚函数表中的虚函数就会将父类的虚函数进行覆盖

 即,

静态绑定

首先说明,所谓绑定,就是指函数的调用

接下来,我们直接看一段代码来说明问题

class Base
{
public:
    Base(int data=10):m_a(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

上述代码中,定义了一个Base类和一个Derive类,并且Derive类继承了Base类,其中

Base类中有一组互为重载关系的成员函数show

Derive类中有一个与Base类中同名的成员函数,因此,Derive::show()与Base::show()、Base::show(int)构成了隐藏关系。

接下来,我们写一段测试代码来说明问题,这段测试代码包括

  • 定义一个基类指针,并指向其子类对象
  • 并使用基类指针调用show成员函数,观察运行结果
  • 查看基类指针的类型和基类指针所指对象的类型
void test()
{
    Derive d(50);
    Base* pb=&d;
    pb->show();
    pb->show(11);

    cout<<"Base size:"<<sizeof(Base)<<endl;
    cout<<"Derive size:"<<sizeof(Derive)<<endl;

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;
}

 

通过实验结果可以看到,尽管基类指针(Base* pb)指向的是子类对象,但是通过pb所调用的函数仍旧是基类作用域下的成员函数。

静态绑定就是指在编译期间就确定好了函数的具体实现版本,由于pb的类型是Base*(也被称为静态类型),因此通过pb所调用的成员函数show在编译期间就被确定在Base作用域下的show成员函数

  • 静态绑定适用于非虚函数和静态函数
  • 静态绑定中,函数调用的实现版本在编译期间就已经确定,无法在运行期间改变

为加深理解,我们再写一例

class Base {
public:
    void func() {
        cout << "Base::func()" << endl;
    }
};

class Derived : public Base {
public:
    void func() {
        cout << "Derived::func()" << endl;
    }
};

int main() {
    Base b;
    Derived d;

    b.func(); // 静态绑定,调用 Base::func()
    d.func(); // 静态绑定,调用 Derived::func()

    Base* p = &d;
    p->func(); // 静态绑定,调用 Base::func(),因为 p 的静态类型是 Base*
    return 0;
}

多态与动态绑定

知道了上述原理之后,接下来,我们修改测试代码,来看看多态的实现原理

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

void test()
{
    Derive d(50);
    Base* pb=&d;
    pb->show();
    pb->show(11);

    cout<<"Base size:"<<sizeof(Base)<<endl;
    cout<<"Derive size:"<<sizeof(Derive)<<endl;

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;
}

接下来,我们再来看测试代码中这行代码的运行结果的差异

    pb->show();

可以看到,在成员函数show没有被声明为virtual之前,该行代码执行的是Base::show(),而当其被声明为虚函数后,执行结果就成为了Derive::show()

这是因为,代码在执行到pb->show()时,如果发现show不是虚函数,就进行静态绑定,如果发现show是虚函数,就进行动态绑定。

所谓动态绑定,实质上是因为其汇编过程为

mov eax dword ptr[pb]
mov ecx dword ptr[eax]
call ecx(虚函数的地址)
  • 第一行汇编代码执行:将指针pb指向的地址(虚函数表的首地址)放到寄存器eax中
  • 第二行汇编代码执行:将eax中的前四个字节的地址(也就是对应show()函数的地址)放到ecx寄存器中
  • 第三行汇编代码执行:执行ecx寄存器中的代码

从上述汇编过程可以看到,由于我们执行的是ecx寄存器中的代码,但是ecx中保存的地址需要等到运行时期才能确定。

这种在程序运行时需要根据对象的实际类型来确定调用哪个方法或函数的机制就叫动态绑定

接下来,我们再来看指针pb和*pb的类型变化

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;

可以看到,

  • pb的类型:无论是否有虚函数,基类指针pb的类型永远都是Base
  • *pb的类型:
    • 如果Base有虚函数,*pb识别的就是运行时期的类型(RTTI类型)
    • 如果Base没有虚函数,*pb识别的就是编译时期的类型(Base类型)

多态总结

有了上述的说明后,我们可以进行如下总结:

  • 多态可分为静态多态和动态多态
  • 静态(编译时期)多态有:函数重载、模版
  • 动态多态(运行时期)多态是指,在继承结构中,当基类指针(引用)指向(引用)派生类对象时,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就调用哪个派生类对象的覆盖放啊
  • 用对象本身调用虚函数,是静态绑定
  • 动态绑定必须有指针或者引用调用虚函数,只要有基类指针,无论该指针是指向基类还是子类,都是动态绑定

 

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值