程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++對象模型那點事兒(成員篇)

C++對象模型那點事兒(成員篇)

編輯:C++入門知識

1 前言


上篇提到了類的數據成員有兩種:static和nonstatic。類中的函數成員有三種:static,nonstatic和virtual。不知道大家有沒有想過類到底是怎封裝數據的?為什麼只能通過對象或者成員函數來訪問?static數據既然不單獨屬於某個對象,外界可否訪問?類的函數成員不存在於單個對象中,為何外界又不能訪問這些函數成員?這些都是怎麼做到的? 讓我們帶著這些問題開始這一章的閱讀。

2 數據成員


我們先來看一個例子:
class Point3d{
public:
	//......

	float x;
	static list *freelist;
	float y;
	static const int chunksize = 250;
	float z;
};

任何靜態數據成員都不會被放進對象的布局之中,而被放在程序的data segment中,與個別的對象無關。對於同一access section,nonstatic數據成員在對象之中的排列順序和其被聲明的順序是一樣的。C++standard 要求“較晚出現的members在對象中具有較高的地址“,也就是說由於會進行內存對齊優化,members在對象中不一定為連續排列。
下面我們來看看數據成員的訪問。 在這裡我先拋出一個問題,如下:
Point3d origin,*pt;
origin.x = 0;
pt->x = 0;
通過origin存取x與通過pt存取x有什麼差異?

2.1 static 數據成員

static數據成員被編譯器提出於class之外,被視為一個在class聲明周期范圍之內可見的全局變量。每一個static Data member 的存取,以及與class的關聯,並不會招致任何時間上或空間上的開銷。每次程序訪問static members時,編譯器內部會發生如下轉化:
//我們知道chunksize 為Point3d中的一個靜態數據成員
origin.x == 250;//訪問chunksize 並判斷
pt->x == 250; // 訪問chunksize 並判斷

顯然外界不可訪問chunksize。我們來猜測下原因。 不知道大家是否知道name-mangling手法(編譯器會對static data member重新命名)。我們來看看下面的代碼:
class A{
	static int x;
};
class B{
	static int x;
};

A和B中的x都放在data segment中,為何兩個變量沒有沖突? 答案似乎很明顯了,編譯器將A中的x與B中的x進行了重命名。這個新名字獨一無二,且與各自的作用域類名有關。而這個重命名算法就是name-mangling。 所以外界想訪問data segment中的chunksize,根本訪問不到,人家已經隱姓埋名了。而新的名稱只有作用域類知道。 有木有豁然開朗的感覺? 我們來接著上文的轉化。
origin.chunksize == 250 ;
//===>>被編譯器轉化為 Point3d::chunksize == 250;
pt->chunksize == 250;
//===>>被編譯器轉化為 Point3d::chunksize == 250

此時,分別通過origin和pt存取chunksize是沒有差異的。 若chunksize是繼承自基類而來,或者繼承自虛基類,情況又會發生什麼變化呢? 答案是static member成員還是只有一個實例,其存取路徑仍然是那麼直接。

2.2 nonstatic 數據成員


nonstatic data member直接存放在每一個class對象之中,除非經由顯式的或者隱式的類型class object調用,否則沒有辦法直接存取它們。 還是上面的例子:
class Point3d{
public:
	//......
	float x;
	float y;
	float z;
};
Point3d origin;
//那麼地址&origin.x等於多少?
cout<<"&origin: "<<&origin<
程序運行結果:
\ 運行結果是不是已經很清楚 了?訪問對象中的數據成員即是在對象起始地址的基礎上增加一個偏移量: &Z喎?http://www.Bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcmlnaW4mIzQzOygmYW1wO1BvaW50M2Q6OngtMSkKtvjV4rj2xqvSxsG/1Nqx4NLryrHG2ry0v8m78daqoaMKudjT2sDg1tC1xLPJ1LG6r8r9ttTT2sr9vt2zydSxtcS3w87KyOfPwqO6CjxwcmUgY2xhc3M9"brush:java;">//我們假設Point3d中有一個成員函數如下 Point3d Point3d::translate(const Point3d &pt){ x += pt.x; y += pt.y; z += pt.z; }

