C++ 多态的实现原理与内存模型

本文详细解析了C++中多态的概念、实现原理及其应用,通过实例展示了如何利用虚函数机制实现多态,并解释了父类指针指向子类实例时的调用过程,以及多态在数组释放时可能导致的问题。文章最后提供了关键知识点的总结,帮助读者深入理解C++多态的精髓。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  多态在C++中是一个重要的概念,通过虚函数机制实现了在程序运行时根据调用对象来判断具体调用哪一个函数。

     具体来说就是:父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。在每个包含有虚函数的类的对象的最前面(是指这个对象对象内存布局的最前面)都有一个称之为虚函数指针(vptr)的东西指向虚函数表(vtbl),这个虚函数表(这里仅讨论最简单的单一继承的情况,若果是多重继承,可能存在多个虚函数表)里面存放了这个类里面所有虚函数的指针,当我们要调用里面的函数时通过查找这个虚函数表来找到对应的虚函数,这就是虚函数的实现原理。注意一点,如果基类已经插入了vptr, 则派生类将继承和重用该vptr。vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的。

  以上这些概念都是C++程序员很熟悉的,下面通过一些具体的例子来强化一下对这些概念的理解。

1. 

#include<iostream>
using namespace std;

class IRectangle
{
public:
    virtual ~IRectangle() {}
    virtual void Draw() = 0;
};

class Rectangle: public IRectangle
{
public:
    virtual ~Rectangle() {}
    virtual void Draw(int scale)
    {
        cout << "Rectangle::Draw(int)" << endl;
    }
    virtual void Draw()
    {
        cout << "Rectangle::Draw()" << endl;
    }
};

int main(void)
{
    IRectangle *pI = new Rectangle;
    pI->Draw();
    pI->Draw(200);
    delete pI;
    return 0;
}

  该段代码编译失败:

C:\Users\zhuyp\Desktop>g++ -Wall test.cpp -o test -g

test.cpp: In function 'int main()':
test.cpp:29:17: error: no matching function for call to 'IRectangle::Draw(int)'
pI->Draw(200);
^
test.cpp:29:17: note: candidate is:
test.cpp:8:18: note: virtual void IRectangle::Draw()
virtual void Draw() = 0;
^
test.cpp:8:18: note: candidate expects 0 arguments, 1 provided

C:\Users\zhuyp\Desktop>

  以上信息表明,在父类IRectangle中并没有Draw(int)这个函数。确实,在父类IRectangle中没有这样签名的函数,但是不是多态吗,new 的不是子类Rectangle吗?我们注意到指针 pI 虽然指向子类,但是本身确是父类 IRectangle 类型,因此在执行 pI->draw(200)的时候查找父类vtable,父类的vtable 中没有Draw(int)类型的函数,因此编译错误。

  如果将 pI->draw(200) 这一句修改,将pI进行一个down cast 则编译正常,dynamic_cast<Rectangle *>(pI)->draw(200); 此时调用的是子类的指针,查找的是子类的vtable,该vtable中有签名为 draw(int) 的函数,因此不会有问题。

2.

#include <iostream>
using namespace std;

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

class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    virtual void fun()
    {
        cout << "Derived::fun()"  << endl;
    }
};

int main()
{
    Derived *dp = new Derived;
    Base *p = dp;
    p->fun();
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;
    cout << (void *)dp << endl;
    cout << (void *)p << endl;
    delete p;
    p = NULL;

    return 0;
}

  编译并运行程序:

C:\Users\zhuyp\Desktop>test.exe
Base::fun()
1
8
0x3856a0
0x3856a0
~Base()

  编译器使用的是gcc4.8.1 可以看出 p 和 pb 的值是相同的,因此可以得出结论,现代C++编译器已经没有为了性能的问题将vptr指针放在类内存模型的最前面了。

3.

#include<iostream>
using namespace std;

class B
{
    int b;
public:
    virtual ~B()
    {
        cout << "B::~B()" << endl;
    }
};

class D: public B
{
    int i;
    int j;
public:
    virtual ~D()
    {
        cout << "D::~D()" << endl;
    }
};

int main(void)
{
    cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl;
    char *ch = NULL;
    
    B *pb = new D[2];

    cout<<"size *pb "<<sizeof(pb)<<"\tend"<<endl;
    
    delete [] pb;

    return 0;
}

  程序运行出错,在输出 pb 的大小之后。可见是在delete [] pb 的时候出了问题。

  我们知道释放申请的数组空间的时候需要使用 delete [] ,那 delete 怎么知道要释放多大的内存呢?delete[]  的实现包含指针的算术运算,并且需要依次调用每个指针指向的元素的析构函数,然后释放整个数组元素的内存。

  由于C++中多态的存在,父类指针可能指向的是子类的内存空间。由于上面的例子中delete [] 释放的是多态数组的空间,delete[] 计算空间按照 B 类的大小来计算,每次偏移调用析构函数是按照B类来进行的,而该数组实际上存放的是D类的指针释放的大小不对(由于 sizeof(B) != sizeof(D) ,),因此会崩溃。

