程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 初始化C++類成員和在你的MFC應用中加入位置欄

初始化C++類成員和在你的MFC應用中加入位置欄

編輯:關於C++

問題 

我的問題是關於初始化C++類成員的。我見過許多這樣的代碼(包括在你的欄目中也見到過):

  CSomeClass::CSomeClass()
  {
  x=0;
  y=1;
  }

而在別的什麼地方則寫成下面的樣子:

  CSomeClass::CSomeClass() : x(0), y(1)
  {
  }

我的一些程序員朋友說第二種方法比較好,但他們都不知道為什麼是這樣。你能告訴我這兩種類成員初始化方法的區別嗎?

回答

從技術上說,你的程序員朋友是對的,但是在大多數情況下,兩者實際上沒有區別。有兩個原因使得我們選擇第二種語法,它被稱為成員初始化列表:一個原因是必須的,另一個只是出於效率考慮。

讓我們先看一下第一個原因——必要性。設想你有一個類成員,它本身是一個類或者結構,而且只有一個帶一個參數的構造函數。

  class CMember {
  public:
  CMember(int x) { ... }
  };

因為Cmember有一個顯式聲明的構造函數,編譯器不產生一個缺省構造函數(不帶參數),所以沒有一個整數就無法創建Cmember的一個實例。

CMember* pm = new CMember;    // Error!!

CMember* pm = new CMember(2);   // OK

如果Cmember是另一個類的成員,你怎樣初始化它呢?你必須使用成員初始化列表。

  class CMyClass {
  CMember m_member;
  public:
  CMyClass();
  };
  //必須使用成員初始化列表
  CMyClass::CMyClass() : m_member(2)
  {
  •••
  }

沒有其它辦法將參數傳遞給m_member,如果成員是一個常量對象或者引用也是一樣。根據C++的規則,常量對象和引用不能被賦值,它們只能被初始化。

第二個原因是出於效率考慮,當成員類具有一個缺省的構造函數和一個賦值操作符時。MFC的Cstring提供了一個完美的例子。假定你有一個類CmyClass具有一個Cstring類型的成員m_str,你想把它初始化為"yada yada."。你有兩種選擇:

  CMyClass::CMyClass() {
  // 使用賦值操作符
  // CString::operator=(LPCTSTR);
  m_str = _T("yada yada");
  }
  //使用類成員列表
  // and constructor CString::CString(LPCTSTR)
  CMyClass::CMyClass() : m_str(_T("yada yada"))
  {
  }

在它們之間有什麼不同嗎?是的。編譯器總是確保所有成員對象在構造函數體執行之前初始化,因此在第一個例子中編譯的代碼將調用CString::Cstring來初始化m_str,這在控制到達賦值語句前完成。在第二個例子中編譯器產生一個對CString:: CString(LPCTSTR)的調用並將"yada yada"傳遞給這個函數。結果是在第一個例子中調用了兩個Cstring函數(構造函數和賦值操作符),而在第二個例子中只調用了一個函數。在Cstring的例子裡這是無所謂的,因為缺省構造函數是內聯的,Cstring只是在需要時為字符串分配內存(即,當你實際賦值時)。但是,一般而言,重復的函數調用是浪費資源的,尤其是當構造函數和賦值操作符分配內存的時候。在一些大的類裡面,你可能擁有一個構造函數和一個賦值操作符都要調用同一個負責分配大量內存空間的Init函數。在這種情況下,你必須使用初始化列表,以避免不要的分配兩次內存。在內部類型如ints或者longs或者其它沒有構造函數的類型下,在初始化列表和在構造函數體內賦值這兩種方法沒有性能上的差別。不管用那一種方法,都只會有一次賦值發生。有些程序員說你應該總是用初始化列表以保持良好習慣,但我從沒有發現根據需要在這兩種方法之間轉換有什麼困難。在編程風格上,我傾向於在主體中使用賦值,因為有更多的空間用來格式化和添加注釋,你可以寫出這樣的語句:x=y=z=0;

或者memset(this,0,sizeof(this));

注意第二個片斷絕對是非面向對象的。

當我考慮初始化列表的問題時,有一個奇怪的特性我應該警告你,它是關於C++初始化類成員的,它們是按照聲明的順序初始化的,而不是按照出現在初始化列表中的順序。

  class CMyClass {
  CMyClass(int x, int y);
  int m_x;
  int m_y;
  };
  CMyClass::CMyClass(int i) : m_y(i), m_x(m_y)
  {
  }

你可能以為上面的代碼將會首先做m_y=I,然後做m_x=m_y,最後它們有相同的值。但是編譯器先初始化m_x,然後是m_y,,因為它們是按這樣的順序聲明的。結果是m_x將有一個不可預測的值。我的例子設計來說明這一點,然而這種bug會更加自然的出現。有兩種方法避免它,一個是總是按照你希望它們被初始化的順序聲明成員,第二個是,如果你決定使用初始化列表,總是按照它們聲明的順序羅列這些成員。這將有助於消除混淆。

問題

我剛剛在幾台機器上安裝了Windows® 2000 Release Candidate 1,不知道怎樣在我的MFC應用中得到具有新的Outlook風格欄目的Open對話框(見圖1)。

Figure 1 The New Open Dialog

我能否只設置一個標志,或者我是否需要一個新的頭文件和一個新的公共對話框的DDL?我注意到一些舊的應用程序如Notepad好像可以得到新的Open對話框而無須重新編譯,但它們不是MFC應用。理想情況,我希望在Windows 9x 和Windows NT®下得到一個使用舊對話框的應用,而在Windows 2000下使用新的對話框。

Warren Stevens

回答

這個問題恐怕沒有令你高興的答復。Windows 2000新的Open對話框是用一個新版本的commdlg.dll實現的,它在邊上包含“Places”欄目。顯示它的函數是GetOpenFileName,與在Windows 9x 和Windows NT®下使用的相同。然而,GetOpenFileName現在使用一個新版本的OPENFILENAME,這是一個在你的應用和對話框之間傳遞信息的結構。新的結構有一些額外的成員:

  typedef struct tagOFN {
  DWORD lStructSize; // 很重要!
  •••
  // 正想你總是知道並且喜歡的那樣
  #if (_WIN32_WINNT >= 0x0500)
   void* pvReserved;
   DWORD dwReserved;
   DWORD FlagsEx;
  #endif // (_WIN32_WINNT >= 0x0500)
  } OPENFILENAME, *LPOPENFILENAME;

對,是這樣。Windows 2000是Windows的第5個版本,用16進制表示是0x500。如果你用_WIN32_WINNT = 0x0500編譯程序,OPENFILENAME就會得到3個新成員。前兩個是保留的,第三個標志域,FlagsEx,有一個新的OFN_EX_NOPLACESBAR欄目,它屏蔽了Places欄目。Windows——或者更准確的說,commdlg.dll——使用OPENFILENAME第一個成員lStructSize來決定顯示那個對話框,如果lStructSize是76(舊的大小),Windows就運行舊的對話框;如果是76+3´4=88(新的大小),它就運行新的對話框——這是友好的Redmondtonians最初告訴我的。在我的研究中間,我發現這並不是完整的圖畫。

但是在我詳細說明之前,先讓我們走馬觀花的看一下MFC,討論另外一個問題。在MFC應用中,你並不經常直接調用GetOpenFileName,而是使用CfileDialog——或者,框架使用CfileDialog。當用戶調用File | Open,控制稀裡嘩啦的一路經過CWinApp::OnFileOpen和幾個其它的函數,最終到達CDocManager::DoPromptFileName,這個函數創建一個CfileDialog。CfileDialog具有一個OPENFILENAME結構的數據成員:

  class CFileDialog : public CCommonDialog {
   OPENFILENAME m_ofn;
  •••
  };

這個結構的大小是當友好的Redmondtonians 編譯MFC42.DLL 時OPENFILENAME的大小;換句話說,舊的大小。而且,如果你正在進行一個靜態連接,MFC代碼在MFC42.DLL或NAFXCW.LIB裡是凍結的,你不能僅僅設置m_ofn.lStructSize為新的大小,因為CfileDialog除m_ofn外還有其它數據成員,它們將被新的OPENFILENAME的成員覆蓋。不再耽擱了,我開始使用極端的方法避開這個問題。我考慮可以做些什麼,類似於MFC中使用CpropertyPage那樣。PROPSHEETPAGE和PROPSHEETHEADER的大小在從Windows 95到Windows 98的過程中的某處增加了,這是為了支持wizard風格的頁面。為了支持新膨脹的結構,MFC提供了CpropertyPageEx和CpropertySheetEx。最初的類(不帶Ex的)仍然使用舊的結構;而新的類使用新的結構。這是一種雜湊,尤其是因為afxdlgs.h具有自己的舊的結構的定義(AFX_ OLDPROPSHEETPAGE和AFX_OLDPROPSHEETHEADER),但是這樣卻行得通。

