天天看點

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

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

       類是什麼,對象是什麼,  這兩個問題在各個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++裡面的函數特征包括,所屬類名,函數名,參數類型,參數多少等;當然也有一些沒有所屬類的方法也就是全局方法; 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 near

var_4= byte ptr -4
argc= dword ptr  8
argv= dword ptr  0Ch
envp= dword ptr  10h

push    ebp
mov     ebp, esp
push    ecx
lea     ecx, [ebp+var_4]
call    [email protected]@std@@[email protected] ; std::locale::facet::~facet(void)
xor     eax, eax
mov     esp, ebp
pop     ebp
retn
_main endp
           
我們可以這到這個底層,隻調用了一個函數
           
call    [email protected]@std@@[email protected] ; std::locale::facet::~facet(void)
           
[email protected]@std@@[email protected]就是名稱粉碎後的結果,它辨別了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 near

argc= dword ptr  8
argv= dword ptr  0Ch
envp= dword ptr  10h

push    ebp
mov     ebp, esp
push    ecx
call    sub_40107E
xor     eax, eax
mov     esp, ebp
pop     ebp
retn
_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++之類與對象的深入剖析
,彙編自然不會走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++的函數是一種限制了的語言,對象就像皇宮,私有成員像是後宮部分...編譯器就是把門的,隻有擁有了指針也就是位址才能潛入後宮做各種友好通路。。。

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

c++

繼續閱讀