深入剖析:C++“多態性”在編譯器中的實現

來源:互聯網
上載者:User

作者:feijj2002

理論

如下內容,屬個人的理解.與大家共用.錯誤之處,請指正

程式,就是通過CPU指令(CPU指令就是CPU能識別的二進位流,CPU通過解釋指令,能發出各種電流脈衝,以達到控制其他電子電路的狀態),對記憶體中資料資源的操作,也就是改變記憶體的位元,也是改變高低電平。

記憶體中,都是位元據,哪是指令,哪是資料?

PC指令計數器所指向的記憶體單元,就是指令。

PC指向哪,哪就是指令,所以資料也是指令,指令也是資料,程式只要管好PC寄存器就可以了.

語言級上,程式,是由函數和資料群組成,函數調用,實際上也是改變PC值,地址轉移。

資料又分為兩種:

一種就是在編譯時間,就分配地址空間.如全域資料

一種是在運行時,靠編譯器所維護的棧頂指標,相對棧頂指標的位移量,來分配地址空間.如局部資料.

(另一種就是在堆中動態分配的)

-------------------------------------------------------------------------------------------------------------------------------------

一個VC工程中,由許多H和CPP檔案組成,

編譯器,負責收集CPP檔案(H檔案被包含在CPP內)中出現的所有標識或符號,並負責形成邏輯、文法正確的二進位代碼(obj檔案)

連接器,負責確定、調整函數的相對位址,並保證在CPP中,每次函數調用(也就是地址轉移),都是有效,每個對記憶體資料的訪問的地址,是有效。

最終由連接器形成翻譯成彙編代碼或機器代碼。(DLL和EXE可執行檔檔案)

為收集標識和符號,編譯器維護一個符號映射表,有3欄(或3個欄位):
1。類型欄(變數類型或函數類型,在編譯時間,由編譯器負責填寫)
2。名字欄(變數名或函數名,在編譯時間,由編譯器負責填寫)
3。地址欄(是相對位址,成員變數類型的這一欄就放位移值,載入後,由作業系統重新調整,也就是地址重定位,在串連時,由連接器負責填寫)

-------------------------------------------------------------------------------------------------------------------------------------

實際應用

每聲明定義一個變數或成員函數時,編譯器就產生一個映射元素

如:

int a;

class A   

{

int m_a;

int m_b;

void fun1();

}

產生的兩條映射表記錄為:



名字欄  |    類型欄    |  地址欄()

   a          |     int           |   指向a的聲明定義處,相對位址,載入後,由作業系統重新調整

  A::m_a |  int             |  位移地址0,運行時,實際地址:this + 0

  A::m_b |  int             |   位移地址4,運行時,實際地址:this + 4

  A::fun1 | A::fun1    |   指向A::fun1()定義處,相對位址,加栽後,由作業系統重新調整

有關編譯器動態綁定技術,請看如下列:

class a
{

public:
  virtual fun1();

 virtual fun6();

  void fun2();

/*

出現virtual 關鍵字,編譯器為該類建立虛表

  索引           |         函數指標

  0                |     指向 a::fun1()定義處

  1               |     指向 a::fun6()定義處

出現成員函式宣告,編譯器填寫映射表

名字欄       |    類型欄    |  地址欄

a::fun2()    |   a::fun2      | (由連接器填寫)指向a::fun2()定義處

*/
}

//b繼承自a

class b :public a

{

public:
  virtual fun1();

  void fun3();

/*

虛函數fun1():

重新定義,建立虛表,並繼承了父類的虛表項,

修改了虛表中第一項(“a::fun1()函數”)的指標,使指向自己定義b::fun1()函數

  索引           |         函數指標

  0                |     指向 b::fun1()定義處(重定義)

 1                |     指向 a::fun6()定義處(沒有重定義)

出現成員函數fun3()聲明,映射表中增加一欄:

名字欄       |    類型欄    |  地址欄

b::fun3()    |   b::fun3     | 指向b::fun3()定義處

從父類中繼承的成員函數fun2()

映射表中增加一欄:記錄從a繼承的fun2()函數

名字欄       |    類型欄    |  地址欄

b::fun2()    |   a::fun2    | 指向父類a::fun2()定義處

*/
}

//c繼承自a
class c :public a

