C++中多態的實現原理

來源:互聯網
上載者:User

多態是物件導向的基本特徵之一。而虛函數是實現多態的方法。那麼virtual function到底如何?多態的呢?

1 基類的記憶體分布情況
請看下面的sample

class A
{
void g(){.....}
};
則sizeof(A)=1;
如果改為如下:
class A
{
public:
    virtual void f()
    {
       ......
    }
    void g(){.....}
}
則sizeof(A)=4! 這是因為在類A中存在virtual function,為了實現多態,每個含有virtual function的類中都隱式包含著一個靜態虛指標vfptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口地址
例如 我們declare 一個A類型的object :
    A c;
    A d;
則編譯後其記憶體分布如下:

從 vfptr所指向的vtable可以看出,每個virtual function都佔有一個entry,例如本例中的f函數。而g函數因為不是virtual類型,故不在vtable的表項之內。說明:vtab屬於類成員靜態pointer,而vfptr屬於對象pointer
2 繼承類的記憶體分布狀況
假設代碼如下:
public B:public A
{
public :
    int f() //override virtual function
    {
        return 3;
    }
};


A c;
A d;
B e;
編譯後,其記憶體分布如下:

從中我們可以看出,B類型的對象e有一個vfptr指向vtable address:0x00400030 ,而A類型的對象c和d共同指向類的vtable address:0x00400050a

3 動態綁定過程的實現
    我們說多態是在程式進行動態綁定得以實現的,而不是編譯時間就確定對象的調用方法的靜態繫結。
    其過程如下:
    程式運行到動態綁定時,通過基類的指標所指向的物件類型,通過vfptr找到其所指向的vtable,然後調用其相應的方法,即可實現多態。
例如:
A c;
B e;
A *pc=&e; //設定breakpoint,運行到此處
pc=&c;
此時記憶體中各指標狀況如下:

可以看出,此時pc指向類B的虛表地址,從而調用對象e的方法。

繼續運行,當運行至pc=&c時候,此時pc的vptr值為0x00420050,即指向類A的vtable地址,從而調用c的方法。
這就是動態綁定!(dynamic binding)或者叫做遲後聯編(lazy compile)。

 

為了更加透析多態的原理,我們可以debug 程式在runtime時候的對象記憶體分布情況。

以下面這段簡單的程式為例

// SimpleStack.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

class Base
{
public:
    int m_data;
    static int m_staticvalue;
    Base(int data)
    {
        m_data=data;
    }
    virtual void DoWork()
    {
    }
};

class AnotherBase
{
public:
    virtual void AnotherWork()
    {}
   
};

class DerivedClass:public Base,public AnotherBase
{
public:
    DerivedClass(int t_data):Base(t_data)
    {}

    virtual    void  DoWork()
    {
    }

    virtual void AnotherWork()
    {
    }
};

int Base::m_staticvalue=1;

int main(int argc, char* argv[])
{
   
    DerivedClass b(1);
    b.DoWork();

    return 0;
}

 

當程式運行後我們設定很簡單的breakpoint: bp simplestack!derivedclass::dowork. 斷點命中後的call stack如下:

0:000> kb
ChildEBP RetAddr  Args to Child             
0012ff20 0040102a 00daf6f2 00daf770 7ffd7000 SimpleStack!DerivedClass::DoWork
0012ff80 004012f9 00000001 00420e80 00420dc0 SimpleStack!main+0x2a
0012ffc0 7c817077 00daf6f2 00daf770 7ffd7000 SimpleStack!mainCRTStartup+0xe9
0012fff0 00000000 00401210 00000000 78746341 kernel32!BaseProcessStart+0x23

這時,我們可以看看DerivedClass對象的記憶體內分布情況:

0:000> dt SimpleStack!DerivedClass 0012ff74
   +0x000 __VFN_table : 0x0040c020  //指向虛表的指標1
   +0x004 m_data           : 1
   =0040d030 Base::m_staticvalue : 1  //(類成員)
   +0x008 __VFN_table : 0x0040c01c  //指向虛表的指標2

可以看到,DerivedClass對象中包含兩個指向虛表的指標,地址分別為0x0040c020 和0x0040c01c 。一個為指向override了BaseClass的方法的虛表,一個指向orverride了AnotherBase方法的虛表。

可以查看對應虛表中的方法:

0:000> dds 0x0040c01c
0040c01c  00401140 SimpleStack!DerivedClass::AnotherWork
0040c020  00401110 SimpleStack!DerivedClass::DoWork
0040c024  004010e0 SimpleStack!Base::DoWork
0040c028  004011a0 SimpleStack!AnotherBase::AnotherWork
......

通過以上分析,應該可以透析多態的本質了。

 

這種看記憶體配置方案真的不錯的,^-^

 

自己也試了一下,雖然常用VC,不過也沒注意過

 

