迁移指南:Spy++Porting Guide: Spy++

此移植案例研究旨在让你了解典型的移植项目、可能遇到的问题类型,以及解决移植问题的一些常用提示和技巧。This porting case study is designed to give you an idea of what a typical porting project is like, the types of problems you might encounter, and some general tips and tricks for addressing porting problems. 这并不是权威的移植指南,因为移植项目的体验很大程度取决于代码的详细信息。It's not meant to be a definitive guide to porting, since the experience of porting a project depends very much on the specifics of the code.


Spy++ 是广泛使用的 GUI 诊断工具,适用于提供有关 Windows 桌面上用户界面元素的各种类型信息的 Windows 桌面。Spy++ is a widely used GUI diagnostic tool for the Windows desktop that provides all sorts of information about user interface elements on the Windows desktop. 它显示了 Windows 的完整层次结构,并提供有关每个窗口和控件的元数据的访问。It shows the complete hierarchy of windows and provides access to metadata about each window and control. 多年来,这款有用的应用程序均与 Visual Studio 一起提供。This useful application has shipped with Visual Studio for many years. 我们找到了上次在 Visual C++ 6.0 中编译的旧版本应用程序,并将其移植到了 Visual Studio 2015。We found an old version of it that was last compiled in Visual C++ 6.0 and ported it to Visual Studio 2015. 对于 Visual Studio 2017 来说,步骤几乎完全相同。The experience for Visual Studio 2017 should be almost identical.

我们认为这是最典型的移植使用 MFC 和 Win32 API 的 Windows 桌面应用程序的情况,尤其是对于尚未使用从 Visual C++ 6.0 版本开始的任意 Visual C++ 版本进行更新的旧项目。We considered this case to be typical for porting Windows desktop applications that use MFC and the Win32 API, especially for old projects that have not been updated with each release of Visual C++ since Visual C++ 6.0.

步骤 1.Step 1. 转换项目文件。Converting the project file.

项目文件(Visual C++ 6.0 中两个旧的 .dsw 文件)轻松转换,且未产生任何需要进一步关注的问题。The project file, two old .dsw files from Visual C++ 6.0, converted easily with no issues that require further attention. 其中一个项目是 Spy++ 应用程序。One project is the Spy++ application. 另一个项目是 SpyHk,它是用 C 语言编写的支持 DLL。The other is SpyHk, written in C, a supporting DLL. 此处所述,更复杂的项目可能无法轻松升级。More complex projects might not upgrade as easily, as discussed here.

升级两个项目后,我们的解决方案如下所示:After upgrading the two projects, our solution looked like this:

Spy ++ 解决方案The Spy++ Solution

我们共有两个项目,一个具有大量的 C++ 文件,另一个是用 C 语言编写的 DLL。We have two projects, one with a large number of C++ files, and another a DLL that's written in C.

步骤 2.Step 2. 头文件问题Header file problems

生成新转换的项目后,通常首先发现的是,找不到自己项目使用的头文件。Upon building a newly converted project, one of the first things you'll often find is that header files that your project uses are not found.

Spy++ 中无法找到的文件是 verstamp.h。One of the files that couldn't be found in Spy++ was verstamp.h. 通过搜索 Internet,我们确定这是因为 DAO SDK(一种过时的数据技术)。From an Internet search, we determined that this came from a DAO SDK, an obsolete data technology. 我们想知道使用了该头文件中的哪些符号,以确定是否真的需要该文件或者是否在其他地方定义了这些符号,因此我们注释了掉头文件声明并重新进行了编译。We wanted to find out what symbols were being used from that header file, to see if that file was really needed or if those symbols were defined elsewhere, so we commented out the header file declaration and recompiled. 事实证明只需要一个符号,即 VER_FILEFLAGSMASK。It turns out there is just one symbol that is needed, VER_FILEFLAGSMASK.

1>C:\Program Files (x86)\Windows Kits\8.1\Include\shared\common.ver(212): error RC2104: undefined keyword or key name: VER_FILEFLAGSMASK  

在可用的包含文件中查找符号最简单的方法是,使用“在文件中查找”(Ctrl+Shift+F),并指定“Visual C++ 包含目录”。The easiest way to find a symbol in the available include files is to use Find in Files (Ctrl+Shift+F) and specify Visual C++ Include Directories. 我们在 ntverp.h 中找到了该符号。We found it in ntverp.h. 我们将 verstamp.h 包含文件替换为 ntverp.h 后,此错误消失。We replaced the verstamp.h include with ntverp.h and this error disappeared.

步骤 3.Step 3. 链接器 OutputFile 设置Linker OutputFile setting

有时,旧项目将文件放在非常规位置,升级后这可能会导致问题。Older projects sometimes have files placed in unconventional locations that can cause problems after upgrading. 在这种情况下,我们必须将 $(SolutionDir) 添加到项目属性的包含路径,以确保 Visual Studio 可以找到放在此处而非放在某个项目文件夹中的头文件。In this case, we have to add $(SolutionDir) to the Include path in the project properties to ensure that Visual Studio can find some header files that are placed there, rather than in one of the project folders.

MSBuild 发出 MSB8012,报告 Link.OutputFile 属性与 TargetPath 和 TargetName 值不匹配。MSBuild complains that the Link.OutputFile property does not match the TargetPath and TargetName values, issuing MSB8012.

warning MSB8012: TargetPath(...\spyxx\spyxxhk\.\..\Debug\SpyxxHk.dll) does not match the Linker's OutputFile property value (...\spyxx\Debug\SpyHk55.dll). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).warning MSB8012: TargetName(SpyxxHk) does not match the Linker's OutputFile property value (SpyHk55). This may cause your project to build incorrectly. To correct this, please make sure that $(OutDir), $(TargetName) and $(TargetExt) property values match the value specified in %(Link.OutputFile).  

Link.OutputFile 是生成输出(例如 EXE、DLL),通常由 $(TargetDir)$(TargetName)$(TargetExt)(提供路径、文件名和扩展名)构造。Link.OutputFile is the build output (EXE, DLL, for example), and it is normally constructed from $(TargetDir)$(TargetName)$(TargetExt), giving the path, filename and extension. 这是一种常见错误,发生在将项目从旧的 Visual C++ 生成工具 (vcbuild.exe) 迁移到新的生成工具 (MSBuild.exe) 时。This is a common error when migrating projects from the old Visual C++ build tool (vcbuild.exe) to the new build tool (MSBuild.exe). 由于 Visual Studio 2010 中的生成工具发生变化,因此每当你将 2010 之前的项目迁移到 2010 或更高版本时,就可能会遇到此问题。Since the build tool change occurred in Visual Studio 2010, you might encounter this issue whenever you migrate a pre-2010 project to a 2010 or later version. 基本问题是,项目迁移向导不会更新 Link.OutputFile 值,因为并不总是可以根据其他项目设置来确定其值。The basic problem is that the project migration wizard doesn’t update the Link.OutputFile value since it’s not always possible to determine what its value should be based on the other project settings. 因此,通常必须进行手动设置。Therefore, you usually have to set it manually. 有关详细信息,请参阅 Visual C++ 博客上的这篇文章For more details, see this post on the Visual C++ blog.

