1. 程式人生 > >【深度探索C++對象模型】第一章 關於對象

【深度探索C++對象模型】第一章 關於對象

mar 基礎 對象 ssa 尋址 member 如果 virtual const

第一章 關於對象(Object Lessons) —— 本書作者:Stanley B.Lippman 一、前言 什麽是 C++ 對象模型:簡單的說。就是 C++ 中面向對象的底層實現機制。

本書組織: 第 1 章,關於對象(Object Lessons),介紹 C++ 對象的基礎概念,給讀者一個粗略的了解。 第 2 章,構造函數語意學(The Semantics of Constructors),構造函數什麽時候會被編譯器合成?它給我們的程序效率帶來了如何的影響? 第 3 章。Data語意學(The Semantics of Data)

,討論 data members 的處理。 第 4 章,Function語意學(The Semantics of Function),討論類的各種成員函數,特別是 Virtual 。

第 5 章,構造、析構、拷貝語意學(Semantics of Construction, Destruction, and Copy),探討怎樣支持 class 對象模型,以及 object 的生命周期。

第 6 章,運行期語意學(Runtime Semantics),暫時對象的生與死,new 與 delete 的支持。

第 7 章,在對象模型的頂端(On the Cusp of the Object Model)

,專註於 exception handling, template support, runtime type identification(RTTI)。 讀完此書,或者此系列blogs,會讓你對 C++ 的 class 有更深的了解。你將知道虛函數的實現方式,以及它所帶來的負擔。等等等等,這裏有你想知道關於 class 的一切。

在 C 語言中,“數據”和“處理數據的操作(函數)”是分開來聲明的。也就是說,語言本身並沒有支持“數據和函數”之間的關聯性。

