国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

高質量C++/C編程指南 -- 第9章 類的構造函數、析構函數與賦值函數

2019-11-17 05:18:18
字體:
來源:轉載
供稿:網友

  構造函數、析構函數與賦值函數是每個類最基本的函數。它們太普通以致讓人輕易麻痹大意,其實這些貌似簡單的函數就象沒有頂蓋的下水道那樣危險。 每個類只有一個析構函數和一個賦值函數,但可以有多個構造函數(包含一個拷貝構造函數,其它的稱為普通構造函數)。對于任意一個類A,假如不想編寫上述函數,C++編譯器將自動為A產生四個缺省的函數,如A(void); // 缺省的無參數構造函數A(const A &a); // 缺省的拷貝構造函數~A(void); // 缺省的析構函數A & Operate =(const A &a); // 缺省的賦值函數這不禁讓人迷惑,既然能自動生成函數,為什么還要程序員編寫?原因如下:(1)假如使用“缺省的無參數構造函數”和“缺省的析構函數”,等于放棄了自主“初始化”和“清除”的機會,C++發明人Stroustrup的好心好意白費了。(2)“缺省的拷貝構造函數”和“缺省的賦值函數”均采用“位拷貝”而非“值拷貝”的方式來實現,倘若類中含有指針變量,這兩個函數注定將出錯。對于那些沒有吃夠苦頭的C++程序員,假如他說編寫構造函數、析構函數與賦值函數很輕易,可以不用動腦筋,表明他的熟悉還比較膚淺,水平有待于提高。本章以類String的設計與實現為例,深入闡述被很多教科書忽視了的道理。String的結構如下:class String{public:String(const char *str = NULL); // 普通構造函數String(const String &other); // 拷貝構造函數~ String(void); // 析構函數String & operate =(const String &other); // 賦值函數PRivate:char *m_data; // 用于保存字符串}; 9.1 構造函數與析構函數的起源