在这种情况下,对于 Spy++ 项目,转换后的项目中的 Link.OutputFile 属性将设置为 .\Debug\Spyxx.exe 或 .\Release\Spyxx.exe,具体取决于配置。In this case, the Link.OutputFile property in the converted project was set to .\Debug\Spyxx.exe and .\Release\Spyxx.exe for the Spy++ project, depending on the configuration. 最好的方法就是,针对所有配置将这些硬编码值替换为 $(TargetDir)$(TargetName)$(TargetExt)。The best bet is to simply replace these hardcoded values with $(TargetDir)$(TargetName)$(TargetExt) for All Configurations. 如果不起作用,则可以从此处自定义,或更改设置这些值的“常规”部分中的属性(属性为“输出目录”、“目标文件名”和“目标文件扩展名”)。If that doesn’t work, you can customize from there, or change the properties in the General section where those values are set (the properties are Output Directory, Target Name, and Target Extension. 记住,如果正在查看的属性使用宏,则可以选择下拉列表中的“编辑”打开一个对话框,该对话框显示最终的字符串和已进行的宏替换。Remember that if the property you are viewing uses macros, you can choose Edit in the dropdown list to bring up a dialog box that shows the final string with the macro substitutions made. 你可以通过选择“宏”按钮查看所有可用宏及其当前值。You can view all available macros and their current values by choosing the Macros button.

步骤 4.Step 4. 更新目标 Windows 版本Updating the Target Windows Version

下一个错误指示 WINVER 版本不再受 MFC 支持。The next error indicates that WINVER version is no longer supported in MFC. 适用于 Windows XP 的 WINVER 是 0x0501。WINVER for Windows XP is 0x0501.

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxv_w32.h(40): fatal error C1189: #error:  MFC does not support WINVER less than 0x0501.  Please change the definition of WINVER in your project properties or precompiled header.  

Microsoft 不再为 Windows XP 提供支持,因此,即使 Visual Studio 2015 中允许面向 Windows XP,你仍应在应用程序中逐步取消对此版本的支持,并鼓励你的用户采用新版本的 Windows。Windows XP is no longer supported by Microsoft, so even though targeting it is allowed in Visual Studio 2015, you should be phasing out support for it in your applications, and encouraging your users to adopt new versions of Windows.

若要消除此错误,请将“项目属性”设置更新为当前要面向的最低版本的 Windows,以定义 WINVER。To get rid of the error, define WINVER by updating the Project Properties setting to the lowest version of Windows you currently want to target. 此处可找到包含各种 Windows 版本的值的表。Find a table of values for various Windows releases here.

Stdafx.h 文件包含一些宏定义。The stdafx.h file contained some of these macro definitions.

#define WINVER       0x0500  // these defines are set so that we get the  
#define _WIN32_WINNT 0x0500  // maximum set of message/flag definitions,  
#define _WIN32_IE    0x0400  // from both winuser.h and commctrl.h.  

将设置为 Windows 7 的 WINVER。WINVER we will set to Windows 7. 如果将宏而不是值本身 (0x0601) 用于 Windows 7 (_WIN32_WINNT_WIN7),之后读取代码将会更容易。It’s easier to read the code later if you use the macro for Windows 7 (_WIN32_WINNT_WIN7), rather than the value itself (0x0601).

#define WINVER _WINNT_WIN32_WIN7 // Minimum targeted Windows version is Windows 7  

步骤 5.Step 5. 链接器错误Linker Errors

进行这些更改后,可以生成 SpyHk (DLL) 项目,但会产生链接器错误。With these changes, the SpyHk (DLL) project builds but produces a linker error.

LINK : warning LNK4216: Exported entry point _DLLEntryPoint@12  

不应导出 DLL 的入口点。The entry point for a DLL should not be exported. 入口点仅用于在 DLL 首次加载到内存中时,由加载程序进行调用,因此它不应该在其他调用方所在的导出表中。The entry point is only intended to be called by the loader when the DLL is first loaded into memory, so it should not be in the export table, which is for other callers. 我们只需确保未向入口点附加 __declspec(dllexport) 指令。We just need to make sure it does not have the __declspec(dllexport) directive attached to it. 在 spyxxhk.cz 中,我们必须从 DLLEntryPoint 的声明和定义这两个地方删除入口点。In spyxxhk.c, we have to remove it from two places, the declaration and definition of DLLEntryPoint. 使用此指令没有任何意义,但以前版本的链接器和编译器并未将其标记为问题。It never made sense to use this directive, but previous versions of the linker and compiler did not flag it as problem. 而较新版本的链接器会发出警告。The newer versions of the linker give a warning.

// deleted __declspec(dllexport)  
BOOL WINAPI DLLEntryPoint(HINSTANCE hinstDLL,DWORD fdwReason, LPVOID lpvReserved);  

现在,可以生成 C DLL 项目 SpyHK.dll,而且链接不会出错。The C DLL project, SpyHK.dll, now builds and links without error.

步骤 6.Step 6. 更多过时的头文件More outdated header files

从这一步骤开始,我们将对主要的可执行项目 Spyxx 执行操作。At this point we start working on the main executable project, Spyxx.

找不到其他几个包含文件:ctl3d.h 和 penwin.h。A couple of other include files could not be found: ctl3d.h and penwin.h. 虽然通过搜索 Internet 尝试识别包含标头的内容可能有用,但有时信息作用并不大。While it might be helpful to search the Internet to try to identify what included the header, sometimes the information isn’t that helpful. 我们发现 ctl3d.h 是 Exchange 开发工具包的一部分,并且对 Windows 95 上的某些控件样式以及与 Window Pen Computin(一个过时的 API)相关的 penwin.h 提供支持。We found out that ctl3d.h was part of the Exchange Development Kit and provided support for a certain style of controls on Windows 95, and penwin.h relates to Window Pen Computing, an obsolete API. 在这种情况下,我们只需注释掉 #include 行,并用处理 verstamp.h 的方式处理未定义的符号。In this case, we simply comment out the #include line, and deal with the undefined symbols as we did with verstamp.h. 与 3D 控件或 Pen Computing 相关的所有内容均已从项目中删除。Everything that relates to 3D Controls or Pen Computing was removed from the project.

给定具有许多将逐渐消除的编译错误的项目,删除 #include 指令时立即找到所有使用过时 API 的情况并不现实。Given a project with many compilation errors that you are gradually eliminating, it's not realistic to find all the uses of an outdated API right away when you remove the #include directive. 我们没有立即检测到它,却在稍后出现一个未定义 WM_DLGBORDER 的错误。We didn't detect it immediately, but rather at some later point came to an error that WM_DLGBORDER was undefined. 它实际上是来自 ctl3d.h 的许多未定义符号之一。It is actually just one many undefined symbols that come from ctl3d.h. 一旦确定它与过时的 API 相关后,则可以在代码中删除对它所有的引用。Once we've determined that it relates to an outdated API, we removed all references in code to it.

步骤 7.Step 7. 更新旧的 iostreams 代码Updating old iostreams code

下一个错误是使用 iostreams 的旧 C++ 代码的常见错误。The next error is common with old C++ code that uses iostreams.

mstream.h(40): fatal error C1083: Cannot open include file: 'iostream.h': No such file or directorymstream.h(40): fatal error C1083: Cannot open include file: 'iostream.h': No such file or directory

问题在于已删除并已替换旧的 iostreams 库。The issue is that the old iostreams library has been removed and replaced. 我们必须将旧的 iostreams 替换为较新的标准。We have to replace the old iostreams with the newer standards.

#include <iostream.h>  
#include <strstrea.h>  
#include <iomanip.h>  

这些是更新的包含项:These are the updated includes:

#include <iostream>  
#include <sstream>  
#include <iomanip>  

进行此更改后,ostrstream 出现问题,因此不再使用。With this change, we have problems with ostrstream, which is no longer used. 适当的替换为 ostringstream。The appropriate replacement is ostringstream. 我们尝试添加 ostrstream 的 typedef,以避免修改太多代码(至少作为一个开始)。We try adding a typedef for ostrstream to avoid modifying the code too much, at least as a start.

typedef std::basic_ostringstream<TCHAR> ostrstream;  

当前,项目正使用 MBCS(多字节字符集)进行生成,因此 char 是合适的字符数据类型。Currently the project is built using MBCS (Multi-byte Character Set), so char is the appropriate character data type. 但是,为了更轻松地将代码更新为 UTF-16 Unicode,我们将其更新为 TCHAR,它解析为 charwchar_t,具体取决于项目设置中的“字符集”属性是否设置为 MBCS 或 Unicode。However, to allow an easier update the code to UTF-16 Unicode, we update this to TCHAR, which resolves to char or wchar_t depending on whether the Character Set property in the project settings is set to MBCS or Unicode.

其他一些代码需要进行更新。A few other pieces of code need to be updated. 我们将基类 ios 替换为了 ios_base,并将 ostream is 替换为了 basic_ostream<T>。We replaced the base class ios with ios_base, and we replaced ostream is by basic_ostream<T>. 我们添加了两个额外的 typedef,并编译此部分。We add two additional typedefs, and this section compiles.

typedef std::basic_ostream<TCHAR> ostream;  
typedef ios_base ios;  

使用这些 typedef 只是一种临时解决方案。Using these typedefs is just a temporary solution. 为了实现更永久的解决方案,可以更新对重命名或过时 API 的每个引用。For a more permanent solution, we could update each reference to the renamed or outdated API.

以下是下一个错误。Here’s the next error.

error C2039: 'freeze': is not a member of 'std::basic_stringbuf<char,std::char_traits<char>,std::allocator<char>>'  

下一个问题是 basic_stringbuf 没有冻结方法。The next issue is that basic_stringbuf doesn’t have a freeze method. 冻结方法用于防止旧 ostream 发生内存泄漏。The freeze method is used to prevent a memory leak in the old ostream. 现在使用了新的 ostringstream,就不再需要此方法。We don’t need it now that we’re using the new ostringstream. 我们可以删除对冻结的调用。We can delete the call to freeze.


接下来的两个错误发生在相邻的行上。The next two errors occurred on adjacent lines. 第一个错误报告使用结束,这是旧 iostream 库中将 null 终止符添加到字符串的 IO 操控程序。The first complains about using ends, which is the old iostream library’s IO manipulator that adds a null terminator to a string. 第二个错误说明 str 方法的输出不能分配给非常量指针。The second of these errors explains that the output of the str method can’t be assigned to a non-const pointer.

// Null terminate the string in the buffer and  
// get a pointer to it.  
*this << ends;  
LPSTR psz = str();  
2>mstream.cpp(167): error C2065: 'ends': undeclared identifier2>mstream.cpp(168): error C2440: 'initializing': cannot convert from 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>' to 'LPSTR'  

使用新的流库时,由于字符串始终不以 null 结尾,因此无需结束,以便删除该行。Using the new stream library, ends is not needed since the string is always null-terminated, so that line can be removed. 第二个问题在于现在 str() 不返回指向字符串字符数组的指针,而是返回 std::string 类型。For the second issue, the problem is that now str() doesn’t return a pointer to the character array for a string; it returns the std::string type. 第二个问题的解决方案是,将类型更改为 LPCSTR 使用 c_str() 方法请求指针。The solution to the second is to change the type to LPCSTR and use the c_str() method to request the pointer.

//*this << ends;  
LPCTSTR psz = str().c_str();  

此代码上曾出现过令人困扰的错误。An error that puzzled us for a while occurred on this code.

MOUT << _T(" chUser:'") << chUser  
<< _T("' (") << (INT)(UCHAR)chUser << _T(')');  

MOUT 解析为 *g_pmout,这是类型为 mstream 的对象。The macro MOUT resolves to *g_pmout which is an object of type mstream. Mstream 类派生自标准输出字符串类 std::basic_ostream<TCHAR>.,但字符串文本周围有 _T,我们放置此宏的目的在于为转换到 Unicode 做准备,运算符 << 的重载解决方案失败,并出现以下错误消息:The mstream class is derived from the standard output string class, std::basic_ostream<TCHAR>. However with _T around the string literal, which we put in in preparation for converting to Unicode, the overload resolution for operator << fails with the following error message:

1>winmsgs.cpp(4612): error C2666: 'mstream::operator <<': 2 overloads have similar conversions
1>  c:\source\spyxx\spyxx\mstream.h(120): note: could be 'mstream &mstream::operator <<(ios &(__cdecl *)(ios &))'
1>  c:\source\spyxx\spyxx\mstream.h(118): note: or       'mstream &mstream::operator <<(ostream &(__cdecl *)(ostream &))'
1>  c:\source\spyxx\spyxx\mstream.h(116): note: or       'mstream &mstream::operator <<(ostrstream &(__cdecl *)(ostrstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(114): note: or       'mstream &mstream::operator <<(mstream &(__cdecl *)(mstream &))'
1>  c:\source\spyxx\spyxx\mstream.h(109): note: or       'mstream &mstream::operator <<(LPTSTR)'
1>  c:\source\spyxx\spyxx\mstream.h(104): note: or       'mstream &mstream::operator <<(TCHAR)'
1>  c:\source\spyxx\spyxx\mstream.h(102): note: or       'mstream &mstream::operator <<(DWORD)'
1>  c:\source\spyxx\spyxx\mstream.h(101): note: or       'mstream &mstream::operator <<(WORD)'
1>  c:\source\spyxx\spyxx\mstream.h(100): note: or       'mstream &mstream::operator <<(BYTE)'
1>  c:\source\spyxx\spyxx\mstream.h(95): note: or       'mstream &mstream::operator <<(long)'
1>  c:\source\spyxx\spyxx\mstream.h(90): note: or       'mstream &mstream::operator <<(unsigned int)'
1>  c:\source\spyxx\spyxx\mstream.h(85): note: or       'mstream &mstream::operator <<(int)'
1>  c:\source\spyxx\spyxx\mstream.h(83): note: or       'mstream &mstream::operator <<(HWND)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1132): note: or       'CDumpContext &operator <<(CDumpContext &,COleSafeArray &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1044): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1042): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1037): note: or       'CArchive &operator <<(CArchive &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1035): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::COleDateTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1030): note: or       'CArchive &operator <<(CArchive &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(1028): note: or       'CDumpContext &operator <<(CDumpContext &,COleCurrency)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(955): note: or       'CArchive &operator <<(CArchive &,ATL::CComBSTR)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(951): note: or       'CArchive &operator <<(CArchive &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxdisp.h(949): note: or       'CDumpContext &operator <<(CDumpContext &,COleVariant)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(248): note: or       'CArchive &operator <<(CArchive &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(247): note: or       'CArchive &operator <<(CArchive &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(246): note: or       'CArchive &operator <<(CArchive &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(242): note: or       'CDumpContext &operator <<(CDumpContext &,const RECT &)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(241): note: or       'CDumpContext &operator <<(CDumpContext &,POINT)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afxwin.h(240): note: or       'CDumpContext &operator <<(CDumpContext &,SIZE)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1639): note: or       'CArchive &operator <<(CArchive &,const CObject *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1425): note: or       'CArchive &operator <<(CArchive &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1423): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTime)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1418): note: or       'CArchive &operator <<(CArchive &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc\include\afx.h(1416): note: or       'CDumpContext &operator <<(CDumpContext &,ATL::CTimeSpan)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(694): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const char *)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(741): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,char)'
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(866): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const _Elem *)'
1>          with
1>          [
1>              _Elem=wchar_t
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(983): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>,wchar_t[10]>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &&,const _Ty (&))'
1>          with
1>          [
1>              _Ty=wchar_t [10]
1>          ]
1>  C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\include\ostream(1021): note: or       'std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &std::operator <<<wchar_t,std::char_traits<wchar_t>>(std::basic_ostream<wchar_t,std::char_traits<wchar_t>> &,const std::error_code &)'
1>  winmsgs.cpp(4612): note: while trying to match the argument list '(CMsgStream, const wchar_t [10])'  