我們把這樣的程序方法稱為“程序性的”。比如。我們聲明一個 struct Point3d:
typedef struct _Point3d { float x; float y; float z; } Point3d;
欲打印一個 Point3D,我們可能須要這樣一個函數:
void Point3d_print( const Point3d* pd ) { printf("(%g, %g, %g)", pd->x, pd->y, pd->z); } //%g和%G是實數的輸出格式符號。它是自己主動選擇%f和%e兩種格式中較短的格式輸出。而且不輸出數字後面沒有意義的零。
在 C++ 中,你可能會這樣來設計一個雙層或者三層的Point3D:
class Point { public: Point( float x = 0.0 ) : _x(x) {}
float x() { return _x; } void x( float val ) { _x = val; } // ... protected: float _x; };
class Point2d : public Point { public: Point2d( float x = 0.0, float y = 0.0 ) : Point( x ), _y( y ) {} // ... protected: float _y; }
class Point3d : public Point2d { public: Point3d( float x = 0.0, float y = 0.0, float z = 0.0 ) : Point2d( x, y ), _z( z ) {} // ... protected: float _z; }
從軟件project的眼光來看,面向對象的特征。使得 C++ 比 C 看起來似乎更好。C 相對而言,更精瘦和簡易。C++ 看起來似乎更復雜,但並不意味著 C++ 不更有威力。 當一個 Point3d 轉換到 C++ 之後,第一個可能會問的問題是:加上了封裝之後,布局成本添加了多少呢?答案是: class Point3d 並沒有添加成本。

三個 data members 直接內涵在每個 class Object 之中。而 成員函數(member functions)盡管在 class 的聲明之內,但卻不會出如今 class 的對象實體(Object)中。每個非 inline member function 僅僅會誕生一個函數實體。而 inline function。會在其每個使用者身上產生一個函數實體。

後面你將看到,C++ 在布局和存取時間上基本的負擔 是由 virtual 引起的。包含 虛函數 以及 虛基類。 二、C++ 的對象模型 首先,C++ 中。
2種成員變量(class data members):靜態的(static) 和 非靜態的(non-static); 3種成員函數(class member functions):靜態的、非靜態的 和 虛擬的(virtual)。
我們來看這麽一個類:
class Point { public: Point( float valx ); virtual ~Point(); float x() const; static int PointCount();
protected: virtual ostream& print( ostream &os ) const;
float _x; static int _point_count; };
那這個 class Point 在機器中將會被怎麽表示呢?這有沒有引起你的求知欲? 【註】原書這裏介紹了 簡單對象模型 表格驅動的對象模型 。這裏跳過這兩個,直接看 C++ 對象模型。 在 C++ 對象模型中。 非靜態的(non-static)成員變量 被配置於每個 class object 之內; 靜態的(static)成員變量 則被存放在全部 class object 之外。也就是全局數據區。(問:假設是這樣。我們的 class 怎麽樣去全局數據區找到屬於它的 static 成員變量?別急,後面會有答案)。 靜態和非靜態的成員函數,也被配置於 每個 class 的實體之外。 虛函數的配置方法是: 1. 每個 class 產生出一堆指向 virtual functions 的指針,並把這些指針放在表格之中。這個表。既是所謂的 虛函數表(virtual table), 或 vtbl; 2. 每個 class 的實體(object) 被加入了一個指針。指向相關的(註意不一定是同一個) virtual table。

通常這個指針被稱為 vptr

vptr 的設定和重置都有每個 class 的 構造函數、析構函數、拷貝以及復制運算符。每個 class 所關聯的 type_info object( 用以支持 runtime type identification, RTTI )也經由 virtual table 被指出來,一般是放在表格的第一個 slot 處。

技術分享
三、C++ 怎樣支持多態 1. 經由一組隱含的轉化操作。比如。把一個 派生類 的指針轉化為一個指向其 public base type 的指針: shape* ps = new circle(); 2. 經由 virtual functions 機制: ps->rotate(); 3. 經由 dynamic_cast 和 typeid 運算符: if ( circle *pc = dynamic_cast< circle* >(ps) )... 多態的主要用途。是經由一個共同的接口。來影響類型的封裝,我們一般會把這個接口定義在一個抽象基類裏面。然後再在派生類裏重寫這個接口。

四、須要多少內存來表現一個 class object? 猜想以下的代碼的 sizeof 結果會是? .eg.1.
class Base { public: Base(); ~Base(); }; // sizeof(Base) = ?
.eg.2.
class Base { public: Base(); ~Base();
protected: double m_Double; int m_Int; char m_BaseName; }; // sizeof(Base) = ?
到底須要多少內存,才幹表現一個 class 的 object 呢?一般而言有: 1. 其 非靜態的成員變量( non-static data members ) 的總和大小。 2. 加上不論什麽因為 內存對齊 的需求而填補上去的控件。 3. 加上為了支持 virtual 而由內部產生的不論什麽額外的負擔。
此外,須要註意的是。一個指針(或是一個 reference)。無論它僅僅想哪一種數據類型。指針本身所需內存大小是固定的。比方。在 win32下,一個指針的大小就是4個字節(byte)。 問題的答案:
第一題: 答案是1。 class Base 裏僅僅有構造函數和析構函數,由前面的內容所知,class 的 member functions 並不存放在 class 以及事實上例內,因此,sizeof(Base) = 1。是的,結構不是0,而是1,原因是由於,class 的不同實例,在內存中的地址各不同樣,一個字節僅僅是作為占位符,用來給不同的實例分配不同的內存地址的。 第二題:答案是16。 double 類型的變量占用8個字節。int 占了4個字節,char 僅僅占一個字節。但這裏它會按 int 進行對齊,Base 內部會填補3個字節的內存大小。

最後的大小就是 8 + 4 + 1 + 3 = 16。

大家能夠調整三個成員變量的位置,看看結果會有什麽不同。

五、指針的類型
Base* p_Base; int* p_Int; vector<string> * p_vs;
請問,一個指向 Base class 的指針和一個指向 int 的指針是怎樣產生不同的呢? 1. 以內存需求的觀點來說,沒有不同。在32位機器上,它們都須要4個字節的內存空間。 2. “指向不同內存的各指針”間的差異。在於其所尋址出來的 object 的類型不同。

也就是說,“指針類型”會教導編譯器怎樣解釋某個特定地址中的內存內容及其涵蓋大小。

比方:一個指向 int 的指針,如果其地址是 1000。在32位及其上。將涵蓋地址空間 1000~1003. 那麽。一個指向地址 1000 的 void* 的指針,將涵蓋如何的地址空間呢?沒錯,我們並不知道。這就是為什麽一個類型為 void* 的指針。僅僅可以含有一個地址,而不可以通過它操作所指的 object 的緣故。 所以。轉型(cast)事實上是一種編譯器指令,它所做的,並非改變指針所含的真正地址,而是教導編譯器該去怎樣解釋指針所涵蓋的地址空間。 六、小結 第一章——關於對象。本章初步介紹了C++的對象模型是如何的,後面的章節將繼續討論這個對象模型的底層實現機制。 在讀完本篇文章之後。你應該理解:

  • 怎樣計算 sizeof(classA) 的大小;
  • 了解 class 的內存布局。
在下一章——構造函數語意學中。我們將了解關於類的構造函數的很多其它知識。

【深度探索C++對象模型】第一章 關於對象