C:\Users\zhuyp\Desktop>test.exe
sizeB:16 sizeD:24
size *pb 8 end

注意:本代码在64bit环境中执行的,因此 *pb 是 8.

 

<think>好的,我现在需要回答用户关于C++多态实现机制的问题,特别是虚函数的作用。首先,我需要回忆多态的基本概念,分为编译时多态和运行时多态。用户提到的虚函数属于运行时多态,这部分需要详细解释。 根据引用[1],多态分为静态和动态两种。静态多态通过函数重载实现,而动态多态则依赖虚函数和继承。用户可能想知道虚函数如何实现动态绑定,所以需要解释虚函数表(vtable)和虚函数表指针(vptr)的机制。引用[2]和[3]提供了虚函数表的例子,说明派生类如何重写基类的虚函数,并新增自己的虚函数。这时候每个类都有自己的虚函数表,实例对象通过vptr指向对应的表。当调用虚函数时,程序根据对象的实际类型查找vtable,找到正确的函数地址。 需要提到内存布局,基类和派生类的vptr如何指向不同的虚表。引用[4]讨论了多态实现原理,包括父类和子类的对象模型,以及多态调用的过程。这里可以结合示例代码,说明虚函数表的结构和函数调用的动态绑定过程。用户可能对虚函数的工作原理有疑问,比如为什么虚函数能实现运行时多态。需要解释在继承关系中,虚函数的重写导致vtable中的条目被替换,从而在运行时根据对象类型调用正确的函数。还要注意区分重写(override)、重载(overload)和重定义(redefining)的区别,引用[1]和[4]提到这些概念,可以简要说明以避免混淆。最后,可能需要提到虚析构函数的重要性,确保派生类对象被正确销毁,但用户的问题中没有提到这点,所以可能暂时不展开。总结回答结构:先介绍多态的分类,重点讲解动态多态实现机制,包括虚函数表、vptr、内存布局,结合引用中的例子说明原理,最后可能生成相关问题。</think>### C++多态实现机制虚函数原理 #### 1. **多态的分类** C++多态分为两种: - **编译时多态**(静态多态):通过函数重载或模板实现,调用目标在编译期确定[^1]。 - **运行时多态**(动态多态):通过虚函数和继承实现,调用目标在运行时根据对象类型动态绑定[^1]。 #### 2. **虚函数动态多态实现** **虚函数**是实现运行时多态的核心机制,其底层依赖**虚函数表(vtable)**和**虚函数表指针(vptr)**: - **虚函数表**:每个包含虚函数的类都有一个虚函数表,表中按顺序存储该类所有虚函数的地址[^2][^4]。 - **虚函数表指针**:每个对象实例的内存起始位置会隐含一个`vptr`,指向其所属类的虚函数表[^3][^4]。 例如以下类结构: ```cpp class Animal { public: virtual void eat() { /*...*/ } virtual void sleep() { /*...*/ } }; class Cat : public Animal { public: void eat() override { /*...*/ } // 重写基类虚函数 virtual void drink() { /*...*/ } // 派生类新增虚函数 }; ``` - `Animal`类的虚函数表包含`eat()`和`sleep()`的地址。 - `Cat`类的虚函数表中,`eat()`被替换为派生类实现,新增`drink()`函数地址[^2][^4]。 #### 3. **内存布局动态绑定** 当通过基类指针调用虚函数时: ```cpp Animal* obj = new Cat(); obj->eat(); // 动态绑定到Cat::eat() ``` 1. 通过`obj`的`vptr`找到`Cat`的虚函数表。 2. 根据函数在表中的偏移量(如`eat()`是第0项)获取实际函数地址。 3. 执行`Cat::eat()`[^3][^4]。 #### 4. **虚函数表的结构验证** 通过指针操作可以直接观察虚函数表(需注意平台差异): ```cpp typedef void(*Fun)(void); Base b; long* vptr = (long*)(&b); // 获取vptr地址 long* vtable = (long*)(*vptr); // 解引用得到虚函数表 Fun pFun = (Fun)vtable[0]; // 获取第一个虚函数地址 pFun(); // 调用Base::virtual_func() ``` 输出结果会显示虚函数表的具体函数地址[^3]。 #### 5. **关键区别:重写 vs 重载** - **重写(Override)**:派生类重新定义基类虚函数,要求函数签名一致。 - **重载(Overload)**:同一作用域内函数名相同但参数不同。 - **重定义(Hide)**:派生类定义基类同名但非虚函数,导致基类函数被隐藏[^1][^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值