存在很多运算符 << 定义,使这种错误看起来很严重。There are so many operator << definitions that this kind of error can be intimidating. 详细查看可用的重载后,可以看到多数都是不相关的,而进一步仔细查看 mstream 类定义后,我们发现了下面的函数,我们认为在这种情况下应调用该函数。After looking more closely at the available overloads, we can see that most of them are irrelevant, and looking more closely at the mstream class definition, we identified the following function that we think should be called in this case.

mstream& operator<<(LPTSTR psz)  
  return (mstream&)ostrstream::operator<<(psz);  

不调用它的原因是,字符串文本具有 const wchar_t[10] 类型(正如你可以从长错误消息的最后一行所看到),因此不会自动转换为非常量指针。The reason it isn't called is because the string literal has the type const wchar_t[10] as you can see from the last line of that long error message, so the conversion to a non-const pointer is not automatic. 但是,该运算符不应修改输入参数,因此更合适的参数类型是 LPCTSTR(作为 MBCS 编译时是 const char*,作为 Unicode 编译时是 const wchar_t*),而不是 LPTSTR(作为 MBCS编译时是 char*,作为 Unicode 编译时是 wchar_t*)。However that operator should not modify the input parameter, so the more appropriate parameter type is LPCTSTR (const char* when compiling as MBCS, and const wchar_t* as Unicode), not LPTSTR (char* when compiling as MBCS, and wchar_t* as Unicode). 进行此更改可修复该错误。Making that change fixes this error.

旧的、不太严格的编译器允许这种类型的转换,但最新的符合性更改则要求更正确的代码。This type of conversion was allowed under the older, less strict compiler, but more recent conformance changes require more correct code.

步骤 8.Step 8. 编译器的更严格转换The compiler's more strict conversions

我们还获得了如下所示的许多错误:We also get many errors like the following:

error C2440: 'static_cast': cannot convert from 'UINT (__thiscall CHotLinkCtrl::* )(CPoint)' to 'LRESULT (__thiscall CWnd::* )(CPoint)'  

该错误发生在只是宏的消息映射中:The error occurs in a message map that is simply a macro:

// other messages omitted...  
ON_WM_NCHITTEST() // Error occurs on this line.  

转到此宏的定义,可以看到它引用了函数 OnNcHitTest。Going to the definition of this macro, we see it references the function OnNcHitTest.

#define ON_WM_NCHITTEST() \  
{ WM_NCHITTEST, 0, 0, 0, AfxSig_l_p, \  
(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(CPoint) > (&ThisClass :: OnNcHitTest)) },  

此问题与指向成员函数类型的指针中的不匹配相关。The problem has to do with the mismatch in the pointer to member function types. 问题不在于从 CHotLinkCtrl(作为类类型)转换为 CWnd(作为类类型),因为这是有效的派生类到基类转换。The problem isn’t the conversion from CHotLinkCtrl as a class type to CWnd as the class type, since that is a valid derived-to-base conversion. 问题是返回类型:UINT 与 LRESULT。The problem is the return type: UINT vs. LRESULT. LRESULT 将解析为 LONG_PTR(即 64 位指针或 32 位指针,具体取决于目标二进制类型),因此 UINT 不转换为此类型。LRESULT resolves to LONG_PTR which is a 64-bit pointer or a 32-bit pointer, depending on the target binary type, so UINT does not convert to this type. 由于在 Visual Studio 2005 中,作为 64 位兼容性更改的一部分,许多消息映射方法的返回类型已从 UINT 更改为 LRESULT,这在升级在 2005 年之前编写的代码时并不罕见。This is not uncommon when upgrading code written before 2005 since the return type of many message map methods changed from UINT to LRESULT in Visual Studio 2005 as part of the 64-bit compatibility changes. 我们将以下代码的返回类型从 UINT 更改为 LRESULT:We change the return type from UINT in the following code to LRESULT:

afx_msg UINT OnNcHitTest(CPoint point);  

更改后,将得到以下代码:After the change we have the following code:

afx_msg LRESULT OnNcHitTest(CPoint point);  

由于从 CWnd 派生的不同类中总共出现了此函数的大约十个匹配项,当光标位于编辑器的函数上时,使用“转到定义”(键盘:F12)和“转到声明”(键盘:Ctrl+F12)有助于从“查找符号”工具窗口定位并导航到这些函数。Since there are about ten occurrences of this function all in different classes derived from CWnd, it’s helpful to use Go to Definition (Keyboard: F12) and Go to Declaration (Keyboard: Ctrl+F12) when the cursor is on the function in the editor to locate these and navigate to them from the Find Symbol tool window. “转到定义”通常是两个选项中更有用的。Go to Definition is usually the more useful of the two. “转到声明”将查找声明而不是定义类声明,例如友元类声明或前向引用。Go to Declaration will find declarations other than the defining class declaration, such as friend class declarations or forward references.

步骤 9.Step 9. MFC 更改MFC Changes

下一个错误也与更改的声明类型有关,并且还会发生在宏中。The next error also relates to a changed declaration type and also occurs in a macro.

error C2440: 'static_cast': cannot convert from 'void (__thiscall CFindWindowDlg::* )(BOOL,HTASK)' to 'void (__thiscall CWnd::* )(BOOL,DWORD)'  

问题在于 CWnd::OnActivateApp 的第二个参数从 HTASK 更改为了 DWORD。The issue is that the second parameter of CWnd::OnActivateApp changed from HTASK to DWORD. 此更改发生在 2002 年版的 Visual Studio、Visual Studio .NET 中。This change occurred in the 2002 release of Visual Studio, Visual Studio .NET.

afx_msg void OnActivateApp(BOOL bActive, HTASK hTask);  

我们必须相应更新派生类中的 OnActivateApp 的声明,如下所示:We have to update the declarations of OnActivateApp in derived classes accordingly as follows:

afx_msg void OnActivateApp(BOOL bActive, DWORD dwThreadId);  

在此步骤中,我们将能够编译项目。At this point, we are able to compile the project. 有几个警告需要解决,但是可以选择部分进行升级,例如从 MBCS 转换为 Unicode 或通过使用安全 CRT 函数提高安全性。There are a few warnings to work through, however, and there are optional parts of the upgrade, such as converting from MBCS to Unicode or improving security by using the Secure CRT functions.

步骤 10.Step 10. 解决编译器警告Addressing compiler warnings

若要获取完整的警告列表,则应对解决方案执行“全部重新生成”而不是普通生成,从而确保以前编译的所有内容均将重新编译,因为你只能从当前的编译获取警告报表。To get a full list of warnings, you should do a Rebuild All on the solution rather than an ordinary build, just to make sure that everything that previously compiled will be recompiled, since you only get warning reports from the current compilation. 另一个问题在于是接受当前警告级别还是使用更高的警告级别。The other question is whether to accept the current warning level or use a higher warning level. 移植大量代码(尤其是旧代码)时,使用更高的警告级别更恰当。When porting a lot of code, especially old code, using a higher warning level might be appropriate. 你可能还想从默认警告级别开始,然后增加警告级别以获取所有警告。You might also want to start with the default warning level and then increase the warning level to get all warnings. 如果使用 /Wall,则可以获得系统头文件中的一些警告,多数人都使用 /W4 来获取有关其代码的大部分警告,而不获取系统头文件的警告。If you use /Wall, you get some warnings in the system header files, so many people use /W4 to get the most warnings on their code without getting warnings for system headers. 如果希望警告显示为错误,则需添加 /WX 选项。If you want warnings to show up as errors, add the /WX option. 这些设置在“项目属性”对话框的 C/C++ 部分中。These settings are in the C/C++ section of the Project Properties dialog box.

CSpyApp 类中的方法之一将产生有关不再受支的持函数的警告。One of the methods in the CSpyApp class produces a warning about a function that is no longer supported.

void SetDialogBkColor() {CWinApp::SetDialogBkColor(::GetSysColor(COLOR_BTNFACE));}  

警告如下所示。The warning is as follows.

warning C4996: 'CWinApp::SetDialogBkColor': CWinApp::SetDialogBkColor is no longer supported. Instead, handle WM_CTLCOLORDLG in your dialog  

WM_CTLCOLORDLG 消息已在 Spy++ 代码中进行了处理 ,因此需要的唯一更改是删除对 SetDialogBkColor 的不再需要的所有引用。The message WM_CTLCOLORDLG was already handled in Spy++ code, so the only change required was to delete any references to SetDialogBkColor, which is no longer needed.

下一个警告的修复非常简单,注释掉变量名称即可。The next warning was straightforward to fix by commenting out the variable name. 我们收到了以下警告:We received the following warning:

warning C4456: declaration of 'lpszBuffer' hides previous local declaration  

生成此警告的代码涉及宏。The code that produces this involves a macro.


    PARM(lpszBuffer, PPACK_STRINGORD, ED2);  


      PARM(lpszBuffer, LPTSTR, ED2);  

此代码中使用大量的宏导致代码哪一维护。Heavy use of macros as in this code tends to make code harder to maintain. 在本例中,宏包括变量的声明。In this case, the macros include the declarations of the variables. 宏 PARM 的定义如下:The macro PARM is defined as follows:

#define PARM(var, type, src)type var = (type)src  