類中的函數成員看似直接訪問的數據成員,事實真是如此嗎?非也,我們看編譯器對這個成員函數干了些什麼事。 上述函數經過轉化:
Point3d Point3d::translate(Point3d *const this,const Point3d &pt){
	this->x += pt.x;
	this->y += pt.y;
	this->z += pt.z;
}

是的,編譯器在每個成員函數的參數列表中加入了一個this指針,以此來激活重載,稍後詳解。 所以類中的nonstatic data member必須通過對象來調用。 那麼我們再回到上面一個問題。
Point3d *pt3d;
pt3d->x = 0;
//效率如何呢?

答曰,其執行效率在x為struct member,class member,單一繼承,多繼承的情況下完全相同。但如果x是一個virtual base class member,存取速度稍慢一些。

老問題:
Point3d origin,*pt;
origin.x = 0;
pt->x = 0;
通過origin存取x與通過pt存取x有無重大差異?

答案是當Point3d繼承自一個virtual base class,而x又是這個virtual base class的一個member時會有差異。這個時候我們不確定pt指向的class類型(即它到底指向的是派生類還是基類對象?)也就不知道編譯時候這個member真正的偏移值。 所以這個存取必須延遲至執行期,經由額外間接引導才能訪問。 然而,origin不會有這些問題。因為其class類型是確定的,無疑為Point3d,而virtual base class中的member的偏移值在編譯的時候已經固定。所以origin.x可以毫無壓力的做到。 好了,關於data member就言盡於此吧。如果大家還想知道更深層次的內容,可以查閱相關資料。 下面我們來看看成員函數的問題。

3 成員函數


上文不是說明member functions 有三種:nonstatic,static和virtual,我們就按這個順序一一討論吧。

3.1 nonstatic 成員函數


C++的設計准則之一就是成員函數必須與普通非成員函數有相同的執行效率,同時外界又不能訪問類中的nonstatic member functions 那麼它是怎麼做到的呢? 道理很簡單,編譯器暗地裡已經向member函數實例轉換為對等的nonmember 函數實例。 舉個例子:
//假設Point3d中有如下一個成員函數
Point3d Point3d::magnitude(){
	//具體實現不是我們所關心的
}
//被編譯器轉化為(此處先不涉及name-mangling)===>>
Point3d Point3d::magnitude(Point3d *const this){
	//具體實現不是我們所關心的
}
//如果member function為const,則被轉化為(此處先不涉及name-mangling)==>>
Point3d Point3d::magnitude(const Point3d *const this){
	//具體實現不是我們所關心的
}

是的,你沒有看錯,編譯器會在member function的參數列表中加入一個指向該對象本身的this指針。至於在參數列表的頭部還是尾部加入則可不比深究。所以,外界無法訪問到member functions,因為參數列表不匹配。 然後再有mangling生成一個新的函數名,成為一個獨一無二的外部函數。所以即使參數列表匹配也無法進行訪問,因為函數名字也改變了。 老問題:
Point3d obj,*pt;
pt = &obj;
obj.magnitude();
pt->magnitude();

大家覺得上述兩種函數的調用有無重大差異? 下面,我們來看看經過編譯器的mangling算法轉化後的樣子。
obj.magnitude();
//==>>
magnitude_7Point3dFv(&obj);

pt->magnitude();
//==>>
magnitude_7Point3dFv(pt);

顯然,幾乎沒有什麼區別。 大家現在是不是對nonstatic member function有一定的了解了呢?那麼,我們接著看static member functions吧。

3.2 靜態成員函數


static member functions與nonstatic member functions的重大差異在於static member functions沒有this指針。那麼,必然導致以下結果: 1 它不能直接存取class中的nonstatic data members; 2 其不能被聲明為const,volatile或virtual。 3 其不需要經由對象來調用,雖然我們一般都是用對象在調用之。 一個static member function 幾乎就是經過mangling的nonstatic member function。 我們來看看mangling對static member function的轉化:
//假設count()為Point3d中的一個static member function
unsigned int Point3d::count(){
	//.....
}
//===>>
unsigned int count_5Point3dSFV(){
}