{

public:
  virtual fun6();

 void fun4();

/*

虛函數fun1():

重新定義,建立虛表,並繼承了父類的虛表項,

修改了虛表中第二項(“a::fun6()函數”)的指標,使指向自己定義c::fun6()函數

  索引           |         函數指標

  0                |     指向 a::fun1()定義處

  1                |     指向 c::fun6()定義處(改寫)

出現 成員函數fun4(),映射表中增加一欄:

名字欄       |    類型欄    |  地址欄

c::fun4()    |   c::fun4     | 指向c::fun4()定義處

從父類中繼承的成員函數fun2()

映射表中增加一欄:記錄從a繼承的fun2()函數

名字欄       |    類型欄    |  地址欄

c::fun2()    |   a::fun2    | 指向父類a::fun2()定義處

*/

}

調用
main()
{
  b     var1;
  c     var2;
  a*    p;
 
 p = &var1;
 

var1.fun2();

/*

var1.fun2()調用,靜態繫結:

編譯到此處時,編譯器到映射表中找名字欄,找到b::fun2()名字(由於var1為b類型),其對應的類型欄為“函數類型,類型名為a::fun2(因為此函數由a類型定義)”,其地址欄的指標值為“指向a::fun2()定義處”,所以,此處函數調用,被編譯器替換為“轉向:地址欄的指標值”,實際上可理解為是修改指令記數器的值為“地址欄的指標值”

*/

*/

p->fun2();

/*

p->fun2(),靜態繫結:這個容易理解,編譯時間,是找“a::fun2()”名字(因為p是a類型)

*/

p->fun2(),靜態繫結:這個容易理解,編譯時間,是找“a::fun2()”名字(因為p是a類型)

*/

p->fun2(),靜態繫結:這個容易理解,編譯時間,是找“a::fun2()”名字(因為p是a類型)

*/

p->fun2(),靜態繫結:這個容易理解,編譯時間,是找“a::fun2()”名字(因為p是a類型)

*/

p->fun2(),靜態繫結:這個容易理解,編譯時間,是找“a::fun2()”名字(因為p是a類型)

*/

p->fun3();

/*

 p->fun3(),這個調用,可能要發生編譯錯誤,因為類型a沒有聲明和定義fun3()函數,找不到a::fun3()名字,只有b::fun3()名字

*/

p->fun1();

/*
p->fun1();fun1()是個虛函數,同理,編譯到此處時,編譯器是不是也到映射表中找“a::fun1()”名字呢?不是的。
因為,在映射表中,是找不到a::fun1()”這個名字的,因為,fun1()名字聲明前有關鍵字"virtual",
在類聲明和定義時,編譯器,為每個出現virtual關鍵字的類,維護一個全域資料結構“虛表”

虛表:

1。索引;2。函數指標(這值在子類重寫虛函數後,發生相應變化)

所以上面調用,在編譯器發現調用的是“虛函數”時,
編譯器做了如下處理:

將p->fun1()  替換為:p->vptr[offset],

vptr名字為虛表指標:
由編譯器維護(上面提到),每個含虛函數的類,其產生對象,在記憶體中,首先的四個位元組就是vptr,這個值是靜態,同一個類型的所有對象的vptr值相同,指向同一個虛表;

offset值隨虛函式宣告次序而定,如果第一個聲明,索引則為0,出現在虛表的第一項,第二個聲明則為1,以次。。。(此時offset 為 0 即是:p->vptr[0])

p為基類,可指向子類的任何對象

(附加:

1。類型轉換實際是:記憶體切割,管轄記憶體從大變小,從大變小,現實世界中,是允許,在C++中,也是允許的;編譯器不允許基類對象向子類對象的轉化,因為從小變大,會導致記憶體訪問越界。

  2。指標的類型,實際上決定了通過該指標能訪問的記憶體範圍

),

所指向對象的類型不同,vptr不同
 所以,p->vptr[0],函數調用地址值在編譯時間,是不可能確定,主要因為
p指標所指向對象不能確定(p是指向b,還是指向c?還是其他。。,但offset是可以確定的)
從而,vptr值不能確定,
直到程式運行時,p->vptr[0]調用的函數地址,視p所指向物件類型而定
如果p指向b子類,則p指向記憶體中的首4位元組的vptr指向b的虛表,虛表第一項的指標指向自己函數定義的地址處
如果p指向c子類,則。。。。
這種直到運行時,才能確定函數調用地址的方式,即為:“動態綁定”*/

p->fun6();

/*

同上,由於b類未改寫需函數fun6(),所以該處調用,實際調用a::fun6(),編譯器做如下處理:

p->fun6() 變為  p->vptr[1],由於,fun6為第二聲明,此時,offset為1

*/

p = &var2;
 p->fun1();//動態綁定,同上, 調用a::fun1()
 p->fun2();//靜態,同上

p->fun6();//調用c::fun6()
 p->fun4();
//編譯出錯,因為fun4()非虛函數,編譯器在映射表中找不到a::fun4()名字,
//除非強行轉換:((c*)p)->fun4(),}

虛表:

1。索引;2。函數指標(這值在子類重寫虛函數後,發生相應變化)

所以上面調用,在編譯器發現調用的是“虛函數”時,
編譯器做了如下處理:

將p->fun1()  替換為:p->vptr[offset],

vptr名字為虛表指標:
由編譯器維護(上面提到),每個含虛函數的類,其產生對象,在記憶體中,首先的四個位元組就是vptr,這個值是靜態,同一個類型的所有對象的vptr值相同,指向同一個虛表;

offset值隨虛函式宣告次序而定,如果第一個聲明,索引則為0,出現在虛表的第一項,第二個聲明則為1,以次。。。(此時offset 為 0 即是:p->vptr[0])

p為基類,可指向子類的任何對象

(附加:

1。類型轉換實際是:記憶體切割,管轄記憶體從大變小,從大變小,現實世界中,是允許,在C++中,也是允許的;編譯器不允許基類對象向子類對象的轉化,因為從小變大,會導致記憶體訪問越界。

  2。指標的類型,實際上決定了通過該指標能訪問的記憶體範圍

),

所指向對象的類型不同,vptr不同
 所以,p->vptr[0],函數調用地址值在編譯時間,是不可能確定,主要因為
p指標所指向對象不能確定(p是指向b,還是指向c?還是其他。。,但offset是可以確定的)
從而,vptr值不能確定,
直到程式運行時,p->vptr[0]調用的函數地址,視p所指向物件類型而定
如果p指向b子類,則p指向記憶體中的首4位元組的vptr指向b的虛表,虛表第一項的指標指向自己函數定義的地址處
如果p指向c子類,則。。。。
這種直到運行時,才能確定函數調用地址的方式,即為:“動態綁定”*/

p->fun6();

/*

同上,由於b類未改寫需函數fun6(),所以該處調用,實際調用a::fun6(),編譯器做如下處理:

p->fun6() 變為  p->vptr[1],由於,fun6為第二聲明,此時,offset為1

*/

p = &var2;
 p->fun1();//動態綁定,同上, 調用a::fun1()
 p->fun2();//靜態,同上

p->fun6();//調用c::fun6()
 p->fun4();
//編譯出錯,因為fun4()非虛函數,編譯器在映射表中找不到a::fun4()名字,
//除非強行轉換:((c*)p)->fun4(),}

虛表:

1。索引;2。函數指標(這值在子類重寫虛函數後,發生相應變化)

所以上面調用,在編譯器發現調用的是“虛函數”時,
編譯器做了如下處理:

將p->fun1()  替換為:p->vptr[offset],

vptr名字為虛表指標:
由編譯器維護(上面提到),每個含虛函數的類,其產生對象,在記憶體中,首先的四個位元組就是vptr,這個值是靜態,同一個類型的所有對象的vptr值相同,指向同一個虛表;

offset值隨虛函式宣告次序而定,如果第一個聲明,索引則為0,出現在虛表的第一項,第二個聲明則為1,以次。。。(此時offset 為 0 即是:p->vptr[0])

p為基類,可指向子類的任何對象

(附加:

1。類型轉換實際是:記憶體切割,管轄記憶體從大變小,從大變小,現實世界中,是允許,在C++中,也是允許的;編譯器不允許基類對象向子類對象的轉化,因為從小變大,會導致記憶體訪問越界。

  2。指標的類型,實際上決定了通過該指標能訪問的記憶體範圍

),

所指向對象的類型不同,vptr不同
 所以,p->vptr[0],函數調用地址值在編譯時間,是不可能確定,主要因為
p指標所指向對象不能確定(p是指向b,還是指向c?還是其他。。,但offset是可以確定的)
從而,vptr值不能確定,
直到程式運行時,p->vptr[0]調用的函數地址,視p所指向物件類型而定
如果p指向b子類,則p指向記憶體中的首4位元組的vptr指向b的虛表,虛表第一項的指標指向自己函數定義的地址處
如果p指向c子類,則。。。。
這種直到運行時,才能確定函數調用地址的方式,即為:“動態綁定”*/

p->fun6();

/*

同上,由於b類未改寫需函數fun6(),所以該處調用,實際調用a::fun6(),編譯器做如下處理:

p->fun6() 變為  p->vptr[1],由於,fun6為第二聲明,此時,offset為1

*/

p = &var2;
 p->fun1();//動態綁定,同上, 調用a::fun1()
 p->fun2();//靜態,同上

p->fun6();//調用c::fun6()
 p->fun4();
//編譯出錯,因為fun4()非虛函數,編譯器在映射表中找不到a::fun4()名字,
//除非強行轉換:((c*)p)->fun4(),}

虛表:

1。索引;2。函數指標(這值在子類重寫虛函數後,發生相應變化)

所以上面調用,在編譯器發現調用的是“虛函數”時,
編譯器做了如下處理:

將p->fun1()  替換為:p->vptr[offset],

vptr名字為虛表指標:
由編譯器維護(上面提到),每個含虛函數的類,其產生對象,在記憶體中,首先的四個位元組就是vptr,這個值是靜態,同一個類型的所有對象的vptr值相同,指向同一個虛表;

offset值隨虛函式宣告次序而定,如果第一個聲明,索引則為0,出現在虛表的第一項,第二個聲明則為1,以次。。。(此時offset 為 0 即是:p->vptr[0])

p為基類,可指向子類的任何對象

(附加:

1。類型轉換實際是:記憶體切割,管轄記憶體從大變小,從大變小,現實世界中,是允許,在C++中,也是允許的;編譯器不允許基類對象向子類對象的轉化,因為從小變大,會導致記憶體訪問越界。

  2。指標的類型,實際上決定了通過該指標能訪問的記憶體範圍

),

所指向對象的類型不同,vptr不同
 所以,p->vptr[0],函數調用地址值在編譯時間,是不可能確定,主要因為
p指標所指向對象不能確定(p是指向b,還是指向c?還是其他。。,但offset是可以確定的)
從而,vptr值不能確定,
直到程式運行時,p->vptr[0]調用的函數地址,視p所指向物件類型而定
如果p指向b子類,則p指向記憶體中的首4位元組的vptr指向b的虛表,虛表第一項的指標指向自己函數定義的地址處
如果p指向c子類,則。。。。
這種直到運行時,才能確定函數調用地址的方式,即為:“動態綁定”*/

p->fun6();

/*

同上,由於b類未改寫需函數fun6(),所以該處調用,實際調用a::fun6(),編譯器做如下處理:

p->fun6() 變為  p->vptr[1],由於,fun6為第二聲明,此時,offset為1

*/

p = &var2;
 p->fun1();//動態綁定,同上, 調用a::fun1()
 p->fun2();//靜態,同上

p->fun6();//調用c::fun6()
 p->fun4();
//編譯出錯,因為fun4()非虛函數,編譯器在映射表中找不到a::fun4()名字,
//除非強行轉換:((c*)p)->fun4(),}

評論:

我要說幾點,這是我的理解
1。程式是靜態,是進程修改資料,:P
2。cpu在取指周期裡取出的是指令,而不是說是PC指令計數器所指向的記憶體單元,就是指令。這句話雖然不錯,但是不準確。呵呵
3。資料是資料,指令是指令,一般系統中指令和資料是混合存放在儲存空間中的,有些系統資料和指令是分開存放的,有指令儲存空間和資料存放區器。
4。cpu通過指令計數器將儲存空間中的資料取出存到指令寄存器中,然後通過解碼器解碼,產生控制訊號操作alu進行運算,最後回寫到儲存空間。
5。程式資料會在資料區段(bss),堆和棧中。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.