#include <iostream>
using namespace std;
class base2
{
public:
virtual std::string ff(){return "base2";}
protected:
private:
};
class base
{
public:
virtual std::string f1(){return "base";}
virtual std::string f2(){return "base";}
virtual std::string f3(){return "base";}
protected:
private:
};
class child : public base , public base2
{
public:
virtual std::string f2(){return "child";}
virtual std::string f3(){return "child";}
protected:
private:
};
class grant : public child
{
public:
virtual std::string f3(){return "grant";}
protected:
private:
};
void main()
{
base a;
child b;
grant c;
string sa = a.f1();
string sb = b.f1();
string sc = c.f1();
}
// 記憶體狀態
- a {...}
- __vfptr 0x0042f024 const base::`vftable'
[0x0] 0x00401014 base::f1(void)
[0x1] 0x00401055 base::f2(void)
[0x2] 0x00401032 base::f3(void)
- b {...}
- base {...}
- __vfptr 0x0042f038 const child::`vftable'{for `base'}
[0x0] 0x00401014 base::f1(void)
[0x1] 0x00401050 child::f2(void)
[0x2] 0x0040106e child::f3(void)
- base2 {...}
- __vfptr 0x0042f034 const child::`vftable'{for `base2'}
[0x0] 0x00401023 base2::ff(void)
- c {...}
- child {...}
- base {...}
- __vfptr 0x0042f05c const grant::`vftable'{for `base'}
[0x0] 0x00401014 base::f1(void)
[0x1] 0x00401050 child::f2(void)
[0x2] 0x0040103c grant::f3(void)
- base2 {...}
- __vfptr 0x0042f058 const grant::`vftable'{for `base2'}
[0x0] 0x00401023 base2::ff(void)

多態是物件導向的基本特徵之一。而虛函數是實現多態的方法。那麼virtual function到底如何?多態的呢?

1 基類的記憶體分布情況
請看下面的sample

class A
{
void g(){.....}
};
則sizeof(A)=1;
如果改為如下:
class A
{
public:
    virtual void f()
    {
       ......
    }
    void g(){.....}
}
則sizeof(A)=4! 這是因為在類A中存在virtual function,為了實現多態,每個含有virtual function的類中都隱式包含著一個靜態虛指標vfptr指向該類的靜態虛表vtable, vtable中的表項指向類中的每個virtual function的入口地址
例如 我們declare 一個A類型的object :
    A c;
    A d;
則編譯後其記憶體分布如下:

從 vfptr所指向的vtable可以看出,每個virtual function都佔有一個entry,例如本例中的f函數。而g函數因為不是virtual類型,故不在vtable的表項之內。說明:vtab屬於類成員靜態pointer,而vfptr屬於對象pointer
2 繼承類的記憶體分布狀況
假設代碼如下:
public B:public A
{
public :
    int f() //override virtual function
    {
        return 3;
    }
};


A c;
A d;
B e;
編譯後,其記憶體分布如下:

從中我們可以看出,B類型的對象e有一個vfptr指向vtable address:0x00400030 ,而A類型的對象c和d共同指向類的vtable address:0x00400050a

3 動態綁定過程的實現
    我們說多態是在程式進行動態綁定得以實現的,而不是編譯時間就確定對象的調用方法的靜態繫結。
    其過程如下:
    程式運行到動態綁定時,通過基類的指標所指向的物件類型,通過vfptr找到其所指向的vtable,然後調用其相應的方法,即可實現多態。
例如:
A c;
B e;
A *pc=&e; //設定breakpoint,運行到此處
pc=&c;
此時記憶體中各指標狀況如下:

可以看出,此時pc指向類B的虛表地址,從而調用對象e的方法。

繼續運行,當運行至pc=&c時候,此時pc的vptr值為0x00420050,即指向類A的vtable地址,從而調用c的方法。
這就是動態綁定!(dynamic binding)或者叫做遲後聯編(lazy compile)。

 

為了更加透析多態的原理,我們可以debug 程式在runtime時候的對象記憶體分布情況。

以下面這段簡單的程式為例

// SimpleStack.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

class Base
{
public:
    int m_data;
    static int m_staticvalue;
    Base(int data)
    {
        m_data=data;
    }
    virtual void DoWork()
    {
    }
};

class AnotherBase
{
public:
    virtual void AnotherWork()
    {}
   
};

class DerivedClass:public Base,public AnotherBase
{
public:
    DerivedClass(int t_data):Base(t_data)
    {}

    virtual    void  DoWork()
    {
    }

    virtual void AnotherWork()
    {
    }
};

int Base::m_staticvalue=1;

int main(int argc, char* argv[])
{
   
    DerivedClass b(1);
    b.DoWork();

    return 0;
}

 

當程式運行後我們設定很簡單的breakpoint: bp simplestack!derivedclass::dowork. 斷點命中後的call stack如下:

0:000> kb
ChildEBP RetAddr  Args to Child             
0012ff20 0040102a 00daf6f2 00daf770 7ffd7000 SimpleStack!DerivedClass::DoWork
0012ff80 004012f9 00000001 00420e80 00420dc0 SimpleStack!main+0x2a
0012ffc0 7c817077 00daf6f2 00daf770 7ffd7000 SimpleStack!mainCRTStartup+0xe9
0012fff0 00000000 00401210 00000000 78746341 kernel32!BaseProcessStart+0x23

這時,我們可以看看DerivedClass對象的記憶體內分布情況:

0:000> dt SimpleStack!DerivedClass 0012ff74
   +0x000 __VFN_table : 0x0040c020  //指向虛表的指標1
   +0x004 m_data           : 1
   =0040d030 Base::m_staticvalue : 1  //(類成員)
   +0x008 __VFN_table : 0x0040c01c  //指向虛表的指標2

可以看到,DerivedClass對象中包含兩個指向虛表的指標,地址分別為0x0040c020 和0x0040c01c 。一個為指向override了BaseClass的方法的虛表,一個指向orverride了AnotherBase方法的虛表。

可以查看對應虛表中的方法:

0:000> dds 0x0040c01c
0040c01c  00401140 SimpleStack!DerivedClass::AnotherWork
0040c020  00401110 SimpleStack!DerivedClass::DoWork
0040c024  004010e0 SimpleStack!Base::DoWork
0040c028  004011a0 SimpleStack!AnotherBase::AnotherWork
......

通過以上分析,應該可以透析多態的本質了。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.