函數名中的大寫字母S就代表著static。 我們還有一個證據,看下面的例子:
&point3d::count()

大家猜猜得到的值得類型是什麼樣子的?unsigned int (Point3d::*)()還是unsigned int (*)() ? 答案顯然是後者,static member function俨然已是半個nonmember function了。 那麼我們再來看看
obj.count();
pt->count();

兩者有無重大差異? 顯然沒有了this指針以後,count()會被轉化為一般的nonmember 函數:
count_7Point3dSFV();

兩者的調用幾乎一樣。

3.3 虛成員函數


我們大家都知道的是對象中會有一個虛表指針,對應的虛表中有各個虛函數的slot。 這個地方水有點深,我不想討論那麼深,原因有二: 1 自己沒把握把這個地方說透。 2 並不是所有人都對那麼深的東西感興趣。 感興趣的朋友可以查閱相關資料。 虛成員函數與nonstatic 成員函數的區別在於其存在於虛表中。 我們直接看下面的例子:
//假設 Point3d 中的第一個虛函數為normalize(),那麼
Point3d obj,*pt;
pt = &obj;
pt->normalize();
obj.normalize();

pt->normalize();要想知道具體函數調用normalize()是哪個,就必須得知道pt所指對象的類型。在這個過程中我們需要知道兩個信息: 1 pt所指對象的類型信息。 2 virtual function的偏移量。 一般做法是將這兩樣信息加入虛表中,即可在編譯期間獲知其具體調用。然而,visual studio 2010似乎不是這樣做的。其具體做法還有待考究。 上述說的是單一繼承,多重繼承的時候會麻煩一些。 在vs2010下面,一個derived class內含n-1個額外的virtual table ,n表示其上一層base class的個數(單一繼承不會有額外的virtual table)。 我們來看一個例子:
class Base1{
public:
	Base1();
	virtual ~base1();
	virtual Base1 *clone()const;
protected:
	float data_Base1;
};
class Base2{
public:
	Bsae2();
	virtual ~Base2();
	virtual Base2 *clone()const;
protected:
	float data_Base2;
};
class Derived:public Base1,public Base2{
public:
	Derived();
	virtual ~Derived();
	virtual Derived *clone()const;
protected:
	float data_Derived;
};

內存布局圖如下所示: \
我們來看下面一組操作:
Base2 *phase2 = new Derived;

編譯器會將上述代碼翻譯如下:
Derived *tmp = new Derived;
Base2 *phase2 = tmp? tmp+sizeof(Base1):0;

新的Derived對象的地址必須調整以指向其Base2子對象。大家現在是否明白了基類指針釋放子類對象的時候如果不將析構函數聲明為虛函數就不能釋放完全的原因了吧! 然而,對於sun編譯器來說,上述形式並不適用,其為了調節執行期間連接器的效率,將多個virtual table連鎖為一個。感興趣的朋友自行查閱相關資料。 我們這裡沒有討論虛擬繼承下的virtual function。 接著上面的話題:
pt->normalize();
obj.normalize();

兩者區別在哪? 首先,pt->normalize();被內部轉化為:(*pt->vptr[0])(pt);這點毋庸置疑。 vptr為指向虛表的指針,0為內部偏移量,pt為zhis指針。 obj.normalize();被內部轉化為:(*obj.vptr[0])(&obj);真是這樣嗎?顯然不是。因為沒必要。 上述由obj調用的函數實例,只可以是Point3d::normalize();經過一個對象調用virtual function總是被編譯器視為像對待一般nonstatic member function一樣。 所以obj.normalize()被內部轉化為normalize_7Point3dFV(&obj); 至此,已大體說完。你現在看到class是否有種赤裸裸的感覺呢?



  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved