面向對象程序設計語言不少,如Smalltalk、Ada、Eiffel、Object Pascal、Visual Basic、C++等等。C++語言最討人喜歡,由於它兼容C 語言,而且具有C 語言的性能。近幾年,一種叫Java 的純面嚮對象語言紅極一時,很多人叫喊着要用Java 革C++的命。我認爲Java 比如是C++的外甥,雖然不是直接遺傳的,但也幾分象樣。外甥在舅舅身上玩耍時灑了一泡尿,倆人不應爲此而爭吵。
關於C++程序設計的書藉很是多,本章不講C++的語法,只講一些小小的編程道理。若是我能早幾年明白這些小道理,就能夠大大改善數十萬行程序的質量了。
1. C++面向對象程序設計的重要概念
早期革命影片裏有這樣一個角色,他說:「我是黨表明,我表明黨,我就是黨。」後來他給同志們帶來了災難。
會用C++的程序員必定懂得面向對象程序設計嗎?
不會用C++的程序員必定不懂得面向對象程序設計嗎?
二者都未必。就象壞蛋入黨後未必能成爲好人,好人不入黨未必變成壞蛋那樣。
我不怕觸犯衆怒地說句大話:「C++沒有高手,C 語言纔有高手。」在用C 和C++編程8年以後,我深深地遺憾本身不是C 語言的高手,更遺憾沒有人點撥我如何進行面向對象程序設計。我和不少C++程序員同樣,在享用到C++語法的好處時便覺得本身已經明白了面向對象程序設計。就象擠掉牙膏賣牙膏皮那樣,真是暴殄天物呀。
人們不懂拼音也會講普通話,若是懂得拼音則會把普通話講得更好。不懂面向對象程序設計也能夠用C++編程,若是懂得面向對象程序設計則會把C++程序編得更好。本節講述三個很是基礎的概念:「類與對象」、「繼承與組合」、「虛函數與多態」。理解這些概念,有助於提升程序的質量,特別是提升「可複用性」與「可擴充性」。
1.1 類與對象
對象(Object)是類(Class)的一個實例(Instance)。若是將對象比做房子,那麼類就是房子的設計圖紙。因此面向對象程序設計的重點是類的設計,而不是對象的設計。類能夠將數據和函數封裝在一塊兒,其中函數表示了類的行爲(或稱服務)。類提供關鍵字public、protected 和private 用於聲明哪些數據和函數是公有的、受保護的或者是私有的。
這樣能夠達到信息隱藏的目的,即讓類僅僅公開必需要讓外界知道的內容,而隱藏其它一切內容。咱們不能夠濫用類的封裝功能,不要把它當成火鍋,什麼東西都往裏扔。
類的設計是以數據爲中心,仍是以行爲爲中心?
主張「以數據爲中心」的那一派人關注類的內部數據結構,他們習慣上將private 類型的數據寫在前面,而將public 類型的函數寫在後面,如表8.1(a)所示。
主張「以行爲爲中心」的那一派人關注類應該提供什麼樣的服務和接口,他們習慣上將public 類型的函數寫在前面,而將private 類型的數據寫在後面,如表8.1(b)所示。
不少C++教課書主張在設計類時「以數據爲中心」。我堅持而且建議讀者在設計類時「以行爲爲中心」,即首先考慮類應該提供什麼樣的函數。Microsoft 公司的COM 規範的核心是接口設計,COM 的接口就至關於類的公有函數[Rogerson 1999]。在程序設計方面,我們不要懷疑Microsoft 公司的風格。
設計孤立的類是比較容易的,難的是正確設計基類及其派生類。由於有些程序員搞不清楚「繼承」(Inheritance)、「組合」(Composition)、「多態」( Polymorphism)這些概念。
1.2 繼承與組合
若是A 是基類,B 是A 的派生類,那麼B 將繼承A 的數據和函數。示例程序以下:
代碼: |
class A { public: void Func1(void); void Func2(void); }; class B : public A { public: void Func3(void); void Func4(void); }; // Example main() { B b; // B的一個對象 b.Func1(); // B 從A 繼承了函數Func1 b.Func2(); // B 從A 繼承了函數Func2 b.Func3(); b.Func4(); } |
這個簡單的示例程序說明了一個事實:C++的「繼承」特性能夠提升程序的可複用性。正由於「繼承」太有用、太容易用,纔要防止亂用「繼承」。咱們要給「繼承」立一些使用規則:
1、若是類A 和類B 絕不相關,不能夠爲了使B 的功能更多些而讓B 繼承A 的功能。
不要以爲「不吃白不吃」,讓一個好端端的健壯青年平白無故地吃人蔘補身體。
2、若是類B 有必要使用A 的功能,則要分兩種狀況考慮:
(1)若在邏輯上B 是A 的「一種」(a kind of ),則容許B 繼承A 的功能。如男人(Man)是人(Human)的一種,男孩(Boy)是男人的一種。那麼類Man 能夠從類Human 派生,類Boy 能夠從類Man 派生。示例程序以下:
代碼: |
class Human { … }; class Man : public Human { … }; class Boy : public Man { … }; |
(2)若在邏輯上A 是B 的「一部分」(a part of),則不容許B 繼承A 的功能,而是要用A和其它東西組合出B。例如眼(Eye)、鼻(Nose)、口(Mouth)、耳(Ear)是頭(Head)的一部分,因此類Head 應該由類Eye、Nose、Mouth、Ear 組合而成,不是派生而成。示例程序以下:
代碼: |
class Eye { public: void Look(void); }; class Nose { public: void Smell(void); }; class Mouth { public: void Eat(void); }; class Ear { public: void Listen(void); }; // 正確的設計,冗長的程序 class Head { public: void Look(void) { m_eye.Look(); } void Smell(void) { m_nose.Smell(); } void Eat(void) { m_mouth.Eat(); } void Listen(void) { m_ear.Listen(); } private: Eye m_eye; Nose m_nose; Mouth m_mouth; Ear m_ear; }; |
若是容許Head 從Eye、Nose、Mouth、Ear 派生而成,那麼Head 將自動具備Look、Smell、Eat、Listen 這些功能:
代碼: |
// 錯誤的設計 class Head : public Eye, public Nose, public Mouth, public Ear { }; |
上述程序十分簡短而且運行正確,可是這種設計倒是錯誤的。不少程序員經不起「繼承」的誘惑而犯下設計錯誤。
一隻公雞使勁地追打一隻剛下了蛋的母雞,你知道爲何嗎?
由於母雞下了鴨蛋。
本書3.3 節講過「運行正確」的程序不見得就是高質量的程序,此處就是一個例證。
1.3 虛函數與多態
除了繼承外,C++的另外一個優良特性是支持多態,即容許將派生類的對象看成基類的對象使用。若是A 是基類,B 和C 是A 的派生類,多態函數Test 的參數是A 的 指針。那麼Test 函數能夠引用A、B、C 的對象。示例程序以下:
代碼: |
class A { public: void Func1(void); }; void Test(A *a) { a->Func1(); } class B : public A { … }; class C : public A { … }; // Example main() { A a; B b; C c; Test(&a); Test(&b); Test(&c); }; |
以上程序看不出「多態」有什麼價值,加上虛函數和抽象基類後,「多態」的威力就顯示出來了。
C++用關鍵字virtual 來聲明一個函數爲虛函數,派生類的虛函數將(override)基類對應的虛函數的功能。示例程序以下:
代碼: |
class A { public: virtual void Func1(void){ cout<< 「This is A::Func1 \n」} }; void Test(A *a) { a->Func1(); } class B : public A { public: virtual void Func1(void){ cout<< 「This is B::Func1 \n」} }; class C : public A { public: virtual void Func1(void){ cout<< 「This is C::Func1 \n」} }; // Example main() { A a; B b; C c; Test(&a); // 輸出This is A::Func1 Test(&b); // 輸出This is B::Func1 Test(&c); // 輸出This is C::Func1 }; |
若是基類A 定義以下:
代碼: |
class A { public: virtual void Func1(void)=0; }; |
那麼函數Func1 叫做純虛函數,含有純虛函數的類叫做抽象基類。抽象基類只管定義純虛函數的形式,具體的功能由派生類實現。
結合「抽象基類」和「多態」有以下突出優勢:
(1)應用程序沒必要爲每個派生類編寫功能調用,只須要對抽象基類進行處理便可。這一
招叫「以不變應萬變」,能夠大大提升程序的可複用性(這是接口設計的複用,而不是代碼實現的複用)。
(2)派生類的功能能夠被基類指針引用,這叫向後兼容,能夠提升程序的可擴充性和可維護性。之前寫的程序能夠被未來寫的程序調用不足爲奇,可是未來寫的程序能夠被之前寫的程序調用那可了不得。
2 良好的編程風格
內功深厚的武林高手出招每每平淡無奇。同理,編程高手也不會用奇門怪招寫程序。良好的編程風格是產生高質量程序的前提。
2.1 命名約定
有很多人編程時用拼音給函數或變量命名,這樣作並不能說明你很愛國,卻會讓用此程序的人迷糊(不少南方人不懂拼音,我就不懂)。程序中的英文通常不會太複雜,用詞要力求準確。匈牙利命名法是Microsoft 公司倡導的[Maguire 1993],雖然很煩瑣,但用習慣了也就成了天然。沒有人強迫你採用何種命名法,但有一點應該作到:本身的程序命名必須一致。
如下是我編程時採用的命名約定:
(1)宏定義用大寫字母加下劃線表示,如MAX_LENGTH;
(2)函數用大寫字母開頭的單詞組合而成,如SetName, GetName ;
(3)指針變量加前綴p,如*pNode ;
(4)BOOL 變量加前綴b,如bFlag ;
(5)int 變量加前綴i,如iWidth ;
(6)float 變量加前綴f,如fWidth ;
(7)double 變量加前綴d,如dWidth ;
(8)字符串變量加前綴str,如strName ;
(9)枚舉變量加前綴e,如eDrawMode ;
(10)類的成員變量加前綴m_,如m_strName, m_iWidth ; 對於int, float, double 型的變量,若是變量名的含義十分明顯,則不加前綴,避免煩瑣。如用於循環的int 型變量i,j,k ;float 型的三維座標(x,y,z)等。
2.2 使用斷言
程序通常分爲Debug 版本和Release 版本,Debug 版本用於內部調試,Release 版本發行給用戶使用。斷言assert 是僅在Debug 版本起做用的宏,它用於檢查「不該該」發生的狀況。如下是一個內存複製程序,在運行過程當中,若是assert 的參數爲假,那麼程序就會停止(通常地還會出現提示對話,說明在什麼地方引起了assert)。
代碼: |
//複製不重疊的內存塊 void memcpy(void *pvTo, void *pvFrom, size_t size) { void *pbTo = (byte *) pvTo; void *pbFrom = (byte *) pvFrom; assert( pvTo != NULL && pvFrom != NULL ); while(size - - > 0 ) *pbTo + + = *pbFrom + + ; return (pvTo); } |
assert 不是一個倉促拼湊起來的宏,爲了避免在程序的Debug 版本和Release 版本引發差異,assert 不該該產生任何反作用。因此assert 不是函數,而是宏。程序員能夠把assert 當作一個在任何系統狀態下均可以安全使用的無害測試手段。
不多有比跟蹤到程序的斷言,殊不知道該斷言的做用更讓人沮喪的事了。你化了不少時間,不是爲了排除錯誤,而只是爲了弄清楚這個錯誤究竟是什麼。有的時候,程序員偶爾還會設計出有錯誤的斷言。因此若是搞不清楚斷言檢查的是什麼,就很難判斷錯誤是出如今程序中,仍是出如今斷言中。幸運的是這個問題很好解決,只要加上清晰的註釋便可。這本是顯而易見的事情,但是不多有程序員這樣作。這比如一我的在森林裏,看到樹上釘着一塊「危險」的大牌子。但危險究竟是什麼?樹要倒?有廢井?有野獸?除非告訴人們「危險」是什麼,不然這個警告牌難以起到積極有效的做用。難以理解的斷言經常被程序員忽略,甚至被刪除。[Maguire 1993]
如下是使用斷言的幾個原則:
(1)使用斷言捕捉不該該發生的非法狀況。不要混淆非法狀況與錯誤狀況之間的區別,後者是必然存在的而且是必定要做出處理的。
(2)使用斷言對函數的參數進行確認。
(3)在編寫函數時,要進行反覆的考查,而且自問:「我打算作哪些假定?」一旦肯定了的
假定,就要使用斷言對假定進行檢查。
(4)通常教科書都鼓勵程序員們進行防錯性的程序設計,但要記住這種編程風格會隱瞞錯誤。當進行防錯性編程時,若是「不可能發生」的事情的確發生了,則要使用斷言進行報警。
2.3 new、delete 與指針
在C++中,操做符new 用於申請內存,操做符delete 用於釋放內存。在C 語言中,函數malloc 用於申請內存,函數free 用於釋放內 存。因爲C++兼容C 語言,因此new、delete、malloc、free 都有可能一塊兒使用。new 能比malloc 幹更多的事,它能夠申請對象的內存,而malloc 不能。C++和C 語言中的指針威猛無比,用錯了會帶來災難。對於一個指針p,若是是用new申請的內存,則必須用delete 而不能用free 來釋放。若是是用malloc 申請的內存,則必須用free 而不能用delete 來釋放。在用delete 或用free 釋放p 所指的內存後,應該立刻顯式地將p 置爲NULL,以防下次使用p 時發生錯誤。示例程序以下:
代碼: |
void Test(void) { float *p; p = new float[100]; if(p==NULL) return; …// do something delete p; p=NULL; // 良好的編程風格 // 能夠繼續使用p p = new float[500]; if(p==NULL) return; …// do something else delete p; p=NULL; } |
咱們還要預防「野指針」,「野指針」是指向「垃圾」內存的指針,主要成因有兩種:
(1)指針沒有初始化。
(2)指針指向已經釋放的內存,這種狀況最讓人防不勝防,示例程序以下:
代碼: |
class A { public: void Func(void){…} }; void Test(void) { A *p; { A a; p = &a; // 注意a 的生命期 } p->Func(); // p 是「野指針」,程序出錯 } |
2.4 使用const
在定義一個常量時,const 比#define 更加靈活。用const 定義的常量含有數據類型,該常量能夠參與邏輯運算。例如:
代碼: |
const int LENGTH = 100; // LENGTH 是int 類型 const float MAX=100; // MAX 是float 類型 #define LENGTH 100 // LENGTH 無類型 #define MAX 100 // MAX 無類型 |
除了能定義常量外,const 還有兩個「保護」功能:
1、強制保護函數的參數值不發生變化
如下程序中,函數f 不會改變輸入參數name 的值,可是函數g 和h 都有可能改變name的值。
代碼: |
void f(String s); // pass by value void g(String &s); // pass by referance void h(String *s); // pass by pointer main() { String name=「Dog」; f(name); // name 的值不會改變 g(name); // name 的值可能改變 h(name); // name 的值可能改變 } |
對於一個函數而言,若是其‘&’或‘*’類型的參數只做輸入用,不做輸出用,那麼應當在該參數前加上const,以確保函數的代碼不會改變該參數的值(若是改變了該參數的值,編譯器會出現錯誤警告)。所以上述程序中的函數g 和h 應該定義成:
代碼: |
void g(const String &s); void h(const String *s); |
2、強制保護類的成員函數不改變任何數據成員的值
如下程序中,類stack 的成員函數Count 僅用於計數,爲了確保Count 不改變類中的任何數據成員的值,應將函數Count 定義成const 類型。
代碼: |
class Stack { public: void push(int elem); void pop(void); int Count(void) const; // const 類型的函數 private: int num; int data[100]; }; int Stack::Count(void) const { ++ num; // 編譯錯誤,num 值發生變化 pop(); // 編譯錯誤,pop 將改變成員變量的值 return num; } |
2.5 其它建議
(1)不要編寫一條過度複雜的語句,緊湊的C++/C 代碼並不見到能獲得高效率的機器代碼,卻會下降程序的可理解性,程序出錯誤的概率也會提升。
(2)不要編寫集多種功能於一身的函數,在函數的返回值中,不要將正常值和錯誤標誌混在一塊兒。
(3)不要將BOOL 值TRUE 和FALSE 對應於1 和0 進行編程。大多數編程語言將FALSE定義爲0,任何非0 值都是TRUE。Visual C++將TRUE 定義爲1,而Visual Basic 則將TRUE定義爲-1。示例程序以下:
代碼: |
BOOL flag; … if(flag) { // do something } // 正確的用法 if(flag==TRUE) { // do something } // 危險的用法 if(flag==1) { // do something } // 危險的用法 if(!flag) { // do something } // 正確的用法 if(flag==FALSE) { // do something } // 不合理的用法 if(flag==0) { // do something } // 不合理的用法 |
(4)當心不要將「= =」寫成「=」,編譯器不會自動發現這種錯誤。
(5)不要將123 寫成0123,後者是八進制的數值。
(6)將本身常常犯的編程錯誤記錄下來,製成表格貼在計算機旁邊。 3 小結 C++/C 程序設計如同少林寺的武功同樣博大精深,我練了8 年,大概只學到二三成。因此不管何時,都不要以爲本身的編程水平天下第一,看到別人好的技術和風格,要虛心學習。本章的內容少得可憐,就象口渴時只給你一顆楊梅吃,你必定不過癮。我借花獻佛,推薦一本好書:Marshall P. Cline 著的《C++ FAQs》[Cline 1995]。你看了後必定會讚不絕口。會編寫C++/C 程序,不要所以得意洋洋,這只是程序員基本的技能要求而已。若是把系統分析和系統設計比做「戰略決策」,那麼編程充其量只是「戰術」。若是指揮官是個大笨蛋,士兵再勇敢也會吃敗仗。因此咱們程序員不要只把眼光盯在程序上,要讓本身博學多才。咱們應該向北京胡同裏的小孩們學習,他們小小年紀就能指點江山,評論世界大事。