因此,lpszBuffer 变量在相同的函数中进行了两次声明。Therefore the lpszBuffer variable gets declared twice in the same function. 如果代码未使用宏(只需删除第二个类型声明),则修复这个问题并不容易。It's not that straightfoward to fix this as it would be if the code were not using macros (simply remove the second type declaration). 事实上,我们面对的选择很艰难,必须决定是将宏代码重写为普通代码(单调乏味并且可能出错的任务)还是禁用该警告。As it is, we have the unfortunate choice of having to decide whether to rewrite the macro code as ordinary code (a tedious and possibly error-prone task) or disable the warning.

在本例中,我们选择禁用该警告。In this case, we opt to disable the warning. 可以通过添加如下杂注实现此操作:We can do that by adding a pragma as follows:

#pragma warning(disable : 4456)  

禁用警告时,你可能需要将禁用限制为只对产生警告的代码起作用,以避免在可能提供有用信息时禁止显示警告。When disabling a warning, you might want to restrict the disabling effect to just the code you that produces the warning, to avoid suppressing the warning when it might provide useful information. 我们在产生警告的行之后添加代码以还原警告,或者因为宏中发生警告,最好使用在宏中起作用的 __pragma 关键字(#pragma 在宏中不起作用)。We add code to restore the warning just after the line that produces it, or better yet, since this warning occurs in a macro, use the __pragma keyword, which works in macros (#pragma does not work in macros).

#define PARM(var, type, src)__pragma(warning(disable : 4456))  \  
type var = (type)src \  
__pragma(warning(default : 4456))  

下一个警告需要进行一些代码修订。The next warning requires some code revisions. Win32 API GetVersion(和 GetVersionEx)已被弃用。The Win32 API GetVersion (and GetVersionEx) is deprecated.

warning C4996: 'GetVersion': was declared deprecated  

下面的代码演示如何获取版本。The following code shows how the version is obtained.

// check Windows version and set m_bIsWindows9x/m_bIsWindows4x/m_bIsWindows5x flags accordingly.  
DWORD dwWindowsVersion = GetVersion();  

后面紧跟检查 dwWindowsVersion 值以确定是否在 Windows 95 上运行以及确定 Windows NT 版本的大量代码。This is followed by a lot of code that examines the dwWindowsVersion value to determine whether we're running on Windows 95, and which version of Windows NT. 由于这已过时,我们将删除代码并处理对这些变量的所有引用。Since this is all outdated, we remove the code and deal with any references to those variables.

文章 Operating system version changes in Windows 8.1 and Windows Server 2012 R2(Windows 8.1 和 Windows Server 2012 R2中的操作系统版本更改)解释了这一情况。The article Operating system version changes in Windows 8.1 and Windows Server 2012 R2 explains the situation.

CSpyApp 类中存在查询操作系统版本的方法:IsWindows9x、IsWindows4x 和 IsWindows5x。There are methods in the CSpyApp class that query the operating system version: IsWindows9x, IsWindows4x and IsWindows5x. 对于旧应用程序使用的技术而言,良好的开端在于假定我们计划支持的 Windows 版本(Windows 7 和更高版本)都非常接近 Windows NT 5。A good starting point is to assume that the versions of Windows that we intend to support (Windows 7 and later) are all close to Windows NT 5 as far the technologies used by this older application is concerned. 这些方法用于应对旧操作系统的限制。The uses of these methods were to deal with limitations of the older operating systems. 因此,我们更改了这些方法,以对 IsWindows5x 返回 TRUE,对其他返回 FALSE。So we changed those methods to return TRUE for IsWindows5x and FALSE for the others.

BOOL IsWindows9x() {/*return(m_bIsWindows9x);*/ return FALSE;  }  
BOOL IsWindows4x() {/*return(m_bIsWindows4x);*/ return FALSE;  }  
BOOL IsWindows5x() {/*return(m_bIsWindows5x);*/ return TRUE;  }  

只剩下少数几个直接使用内部变量的地方。That left only a few places where the internal variables were used directly. 由于删除了这些变量,我们将获得几个必须显式处理的错误。Since we removed those variables, we get a few errors that have to deal with explicitly.

error C2065: 'm_bIsWindows9x': undeclared identifier  
void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)  
  pCmdUI->Enable(m_bIsWindows9x || hToolhelp32 != NULL);  

可以将其替换为方法调用,或者仅传递 TRUE 并删除 Windows 9x 中的旧特殊用例。We could replace this with a method call or simply pass TRUE and remove the old special case for Windows 9x.

void CSpyApp::OnUpdateSpyProcesses(CCmdUI *pCmdUI)  
  pCmdUI->Enable(TRUE /*!m_bIsWindows9x || hToolhelp32 != NULL*/);  

默认级别 (3) 的最终警告与位域相关。The final warning at the default level (3) has to do with a bitfield.

treectl.cpp(1656): warning C4463: overflow; assigning 1 to bit-field that can only hold values from -1 to 0  

触发此错误的代码如下所示。The code that triggers this is as follows.

m_bStdMouse = TRUE;  

M_bStdMouse 的声明指示它是位域。The declaration of m_bStdMouse indicates that it is a bitfield.

class CTreeListBox : public CListBox  


  int ItemFromPoint(const CPoint& point);  

  class CTreeCtl* m_pTree;  
  BOOL m_bGotMouseDown : 1;  
  BOOL m_bDeferedDeselection : 1;  
  BOOL m_bStdMouse : 1;  

此代码在 Visual C++ 支持内置的 bool 类型前编写。This code was written before the built-in bool type was supported in Visual C++. 在此类代码中,BOOL 是 int 的 typedef。int 类型是带符号类型,而带符号 int 的位表示是将第一个位用作符号位,以便 int 类型的位域可以解释为表示 0 或 -1,结果可能超出预期。In such code, BOOL was a typedef for int. The type int is a signed type, and the bit representation of a signed int is to use the first bit as a sign bit, so a bitfield of type int could be interpreted as representing 0 or -1, probably not what was intended.

通过查看代码无法知道这些为什么是位域。You wouldn't know by looking at the code why these are bitfields. 是否打算保持较小的对象尺寸?或者是否有任何地方使用该对象的二进制布局?Was the intent to keep the size of the object small, or is there anywhere where the binary layout of the object is used? 由于没有使用位域的原因,我们将它们更改为了普通 BOOL 成员。We changed these to ordinary BOOL members since we didn't see any reason for the use of a bitfield. 使用位域来使对象保持小尺寸并不能保证有效。Using bitfields to keep an object's size small isn't guaranteed to work. 这取决于编译器如何布局类型。It depends on how the compiler lays out the type.

你可能想知道使用整个标准类型 bool 是否有用。You might wonder if using the standard type bool throughout would be helpful. 很多旧代码模式(如 BOOL 类型)都用于解决之后在标准 C++ 中解决的问题,因此从 BOOL 更改为 bool 内置类型只是此类更改的其中一个示例,当你获取最初在新版本中运行的代码后,通常会考虑此更改。Many of the old code patterns such as the BOOL type were invented to solve problems that were later solved in standard C++, so changing from BOOL to the bool built-in type is just one example of such a change that you consider doing after you get your code initially running in the new version.

处理完默认级别(等级 3)出现的所有警告后,更改为级别 4,以捕获其他警告。Once we've dealt with all the warnings that appear at the default level (level 3) we changed to level 4 to catch a few additional warnings. 首先出现如下警告:The first to appear was as follows:

warning C4100: 'nTab': unreferenced formal parameter  

产生此警告的代码如下所示。The code that produced this warning was as follows.

virtual void OnSelectTab(int nTab) {};  

这看起来无害,但由于我们希望使用 /W4 和 /WX 集进行干净的编译,因此只需注释掉变量名,保留以便于阅读。This seems harmless enough, but since we wanted a clean compilation with /W4 and /WX set, we simply commented out the variable name, leaving it for the sake of readability.

virtual void OnSelectTab(int /*nTab*/) {};  

我们收到的其他警告有助于进行常规的代码清理。Other warnings we received were useful for general code cleanup. 存在大量的从 intunsigned intWORD(即 unsigned short 的 typedef)的隐式转换。There are a number of implicit conversions from int or unsigned int to WORD (which is a typedef for unsigned short). 这可能会导致数据丢失。These involve a possible loss of data. 在这些情况下,我们添加了到 WORD 的强制转换。We added a cast to WORD in these cases.

以下是我们获得的关于此代码的另一个级别 4 警告:Another level 4 warning we got for this code was:

