由於本文是面對C語言基礎的(因為我就是從C學起來的),而MFC是利用C++類技術構建起來的。因此有必要在此為只了解C的朋友們,普及一下C++語言中類的概念。熟悉C++的朋友可以跳過本部分。從總體來說C++是向下兼容C的,你可以很不費力氣地將用C編好了的程序拿到C++環境下編譯執行。其C++只不過是在C的基礎上添加面向對象技術(OOP),也就是類的概念,且值得一提的是C與C++都是由美國的貝爾實驗室(在之前我只知道發明過電話)發明的。
一、什麼是類?
按一些書本上的定義來說“就是一種復雜的數據類型,它是將不同類型的數據和與這些數據相關的操作封裝在一起的集合體。因此,類中的數據具有隱藏性,類還具有封裝性。”嗯,類還像上面的那句話一樣,具有很強的抽象性。讓我來用一個例子來解釋類吧。 嗯,我們世界上有一個生物種類叫做鳥,在C++上世界我們也可以制作一個類叫做鳥類。它應該有頭,有軀干,有腿,有內髒,還有一個非常重要的翅膀。於是,其類描述如下:
class Aves
{
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
char m_strBowels[10];
};
哈,這樣一個鳥類建立好了,怎麼樣與C中結構體沒什麼兩樣吧。(在C++中struct與class基本上是同義詞,過一會兒會說到它們有什麼不同的。)如果你想建立一個小鳥的話,不用像C中那樣麻煩地打struct Aves XXX,而是直接使用Aves XXX就可以了,不打前面的struct或class。在人類對鳥類形成概念之前,鳥的翅膀、軀體等等就真的存在了(沒有人有疑議吧?),但在人們根本不知道鳥的那對長滿羽毛的撲扇撲扇就可以飛的東西叫什麼名字,也不會知道翅膀這個詞指的是什麼意思。現在我們的這個C++鳥類也正處於這個狀態,在那些成員變量中沒有被賦與任何值。而現實生活中,一個種類中的具體名字是在一個類對象形成初期被命名的,這是一個名詞初始化的過程。在C++類中,當建立一個類對象時總也要有一個初始化各成員變量的過程,於是構造函數被引入了。它在一個實例被聲明和被建立(這兩個有一些區別)時調用。我們的C++鳥類各個成員變量的賦值命名就可以利用它來實現:
class Aves
{
Aves ()
{
strcpy(m_strHead, "Head");
strcpy(m_strTrunk, "Trunk");
strcpy(m_strCrura, "Crura");
strcpy(m_strWing, "Wing");
strcpy(m_strBowels,”Bowels”);
}
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
};
瞧,我是怎麼在類裡面建立一個構造函數的。一個類的構造函數的名字要與其類名同名,且不能有返回值,void也不行。我們在構造函數中,對各個成員函數命名。當Aves bird;時(聲明一個bird對象),Aves類的構造函數將會被調用,把bird.m_strHead,bird.m_strTrunk等等成員變量分別賦值為”Head”,”Trunk”。這樣一講,希望大家對構造函數有了一定的了解了。我們既然有構造函數可以對類成員進行初始化,那麼用什麼來對類成員銷注呢?說白了就是有在建立類對象時調用的函數,什麼函數是在類對象被刪除時調用的函數呢?那就是析構函數,其命名規則與構造函數是一樣的,只不過需要在其函數名前緊加一個~(波浪號),且不能有參數。如我們的類就是~Aves(); 至於析構函數具體作用嘛…舉個例子來說,當在構造函數中申請了一段內存,我們就必須在析構函數中釋放這段內存,否則會內存洩漏。那麼什麼時候會引發聲明的類對象被刪除呢?要解決這個問題,我還需要借用一個叫名域(name space)的概念。當系統執行指針離開聲明的類對象所在名域時,就會引發類對象的刪除(類型的有效型也可以如此解釋)。而名域這個概念最實稱的理解就是一對大括號,在這對括號內的空間就是一個名域。(當然名域其實不是這麼簡單的。如類本身就是一個名域,還可以自己設定一個名域,用於類型聲明設定,可以用已有的類型沖突。名域真實用途是這個。具體含義參見《C++標准庫》,圖書大廈有侯捷先生的譯本)比如:
{//名域1
char * strValue;
{//名域2
Aves bird;
{//名域3
strValue=bird.m_strWing;
}
}//<<就在後大括號這裡引發了bird對象的析構函數
strValue=”Blue Atlantis”;
}
這裡有3個名域,我在名域2中聲明了一只小鳥。因為名域3也被包括在名域2裡,所以名域3中的空間也屬於名域2,於是在名域3中引用小鳥對象是正確的。當執行指針離開名域2那一瞬,C++系統將會把在名域2中聲明的所有變量及對象刪除掉。當對象被刪除時,首先會執行析構函數,然後系統再去釋放對象所占用的內存空間。所以當執行到strValue="Blue Atlantis";這一句時,這只小鳥就已經不存在,再去引用它就會編譯錯誤。另外,要講一講對象的建立。一種是像變量一樣聲明建立起來對象,像上面的Aves bird;另一種就是用new語句來建立起來對象。如:
Aves *bird;
bird= new Aves();
new語句跟著一個類的構造函數,它會在內存建立起來一個對象,並把這個對象的指針返回出來。這樣建立起來的對象沒有名域空間的限制。如果要將這個對象刪除掉必須手動的使用delete語句。如:
delete bird;
delete後面跟著指向要刪除對象的指針變量。注意,這個指針變量的類型直接影響到對象的刪除時所使用的析構函數。所以,什麼類型的對象,就要用什麼類型的指針來指向刪除。真實的鳥類應該是可以飛翔(絕大部分),可以發出叫聲,可以在陸上跑(至少可以跳)。所以我們也應該讓我們的鳥類也可以跳,可以飛吧。於是,我要向類中添加成員函數。聲明成員函數可以有兩種方法,一種是在類的聲明體裡面,像上面例子中構造函數的聲明方法,另一種是在類的聲明體外面。外部的表現寫法為 返回值 類名::函數名(參數列表)。注意::是由於兩冒號組成。下面的代碼就是使用第二種聲明方法(當然,兩種方法可以混用):
#include <iostream.h>
#include <string.h>
class Aves
{
public:
Aves ();
~Aves ();
void tweet();
void run();
void fly();
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
private:
char m_strBowels[10];
};
Aves::Aves()
{
strcpy(m_strHead, "Head");
strcpy(m_strTrunk, "Trunk");
strcpy(m_strCrura, "Crura");
strcpy(m_strWing, "Wing");
strcpy(m_strBowels, "Bowels");
cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
cout<<"I can fly by "<<m_strWing<<endl;
}
void main()
{
Aves bird;
bird.fly();
bird.run();
}
我們可以在主函數的運行下看到整個類的運作。在聲明時,一只小鳥誕生,構造函數被運行,輸出”a bird born”。當對象被刪除時,析構函數被執行,輸出”a bird die”。請注意,在類聲明體中,我添加了public:和private:這樣的語句。這是設定訪問權限的語句,它是將從它這一行起直到下一個訪問權限語句前的所有成員變量和成員函數設置成它指定的權限。如public則設置成公有權限,設置這種權限的成員可以被外部所使用,private則設置私有權限,設置成這種權限的成員是不可以被外部所使用,只能夠在其成員函數內被使用,如:使用bird. m_strBowels是非法的,因為其是私有成員變量。因為鳥的內髒在外部是看不見的,它只能被鳥本身所使用。前面說過,在C++中struct和class基本上是同義詞。在class中如果一開始沒有指定權限關鍵字,那麼默認權限為private,而在struct中默認權限是public。當成員函數需要使用其它成員時,可以直接寫其名稱,如在run()函數中直接使用其成員變量m_strCrura。在實際上,完全的寫法應該是this->XXX成員。this是一個本類的指針,在這個類中就是Aves*。它代表當前實例對象的指針,在bird對象中應用run()時,this就是&bird。可以將run函數改寫為
void Aves::run()
{
cout<<"I can run by "<<this->m_strCrura<<endl;
}
講了這麼多,相信大家應該可以寫一個自己的類了吧?快用你的VC,建立一個Win32 Console Application工程,用新建Files向工程添加一個C++ Source file文件,來試試寫一個自己的C++程序吧。
二、類的繼承
接著,要來說說類的繼承。 再拿鳥類來舉例子吧。鳥類還可以細分成很多類,雞啊,鴕鳥啦,什麼的。它們都是鳥類,在整體上有著共同的特征和行為,但也有其它不同點。C++類中也會存在相似的地方。怎麼辦?重新再寫一個類,重新再寫那些相同的成員嗎?這時就需要繼承了。我們可以寫這樣一個類:
class Ostrich : public Aves
{
}
繼承的寫法為:
class 派生類名 : 權限關鍵字(在VC中一般為public) 基類名<,基類名2<…,基類名n>>
這個Ostrich類繼承於Aves類,Ostrich類現在就擁有Aves類中所有的成員。如果從Ostrich類聲明一對象aOstrich,就可以直接調用其aOstrich.run()。實際上就是在調用Aves類中run成員函數。現在要是在Ostrich類上添加成員的話,就是在Aves類的基礎上添加成員。需要說的,在Aves類中有一個私有成員m_strBowels。因為其是私有成員,所以對於其派生類Ostrich也是不可見的。為了解決這個問題,需要將Aves類中的private關鍵字改為protected關鍵字。將m_strBowels成員描述為保護型。保護型,對其派生類是可見,對於外部和私有一樣是不可見的。在現實中,鴕鳥是不會飛的,叫聲也不一樣,那麼我們就需要更改其行為。代碼如下:
class Ostrich : public Aves
{
public:
void tweet();
void fly();
}
void Ostrich::tweet()
{
cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
cout<<"I can''t fly by "<<m_strWing<<endl;
}
我們在Ostrich類的基礎寫了fly(),tweet()成員函數,這是與基類的成員函數名字相同。那麼它們將覆蓋基類的函數,如果再調用Ostrich類的fly(),tweet()函數的話則會調用我們新寫的這兩個函數了。基本代碼如下:
#include <iostream.h>
#include <string.h>
class Aves
{
public:
Aves ();
~Aves ();
void tweet();
void run();
void fly();
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
protected:
char m_strBowels[10];
};
Aves::Aves()
{
strcpy(m_strHead, "Head");
strcpy(m_strTrunk, "Trunk");
strcpy(m_strCrura, "Crura");
strcpy(m_strWing, "Wing");
strcpy(m_strBowels, "Bowels");
cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
cout<<"I can fly by "<<m_strWing<<endl;
}
class Ostrich : public Aves
{
public:
void tweet();
void fly();
};
void Ostrich::tweet()
{
cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
cout<<"I can''t fly by "<<m_strWing<<endl;
}
void main()
{
{
Aves bird;
bird.fly();
bird.run();
bird.tweet();
}
cout <<"====================="<<endl;
{
Ostrich aOstrich;
aOstrich.fly();
aOstrich.run();
aOstrich.tweet();
}
}
在主函數中我加多加了兩對大括號,請大家分析一下bird,aOstrich生存區域。 以上是一個單繼承的例子,至於多繼承解釋理論是一樣。大家可以自己嘗試。在後的第七部分中的COM編寫中將出現多繼承的現象。 在繼承派生還記得,派生類對象也是其基類的對象,基類的指針是可以指向派生類的對象的。如我們有是一個Aves *lpBird;指針,那麼我們寫lpBird=&aOstrich是合法的,因為鴕鳥也是一種鳥。 現在,要提到類最後的一個重要概念就是虛成員函數。
三、虛函數
上一段文字裡說到一個基類指針可以指向一個派生類的對象。如果當lpBird指向了aOstrich,那麼調用lpBird->fly();的結果會是什麼呢?哇,是”I can fly by Wing”,快來看吶,我們指的那只鳥居然會飛了!顯然這是我們不希望看到的結果。為了解決這個問題,我在Aves類聲明體中將所有成員函數定義為virtual虛函數。
class Aves
{
public:
Aves ();
~Aves ();
virtual void tweet();
virtual void run();
virtual void fly();
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
protected:
char m_strBowels[10];
};
再試試看,結果成為我們要的” I can’t fly by Wing”了。為什麼呢?是這樣的。當一個類中有虛函數(包括基類含有的)的時候,會給這個類的所有虛函數建立起一個表,函數名與函數地址的映射(包括基類的虛函數)。當對象執行一個虛函數時,則系統先會查這個虛函數表(vtable),找到這個函數名對應的函數地址,調用它。當在派生類添加了與基類虛函數同名的函數,系統會自動將其設定為虛函數。並將這個函數地址改寫到虛函數表中。如果再調用這個虛函數時,就會調用新添加的虛函數。像上面的例子,當調用lpBird->fly()時,系統會先查lpBird指向對象的虛函數表,而不會不管三七二十一地直接調用其本類的函數。示例代碼如下:
#include <iostream.h>
#include <string.h>
class Aves
{
public:
Aves ();
~Aves ();
virtual void tweet();
virtual void run();
virtual void fly();
char m_strHead[10];
char m_strTrunk[10];
char m_strCrura[10];
char m_strWing[10];
protected:
char m_strBowels[10];
};
Aves::Aves()
{
strcpy(m_strHead, "Head");
strcpy(m_strTrunk, "Trunk");
strcpy(m_strCrura, "Crura");
strcpy(m_strWing, "Wing");
strcpy(m_strBowels, "Bowels");
cout<<"a bird born!"<<endl;
}
Aves::~Aves()
{
cout<<"a bird die!"<<endl;
}
void Aves::tweet()
{
cout<<"jijijijijijiji"<<endl;
}
void Aves::run()
{
cout<<"I can run by "<<m_strCrura<<endl;
}
void Aves::fly()
{
cout<<"I can fly by "<<m_strWing<<endl;
}
class Ostrich : public Aves
{
public:
void tweet();
void fly();
};
void Ostrich::tweet()
{
cout<<"gugugugugugugu"<<endl;
}
void Ostrich::fly()
{
cout<<"I can''t fly by "<<m_strWing<<endl;
}
void main()
{
{
Aves bird;
bird.fly();
bird.run();
bird.tweet();
}
cout <<"====================="<<endl;
{
Ostrich aOstrich;
aOstrich.fly();
aOstrich.run();
aOstrich.tweet();
Aves *lpBird;
lpBird = &aOstrich;
lpBird->fly();
}
}
關於虛函數更具體的情況,請參看vckbase第12期的《解析動態聯編》。關於其它C++語法,請自行查看C++教材。 那麼MFC類呢?簡單地來說,MFC類只是將許多有關聯的API函數將其封裝在一起。在WinSDK中WinAPI都是一些零零散散的函數,它們大部分中都會有一個參數是它的服務對象的句柄。比如,CreateWindow函數會需要一個句柄輸出來返回一個窗口句柄來表達其建立的窗口對象,ShowWindow函數需要一個窗口句柄來指定哪一個窗口要改變顯示狀態,CloseWindow函數需要一個窗口句柄來指定哪一個窗口要被關閉。可以理解為是句柄在圍繞著函數轉,句柄在以函數為中心。而MFC是將幾個若干有服務關聯的函數封裝在一起成為成員函數,每一個類會有一個保護的句柄成員變量來保存當前類對象所代表的服務對象,在對外調用上看就可以將其類對象看成其服務對象,這些成員函數就可以看成其服務對象本來固有的方法。在使用上比使用WinAPI更為形象和理解。下面做一個比照的例子.
SDK寫法
HWND hCurrentWnd;
hCurrentWnd = ::CreateWindow (...);
::ShowWindow(hCurrent,SW_SHOW);
::CloseWindow(hCurrent);
MFC寫法
CWnd CurrentWnd;
CurrentWnd.CreateWindow (…);
CurrentWnd.ShowWindow (SW_SHOW);
CurrentWnd.CloseWindow();
怎麼樣,MFC沒有枯燥零碎的句柄的概念。我們足可以想象成一個類對象就是一個服務對象,它本身有許多對其控制的方法。這就是制作MFC的主要目的。 所有的MFC類的基類是CObject的。你可以用CObject的指針指代所有MFC類。CWnd類是所有關於窗口的API函數進行了封裝。所有的控件都是派生於這個類的如CEdit,CButton,CDialog, CFrameWnd, CMDIFrameWnd, CMDIChildWnd, CView, CDialog等 MFC更深的理論比如消息映射,CRuntimeClass等在《深入淺出MFC》,《C++技術內幕》等書有詳細探討與講解,強力推薦。我就是看這幾本書入的門。MFC類的各個功能也參見MSDN。 下一部分將詳細解釋一個MFC對話框程序。 下面,我要介紹一下一些C++程序建議編寫規范。
一、變、常、參量的建議:
1.常、變量應當定義函數體最前面或一對大括號的最前面,全局常、變量就當放在整個文件的最前面。這樣便於管理與維護。 2.聲明變、參量的應當使用匈牙利命名法。為變、參量添加適當的前綴,並以有意義的可拼讀的名詞性英文單詞來命名,每個英文語素首字母都應當大寫。如: m_nCount;則表示這是一個類的成員,為整型,是用做計數器用的。 常用的前綴有:
前綴 表示內容 _或Afx 表示為全局 m_ 表示為某個類的成員 b 表示為布爾型 h 表示為句柄 c或ch 表示為字符型 l 表示為長型 clr 表示為32位顏色值 n 表示為整型 cx或cy 表示為坐標的水平或垂直值 p或lp 表示為指針 w 表示為字(WORD)型 sz 表示為以0結尾的字符串 str 表示為CString字符型 dw 表示為雙字(DWORD)型 3.常量應當用const來定義,而不是用預處理指令#define。並且常量名應當大寫。
4.如果是全局的常、變量應當加前綴_。
二、函數的建議:
1.參數的定義位置要附和人性化,輸出參數在前,輸入參數在後。
2.如果參數是指針,且僅作輸入用,則應在類型前加const,以防止該指針在函數體內被意外修改。
3.函數名如果是全局的應當加前綴Afx,函數名以有意義的可拼讀的動詞性英文單詞或短語來命名,且每個英文語素首字母都就應當大寫。如:AfxGetMessage(),Close()。
4.一般函數的返回值最好用來返回錯誤標志,而真正的返回值應當用輸出參數來返回。
5.在函數的前幾行,應當對入口參數進行有效性檢查。
6.函數體的規模要小,盡量控制在50 行代碼之內。否則,應當進行拆分。
7.函數體中不要聲明使用靜態變量,那樣會使用函數難以控制。
三、 類的建議:
1.所有的成員變量應有m_前綴。
2.在類的聲明體中應遵循公有,保護,最後私有,前函數,後成員的順序進行聲明。
3.不能在類的聲明體中進行操作,這是不正確的,也是編譯器所不允許的。即是初始化(例如:int m_nCount=0;)也不可以。
4.應當在一個以類名為名字,後綴為.h的文件中寫入類的聲明體,應當在一個以類名為名字,後綴為.cpp的文件中寫入類的成員函數的實現語句。並且在.cpp文件的第一行寫上#include “類名.h”。
四、其它的建議:
1.for,if,do,while等語句中,無論後面的語句是否為一行,必須用大括號括起來。
2.不可以在一行上寫入多條語句,那樣會使程序的可讀性降低。
3.應當在不易理解的程序段或行上(比如內聯匯編語句),加上注釋。不要在顯而易懂的語句上加上注釋,(如:i++;)
4.在編程中,應當想到今後的可重性,給今後便於修改留下余地。
5.在要求技術性的程序上,盡量以最簡捷的代碼來完成功能。除非必要,否則不要去追求視覺界面效果。因為,界面代碼要比功能代碼混亂得多。會使代碼不易維護。
6.應當將一些常用的功能整理成可以直接使用的類,這樣不僅省功且使代碼看起來簡潔。如界面代碼等。
7.在正式編程的時候,切勿直接編寫代碼。應當先規劃好程序結構及其算法等程序實現,再去動手。因為在編程中重要的不是如何去代碼,而是程序的實現方法。而且直接寫碼,肯定會導致程序代碼的修修改改,使代碼看起來很雜亂。
8.要習慣於在大部分時間去書寫文檔,因為程序關鍵是在於是否會被人能夠接受使用。
9.要用70%的時間去設想程序的算法,要用27%的時間去書寫文檔,只能用1%的時間去編寫代碼,最後用2%的時間去調試代碼。
(第二部分完)