作為比C更先進的語言,C++提供了更好的機制來增強程序的安全性。C++編譯器具有嚴格的類型安全檢查功能,它幾乎能找出程序中所有的語法問題,這的確幫了程序員的大忙。但是程序通過了編譯檢查并不表示錯誤已經不存在了,在“錯誤”的大家庭里,“語法錯誤”的地位只能算是小弟弟。級別高的錯誤通常隱藏得很深,就象狡猾的罪犯,想逮住他可不輕易。根據經驗,不少難以察覺的程序錯誤是由于變量沒有被正確初始化或清除造成的,而初始化和清除工作很輕易被人遺忘。Stroustrup在設計C++語言時充分考慮了這個問題并很好地予以解決:把對象的初始化工作放在構造函數中,把清除工作放在析構函數中。當對象被創建時,構造函數被自動執行。當對象消亡時,析構函數被自動執行。這下就不用擔心忘了對象的初始化和清除工作。構造函數與析構函數的名字不能隨便起,必須讓編譯器認得出才可以被自動執行。Stroustrup的命名方法既簡單又合理:讓構造函數、析構函數與類同名,由于析構函數的目的與構造函數的相反,就加前綴‘~’以示區別。除了名字外,構造函數與析構函數的另一個非凡之處是沒有返回值類型,這與返回值類型為void的函數不同。構造函數與析構函數的使命非常明確,就象出生與死亡,光溜溜地來光溜溜地去。假如它們有返回值類型,那么編譯器將不知所措。為了防止節外生枝,干脆規定沒有返回值類型。(以上典故參考了文獻[Eekel, p55-p56])9.2 構造函數的初始化表
構造函數有個非凡的初始化方式叫“初始化表達式表”(簡稱初始化表)。初始化表位于函數參數表之后,卻在函數體 {} 之前。這說明該表里的初始化工作發生在函數體內的任何代碼被執行之前。構造函數初始化表的使用規則:u 假如類存在繼續關系,派生類必須在其初始化表里調用基類的構造函數。例如class A{…A(int x); // A的構造函數}; class B : public A{…B(int x, int y);// B的構造函數};B::B(int x, int y): A(x) // 在初始化表里調用A的構造函數{…} u 類的const常量只能在初始化表里被初始化,因為它不能在函數體內用賦值的方式來初始化(參見5.4節)。u 類的數據成員的初始化可以采用初始化表或函數體內賦值兩種方式,這兩種方式的效率不完全相同。非內部數據類型的成員對象應當采用第一種方式初始化,以獲取更高的效率。例如class A{…A(void); // 無參數構造函數A(const A &other); // 拷貝構造函數A & operate =( const A &other); // 賦值函數};class B{public:B(const A &a); // B的構造函數private: A m_a; // 成員對象};示例9-2(a)中,類B的構造函數在其初始化表里調用了類A的拷貝構造函數,從而將成員對象m_a初始化。示例9-2 (b)中,類B的構造函數在函數體內用賦值的方式將成員對象m_a初始化。我們看到的只是一條賦值語句,但實際上B的構造函數干了兩件事:先暗地里創建m_a對象(調用了A的無參數構造函數),再調用類A的賦值函數,將參數a賦給m_a。B::B(const A &a): m_a(a) { … }
B::B(const A &a){m_a = a;…
}
示例9-2(a) 成員對象在初始化表中被初始化 示例9-2(b) 成員對象在函數體內被初始化對于內部數據類型的數據成員而言,兩種初始化方式的效率幾乎沒有區別,但后者的程序版式似乎更清楚些。若類F的聲明如下:class F{public:F(int x, int y); // 構造函數private:int m_x, m_y;int m_i, m_j;}示例9-2(c)中F的構造函數采用了第一種初始化方式,示例9-2(d)中F的構造函數采用了第二種初始化方式。F::F(int x, int y): m_x(x), m_y(y) { m_i = 0; m_j = 0;}
F::F(int x, int y){ m_x = x;m_y = y;m_i = 0; m_j = 0;}
示例9-2(c) 數據成員在初始化表中被初始化 示例9-2(d) 數據成員在函數體內被初始化9.3 構造和析構的次序
構造從類層次的最根處開始,在每一層中,首先調用基類的構造函數,然后調用成員對象的構造函數。析構則嚴格按照與構造相反的次序執行,該次序是唯一的,否則編譯器將無法自動執行析構過程。一個有趣的現象是,成員對象初始化的次序完全不受它們在初始化表中次序的影響,只由成員對象在類中聲明的次序決定。這是因為類的聲明是唯一的,而類的構造函數可以有多個,因此會有多個不同次序的初始化表。假如成員對象按照初始化表的次序進行構造,這將導致析構函數無法得到唯一的逆序。[Eckel, p260-261]9.4 示例:類String的構造函數與析構函數
// String的普通構造函數String::String(const char *str){if(str==NULL){m_data = new char[1];*m_data = ‘/0’;} else{int length = strlen(str);m_data = new char[length+1];strcpy(m_data, str);}} // String的析構函數String::~String(void){delete [] m_data; // 由于m_data是內部數據類型,也可以寫成 delete m_data;}9.5 不要輕視拷貝構造函數與賦值函數
由于并非所有的對象都會使用拷貝構造函數和賦值函數,程序員可能對這兩個函數有些輕視。請先記住以下的警告,在閱讀正文時就會多心:u 本章開頭講過,假如不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省的函數。倘若類中含有指針變量,那么這兩個缺省的函數就隱含了錯誤。以類String的兩個對象a,b為例,假設a.m_data的內容為“hello”,b.m_data的內容為“world”。現將a賦給b,缺省賦值函數的“位拷貝”意味著執行b.m_data = a.m_data。這將造成三個錯誤:一是b.m_data原有的內存沒被釋放,造成內存泄露;二是b.m_data和a.m_data指向同一塊內存,a或b任何一方變動都會影響另一方;三是在對象被析構時,m_data被釋放了兩次。u 拷貝構造函數和賦值函數非常輕易混淆,常導致錯寫、錯用。拷貝構造函數是在對象被創建時調用的,而賦值函數只能被已經存在了的對象調用。以下程序中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了賦值函數嗎?String a(“hello”);String b(“world”);String c = a; // 調用了拷貝構造函數,最好寫成 c(a);c = b; // 調用了賦值函數本例中第三個語句的風格較差,宜改寫成String c(a) 以區別于第四個語句。9.6 示例:類String的拷貝構造函數與賦值函數
// 拷貝構造函數String::String(const String &other){ // 答應操作other的私有成員m_dataint length = strlen(other.m_data); m_data = new char[length+1];strcpy(m_data, other.m_data);}// 賦值函數String & String::operate =(const String &other){ // (1) 檢查自賦值if(this == &other)return *this;// (2) 釋放原有的內存資源delete [] m_data;// (3)分配新的內存資源,并復制內容int length = strlen(other.m_data); m_data = new char[length+1];strcpy(m_data, other.m_data);// (4)返回本對象的引用return *this;} 類String拷貝構造函數與普通構造函數(參見9.4節)的區別是:在函數入口處無需與NULL進行比較,這是因為“引用”不可能是NULL,而“指針”可以為NULL。類String的賦值函數比構造函數復雜得多,分四步實現:(1)第一步,檢查自賦值。你可能會認為多此一舉,難道有人會愚蠢到寫出 a = a 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如
// 內容自賦值b = a;…c = b;…a = c;
// 地址自賦值b = &a;…a = *b;
也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓對象復制自己而已,反正不會出錯!”他真的說錯了。看看第二步的delete,自殺后還能復制自己嗎?所以,假如發現自賦值,應該馬上終止函數。注重不要將檢查自賦值的if語句if(this == &other)錯寫成為if( *this == other)(2)第二步,用delete釋放原有的內存資源。假如現在不釋放,以后就沒機會了,將造成內存泄露。(3)第三步,分配新的內存資源,并復制字符串。注重函數strlen返回的是有效字符串長度,不包含結束符‘/0’。函數strcpy則連‘/0’一起復制。(4)第四步,返回本對象的引用,目的是為了實現象 a = b = c 這樣的鏈式表達。注重不要將 return *this 錯寫成 return this 。那么能否寫成return other 呢?效果不是一樣嗎?不可以!因為我們不知道參數other的生命期。有可能other是個臨時對象,在賦值結束后它馬上消失,那么return other返回的將是垃圾。9.7 偷懶的辦法處理拷貝構造函數與賦值函數
假如我們實在不想編寫拷貝構造函數和賦值函數,又不答應別人使用編譯器生成的缺省函數,怎么辦?偷懶的辦法是:只需將拷貝構造函數和賦值函數聲明為私有函數,不用編寫代碼。例如:class A{ …private:A(const A &a); // 私有的拷貝構造函數A & operate =(const A &a); // 私有的賦值函數};假如有人試圖編寫如下程序:A b(a); // 調用了私有的拷貝構造函數b = a; // 調用了私有的賦值函數編譯器將指出錯誤,因為外界不可以操作A的私有函數。9.8 如何在派生類中實現類的基本函數
基類的構造函數、析構函數、賦值函數都不能被派生類繼續。假如類之間存在繼續關系,在編寫上述基本函數時應注重以下事項:u 派生類的構造函數應在其初始化表里調用基類的構造函數。u 基類與派生類的析構函數應該為虛(即加virtual<span style='font-family:宋體;mso-ascii-font-family: "Times New Roman";mso-hansi-font-family:"Times New Ro


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 白城市| 监利县| 诸城市| 黔东| 隆德县| 水城县| 石景山区| 蕉岭县| 公主岭市| 湛江市| 渑池县| 松滋市| 宁明县| 平顶山市| 南靖县| 宁化县| 出国| 盐亭县| 若羌县| 双辽市| 新宁县| 尼勒克县| 大新县| 柳林县| 当涂县| 伊春市| 长岛县| 蚌埠市| 榆社县| 正宁县| 梅州市| 仙游县| 嘉兴市| 昌吉市| 余江县| 六盘水市| 松潘县| 特克斯县| 新营市| 凭祥市| 监利县|