warning C4211: nonstandard extension used: redefined extern to static  

该问题发生在变量首先声明 extern 再声明 static 时。The problem occurs when a variable was first declared extern, then later declared static. 这两个存储类说明符的含义是互斥的,但作为 Microsoft 扩展这是允许的。The meaning of these two storage class specifiers is mutually exclusive, but this is allowed as a Microsoft extension. 如果希望代码可移植到其他编译器,或者希望使用 /Za(ANSI 兼容性)来编译代码,则可以更改声明以获得匹配的存储类说明符。If you wanted the code to be portable to other compilers, or you wanted to compile it with /Za (ANSI compatibility), you would change the declarations to have matching storage class specifiers.

步骤 11.Step 11. 从 MBCS 移植到 UnicodePorting from MBCS to Unicode

注意,在 Windows 世界中,当说到 Unicode 时,通常是指 UTF-16。Note that in the Windows world, when we say Unicode, we usually mean UTF-16. 其他操作系统(如 Linux)使用 UTF-8,但 Windows 通常不使用。Other operating systems such as Linux use UTF-8, but Windows generally does not. Visual Studio 2013 和 Visual Studio 2015 中弃用了 MFC 的 MBCS 版本,但是 Visual Studio 2017 将不再弃用它。The MBCS version of MFC was deprecated in Visual Studio 2013 and 2015, but it is no longer deprecated in Visual Studio 2017. 如果使用的是 Visual Studio 2013 或 Visual Studio 2015,在执行实际将 MBCS 代码移植到 UTF-16 Unicode 的步骤之前,我们可能需要暂时消除已弃用 MBCS 的警告,以便执行其他工作或将移植推迟到方便的时间。If using Visual Studio 2013 or 2015, before taking the step to actually port MBCS code to UTF-16 Unicode, we might want to temporarily eliminate the warnings that MBCS is deprecated, in order to do other work or postpone the porting until a convenient time. 当前的代码使用 MBCS,若要继续使用,则需要安装 ANSI/MBCS 版本的 MFC。The current code uses MBCS and to continue with that we need to install the ANSI/MBCS version of MFC. Visual Studio 使用 C++ 的桌面开发的默认安装内容并不包括较大的 MFC 库,因此需要在安装程序的可选组件中将其选中。The rather large MFC library is not part of the default Visual Studio Desktop development with C++ installation, so it must be selected from the optional components in the installer. 请参阅 MFC MBCS DLL 加载项See MFC MBCS DLL Add-on. 完成下载并重启 Visual Studio 后,可以使用 MBCS 版本的 MFC 进行编译和链接,但若要在使用 Visual Studio 2013 和 Visual Studio 2015 时完全删除关于 MBCS 的警告,则还应将 NO_WARN_MBCS_MFC_DEPRECATION 添加到项目属性预处理器部分的预定义宏列表,或者添加到 stdafx.h 头文件或其他常见头文件的开头。Once you download this and restart Visual Studio, you can compile and link with the MBCS version of MFC, but to get rid of the warnings about MBCS if you are using Visual Studio 2013 or 2015, you should also add NO_WARN_MBCS_MFC_DEPRECATION to your list of predefined macros in the Preprocessor section of project properties, or at the beginning of your stdafx.h header file or other common header file.

现在我们将获得一些链接器错误。We now have some linker errors.

fatal error LNK1181: cannot open input file 'mfc42d.lib'  

出现 LNK1181 错误的原因是链接器输入中包含 mfc 的过时静态库版本。LNK1181 occurs because an outdated static library version of mfc is included on the linker input. 不再需要静态库,由于我们可以动态链接 MFC,因此只需从项目属性链接器部分的输入属性中删除所有 MFC 静态库即可。This isn’t required anymore since we can link MFC dynamically, so we just need to remove all MFC static libraries from the Input property in the Linker section of the project properties. 此项目还使用 /NODEFAULTLIB 选项,并改为列出所有库依赖项。This project is also using the /NODEFAULTLIB option, and instead it lists all the library dependencies.


现在让我们实际将旧的多字节字符集 (MBCS) 代码更新为 Unicode。Now let us actually update the old Multi-byte Character Set (MBCS) code to Unicode. 由于这是一个 Windows 应用程序,它与 Windows 桌面平台联系紧密,因此我们将该应用程序移植到 Windows 使用的 UTF-16 Unicode。Since this is a Windows application, intimately tied to the Windows desktop platform, we will port it to UTF-16 Unicode that Windows uses. 如果你正编写跨平台代码或正将 Windows 应用程序移植到另一个平台,则可能需要考虑移植到其他操作系统广泛使用的 UTF-8。If you are writing cross-platform code or porting a Windows application to another platform, you might want to consider porting to UTF-8, which is widely used on other operating systems.

移植到 UTF-16 Unicode 时,必须决定是否仍然需要编译为 MBCS 的选项。Porting to UTF-16 Unicode, we must decide whether we still want the option to compile to MBCS or not. 如果需要具有支持 MBCS 的选项,则应将 TCHAR 宏用作字符类型,它将解析为 charwchar_t,具体取决于是否在编译期间定义了 _MBCS 或 _UNICODE。If we want to have the option to support MBCS, we should use the TCHAR macro as the character type, which resolves to either char or wchar_t, depending on whether _MBCS or _UNICODE is defined during compilation. 切换到 TCHAR 及 TCHAR 版本的各种 API 而不是 wchar_t 及其关联 API,意味着你能回到 MBCS 版本的代码,只需定义 _MBCS 宏而不是 _UNICODE。Switching to TCHAR and the TCHAR versions of various APIs instead of wchar_t and its associated APIs means that you can get back to an MBCS version of your code simply by defining _MBCS macro instead of _UNICODE. 除 TCHAR 外,还存在各种 TCHAR 版本,如广泛使用的 typedef、宏和函数。In addition to TCHAR, a variety of TCHAR versions of such as widely used typedefs, macros, and functions exists. 例如,LPCTSTR 而非 LPCSTR,等等。For example, LPCTSTR instead of LPCSTR, and so on. 在项目属性对话框中,在“配置属性”的“常规”部分,将“字符集”属性从“使用 MBCS 字符集”更改为“使用 Unicode 字符集”。In the project properties dialog, under Configuration Properties, in the General section, change the Character Set property from Use MBCS Character Set to Use Unicode Character Set. 此设置会影响编译期间预定义的宏。This setting affects which macro is predefined during compilation. 同时存在 UNICODE 宏和 _UNICODE 宏。There is both a UNICODE macro and a _UNICODE macro. 项目属性对两者的影响一致。The project property affects both consistently. Windows 头文件使用 UNICODE,而 Visual C++ 头文件(如 MFC)则使用 _UNICODE,但定义其中一个后,另一个也将得到定义。Windows headers use UNICODE where Visual C++ headers such as MFC use _UNICODE, but when one is defined, the other is always defined.

有一个使用 TCHAR 从 MBCS 移植到 UTF-16 Unicode 的好方法A good guide to porting from MBCS to UTF-16 Unicode using TCHAR exists. 选择此路由。We choose this route. 首先,将“字符集”属性设置为“使用 Unicode 字符集”并重新生成项目。First, we change the Character Set property to Use Unicode Character Set and rebuild the project.

代码中的一些地方已经使用 TCHAR,显然预期最终支持 Unicode。Some places in the code were already using TCHAR, apparently in anticipation of eventually supporting Unicode. 有些不使用。Some were not. 我们搜索 CHAR 的实例,即 char 的 typedef,并将大多数实例替换为了 TCHAR。We searched for instances of CHAR, which is a typedef for char, and replaced most of them with TCHAR. 此外,我们还搜索了 sizeof (CHAR)Also, we looked for sizeof (CHAR). 每当从 CHAR 更改为 TCHAR 时,通常不得不更改为 sizeof(TCHAR),因为这通常用于确定字符串中的字符数。Whenever we changed from CHAR to TCHAR, we usually had to change to sizeof(TCHAR) since this was often used to determine the number of characters in a string. 此处使用错误的类型不会产生编译器错误,因此需要注意此情况。Using the wrong type here does not produce a compiler error, so it's worth paying a bit of attention to this case.

切换到 Unicode 后,这种类型的错误很常见。This type of error is very common just after switching to Unicode.

error C2664: 'int wsprintfW(LPWSTR,LPCWSTR,...)': cannot convert argument 1 from 'CHAR [16]' to 'LPWSTR'  

以下是产生此错误的代码示例:Here’s an example of code that produces this:

wsprintf(szTmp, "%d.%2.2d.%4.4d", rmj, rmm, rup);  

我们将 _T 放在该字符串文本周围以删除此错误。We put _T around the string literal to remove the error.

wsprintf(szTmp, _T("%d.%2.2d.%4.4d"), rmj, rmm, rup);  

_T 宏可以使字符串文本编译为 char 字符串或 wchar_t 字符串,具体取决于 MBCS 或 UNICODE 的设置。The _T macro has the effect of making a string literal compile as a char string or a wchar_t string, depending on the setting of MBCS or UNICODE. 若要在 Visual Studio 中将所有字符串替换为 _T,首先需要打开“快速替换”框(键盘:Ctrl+F)或“在文件中替换”(键盘:Ctrl+Shift+H),然后选中“使用正则表达式”复选框。To replace all strings with _T in Visual Studio, first open the Quick Replace (Keyboard: Ctrl+F) box or the Replace In Files (Keyboard: Ctrl+Shift+H), then choose the Use Regular Expressions checkbox. 输入 ((\".*?\")|('.+?')) 作为搜索文本,输入 _T($1) 作为替换文本。Enter ((\".*?\")|('.+?')) as the search text and _T($1) as the replacement text. 如果某些字符串周围已存在 _T 宏,此过程将重新添加该宏,并且可能还会发现不需要 _T 的情况(例如使用 #include 时),因此最好使用“替换下一个”而不是“全部替换”。If you already have the _T macro around some strings, this procedure will add it again, and it might also find cases where you don't want _T, such as when you use #include, so it's best to use Replace Next rather than Replace All.

此特定函数 wsprintf 实际上在 Windows 标头中已定义,相关文档建议不使用此函数,因为可能会发生缓冲区溢出。This particular function, wsprintf, is actually defined in the Windows headers, and the documentation for it recommends that it not be used, due to possible buffer overrun. szTmp 缓冲区未给定大小,因此函数无法检查该缓冲区是否可容纳要写入的所有数据。No size is given for the szTmp buffer, so there is no way for the function to check that the buffer can hold all the data to be written to it. 请参阅下一节有关移植到安全 CRT 的内容,我们将在下一节修复其他类似的问题。See the next section about porting to the Secure CRT, in which we fix other similar problems. 最终使用 _stprintf_s 进行替换。We ended up replacing it with _stprintf_s.

这是转换为 Unicode 时的另一个常见错误。Another common error you’ll see in converting to Unicode is this.

error C2440: '=': cannot convert from 'char *' to 'TCHAR *'  

生成此错误的代码如下所示:The code that produces it is as follows:

pParentNode->m_szText = new char[strTitle.GetLength() + 1];  
_tcscpy(pParentNode->m_szText, strTitle);  

尽管使用了 _tcscpy 函数(复制字符串的 TCHAR strcpy 函数),但已分配的缓冲区是 char 缓冲区。Even though the _tcscpy function was used, which is the TCHAR strcpy function for copying a string, the buffer that was allocated was a char buffer. 可轻松更改为 TCHAR。This is easily changed to TCHAR.

pParentNode->m_szText = new TCHAR[strTitle.GetLength() + 1];  
_tcscpy(pParentNode->m_szText, strTitle);  

同样,当编译器错误保证时,我们分别将 LPSTR(指向字符串的长指针)和 LPCSTR(指向常量字符串的长指针)更改为了 LPTSTR(指向 TCHAR 字符串的长指针)和 LPCTSTR(指向常量 TCHAR 字符串的长指针)。Similarly, we changed LPSTR (Long Pointer to STRing) and LPCSTR (Long Pointer to Constant STRing) to LPTSTR (Long Pointer to TCHAR STRing) and LPCTSTR (Long Pointer to Constant TCHAR STRing) respectively, when warranted by a compiler error. 我们选择不使用全局搜索和替换来进行此类替换,因为必须逐个检查每种情况。We chose not to make such replacements by using global search and replace, because each situation had to be examined individually. 在某些情况下,需要 char 版本,如处理某些使用具有 A 后缀的 Windows 结构的 Windows 消息时。In some cases, the char version is wanted, such as when processing certain Windows messages which use Windows structures that have the A suffix. 在 Windows API 中,后缀 A 意味着 ASCII 或 ANSI(也适用于 MBCS),而后缀 W 意味着宽字符或 UTF-16 Unicode。In the Windows API, the suffix A means ASCII or ANSI (and also applies to MBCS), and the suffix W means wide characters, or UTF-16 Unicode. 此命名模式在 Windows 头文件使用,但当我们不得不添加 Unicode 版本的函数(仅在 MBCS 版本中定义)时,也将在 Spy++ 代码中进行沿用。This naming pattern is used in the Windows headers, but we also followed it in the Spy++ code when we had to add a Unicode version of a function that was already defined in only an MBCS version.

某些情况下,我们必须替换类型以使用正确解析的版本(例如 WNDCLASS 而非 WNDCLASSA)。In some cases we had to replace a type to use a version that resolves correctly (WNDCLASS instead of WNDCLASSA for example).

在许多情况下,我们必须使用 Win32 API 的通用版本(宏),如 GetClassName(而不是 GetClassNameA)。In many cases we had to use the generic version (macro) of a Win32 API like GetClassName (instead of GetClassNameA). 在消息处理程序 switch 语句中,某些消息特定于 MBCS 或 Unicode,在这些情况下,我们必须更改代码以显式调用 MBCS 版本,因为已将常规命名的函数替换为了具有 A 和 W 后缀的特定函数,并添加了通用名称的宏,该名称可基于是否定义了 UNICODE 来解析为正确的 A 或 W 名称。In message handler switch statement, some messages are MBCS or Unicode specific, in those cases, we had to change the code to explicitly call the MBCS version, because we replaced the generically named functions with A and W specific functions, and added a macro for the generic name that resolves to the correct A or W name based on whether UNICODE is defined. 在代码的许多部分,当我们切换到定义 _UNICODE 时,即使需要 A 版本,现在也会选择 W 版本。In many parts of the code, when we switched to define _UNICODE, the W version is now chosen even when the A version is what's wanted.

有几个地方必须采用特殊的操作。There are a few places where special actions had to be taken. 使用 WideCharToMultiByte 或 MultiByteToWideChar 均需要详细了解。Any use of WideCharToMultiByte or MultiByteToWideChar might require a closer look. 以下是使用 WideCharToMultiByte 的一个示例。Here's one example where WideCharToMultiByte was being used.

BOOL C3dDialogTemplate::GetFont(CString& strFace, WORD& nFontSize)  
  ASSERT(m_hTemplate != NULL);  

  DLGTEMPLATE* pTemplate = (DLGTEMPLATE*)GlobalLock(m_hTemplate);  
  if ((pTemplate->style & DS_SETFONT) == 0)  
    return FALSE;  

  BYTE* pb = GetFontSizeField(pTemplate);  
  nFontSize = *(WORD*)pb;  
  pb += sizeof (WORD);  
  WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,  
  strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);  
  return TRUE;  

若要解决此问题,我们必须了解使用它的原因需要将表示字体名称的宽字符字符串复制到 CString、strFace 的内部缓冲区。To address this, we had to understand that the reason this was done was to copy a wide character string representing the name of a font into the internal buffer of a CString, strFace. 多字节 CString 字符串需要的代码与宽字符 CString 字符串稍有不同,因此我们在此案例种添加了 #ifdef。This required slightly different code for multibyte CString strings as for wide character CString strings, so we added an #ifdef in this case.

#ifdef _MBCS  
WideCharToMultiByte(CP_ACP, 0, (LPCWSTR)pb, -1,  
strFace.GetBufferSetLength(LF_FACESIZE), LF_FACESIZE, NULL, NULL);  
wcscpy(strFace.GetBufferSetLength(LF_FACESIZE), (LPCWSTR)pb);  

当然,我们应该使用 wcscpy_s 而非 wcscpy,前者是更安全的版本。Of course, instead of wcscpy we really should use wcscpy_s, the more secure version. 下一节将解决此问题。The next section addresses this.

作为操作检查,我们应将“字符集”重置为“使用多字节字符集”,并确保代码仍使用 MBCS 和 Unicode 进行编译。As a check on our work, we should reset the Character Set to Use Multibyte Character Set and make sure that the code still compiles using MBCS as well as Unicode. 当然,发生所有这些更改后,应在重新编译的应用上执行完整的测试轮次。Needless to say, a full test pass should be executed on the recompiled app after all these changes.

在使用此 Spy++ 解决方案的工作中,对于普通 C++ 开发人员而言,将代码转换为 Unicode 大约需要两天的工作时间。In our work with this Spy++ solution, it took about two working days for an average C++ developer to convert the code to Unicode. 这并不包括再测试的时间。That did not include the retesting time.

步骤 12.Step 12. 移植以使用安全 CRTPorting to use the Secure CRT

下一步是移植代码以使用安全版本(带 _s 后缀的版本)的 CRT 函数。Porting the code to use the secure versions (the versions with the _s suffix) of CRT functions is next. 在这种情况下,常规策略是将函数替换为 _s 版本,然后通常会添加所需的附加缓冲区大小参数。In this case, the general strategy is to replace the function with the _s version and then, usually, add the required additional buffer size parameters. 许多情况下这非常简单,因为大小是已知的。In many cases this is straightforward since the size is known. 在其他情况下,当不能立即知道大小时,则需要向正使用 CRT 函数的函数添加其他参数,或者可以检查目标缓冲区的使用情况并查看具体的适当大小限制。In other cases, where the size is not immediately available, it’s necessary to add additional parameters to the function that’s using the CRT function, or perhaps examine the usage of the destination buffer and see what the appropriate size limits are.

Visual C++ 提供了技巧,可以更加轻松地获取代码安全而无需添加许多大小参数,添加参数是通过使用模板重载实现的。Visual C++ provides a trick to make it easier to get code secure without adding as many size parameters, and that is by using the template overloads. 由于这些重载都是模板,因此仅在作为 C++ 编译时可用,而作为 C 时不可用。Spyxxhk 是 C 项目,所以这个技巧不起作用。Since these overloads are templates, they are only available when compiling as C++, not as C. Spyxxhk is a C project, so the trick won't work for that. 但 pyxx 不是 C 项目,可以使用该技巧。However, Spyxx is not and we can use the trick. 该技巧是在该项目每个文件中将进行编译的地方添加类似的行,例如在 stdafx.h 中:The trick is to add a line like this in a place where it will be compiled in every file of the project, such as in stdafx.h:


定义后,只要该缓冲区是一个数组而不是原始的指针,则可以从数组类型推断其大小,并用作大小参数,因此无需自己提供大小。When you define that, whenever the buffer is an array, rather than a raw pointer, its size is inferred from the array type and that is used as the size parameter, without you having to supply it. 这样有助于减少重写代码的复杂性。That helps to cut down the complexity of rewriting the code. 你仍然必须将函数名称替换为 _s 版本,但通常可以通过搜索和替换操作实现。You still have to replace the function name with the _s version, but that can often be done by a search and replace operation.

某些函数的返回值已更改。The return values of some functions changed. 例如,_itoa_s(_itow_s 和宏 _itot_s)将返回错误代码 (errno_t) 而不是字符串。For example, _itoa_s (and _itow_s and the macro _itot_s) returns an error code (errno_t), rather than the string. 因此在这些情况下,必须将对 _itoa_s 的调用移到单独的行上,并替换为缓冲区的标识符。So in those cases, you have to move the call to _itoa_s onto a separate line and replace it with the buffer's identifier.

一些常见的情况:对于 memcpy,切换到 memcpy_s 时,经常会添加要复制结构的大小。Some of the common cases: for memcpy, when switching to memcpy_s, we frequently added the size of the structure being copied to. 同样,对于大多数字符串和缓冲区,数组或缓冲区的大小可以轻松从缓冲区的声明确定或通过查找最初分配缓冲区的位置来确定。Similarly, for most strings and buffers, the size of the array or buffer is easily determined from the declaration of the buffer or by finding where the buffer was originally allocated. 在某些情况下,你需要确定大的缓冲区如何实际可用,而如果正在修改的函数范围内该信息不可用,它应被添加为一个附加参数,并且应修改调用代码来提供信息。For some situations, you need to determine how big of a buffer is actually available, and if that information is not available in the scope of the function that you’re modifying, it should be added as an additional parameter and the calling code should be modified to provide the information.

使用这些技巧,大约只需要半日时间即可转换代码以使用安全的 CRT 函数。With these techniques, it took about half a day to convert the code to use the secure CRT functions. 如果不选择添加到模板重载,而是选择手动添加大小参数,则可能需要两倍或三倍的时间。If you choose not to the template overloads and to add the size parameters manually, it would probably take twice or three times more time.

步骤 13.Step 13. /Zc:forScope 已弃用/Zc:forScope- is deprecated

自 Visual C++ 6.0 起,编译器符合当前的标准,该标准将在循环中声明的变量的范围限制为循环的范围。Since Visual C++ 6.0, the compiler conforms to the current standard, which limits the scope of variables declared in a loop to the scope of the loop. 编译器选项 /Zc:forScope(项目属性中的“强制 for 循环范围中的符合性”)控制是否将其报告为错误。The compiler option /Zc:forScope (Force Conformance for Loop Scope in the project properties) controls whether or not this is reported as an error. 我们应更新我们的代码以使其符合标准,并在循环外部添加声明。We should update our code to be conformant, and add declarations just outside the loop. 若要避免更改代码,可以将“语言”部分的 C++ 项目属性中的设置更改为“否(/Zc:forScope-)”。To avoid making the code changes, you can change that setting in the Language section of the C++ project properties to No (/Zc:forScope-). 但请记住,未来版本的 Visual C++ 中可能会删除 /Zc:forScope-,因此你的代码最终同样需要更改以符合标准。However, keep in mind that /Zc:forScope- might be removed in a future release of Visual C++, so eventually your code will need to change to conform to the standard.

这些问题的修复相对轻松,但具体取决于你的代码,此问题可能会影响大量代码。These issues are relatively easy to fix, but depending on your code, it might affect a lot of code. 下面是一个典型问题。Here's a typical issue.

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const  
  for (int n = 0; mszStrings[0] != 0; n++)  
  mszStrings = _tcschr(mszStrings, 0) + 1;  

上面的代码生成错误:The above code produces the error:

'n': undeclared identifier  

这是因为编译器已弃用允许不再符合 C++ 标准的代码的编译器选项。This occurs because the compiler has deprecated a compiler option that allowed code that no longer complies with the C++ standard. 在该标准中,在循环内声明一个变量会将其范围限制为仅在循环内,因此在循环外使用循环计数器的常见做法还要求将计数器的声明移到循环之外,如以下修改后的代码所示:In the standard, declaring a variable inside a loop restricts its scope to the loop only, so the common practice of using a loop counter outside of the loop requires that the declaration of the counter also be moved outside the loop, as in the following revised code:

int CPerfTextDataBase::NumStrings(LPCTSTR mszStrings) const  
  int n;  
  for (n = 0; mszStrings[0] != 0; n++)  
  mszStrings = _tcschr(mszStrings, 0) + 1;  


将 Spy++ 从原始的 Visual C++ 6.0 代码移植到最新的编译器需要花费大约 20 个小时的编码时间,历经一周的过程。Porting Spy++ from the original Visual C++ 6.0 code to the latest compiler took about 20 hours of coding time over the course of about a week. 我们跳过了该产品的 8 个版本(从 Visual Studio 6.0 到 Visual Studio 2015)直接升级。We upgraded directly through eight releases of the product from Visual Studio 6.0 to Visual Studio 2015. 现在,这是所有大小型项目升级的推荐方法。This is now the recommended approach for all upgrades on projects large and small.

请参阅See Also

移植和升级:示例和案例研究 Porting and Upgrading: Examples and Case Studies
上一个案例研究:COM SpyPrevious case study: COM Spy