作者: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),堆和棧中。