我對CfileDialog做了同樣的事情。首先我派生一個新的CfileDialogEx類,它帶有一個新的m_ofn,包含著新的OPENFILENAMEEX結構,我模仿0x500版本加以定義。我加入這3個新的成員並且使用m_ofn.重寫了CfileDialog函數。不幸的是,因為大多數的MFC代碼是固定的,沒有任何虛擬功能,這就意味著復制原來的整個類。但是我已經下了決心。

在我認為已經找到了m_ofn出現的所有地方以後,我重寫了它,高高興興的編譯了我的代碼(在Windows 98上),然後運行——結果發現我得到的仍是舊風格的對話框。而起,有一個謎團我忘了考慮:如果Windows 2000使用lStructSize來決定運行那個Open對話框,為什麼Windows 98的應用程序(象Notepad)在Windows 2000下運行時得到了新的對話框呢?啊!隨Windows 98出現的NOTEPAD.EXE顯然在lStructSize 上有舊的OPENFILENAME的大小,因此Windows 2000必須使用lStructSize之外的某種東西來決定運行那個對話框。

到這裡,我決定回過頭去重新考慮問題。我將MFC放到一邊,嘗試直接調用GetOpenFileName。我重寫了我的應用程序的OnFileOpen:

  void CMyApp::OnFileOpen()
  {
   OPENFILENAME ofn; // older version
   memset(&ofn, 0, sizeof(ofn));
   ofn.lStructSize = sizeof(ofn);
   int nResult = ::GetOpenFileName(&ofn);
  }

因為貫穿本練習,我使用了舊的0x400版本的SDK文件(因為我希望應用程序既可以在Windows 2000上運行,也可以在Windows 9x上運行),ofn.lStructSize就有了舊的大小。當我編譯並運行時,我在Windows 98上得到了舊的對話框,而在Windows 2000上得到了新的對話框——就象Notepad一樣!因此可以說,實際上,Windows 2000足夠精明的為舊的應用使用新的對話框——但不是舊的MFC應用。它毫無意義。一個MFC應用的不同之處在哪裡呢?

一定是標志。為了發現真相,我在OPENFILENAME結構中手工添加了不同的標志,直到我的程序產生了不帶Places欄目的舊風格的窗口。你瞧,當我為ofn.Flags加入標志OFN_ENABLEHOOK時,我的對話框回到了從前。我將此奇怪的行為報告給Redmondtonians,他們證實“這種行為是設計的”。

那麼,Windows 2000判斷OPENFILENAME的大小以及對話框是否使用hook過程。如果OPENFILENAME有舊的大小,Windows 2000使用OFN_ENABLEHOOK來決定運行哪個對話框。如果OPENFILENAME使用hook過程(或者設置了ORN_ENABLETEMPLATE),Windows 2000按照舊的風格顯示對話框;否則,顯示新的對話框。這就解釋了為什麼MFC應用顯示了舊的對話框——因為CfileDialog,就象所有MFC的公用對話框一樣,使用hook過程。這是MFC將公用對話框嵌入它的消息映射系統的方式,它用同樣的方式使用AfxWndProc嵌入其它的窗口。

現在你看到了結果:你給迷惑了。在一個MFC應用中得到新風格的對話框的唯一的辦法是完全繞開CfileDialog,直接調用GetOpenFileName,並且不使用hook過程。即使你用新的SDK文件和WINVER = 0x500編譯你的應用,你仍不能使用MFC,因為它的庫和DLLs有舊的大小。你可以使用WINVER = 0x500自行編譯MFC,但是誰知道那將怎麼樣呢?而且如果你真的BUILD了新的MFC,你將不得不將新的DLL和你的應用一塊發布,給它起個不同的名字,因為你的新的MFC DLL肯定不會與其它的希望使用CFileDialog 和其它結構的舊的大小的MFC應用兼容。或者,你可以重新生成MFC,然後靜態的連接,這將極大的增加你的可執行文件的大小,如果你不重新實現Windows的話。

到截稿時間為止,我從Redmond聽說在即將發布的新的Visual C++®中,將會有一個不同名字的新版本的MFC DLL。新版本的MFC將支持Windows 2000中出現的新的UI和APIs。  

同時,我將圖2留給你研究,它取自最新的SDK文檔,"Header File Conventions",提供了Windows版本問題的鑰匙。它向你說明,為了達到某個版本的Windows和Microsoft® Internet Explorer的目標你必須使用SDK頭文件定義哪些宏。努力別跌倒在生活的快車道上。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved