標籤:指標 south c++ prot rtu val luci 用途 複製
第一章 關於對象(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++物件模型】第一章 關於對象