1. C++對象模型:
其他的兩種曾經提出的對象模型:
1. 簡單對象模型:
對象是一個slot集合,每一個slot即一個指針,指向成員變量或成員函數
如下圖所示,對象是由一系列slot組成,一個slot執行一個類方法或類變量。

2. 表格驅動模型對象:
對象成員變量和成員變量函數各自放在一個表中,對象中有兩個指針分别指向這兩個表。
3. C++對象模型:
1. nonstatic data memger 放置在每個class object中
2. static data members放在class object之外的地方
3. static 與 nonstatic function members被放在class objects之外的地方
4. virtual functions 以兩個步驟支援:
class 産生指向virtural functions的指針,放在表格中,這個表格成為virtural table。
每個對象安插一個指針,指向相關的virtual table,成為vptr。vptr的設定和重置由constructor, destructor, copy assignment完成。
type_info object (它是支援RTTI的)也是由tirtural table指出,位于表格的第一個slot中。
類對象,靜态成員,靜态方法,虛拟表等部分組成。
2. 對象的記憶體布局:
nonstatic object member + vptr
vptr 為指針,在32系統中,指針所占的記憶體是4個位元組
#include <iostream>
using namespace std;
class Animal
{
public:
// virtual void Shout(){};
// virtual void Eat(){};
// int GetAge(){return 1;};
int GetAge();
private:
// int age;
};
int Animal::GetAge()
{
return 1;
}
int main()
{
Animal animal;
cout<<"The size of class without member:" << sizeof(animal) <<endl;
cout << "End" << endl;
return 0;
}
上述代碼驗證:
1. 如果沒有虛函數、沒有成員變量、沒有普通成員函數,類定義的對象的大小為1 (使用sizeof()得到)
這一個位元組是為了編譯器為了訓示該類的存在而設定,如果類有了虛函數或成員變量,則不再需要改填充位元組。
2. 如果單純設定虛函數,類對象的大小為4個位元組。因為它包含了指向虛函數表的指針 vptr
此時不需要那一個位元組的填充,如果這一個位元組是類必須的,此時的類對象的大小應該是 5
3. 同理如果僅僅有一個int型的成員變量age,此時類對象的大小也是4個位元組.
4. 如果成員變量age和虛函數都存在則此時的類對象的大小為8個位元組,也即int型age占有四個位元組, vptr指針占有四個位元組
5. 如果是兩個純虛函數,此時的類對象大小仍然是 4個位元組
6. 普通成員函數,其并不位于類對象之中,它位于類記憶體之中,并不占用類對象的記憶體。
問題:
1. 将子類對象賦給父類對象(非指針或引用形式指派),對象記憶體将被裁減(sliced),該裁減如何完成:
複制一個父類對象?還是其他的方法。
2. 裁減之後,虛函數表指針vptr也被複制(不會是單純的複制)?還是會被重新複制,重新複制由誰完成?
此處的vptr不會被複制,它會被父類的複制構造函數重新指派新的虛拟函數表。
3. 構造函數
需要合成預設構造函數的情況:
1. 帶有預設構造函數的成員類對象(member class object),包含該類對象的類會在自己的預設構造函數中隐式(由編譯器完成)加入
成員類對象的預設構造函數,有多個成員類對象時,調用構造函數的順序按照類對象定義的順序添加。
2. 帶有預設構造函數的基類,其子類中如果沒有構造函數,則會合成以訛預設構造函數,在該合成的構造函數中其中會調用基類的預設構造函數。
3. 帶有虛函數的類:
1. 類聲明或繼承一個virtual function
2. 類繼承鍊中有一個類或多個類為虛基類
由于vptr指針要由構造函數類初始化,是以對有虛函數的類或者派生類藥合成構造函數來做這些工作
4. 帶有虛基類的類:多繼承需要确定虛基類成員所在的記憶體位址,也即生成一個唯一指向虛基類成員的指針,這個指針需要由構造函數完成。
注:
盡管合成構造函數,但是非成員類對象還是不會進行初始化,也即非靜态的普通非對象成員變量。
4. 拷貝構造函數
使用拷貝構造函數(copy constructor)的情況:
class Animal{};
1. 使用一個對象初始化另外一個對象時
Animal A;
Animal B(A)
Animal C = A;
Animal D = Animal(A);
2. 以一個類對象作為函數的參數
void GetName(Animal A){}
3. 函數中傳回一個類對象
void GetAClass()
{
Animal A;
return A;
}
三種情況下需要用到拷貝構造函數,如果提供了則進行顯示調用,如果沒有顯示提供,則需要合成預設的拷貝構造函數
預設的拷貝構造函數實作的是按位拷貝,其中的指針和引用會按照位進行拷貝,不會對他們指向的内容進行拷貝。
對于成員類對象,如果該類亦沒有顯示提供拷貝構造函數,它也是按照位拷貝進行。
非位拷貝的情況:
1. 成員類對象有拷貝構造函數
2. 本類所繼承的基類具有拷貝構造函數
3. 類中聲明了一個或多個虛函數
4. 類繼承鍊中有一個或多個virtual base class 存在。
其中3,4不按照位拷貝是需要對vptr進行正确複制,需要合成拷貝構造函數對vptr進行正确指派
Bear作為ZooAnimal的子類,将Bear的對象指派給ZooAnimal類對象,需要對對象的virtual table 做重新指派。
即如将子類對象初始化基類對象,不能按照位拷貝來進行複制,需要對基類獨享的virtual table指針重新指派,是以需要合成拷貝構造函數。
拷貝構造函數分别對上述三種情況都做了修改:
1.
Animal A;
Animal B(A)
Animal C = A;
Animal D = Animal(A);
改寫為:
Animal B;
Animal C;
Animal D;
調用拷貝構造函數
B.Animal::Animal(A);
C.Animal::Animal(A);
D.Animal::Animal(A);
2.
void GetName(Animal A){}
修改為:
Animal temp;
temp.Animal::Animall(A);
void GetName(temp);
3.
Animal GetAClass()
{
Animal A;
return A;
}
改寫為:
void GetAClass( Animal & _A)
{
Animal A;
A.Animal::Animal();
_A = &A;
return ;
}
5. 使用member initialization list的情況:
1. 初始化一個reference member時
2. 初始化一個const member時
3. 調用一個base class的constructor時,而它有一組參數
4. 當調用一個member class的constructor,而它具有一組參數時
對于成員類對象放到參數類表中,初始化效率更高
如:
class Animal
{
Animal()
{
name = "";
age = 0;
}
private:
string name;
int age;
}
Animal()
{
name = "";
age = 0;
}
被改寫為:
Animal()
{
string temp;
temp.string::string("");
name = temp;
temp.destrucor();
age = 0;
}
Animal(): name("")
{
age = 0;
}
被改寫為:
Animal()
{
name.string::string("");
age = 0;
}
省去了臨時對象的建立和銷毀的過程。
構造函數會将成員清單中的變量初始化安插在函數體内其他的初始化變量代碼前,順序按照聲明順序進行。
6. 不同編譯器對類對象大小的影響:
對于一個空類(不包含任何資料成員和成員函數),為了在記憶體中有效表示類對象,編譯器為對象額外填補了一個位元組的資料(無用)
由于編譯器的不同,有的編譯器這一位元組會被其子類所“繼承”,那麼子類的大小為 1 Byte + sizeof(vptr) = 5 Bytes,
由于C++編譯器實作位元組對齊,那麼最終的類對象大小将是 8 Bytes
對于多繼承,如果每一個父類都有虛函數,也即有序函數表,那麼子類中将分别繼承他們,子類對象中要包含每一個父類的vptr指針。
7. 資料成員:
靜态資料成員放在程式的資料段,為類所擁有,不屬于任何一個個别類。
非靜态資料成員按照不同的通路區(Access sections),按照聲明的先後順序在對象中排列。
資料成員存取:
1. static data menber: 靜态資料成員,對應于類放在程式的data segment之中,對其存取不是使用類對象,
而是使用類,如Point3d::chunkSize來實作。
如果多個類有相同的靜态變量,那麼編譯器會将他們改名,按照類來進行。
2. 非靜态資料成員(nonstatic data members)
在成員函數中,對nonstatic data members的存取由一個implicit class object(由this指針表達)完成。
被改寫為:
為translate()函數自動加入了一個類的this指針。
對于nonstatic data members的存取操作:編譯器将class object的其實位址加上 Data member的偏移位置(offset)得到。
Data member的偏移位置offset在編譯期間可以獲知,繼承的member也可以獲知。nonstatic data member 效率和存取一個C struct member或
非派生類的成員是一樣的。
origin.x = 0.0;
pt->x = 0.0
兩者差別,若對于非繼承類,那麼兩者相同,對于Point3d這樣的繼承類,且其中有一個Virtual base class
那麼pt->x 則需要額外工作來确定其真實類型在通路,而對象則不必,它的類型在定義時已經确定。
因為有可能将子類對象指派給父類對象指針。
8. Data Member分四種情況讨論:
1. 單一繼承且不含有virtual functions
2. 單一繼承并且包含有virtual functions
3. 多重繼承
4. 虛拟繼承
1. 對于concrete derive來說,繼承不會給子類通路資料成員帶來時間和空間上的額外的消耗。
對象資料記憶體中的布局例子如下:
此時會出現子類對象比你實際理論計算的記憶體空間要大。
由于Bytes padding(位元組填充)父類中會有填充位元組,而子類派生父類,添加新的變量,這些變量是在父類的padding位元組之後加入。
如下例:
此類對象占用的記憶體即 int + char + char + char + padding.
将它修改為繼承的形式,每一次繼承,添加一個成員:
而此時Contrete3的對象記憶體布局如下圖所示:
顯然Concrete3的對象所占的記憶體比Concreate類對象要大不少,但是他們的資料成員是一樣的。
是否可以如同Concreate類一樣,讓Concrete3的成員“擠一下”?
由此圖,明顯地,他們是無法“擠一下”的,因為這樣在父類對象與子類對象之間進行指派時會出現錯誤。
2. 單繼承帶有virtual functions(即實作了多态)
時間與空間上均有增加:父類由于有虛函數,要添加virtual table,并且類對象中要添加vptr指針。對構造函數,如果沒有明确定義
那麼要隐式合成,完成對vptr的正确指派。
對于子類,要繼承一個父類的vptr,但是如果子類中仍然有virtual functions,在自對象中不會再添加virtural table
為何不會再添加,子類中的virtural function 放到那個virtual table中呢?疑問ing......
3. 多重繼承:
多重繼承不如單繼承那麼自然簡單。
如多繼承例子:
他們之間的繼承關系如下圖所示:
他們各自的對象在記憶體中的布局如下:
多繼承下,記憶體的分布如圖所示,Vertex3d将所有的父類內建到自己的記憶體中
4. 虛拟繼承:
按照多繼承的思路,中間類繼承子同一個基類,那麼最底層子類中頂層基類會出現兩次。
即Vertex3d的對象中要Pointed2d 要出現兩次(來自不同的中間基類)
顯然有問題,解決方法:
1. 對虛繼承中,虛基類在子類中對象中防止一個指針指向該虛基類在子類中的記憶體位置,對每一個virtual base class
要放一個指針。
2. 将虛基類在子類對象中的offset放在virtual table中,對虛基類進行通路時,首先拿到其在子類對象記憶體分布中的
offset,計算後通路從虛基類繼承的成員變量。
例子:
他們的繼承關系如下圖:
各個類的對象的資料布局如下圖所示:
Vertex3d由于虛繼承了Point2d,那麼它從Point3d和Vertex中繼承的指向Point2d的subobject的指針,在Vertex3d中指向了同一個Point2d的subobject。這樣保證了在對象位址中僅僅有一個虛基類(Point2d)的subobject對象存在。
8. Member Function 的調用方式
1. nonstatic Member functions(非靜态成員函數)
非靜态成員函數要與非成員函數有一樣的調用代價,不應帶有額外負擔。
編譯器将member functions轉化為了nonmember function。
上述函數由 float Point3d::magnitude3d(){ return sqrt(x*x + y*y + z*z); }方法轉化而來。
該轉化分為三步:
1)改寫函數的原型(signature),安插一個額外的參數到member function中,用以提供一個存取成員變量的管道,使得class object可以調用該函數,該額外參數為this指針。
如:
Point3d Point3d::magnitude(Point3d * const this);
2)将每一個對nonstatic data member的存取操作,改為經由this指針來存取。
{
return sqrt(
this->_x * this->_x +
this->_y * this->_y +
this->_z * this->_z);
}
3)對member function重寫一個外部函數,對函數名稱進行“mangling”處理,使其在程式中成為獨一無二的函數。
Extern magnitude__7Point3dFv( register Point3d * const this);
由此:obj.magnitude(); 操作變為了 Magnitude__7Point3dFv( &obj); 操作
ptr->magnitude(); 操作變為了 Magnitude__7Point3dFv( ptr); 操作
2. 換名處理:
Member 前面會加上class名稱,形成一個獨一無二的命名,對于重載函數換名後可能是同名函數,處理方法是給它加上參數清單。
3. 虛成員函數:
如果normalize()為虛成員函數,那麼ptr->normalize()将轉化為
(*ptr->vptr[1])(ptr);
vptr為virtual table,virtual table中的函數也會被改名,主要由于派生産生的問題。
4. static member functions(靜态成員函數)
C++有時需要一些獨立于class object之外的操作,引入了static member functions。其主要的特征就是沒有this指針。
由此:1. 不可以通路nonstatic members;2. 不能聲明為const,volatile或virtual類型的方法;3. 不需要經由class object被調用
static function 差不多等同于一個nonmember,它可以成為一個callback函數。
5. virtual member functions(多态問題)
為了支援virtual function機制,必須首先能夠對多台類型有某種形式的“執行期類型判斷法”(runtime type resolution),也即要得到指針ptr->z()中ptr的某種對象資訊。
最簡單的方法:
設ptr帶有其所對應對象資訊,如所參考對象位址或對象類型的某種編碼或某個結構,可以唯一辨別該類,以便正确決定z()方法的真實位址。
缺點:需要額外增加空間負擔,破壞了與C語言的連結相容性。
其實此處隻需要知道兩個額外資訊:
ptr所指對象的真實位址,進而可以選擇正确的z()執行個體
z()執行個體的位置,以便可以調用它。
實作中,可以給每一個多态的class object添加兩個member:
1. 字元串或數字,辨別class的類型
2. 一個指針,指向某表格,表格中持有程式的virtual functions執行期的位址。
要在執行期找到函數:
1. 為了找到表格,每一個class object安插一個有編譯器内部産生的指針,指向該表格。
2. 為了找到函數位址,每一個virtual function被指派一個表格索引。
基類Point:
繼承類如下:
其中的虛函數處理如下圖所示:
在繼承類中,需要将虛函數表中的虛函數slot指向的虛函數修改掉。
上述即C++的處理方法:
給虛基類加一個virtual table,其中存放了所有虛函數,而繼承類中會對應地放置改寫了的基類的virtual function位址。這些函數在子類中進行了重寫,virtual bable中放置的是新的函數的位址。
如果子類添加新的virtual functions,這些functions放置在繼承的virtual table中,在virtual table中添加新的slot,将函數添加進去
ptr-> z(); 由編譯器改寫為(*ptr->vptr[4])(ptr);
6. 多重繼承
多重繼承中支援virtual functions,複雜度主要是來自第二個以及後繼的base classes上,必須在執行期間動态調整this指針,讓它指向正确的位址。
按照多繼承的記憶體知,如Base2基類,在Base1基類之後,
Base2 * pbase2 = new Derived();
應該調整為指向Base2 subobject,
Derived * temp = new Derived();
Base2 * pbase2 = temp?temp+ sizeof(Base1): 0;
但是delete pbase2; 時,有需要将指針做調整,讓其指向記憶體起始位置。
指針的調整有如下幾個方法:
1. 對所有的函數通路,計算偏移位址,再調用。
這種方法會将非虛函數也連累進來。
2. 使用Thunk技術
Thunk為一段彙編代碼,用來以适當的offset調整this指針,讓它指向正确的記憶體,在一個作用就是跳轉到virtual function去。
Sun中使用的是所謂的”split functions”技術。Microsoft 使用所謂的“address points”來取代thunk技術。
7. 虛拟繼承
虛拟繼承,将parent class 的subobject的偏移量放置到virtual table中,用于通路父類的subobject。
8. inline functions
内聯函數在編譯期間,将整個函數做優化,如果結果是常量值,直接使用常量值替代;計算的式子,直接将式子內建到代碼中,替代函數。
内聯函數類似define聲明的常量替換,但較它更安全。内聯函數會将函數中的代碼嵌入到主代碼中,使得代碼量增大,對于較大的函數不宜聲明為内聯。内聯由于進行了優化,可以極大地提高函數調用的速度。
9. 特殊指針——指向對象成員的指針
1. 指向成員變量的指針
成員變量按照聲明順序和通路區順序依次排列,以如下的類為例:
class Point3d
{
public:
virtual ~Point3d();
public:
static Point3d origin;
float x, y, z;
};
根據vptr放置位置不同,三個坐标值在對象布局中的offset不同,如果vptr放置在結尾位置,三個坐标值的offset為 0,4,8。如果vptr放置在對象開始處,對象布局中的offset就分别是4,8,12。而事實上,去data members的位址,傳回來的值總是多1,也就是1,5,9,或5,9,13等。
這麼處理的原因是區分一個“沒有指向任何data member”的指針,和一個指向“第一個data member”的指針。如:
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
由于p1和p2相等,是以無法區分。是以給指向資料成員的指針加1,以示差別。
但是測試上述類的結果如下:
列印data member的位址。
cout << "Address of Member Values:" << endl;
printf("&Point3d::x = %p\n", &Point3d::x);
printf("&Point3d::y = %p\n", &Point3d::y);
printf("&Point3d::z = %p\n", &Point3d::z);
結果:
解釋了“指向data members的指針”之後,可以解釋如下兩個式子:
& Point3d::z; 和 & rgin.z;
“個nonstatic data member的位址,将會得到它在class中的offset”,取一個“綁定于真正class object身上的data member”的位址,将會得到該member在記憶體中的真正位址。
多重繼承之下,若要将第二個(或後繼)base class的指針,和一個“與derived class object綁定”的member結合起來,那麼将會因為“需要加入offset值”而變得相當複雜。
2. 指向Member Function的指針
取一個nonstatic member function的位址,如果該函數是nonvirtual,得到的結果是他在記憶體中的真正位址,這個值也是不完全的,需要綁定到某個class object上,才能夠調用該函數。所有的nonstatic member functions都需要對象的位址(以參數this指出)
double 傳回類型
( Point::* 指針類型
pmf) 指針的名稱
(); 參數清單
Double (Point::* coord)() = &Point::x;
Coord = &Point::y;
調用函數:
(origin.*coord)()
(ptr->*coord)()
指向member function的指針的聲明文法,以及指向”member selection運算符”的指針,其作用是作為this指針的空間保留者,這也是為何static member functions(沒有this指針)的類型是“函數指針”,而不是“指向member function的指針”的緣故。
指向virtual member functions的指針
類似虛成員資料,獲得的是一個在類中的偏移setoff,而nonvirtual function獲得的是真正的記憶體位址。這就需要在執行時差別是否是virtual 函數指針。
一部分編譯器中的做法是将指針與~127做位與操作,如果結果為0,說明是虛函數指針,否則是非虛函數指針。進而在程式中執行不同的代碼:
(((int) mpf)& ~127) ? (*pmf)(ptr) : (*ptr->vptr[(int)pmf](ptr));
多繼承之下,指向member function的指針。
為指針專門設計一個結構:
struct __mptr
{
int delta;
int index;
union{
ptrtofunc faddr;
int v_offset;};
};
Index 和 faddr 分别持有virtual table索引和nonvirtual member function位址。進而根據index值是否是0,進而進行不同的調用。
(ptr->*pmf)();
改寫為:
(pmf.index < 0) ? ( *pmf.faddr)( ptr) : (* ptr-> vptr[pmf.index](ptr));
10. 構造函數
1.
class Abstruct_base
{
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char * mumble() const {return _mumble;}
protected:
char * _mumble;
};
抽象的Base class,其中有pure virtual function,使得Abstract base不可能擁有執行個體,但是仍然需要提供一個顯式的構造函數以初始化data member _mumble,沒有這個構造函數,derived class的對象将無法初始化_mumble的初值。
2. 純虛函數可以被調用,隻能以靜态方式調用(invoked statically),不可以經過虛函數機制調用:在子類的方法中調用Abstract_base::interface();
唯一例外的情況是pure virtual destructor,class設計者必須得定義它,每一個derived class constructor會被編譯器加以擴充,以靜态調用的方式調用其中每一個 virtual base class以及上一層base class的destructor,隻要缺少一個base class destructor的定義,就會導緻繼承鍊斷裂而編譯失敗。
#include <iostream>
using namespace std;
class Abstract_base
{
public:
virtual ~Abstract_base() = 0;
virtual void interface() = 0;
virtual const char * mumble() const {return _mumble;}
protected:
char * _mumble;
};
class Concrete_derived: public Abstract_base
{
public:
Concrete_derived();
};
class Third_Derived : public Concrete_derived
{
public:
~Third_Derived() {};
};
int main()
{
Third_Derived thirdDerived;
cout << "Hello world!" << endl;
return 0;
}
出現錯誤:
隻要将base class的析構函數定義為純虛函數,此錯誤就會出現,與中間子類是否定義析構函數沒有關系。
\Constructor\main.cpp:(.text$_ZN16Concrete_derivedD2Ev[Concrete_derived::~Concrete_derived()]+0x16)||undefined reference to `Abstract_base::~Abstract_base()'|
疑問ing????????
較好的解決方法:不要将virtual destructor聲明為pure。
不将destructor聲明為pure,就不會出現錯誤。
3. const 函數:
聲明函數為const,然後發現其繼承類的執行個體會修改 data member,由于聲明為const,無法修改成員。簡單的解決方法是不再用const聲明函數。
4. 無繼承的對象構造:Point local; Point * heap = new Point();
對于new 操作,并不會有default constructor施行與其傳回的Point object上,而delete時,如果沒有顯式的destructor存在,則也不會調用類的析構函數,僅僅簡單地将記憶體釋放即可。
如果沒有顯示的copy constructor,那麼傳回、指派等操作完全是簡單的為拷貝操作。
例如:
type struct
{
float x, y, z;
}Point;
觀念上,編譯器會為Point聲明trival default constructor、一個trival destructor、一個trival copy constructor,以及一個trival copy assignment operator。但是實際上,編譯器會分析這個聲明,為它貼上Plain Ol’ Data标簽。
在實際的使用中,如聲明對象,銷毀對象,對象間指派,指派符号的使用都不會調用這些函數。
4. 帶有虛函數
一旦有了虛函數,就需要對原有的函數等進行擴充,定義constructor代碼中,會在參數清單中加入對象的this指針,以便正确初始化vptr參數。
需要合成copy constructor 和 copy assignment operator,而且操作不再是trival的,不能使用預設的bitwise或memberwise的拷貝或指派方式,同時這些方法也帶有this指針參數,以便正确初始化vptr參數。
繼承體系下的對象構造,construtor可能含有大量的隐藏代碼,因為編譯器會擴充每一個constructor,擴充程度是class T的繼承體系而定。一般而言編譯器所作的擴充大約包含以下幾個方面:
1. 記錄member initialization list中的data members初始化操作會被放到construtor函數體内部,以menbers 的聲明順序為序。
2. 如果有一個member 并沒有出現在member initialization list之中,但它有一個default constructor,那麼該default constructor必須備調用。
3. 在那之前如果class object有virtual table pointers,他們必須設定初始值以指向适當的virtual tables。
4. 在那之前,所有上一層的base class constructors必須被調用,以base class 的聲明順序為順序(與member initialization list中的順序沒關聯)
5. virtual base class constructors必須備調用,從左到有,從淺到深。
5. 虛拟繼承(virtual inheritance)
由于繼承中隻能有一個base class subobject存在,那麼對與base class 的構造函數調用,隻能調用一次。
對與vptr指針的初始化,最适當的地方是base class constructor調用之後,但是在程式員提供的代碼或是member initialization list中的menber初始化操作之前。
解決方法:
有些編譯器将constructor分裂為二,一個針對完整的object,一個針對subobject。
11. 拷貝函數
如果沒有顯式聲明拷貝構造函數,則使用預設的拷貝構造函數則實作的是bitwise拷貝。以下的情況中,不會進行位拷貝:
1. 當class内含有一個member object,而class 有一個copy assignment operator時。
2. 當一個class的base class有一個copy assignment operator時。
3. 當一個class 聲明了任何virtual functions,此時需要對vptr進行正确指派,是以不可用bitwise拷貝方式
4. 當class繼承一個virtual base class,不論此base class有沒有copy operator時。
12. 析構函數
如果class沒有定義destructor,那麼隻有在class内涵的member object抑或class自己的base class擁有destructor的情況下,編譯器才會自動合成一個出來。否則destructor被視為不需要,也就不需要被合成。
在有constructor時,destructor的調用順序是按照與constructor的順序相反的順序來調用,即從最底層派生類開始,一次向上調用。
13. 對象的構造和析構
一般而言,constructor和destructor的安插都如期望一樣:
// C++
{
Point point;
// point.Point::Point();
…
// point.Point::~Point();
}
如果一個區段,或函數中有一個以上的離開點,情況就會複雜很多,Destructor必須被放在每一個離開點之前。
那麼為了避免一些不必要的對象建立與銷毀,可以将local對象的定義放在臨近使用的地方,這樣在之前如果函數或區段傳回,則不再會建立和銷毀該對象。但許多Pascal或C程式員習慣将所有的objects放在函數或區段的起始處聲明與定義。
全局對象:
C++中的所有global objects都被放在了程式的data segment中,如果顯示給定值,則以該值為初值,否則初始化為0。C語言中并不自動為全局變量賦初值。
對于全局對象的構造函數的調用,是在程式啟動時進行的,這樣需要一個靜态初始化。在一部分的編譯器中的解決方法是:
為每一個需要靜态初始化的檔案愛你産生一個_sti()函數,内含有必要的constructor調用操作或inline expansions。類似地有一個靜态的記憶體釋放操作(static deallocation),構造一個_std()函數,包含了destructor調用操作。這樣提供了一組runtime library “munch”函數:一個_main()函數(用以調用可執行檔案中的所有_sti()函數),以及一個exit()函數(以類似方式調用所有的_std()函數)。
統計這些構造函數和析構函數的方法:
使用nm指令,将所有的obj檔案的符号表格輸入該指令,從其中過濾器其中的構造_sti()和析構_std()函數。
另外一個解決方法是system V 1.0版中的做法,每一個可執行檔案是System V COFF格式,這些檔案中包含了_link nodes,這樣将所有的_sti()和_std()函數找到。
建議在程式中不要用那些需要靜态初始化的global objects。
局部靜态變量:
局部靜态變量同全局變量一樣,其在記憶體位置也是data segment。由于是靜态變量,在整個程式(函數)的運作期間,在程式起始時構造出來對象,在程式結束時才析構掉,中間再次調用該函數也不應該再對該函數進行構造。
一個做法是導入一個臨時的對象,以保護靜态變量隻被構造一次。析構同樣可以參考該臨時的對象。
對象數組:
如果對應的類有構造函數,則需要在配置設定記憶體之後,對每一個對象調用構造函數進行初始化,否則隻是單純配置設定記憶體。
同樣對于有構造函數的類,配置設定數組後,會有相應函數對其進行處理。
vec_new( void * array, size_t elem_size, int elem_count, void (*constructor)(void *), void (*destructor)(void *, char));
同樣需要一個delete 函數釋放掉這些對象。
vec_new( void * array, size_t elem_size, int elem_count, void (*destructor)(void *, char));
14. new 和delete
new 與 delete 其中除了配置設定資源之外,還做了錯誤處理。
int * pi = new int(5);
int * pi = __new (sizeof(int));
*pi = 5;
作出錯誤處理,應該如下:
int * pi;
if ( pi = __new( sizeof( int ) ) )
* pi = 5;
同理delete pi;
if ( pi != 0)
__delete( pi );
對于數組:
在使用new構造時,同樣是使用vec_new()方法來進行。
而在delete 時,對于數組,在前編譯器版本中,需要顯式給出數組大小。
如delete [ array_size] p_array;
而後期的編譯器雖然不需要給出大小,但是需要加上[],如delete [] p_array; delete會自動尋找維數。否則将出現delete了數組第一個元素的錯誤。
14. 臨時對象
臨時對象的産生,要注意其析構的時機,不能夠在使用之前就析構了該對象,否則出現錯誤。
15. Template(模闆)
Template原本被看作是對Container classes如lists和Arrays的一項支援。,但是它目前已經成為了标準模闆庫的一部分,或者用于一項所謂的tempalte metaprogram技術。
如:
template<class type>
class Point
{
public:
enum Status { unallocated, normalized};
Point( Type x = 0.0, Type y = 0.0, Type z = 0.0);
~Point();
void * operator new( size_t);
void operator delete( void *, size_t);
private:
static Point<Type> * freeList;
Type _x, _y, _z;
}
類模闆中的靜态元素或函數隻能使用template Point class的某個實力來存取或操作
是以存取Status類型,如下寫:
Point< float >:: Status s; 不可以如此:Point::Status s;
對于靜态資料成員:
freeList,也必須使用模闆類的某個執行個體來調用:
Point< float >:: freeList; 不可以如此:Point::freeList;
對于模闆類執行個體的指針指派0,則該指針指派為空,而引用類型無法引用空對象,是以對于const Point< float > & ref = 0;預設擴充如下:
Point< float > temporary(float( 0));
const Pooint< float > & ref = temporary;
對于template function不應該被“執行個體化”,至少對那些未被使用過的。隻有在member functions被使用的時候,C++ Standard才要求被“執行個體化”。但是目前的編譯器,并不是都支援這個機制。使用使用者主導“執行個體化(instantiation)”規則,原因如下:
1. 空間和時間效率的考慮。對于有些類根本不會用到某些函數,将他們執行個體化,結果占用記憶體,且編譯效率低下。
2. 尚未實作的機制,并不是一個template執行個體化的所有類型都能夠完全支援一組member functions所需要的所有的運算符。
模闆機制現在很不完善,存在一些缺陷。如所有與類型有關的及愛你查,牽扯到了template的參數,都必須延遲到陣陣的執行個體化(instantiation)操作發生才會進行檢查。
Template中的名稱決議法:
scope of the template definition 也就是定義出template的程式端
scope of the template instantiation 執行個體化template的程式端
template <class type>
class ScopeRules
{
public:
void invariant()
{
_member = foo(_val);
}
type type_dependent()
{
return foo(_member);
}
private:
Int _val;
Type _member;
};
情況一:
extern int foo(int);
ScopeRules < int > sr0;
sr0.invariant();
在invariant()調用的為那個foo執行個體呢?
對于一個nonmember name的決議,是根據name的使用是否與“用以執行個體化template的參數類型”有關決定。如果其使用互不相關,那麼以scope of the template definition來決定name,如果其使用互有關聯,那麼就以scope of the template instantiation來決定name。
Sr0.type_dependent();方法與類型相關,其使用scope of the template instantiation來決定,調用extern int foo(int);。
對于函數的執行個體化行為:
兩種政策,一種是編譯時期政策,程式代碼必須在program text file中備妥可用。另一個是連接配接時期政策。
16. 異常處理(exception handling)
C++的異常處理三個主要詞彙:
1. 一個throw子句。它在某處發出一個exception,被抛出的exception可以是内建類型,也可以是自定義的異常類。
2. 一個或多個catch字句,每一個catch字句都是一個exception handler,它用來表示說,這個字句準備處理某種類型的exception,并且封閉的大括号中提供實際的處理程式。
3. 一個try區段,它被圍繞一系列的語句組成,這些語句可能引發異常。
一旦發生了異常,目前函數對程式的控制權将被剝奪,預設的處理程式terminate()會被調用,函數堆棧中的改函數也會被彈出。Unwinding the stack 程式調用時,每個函數被彈出堆棧,其局部的對象都會被釋放。
對與Exception Handling的支援
1. 檢驗發生throw操作的函數
2. 決定throw操作是否發生在try區段中
3. 若是,編譯系統必須吧exception type 拿來和每一個catch子句進行比較
4. 如果比較吻合,流程控制交到catch子句手中
5. 如果throw的發生并不在try區域,或沒有一個catch子句吻合,那麼系統必須摧毀所有active local objects,從堆棧中将目前的函數unwind掉。進行到程式堆棧的下一個函數中去,然後重複上述步驟。
在catch語句中,與對象進行比較時,它裡面産生的是一個臨時對象,這個對象被傳遞到catch内進行處理,如果在catch内進行再一次抛出,那麼抛出的是本臨時對象,不會将原Exception對象抛出。此處一定注意。
17. RTTI(執行期類型識别)
Type-safe Downcast(保證安全的向下轉換操作)
此處是将父類動态轉化為子類的對象
Type-safe Dynamic Cast(保證安全的動态轉換)
動态的轉化類型,将子類對象轉化為父類對象。
References并不是Pointers:
将dynamic_cast施加到引用上,如果轉換成功則傳回true,否則引發bad_cast exception。
結束:
如果對與Conponent Object Model<COM>感興趣,推薦兩本書:
Essential COM(Don Box/ Addison Wesley):第一章和第二章吧軟體元件的本質,問題所在以及COM的解決之道解釋非常好,帶着讀者以一般的,純粹的C++語言完成一個COM程式結構。但是第三章之後較晦澀難懂
Inside COM,全書清爽簡易,但是最好先通讀Essential COM的前兩章之後,有了紮實的基礎再來讀這本書。