VC6向VC9移植時常見BUG
首先可以直接用Visual Studio 2008的開啟VC6的工作區檔案和專案檔(dsw和dsp),並將其升級為VS2008的解決方案格式和項目格式(sln和vcproj),VC9的編譯器相對於VC6有了很大的變化,一些編譯參數和連結參數被廢棄(比如/map:line),有一些改變了名稱,還有新增的選項,不過不用擔心,升級過程會自動對其進行轉換,最終都會得到一個正確的解決方案和VC專案檔,這個過程不會遇到太多的麻煩,問題都出在隨後的編譯過程中,下面就將我在移植的過程中遇到的問題和我的解決方案總結一下,希望對還在用VC6維護代碼的朋友有所協助。
一、_WIN32_WINNT 與 _WIN32_IE 設定衝突
_WIN32_WINNT 與 _WIN32_IE設定不相容會導致如下C1189致命錯誤:
StdAfx.cpp
c:\program files\microsoft sdks\windows\v6.0a\include\sdkddkver.h(217) : fatal error C1189: #error : _WIN32_WINNT settings conflicts with _WIN32_IE setting
StdAfx.cpp通常是項目中第一個編譯的檔案,這個錯誤將導致編譯無法繼續進行。產生這個錯誤的原因是原因是_WIN32_WINNT的版本定義太老,老的VC代碼對_WIN32_WINNT的典型設定是:
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
0x0400相對於VS2008所帶的Plarform SDK(在檔案sdkddkver.h中)中_WIN32_IE的定義來說太老了,導致不相容,可以將其改成0x0501或更高的版本避免這個問題,如下所示:
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0501
#endif
也可以將這三行_WIN32_WINNT定義刪除,這樣就會使用Plarform SDK中的_WIN32_WINNT定義,自然就不存在不相容問題了。不過出於對老版本VC的相容考慮(畢竟以後可能還要使用VC6編譯代碼),最好這樣修改:
#if _MSC_VER <= 1200 // MFC 6.0 or earlier
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
#endif
二、afximpl.h檔案中的語法錯誤
MFC出現的時候STL還沒有成為C++的標準,所以MFC使用一套自己的模版庫,比如CArray、CList、CMap等等,這些型別宣告都在afximpl.h檔案中。原來在VC6編譯器適用的模版文法可能不適用VC9,特別是當以下四個環境變數設定不相容時,就會出現這個編譯錯誤,大致情況如下:
e:\software\microsoft visual studio 9.0\vc\atlmfc\src\mfc\afximpl.h(625) : error C2059: syntax error : ''
e:\software\microsoft visual studio 9.0\vc\atlmfc\src\mfc\afximpl.h(625) : error C2238: unexpected token(s) preceding ';'
e:\software\microsoft visual studio 9.0\vc\atlmfc\src\mfc\afximpl.h(629) : error C2059: syntax error : ''
e:\software\microsoft visual studio 9.0\vc\atlmfc\src\mfc\afximpl.h(629) : error C2238: unexpected token(s) preceding ';'
合理調整stdafx.h中WINVER、_WIN32_WINNT、_WIN32_WINDOWS和_WIN32_IE的設定可以避免這個問題,將三個與Windows版本有關的環境變數設定為0x0501或更高版本,將IE版本的環境變數設定為0x0500以後的版本就可以解決這個問題。當然,考慮到與舊的VC6代碼相容,可以採用上一個問題中提到的最後一個解決辦法,用_MSC_VER進行隔離。
三、 舊的CRT庫和新的安全CRT庫引起的C4996警示
解決了環境變數設定不匹配導致的問題後,編譯過程就真正開始了,不過首先映入眼帘的應該是成堆的C4996編譯警示,對每個使用了含字串參數的CRT庫函數都會有C4996編譯警示,一個典型的輸出如下所示:
f:\project\.....\commonfunc.cpp(280) : warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
e:\software\microsoft visual studio 9.0\vc\include\string.h(74) : see declaration of 'strcpy'
MSDN online 是這樣解釋的:為了顯著增加CRT庫的安全性,許多CRT函數都有了一個更安全的新版本,新版本和舊版本的區別就是新版本函數名多了一個_s尾碼。只要一個CRT函數有新的安全版本,編譯器就會產生一個C4996警示,不過,出現這個警示的目的並不是說舊版本的CRT函數將淡出CRT庫,警示出現只是為了提醒程式員這個函數有更安全的版本存在。一種安全的或者是被鼓勵的做法是用安全版本的函數替換現有的CRT函數,不過對於一個有相當代碼量的項目,替換工作量也是巨大的,這可不是用名稱尋找、替換就能簡單解決的問題,因為許多安全版本的CRT函數參數個數也發生了變化。也可以用預先處理指令消除這個警示:
#pragma warning( disable : 4996 )
或者定義 _CRT_SECURE_NO_WARNINGS 壓制這個警示(在stdafx.h中define或在項目屬性中設定預先處理符號,PreProcessor Definitions)。
除了C語言的CRT函數外,POSIX 相容函數也存在這個警示,解決方案是用POSIX標準名稱替換(比如access換成_access)或者是定義 _CRT_NONSTDC_NO_WARNINGS 壓制這個警示(方法同上)。
四、“CWinApp::Enable3dControls”引起的C4996警示
這個是編譯使用了老的嚮導產生的MFC代碼時遇到的問題,一個典型的警示資訊輸出如下所示:
CrpFileCrack.cpp
f:\project\.....\crpfilecrack.cpp(52) : warning C4996: 'CWinApp::Enable3dControls': CWinApp::Enable3dControls is no longer needed. You should remove this call.
e:\software\microsoft visual studio 9.0\vc\atlmfc\include\afxwin.h(4818) : see declaration of 'CWinApp::Enable3dControls'
通常嚮導產生的程式碼是:
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
這兩個函數的調用是舊的MFC版本對新版本的作業系統特性的支援,在新的(那個時候是新的)Windows 95平台上要這樣調用一下才能使用新的Windows 3D樣式的控制項,否則就是老的Win 3.2樣子的控制項。想當初喜歡OWL就是因為感覺它的控制項比較“酷”,比如那個帶底紋的對話方塊,菱形的checkbox,還有帶表徵圖的“OK”按鈕,看到 MFC作出來的灰灰的介面就覺得土,不過後來就知道MFC做介面也是很漂亮的,比如我做的。。。。,再打住。對於新的MFC版本來說已經不需要再調用這兩個函數了,參考前面的方法,用_MSC_VER對其隔離就行了:
#if _MSC_VER <= 1200 // MFC 6.0 or earlier
#ifdef _AFXDLL
Enable3dControls(); // Call this when using MFC in a shared DLL
#else
Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
#endif
五、.def檔案引起的串連警示
對於普通的DLL項目中使用的.def檔案通常會引起LNK4017連結警示,如下所示:
.\ComFunc.def(4) : warning LNK4017: DESCRIPTION statement not supported for the target platform; ignored
Creating library .\..\Debug/ComFunc.lib and object .\..\Debug/ComFunc.exp
一個典型的.def檔案通常有以下內容:
LIBRARY "XorCryptor"
DESCRIPTION 'XorCryptor Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
..................
消除這個串連警示的方法就是從.def檔案中刪除DESCRIPTION描述資訊,不過這個警示也不是什麼大問題,不刪也可以。另一個可能產生的串連警示是LNK4222,通常出現在ocx控制項和com組件的項目中,一個典型輸出是:
Linking...
.\PlusInModule.def : warning LNK4222: exported symbol 'DllCanUnloadNow' should not be assigned an ordinal
.\PlusInModule.def : warning LNK4222: exported symbol 'DllGetClassObject' should not be assigned an ordinal
.\PlusInModule.def : warning LNK4222: exported symbol 'DllRegisterServer' should not be assigned an ordinal
.\PlusInModule.def : warning LNK4222: exported symbol 'DllUnregisterServer' should not be assigned an ordinal
出現這個警示的原因是舊的項目的.def檔案通常這樣定義ocx和com必需的四個匯出函數:
EXPORTS
DllCanUnloadNow @1 PRIVATE
DllGetClassObject @2 PRIVATE
DllRegisterServer @3 PRIVATE
DllUnregisterServer @4 PRIVATE
其中為這四個重要的匯出函數指定了四個順序號。Windows平台上通常用兩種方式定位DLL檔案中的匯出函數,一種是根據匯出函數名稱,一種是根據順序號,上學時曾經寫過一個顯示圖片的程式,能處理大多數當時流行的映像格式檔案,唯獨jpeg格式的搞不定,有一次看到一個影像處理軟體中包含了一個LoadJpeg.dll,很顯然這個DLL是處理jpeg格式的影像檔的嘛,於是趕快用depends look了一下,頓時高喊:鬼啊~~~。原來這個depends竟然查不到匯出函數的名字,後來才知道還有NONAME參數強制用順序號定位匯出函數,於是就常常弄個沒有匯出函數名字的DLL到處show。。。。嗯,又扯遠了。話說為什麼舊的系統要以此指定這四個匯出函數的順序號我就沒有研究了,反正現在不需要指定了,只要將@1,@2之類的刪除就行了,不過不刪好像也沒什麼問題,它們會被自動忽略。
六、使用MFC的訊息映射宏引起的編譯錯誤
錯誤現象之一:
f:\project\.....\plusmaindlg.cpp(220) : error C2440: 'static_cast' : cannot convert from 'void (__thiscall CPlusMainDlg::* )(int,BOOL)' to 'LRESULT (__thiscall CWnd::* )(WPARAM,LPARAM)'
None of the functions with this name in scope match the target type
錯誤現象之二:
f:\project\.....\crpfileopavdlg.cpp(87) : error C2440: 'static_cast' : cannot convert from 'LRESULT (__thiscall CCrpFileOpavDlg::* )(LPCTSTR,int)' to 'LRESULT (__thiscall CWnd::* )(WPARAM,LPARAM)'
None of the functions with this name in scope match the target type
以上兩個編譯錯誤產生是因為新舊版本的MFC 中對ON_MESSAGE訊息映射宏定義不同引起的,先看看老版本的MFC的ON_MESSAGE訊息宏定義:
#define ON_MESSAGE(message, memberFxn) \
{ message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW)(LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM))&memberFxn },
再看看新版本的ON_MESSAGE定義:
#define ON_MESSAGE(message, memberFxn) \
{ message, 0, 0, 0, AfxSig_lwl, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM) > \
(memberFxn)) },
注意,函數類型沒有變化,都是:
LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM);
類型的函數指標(CWnd以及衍生類別的類成員函數指標),區別之處是新的ON_MESSAGE宏使用C++的 static_cast 操作符代替了C類型的強制轉換。產生這兩個錯誤其實是因為使用者沒有按照ON_MESSAGE宏的約定聲明和定義訊息響應函數造成的,比如,對於某些不需要處理傳回值的訊息響應函數,使用者通常這樣聲明和定義訊息響應函數:
在標頭檔中聲明:
afx_msg void OnFileProcess(WPARAM wParam,LPARAM lParam);
在源檔案中實現:
void CCrpFileOpavDlg::OnFileProcess(WPARAM wParam, LPARAM lParam)
{
.......
}
或者更過分一些,直接指定為實際參數類型:
在標頭檔中聲明:
afx_msg void OnFileProcess(LPCTSTR lpszMessage, int nPercent);
在源檔案中實現:
void CCrpFileOpavDlg::OnFileProcess(LPCTSTR lpszMessage, int nPercent)
{
.......
}
舊版本的ON_MESSAGE使用了C類型的強制轉換,宏解開後的代碼後不會產生錯誤資訊,但是改成對類型檢查很嚴格的static_cast 操作符時就出問題了,因為通不過static_cast 操作符的檢查。解決方案就是修改代碼,同時吸取教訓,普遍使用的方法並不一定就能約定俗成,一切還是要按照規矩來。
錯誤現象之三:
f:\project\.....\WzButton.cpp(74) : error C2440: 'static_cast' : cannot convert from 'UINT (__thiscall CWzButton::* )(CPoint)' to 'LRESULT (__thiscall CWnd::* )(CPoint)'
Cast from base to derived requires dynamic_cast or static_cast
出現這個錯誤的原因可是“人力不可抗拒”之原因造成的,因為舊版本的 ON_WM_NCHITTEST 宏使用了
UINT (__thiscall CWzButton::* )(CPoint);
類型的類成員函數指標,其定義如下:
#define ON_WM_NCHITTEST() \
{ WM_NCHITTEST, 0, 0, 0, AfxSig_wp, \
(AFX_PMSG)(AFX_PMSGW)(UINT (AFX_MSG_CALL CWnd::*)(CPoint))&OnNcHitTest },
但是新版本變成了:
#define ON_WM_NCHITTEST() \
{ WM_NCHITTEST, 0, 0, 0, AfxSig_l_p, \
(AFX_PMSG)(AFX_PMSGW) \
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(CPoint) > (&ThisClass :: OnNcHitTest)) },
注意傳回值類型由UINT改成了LRESULT,再加上static_cast的嚴格檢查,所以就出錯了。修改的方法就是將你的OnNcHitTest函數由:
afx_msg UINT OnNcHitTest(CPoint point);
改成:
afx_msg LRESULT OnNcHitTest(CPoint point);
不必太在意,這個不是你的錯,不過,如果你要維護一個老的介面庫(通常很多控制項的subclass都會用到ON_WM_NCHITTEST),改起來還是很痛苦地,不扯了,繼續下一個。
七、statreg.cpp 和 atlimpl.cpp 的廢棄(obsolete)問題
在編譯老的ATL嚮導產生的程式碼時,會遇到下面的編譯輸出:
StdAfx.cpp
statreg.cpp is obsolete. Please remove it from your project.
atlimpl.cpp is obsolete. Please remove it from your project.
因為老的ATL嚮導產生的程式碼通常在stdafx.cpp檔案中添加以下代碼:
#ifdef _ATL_STATIC_REGISTRY
#include
#include
#endif
#include
根據提示刪除#include 和#include 兩行代碼就行了,不過更好的辦法是這樣改:
#ifdef _ATL_STATIC_REGISTRY
#include
#if _MSC_VER <= 1200 // MFC 6.0 or earlier
#include
#endif
#endif
#if _MSC_VER <= 1200 // MFC 6.0 or earlier
#include
#endif
八、新的C++編譯器不再支援預設類型的變數定義
錯誤現象是:
f:\project\.....\WzCheckBox.cpp(464) : error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
產生這個錯誤的原因是程式中出現了這樣的代碼:
const some_const_var = 10;
或
static some_static_bool = FALSE;
新的C++編譯器嚴格按照C++標準,不再支援預設類型的變數定義方式,必須嚴格指定變數類型,如下使用:
const int some_const_var = 10;
或
static BOOL some_static_bool = FALSE;
九、for 語句的變數範圍問題
考察下面的代碼:
for(int i = 0; i < 120; i++)
{
if(something_happen)
{
break;
}
.............
}
if(i < 120)
{
//something happen
}
在VC6的編譯器中,這樣的代碼是沒有問題的,因為VC6的編譯器為了相容舊的Microsoft C/C++編譯器,沒有嚴格按照C++標準執行,但是從VC7開始,VC的編譯器開始遵守C++標準,所以就會出現“變數i沒有定義的錯誤”。解決的方法也很簡單,按照Jim Hyslop 和 Herb Sutter的經典對話系列的第四篇中的方法,改成如下就可以了:
int i;
for(i = 0; i < 120; i++)
十、字串函數的傳回值問題
strchr(_tcschr)、strpbrk(_tcspbrk ??)、strrchr(_tcsrchr)和strstr(_tcsstr)這四個函數在VC6的CRT庫中定義的傳回值都是char *(TCHAR *),所以以前的代碼通常是這樣使用的:
TCHAR *cp = _tcschr( pszPath, _T('\\') );
//使用*cp,可以通過cp指標修改pszPath的內容
這其實是一個“漏洞”,因為如果pszPath是const char(TCHAR) *字串,那麼就表示它不希望修改字串的內容,但是調用strchr(_tcschr)函數後就可以通過cp指標修改其內容了,這豈不荒謬?所有在新版本的CRT庫中,這幾個函數的傳回值都改成const char *,這就會導致上面的代碼產生編譯錯誤。建議的修改方式是改成如下方式:
const TCHAR *cp = _tcschr( pszPath, _T('\\') );
//不能再通過cp指標修改pszPath的內容
但是這樣修改可能對代碼的影響比較大,比如下面的代碼:
TCHAR buf[256]; //局部緩衝區
......
TCHAR *cp = _tcschr( buf, _T('\\') );
//作為局部緩衝區(非const),希望通過cp修改buf的內容
這種情況怎麼辦呢?對了,C++還有個const_cast操作符,這時就可以排上用場了:
TCHAR *cp = const_char(_tcschr( buf, _T('\\') ));
不過上面的方法要慎用,除非確定buf是非const的,否則最好老老實實地修改代碼。
十一、類成員函數指標做為函數參數的“C3867”錯誤
考察下面的代碼,CWzWindowsHook類的建構函式使用一個該類的成員函數指標,這樣構造對象時可以選擇訊息過濾的handler,可以是MouseMsgFilter,也可以是KeyboardMsgFilter:
typedef BOOL (CWzWindowsHook::*FILTERPROC)(WPARAM wParam, LPARAM lParam);
// A hook used in customization sheet to filter keyboard/mouse events
class CWzWindowsHook
{
private:
FILTERPROC m_pFilter;
BOOL MouseMsgFilter(WPARAM wParam, LPARAM lParam);
BOOL KeyboardMsgFilter(WPARAM wParam, LPARAM lParam);
public:
CWzWindowsHook(FILTERPROC pFilter) : m_pFilter(pFilter)
舊的遺留代碼存在這樣的用法:
CWzWindowsHook mouseHooker(CWzWindowsHook::MouseMsgFilter);
在VC6的編譯器下編譯可能沒有問題,但是在VC9的編譯器下編譯會有如下報錯:
f:\project\.....\WzWindowsHook.cpp(272) : error C3867: 'CWzWindowsHook::MouseMsgFilter': function call missing argument list; use '&CWzWindowsHook::MouseMsgFilter' to create a pointer to member
雖然C++從C繼承來了函數名即是函數地址的文法規則,但是根據C++的標準,類成員函數的指標仍然需要一個取地址符“&”。解決方案很簡單,按照提示改成如下代碼即可:
CWzWindowsHook mouseHooker(&CWzWindowsHook::MouseMsgFilter);
十二、wchar_t *類型與USHORT *的轉換錯誤
VC6的編譯器不支援wchar_t資料類型,wchar_t實際上被定義成unsigned short,VC9的編譯器已經支援wchar_t為內建資料類型,但是由一個編譯選項控制,這個選項預設是開啟的,也就是將wchar_t作為編譯器的內建資料類型。但是OLECHAR和WCHAR的定義仍然是unsigned short,在VC6的編譯環境中,兩者的指標都是USHORT *,相互賦值和做為函數參數傳遞沒有問題,但是如果wchar_t作為編譯器的內建資料類型,那就意味著wchar_t *與OLECHAR *或WCHAR *是兩種不同類型的指標,相互賦值就會報編譯錯誤,下面的資訊就是一個典型的錯誤輸出:
f:\project\.....\shellpidl.cpp(290) : error C2664: 'MultiByteToWideChar' : cannot convert parameter 5 from 'USHORT *' to 'LPWSTR'
Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast
解決的方法就是使用C++的reinterpret_cast操作符或使用C-style強制轉換,當然也可以在項目屬性設定中關閉前面提到的那個選項(這個偶美試過,不知道會不會有其它問題)。