由底層和邏輯說開去——c++之類與對象的深入剖析

來源:互聯網
上載者:User

       類是什麼,對象是什麼,  這兩個問題在各個c++書裡面都以一種抽象的描述方式,給了我們近乎完美的答案,然後我好像就知道什麼是類什麼是對象了,但是當捫心自問,類在哪兒,對象在哪兒,成員方法在哪兒,成員變數在哪兒的時候,這些定義大概只能給出一個同樣抽象的答案。

      其實很大程度上我們不知道問題的答案的原因是我們沒有弄清楚我們的問題究竟是什麼.  類和對象是擁有一堆有存取權限的成員變數和成員方法的集合,那麼我們的問題就可以跟著這個湊合的定義得出,我當然也回答不了這些問題,但是我準備在本文做三件事情,通過這三件事,更加近的認識對象和類:1.從底層實現上講,對象以什麼形式儲存,對象名是什麼 對象的成員變數怎麼儲存  2.從底層上講成員方法是怎麼的一種存在,怎樣把它和全域函數區分開,以及怎麼做到重載 3.從邏輯層面上講怎麼實現存取權限,private ,const,static這些,以及從底層上如果繞過編譯器去突破這些許可權的限制,比如在static方法裡成功訪問對象成員變數,比如在const方法裡面成功修改成員變數,比如在外部成功修改private變數;

然後我們就會發現,所謂c++的類機制,只是編譯器在c的脖子上套上一把枷鎖,又把鑰匙交給了它。

       第一個問題:類和對象的記憶體表示,我以前沒學c++的時候,很多人就說啊,c++是物件導向的,c語言是面向過程的,讓我有一種有對象就是物件導向的感覺,然後我就問物件導向是什麼,然後人家就又說了,物件導向是一種思想,(然後抬頭望向遠方,做沉思狀,讓我有一種一巴掌踹死他的衝動);本文不打算說明物件導向是什麼,因為這個思想的明白過程不是一蹴而就的,那就來說說記憶體中的實實在在的東西吧,畢竟存在的東西才踏實; 類是說給編譯器聽的,在記憶體沒有任何的存在,就像結構體,就像數組,而對象才是是在存在的東西,對象名就像結構體變數名,就像陣列變數名一樣(有人說你扯淡吧,數組名是地址,你那兩個東西算是什麼東西),嗯 
數組名是地址,在底層實現上,名字不都是地址嗎,結構體變數名和對象名也是地址;這三個類型是複合類型,結構體和對象可以是不同類型的複合,數組是相同類型的複合,所以你可以在邏輯上使用數組名加1找到第二個元素的地址(但你要明白這都是邏輯上的,是編譯器的功勞),但是結構體名加1卻不一定;下面我們看一下一個對象建立過程是怎麼分配記憶體的;源碼如下:

#include <iostream>using namespace std;class TextA {private:int a;int b;public:TextA();};TextA::TextA(){a=10;b=20;}int main(){TextA text;//cout<<sizeof(text);return 0;}

上面代碼很簡單,定義一個text(), 為它分配記憶體,讓我們來看一下底層實現

.text:0040109F _main           proc near               ; CODE XREF: start+AFp.text:0040109F.text:0040109F _text           = byte ptr -8.text:0040109F argc            = dword ptr  8.text:0040109F argv            = dword ptr  0Ch.text:0040109F envp            = dword ptr  10h.text:0040109F                                            .text:0040109F                 push    ebp                 .text:004010A0                 mov     ebp, esp             .text:004010A2                 sub     esp, 8           
                                                                  ;上面都不用看;

.text:004010A5 lea ecx, [ebp+_text] .text:004010A8 call _TextA.text:004010AD xor eax, eax.text:004010AF mov esp, ebp.text:004010B1 pop ebp.text:004010B2 retn.text:004010B2 _main endp.text:004010B2

這段代碼其實重要的也就兩句lea ecx,[ebp+_text]這句話大致意思是把text的地址放在ecx裡面,然後

 call    _TextA就是調用TextA()預設建構函式 我來再看看,這個建構函式對text做了什麼;

.text:0040107E _TextA          proc near               ; CODE XREF: _main+9p.text:0040107E.text:0040107E var_4           = dword ptr -4.text:0040107E.text:0040107E                 push    ebp.text:0040107F                 mov     ebp, esp.text:00401081                 push    ecx.text:00401082                 mov     [ebp+var_4], ecx     ;這句意思就是把ecx裡面存的也就是_text標識的那塊記憶體的地址放進ebp-4的記憶體;  .text:00401085                 mov     eax, [ebp+var_4]     ;然後再放進eax裡;.text:00401088                 mov     dword ptr [eax], 0Ah  ;0Ah就是十進位的10 把10放進eax存的地址的記憶體也就是_text標識的text的第一個變數a裡面;.text:0040108E                 mov     ecx, [ebp+var_4]      ;然後又一次把text標識的記憶體的地址放進ecx,.text:00401091                 mov     dword ptr [ecx+4], 14h ;然後ecx裡的地址減去四,得到的記憶體裡面放 十六進位為14h也就是20的東西,顯然這塊記憶體是b;.text:00401098                 mov     eax, [ebp+var_4]       .text:0040109B                 mov     esp, ebp.text:0040109D                 pop     ebp.text:0040109E                 retn.text:0040109E _TextA          endp

看吧 text還是標識它記憶體的首地址,也就是首元素a的地址,所以說從底層上講結構題,數組和對象是一種東西; 在上面我們沒有看到成成員方法啊,那成員方法在哪裡呢?這就是我們的第二個問題了;

2.成員方法在哪裡:這裡面涉及一個命名粉碎機制,當然我也不懂命名粉碎原理,但是大概就像是在編譯的時候 根據你的函數的一些特徵,給你的一個函數裡面的程式碼片段的段首取一個名字,嗯這句話至少包含三個層面的資訊,第一,這個機制是編譯的時候用的,可以讓函數名變過去也可以變回來 第二函數名應用這個機制的時候取的特徵由編譯器決定,不同語言選擇的不同,第三:得到的名字將用來標識原函數裡面程式碼片段的首地址,代碼也是在記憶體裡哦;  
還是有點抽象哈,那麼我們舉幾個例子: c 語言裡面 只要函數名一樣 不管參數類型一樣不一樣 都不能編譯通過  這就說明這個特質是函數名,所以我們就說 c語言的函數名就是函數的地址; c++裡面呢 有了重載就不能這樣了,而且有了類成員函數,所以就不能這樣了,c++裡面的函數特徵包括,所屬類名,函數名,參數類型,參數多少等;當然也有一些沒有所屬類的方法也就是全域方法; c++的函數呢就放在程式碼片段裡面,用函數名(其實是變化後的來標識首地址);所以這在邏輯層上解釋了幾種現象  <1>在邏輯層上一個對象通過  . 
操作符只能訪問到它自己所屬類的方法;<2> 成員方法其實是屬於類的 跟對象沒有關係(這句話說的不嚴謹,可能會引出一些問題,我們在第三個話題裡討論) <3>如果在一個成員方法裡面定義個static類型變數,另一個對象使用該方法時,這個靜態變數依然在;

比如下面的代碼

#include <iostream>using namespace std;class TextA {public:void show(){static int a=1;cout<<++a<<endl;}};int main(){TextA ta;TextA tb;ta.show();tb.show();return 0;}

輸出2之後輸出的是3,說明兩者對象訪問的是同一個地址的代碼,也就是說這些成員方法屬於類而不是對象本身,這就引出幾個問題了,比如static方法老師們說才是類方法啊 比如說成員方法修改物件變數的時候怎麼辦,this指標又是什麼東西;嗯這些問題我們就不留給第三個話題了,就在這裡分析分析;首先我們來看一個成員方法的調用過程

#include <iostream>using namespace std;class Text{private:int a;public:void set_a(){a=10;};};int main(){Text t;t.set_a();return 0;}

為了便於理解我們把代碼寫的很簡單;簡單到連參數都沒有傳,簡單到沒有預設建構函式;(放心編譯器也不會給你加預設建構函式的,雖然老師和很多書上說一定會加,不信看下面彙編代碼,原理我會在下一個部落格解釋 )我們看看這個代碼的底層實現是怎樣的;

_main proc nearvar_4= byte ptr -4argc= dword ptr  8argv= dword ptr  0Chenvp= dword ptr  10hpush    ebpmov     ebp, esppush    ecxlea     ecx, [ebp+var_4]call    ??1facet@locale@std@@UAE@XZ ; std::locale::facet::~facet(void)xor     eax, eaxmov     esp, ebppop     ebpretn_main endp

我們可以這到這個底層,只調用了一個函數

call    ??1facet@locale@std@@UAE@XZ ; std::locale::facet::~facet(void)
 ??1facet@locale@std@@UAE@XZ就是名稱粉碎後的結果,它標識了Text::set_a()的首地址;那麼它是怎麼得到this指標的呢,就是看
lea     ecx, [ebp+var_4]這句話,這句話意思就是把t標識的地址放在寄存器ecx裡面,也就是this指標,函數裡面就可以用它找到a了,後面我們分析一下static方法就會發現它沒有有這句話 所以找不到this指標;
#include<iostream>using namespace std;class TextA {private:int a;public:static void  show();}; void  TextA::show(){cout<<"dragonfive!";}int main(){TextA ta;TextA::show();return 0;}

我們來看看底層實現

_main proc nearargc= dword ptr  8argv= dword ptr  0Chenvp= dword ptr  10hpush    ebpmov     ebp, esppush    ecxcall    sub_40107Exor     eax, eaxmov     esp, ebppop     ebpretn_main endp

看吧這裡就沒有lea這句話,就不能得到this指標(這是編譯器的做法,我們可以自己傳一個,這樣就能突破限制了這就是我們第三部分的內容了;)

3. c++裡面有許多規定啊,顯得莫名奇妙,比如private的成員不能在外界被訪問,今天咱們就來訪問一下試試:

#include<iostream>using namespace std;class TextA {private:int a;     public:    TextA(){a=10;}   void show_a();};void TextA::show_a(){cout<<a<<endl;;}int main(){TextA ta;ta.show_a();int *b=NULL;__asm{lea eax,ta;mov [b],eax;}*b=20;ta.show_a();return 0;}

是吧,第一次輸出的是10,因為初始化為10,然後第二次輸出20,為什麼呢,因為我們得到了a的地址嘛,那是不是說private是假的 自然不是了,因為private是c++的編譯器的限制,我們用的是彙編把a的地址偷偷取到,彙編自然不會走c++編譯器也就不會受private限制,因此我們就知道了這個private啊
只是編譯器的事情,跟變數的儲存沒有任何的關係;
當然由此可以推知其它的一些限制詞也是這樣子的,比如我們可以讓static方法訪問到訪問它的對象的屬性;

#include<iostream>using namespace std;class TextA {private:int a;public:TextA(){a=10;};static void  show();}; void  TextA::show(){int b;__asm{mov eax,[ecx]mov [b],eax}cout<<b;}int main(){TextA ta;__asm{lea ecx,ta;}TextA::show();return 0;}

lea ecx,ta;

只是因為我們在調用之前手動傳遞了一個地址進去額

所以如你所見 在底層實現上c++和其它語言沒有什麼區別 指標依然是那麼強大而危險的存在著;
只是編譯器通過對一些限制詞的檢測來保證一部分安全;為什麼不能絕對安全,因為上一個部落格裡已經說了
c++的妥協性,指標的存在,讓一切都只能把握在一個度內;使用c++便是為了通過這些限制詞 讓編譯器盡可以地檢測出不安全因素,
所以c++的函數是一種限制了的語言,對象就像皇宮,私人成員像是後宮部分...編譯器就是把門的,只有擁有了指標也就是地址才能潛入後宮做各種友好訪問。。。

最後發現好像說跑題了,因為說的貌似是指標的強大(這個可能會誤導初學者),和限制詞只是邏輯層的東西 

聯繫我們

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