在Visual Studio 2005中使用 C++编程
【IT168技术文档】
越来越多的人开始使用Visual Studio 2005,因此现在是一个好的时机来叙述我使用这个新的编译器的经验。是什么让我有这个强烈的愿望了?嘿嘿,我就是这么一个迫不及待的人!我想现在还为时不晚!
对于Visual Studio 2005,你首先发现的一个事情就是它有一个版本管理器,这个管理器通过检查你的项目来决定运行哪个版本的编译器。你可以在Visual Studio .NET 2003 这个版本上升级到Visual Studio 2005;Visual Studio .NET 2003和Visual Studio 2005能同时运行在您的电脑上,至于说用哪一个更好,取决于你是否想让你的项目在Visual Studio 2005下运行。当您打开一个Visual Studio .NET 2003下的项目时并且想修改时,Visual Studio 2005 推荐你保存一个副本,并随后生成一个XML 报告来记录所修改的内容。
一些细微的代码变化
为了揭开Visual Studio 2005 的面纱,我把过去栏目中的六个项目进行编译。这些项目都需要进行较小的修改,这些小变化都是为了让Visual Studio 2005 成为一个流行的C++编译器。许多新的规则在C++标准中是认可的,但是在Visual Studio 2005中是不允许的。Visual Studio 2005里两个最常见的语言变化是循环语句的作用范围和缺省定义int 类型。循环里面的定义的局部变量在循环外面是没有作用的。以前,这样写是可以的:
for (int i=0; i<max; i++) {
// do something}
if (i>0) {
// do something else}
在这个程序片断中,变量i不仅在 for 语句里面起作用,而且在 if 语句里也起作用。严格的说,C++是不允许这么用的,所以现在你不需要重新编写这段代码,如:
int i; // move outside for loop
for (i=0; i<max; i++) {
// do something }
if (i>0) {
// do something else }
无论是静态局部变量,还是静态全局变量,如果没有声明的话,会默认为int 类型。以前,你可以这么写:
const BUFLEN=255;
此时编译器会默认变量BUFLEN为int 类型。现在,这种默认为int 的方式被禁止了。你必须声明你变量的类型,如:
const int BUFLEN=255;
这种方式适用于静态变量,全局变量,数据集,和函数返回的类型。如果你没有声明是int ,你将获得错误信息:“error C4430: missing type specifier - int assumed. Note: C++ does not support default-int”
另外一些变化是关于C/ C++的安全库。这些安全库为旧的C语言运行库函数提供更多安全版本,比如说你知道和喜欢的,如:strcpy, fopen and others 。我准备在以后的栏目中写更多的关于C++安全的文章。如果你等不及了,你可以阅读马蒂.洛菲尔(Martyn Lovell)的文章《Safe! Repel Attacks on Your Code with the Visual Studio 2005 Safe C and C++ Libraries》,这篇文章在05年的第5期。
网址是:(msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC).
谈了很多的C++,那么Visual C++的类库(MFC)又发生了怎样的变化了?正如我前面所讲,Visual Studio 2005 没有对Visual C++的类库进行大的改变,这是一件好事。这意味着类库没有变化。然而,我注意到CWnd::OnNcHitTest的返回类型值由UINT 变成了LRESULT。这使用起来可能又存在较小的别扭,但是它不会破坏你对Visual C++的类库(MFC)的使用。
Visual Studio .NET 2003到Visual Studio 2005的转变
C++和Visual C++的类库(MFC )是非常好的编译系统,因此我并不希望在这两个领域有任何新的变化。Visual Studio .NET 2003和Visual Studio 2005之间最大的变化是在受控扩充件(managed code)领域。正如你所读到的Visual Studio 2005引入了新的C++/CLI语法---你可以把它认为是受控扩充件的第二个版本(Managed Extensions V2)。如果你不想为应付这些费脑筋的话,你可以引入/clr:oldSyntax 来使用以前的受控扩充件(Managed Extensions)。如果你是把你的项目从Visual Studio .NET 2003升级到Visual Studio 2005 ,这个引入是默认为自动添加的。
为了测试受控代码在这个新的编译器的运行情况,我将使用ManWrap库,这个库是我在写2005年第四期的文章时所编写的,这篇文章叫《Wrappers: Use Our ManWrap Library to Get the Best of .NET in Native C++ Code》。ManWrap库包含一个wrapper DLL组件和三个测试程序,分别是:RegexTest, RegexForm, and WordMess(见图一)。ManWrap是一个非常好的库,它不仅小,而且在一些方面很灵活,比如说可以将受控代码和编写代码封装在一个DLL一个组件里面。RegexWrap.dll是一个本地的DLL组件,他封装了通用语言运行层(CLR)的Regex类。

图一 正在测试受控代码
所以我保持以前的语法,并且让这个新的编译器进行编译…..片刻时间后,我的屏幕就会出现错误报告:"C3395 ... __declspec(dllexport) cannot be applied to a function with the __clrcall calling convention."
ManWrap是这样的一个库,它能把受控代码封装成一个纯粹的本地的C++包。也就是说,你可以在公共语言运行层上运行本地的C++代码,而可以不用引入/clr。举个例子,比如说你已经在Visual C++? 6.0环境下写了一个应用程序,而你现在想让这个应用程序调用公共语言运行函数(CLR)。如果在你没有引入文件/clr时,你是不能直接调用受控代码的,所以唯一的方法是他们封装成一个dll组件,然后再来调用受控代码。ManWrap提供了一个普通机制进行封装。
ManWrap的核心在于用到了特殊的预处理程序,预定义_MANAGED符号产生不同的代码。每一个封装的类拥有一个数据成员,一个托管对象的句柄。如:
#if def _MANAGED
# define GCHANDLE(T) gcroot<T*>
#else
# define GCHANDLE(T) intptr_t
#endif
Wrapper类用GCHANDLE来声明这些对象的句柄,如:
// wrapper for managed Object
class CMObject {
GCHANDLE(Object) m_handle;
};
CMObject头文件可以编译成两种方式。当你构造DLL封装器时,你可以引入/clr进行编译,这时_MANAGED被定义,并且编译器把m_handle当作gcroot<Object*>处理。当你编译你的应用程序是通过调用wrapper时,_MANAGED就没有被定义,并且编译器把m_handle当作intptr_t处理。之所以会这样是因为确保gcroot<Object*>和intptr_t大小相同。只有DLL封装器知道句柄是什么。对外部而言,m_handle是一个灵巧的cookie机制—类似于HWND, HINSTANCE和其他句柄,你所知道的仅仅是这个构造函数和赋值函数是显式的,而不是隐式的—因此他们可以访问wrapper。(你不能拷贝intptr_t句柄,你需要通过gcroot命令。)
除了支持句柄,ManWrap封装器定义常用的方法成为一个结构和对象副本。每一个封装器的类定义了一个ctor和一个操作符->,因此编译器可以创建和访问相应本地的封装对象。举个例子,一个构造函数可以从一个已经创建的Regex类生成CMRegex类。Wrapper在内部运用了ctor和->操作符。图二显示了ManWrap.h中的一个程序片断,这个程序片断包含_MANAGED 模块。需要注意的是:当构建成DLL时,整个类变会成WREXPORT,并且扩展成__declspec(dllexport)。这是引起C3395号错误的原因。你不能用__declspec(dllexport)输出托管方法(这个方法是带托管参数的),因为本地函数和托管函数是有不同的调用规则的。好,这就很有意思了—为什么我试图从本地DLL输出一个托管函数?事实上我并没有真的输出他们。他们都被隐式的定义了。对于以前的接口来说,托管方法并不是必需的,而且也是不可见的;但是编译器并不知道这些。很明显,Visual Studio 2005并没有旧的编译器聪明,旧的编译器会让我把所有的类进行封装。或者说它太聪明了,因为从某种意义上讲,从本地代码输出托管方法是没有多大意义的。不管怎么说,如果你的类中有托管方法,Visual Studio 2005是不会让你输出一个类的。
那么接下来我们该怎么办了?我所希望的是告诉编译器,“输出整个类,除了三个方法。”即,我希望找到一个方法来阻止__declspec(dllexport)对具体方法的限制。哎,这里没有别的选择了。据我所考虑的,这里只有两种解决方案:删除类中关于WREXPORT的声明,而把WREXPORT的声明添加到每一个方法之中;或者把这个讨厌的方法都删除掉。计划A更简单一些,这时我所选择的方法。删除类中WREXPORT的声明,再把它加到方法的声明中去是繁琐的,而且更容易倾向出现错误,因为当你添加一个新的方法是很容易忘记WREXPORT声明的。如果你确实要用这种方法来做,那么编译器会提醒你加上WREXPORT声明的。
如果你真的很想,很想将所有的类都封装成dll组件,那么计划B是删除那些讨厌的方法:即构造函数的副本和->操作符。接下来你必须这么写:
(static_cast<MClass*>((Object*)m_handle))->ManagedMethod();
多么神奇,你还可以引入一个宏来保存这些输入 :
THISOBJ(MClass*)->ManagedMethod();
但是还是令人郁闷的是你仍然要解决构造函数的问题。如果你现在已经迷惑了(我想许多读者通常会这样),不要担心,我选择的计划A,这个较简单的方法。图三显示了修订后的所有方法的代码。在我进行这些修改后,ManWrap就能很好的编译了。
如何运行ManWrap
了解了Visual Studio 2005并不代表你能运行你能成功的运行你的代码。通常会发生捶墙,和砸电脑这些郁闷的情况。in the form of an ASSERT bomb way down in dbgheap.c:
ASSERTE(_CrtIsValidHeapPointer(pUserData));
栈跟踪几乎没有多大的作用,大量的系统dll组件没有运行调试记录。下面这种情况是最坏的漏洞和最难跟踪的:你的程序死机了,然而你找不到一丁点的线索。几乎是没有线索。当我编写代码试图去访问一个叫g_Allocator的静态ATL变量时,仔细检查栈跟踪就会显示50个页面。
g_Allocator是一个静态的全局变量。初始化静态的C++变量是一件棘手的事情,特别是在DLLs组件中。在调用DllMain之前,编译器必须生成代码去调用CRT初始化函数来初始化你的静态变量。在自己熟悉的环境,做起事情就会如鱼得水,但是如果你的DLL组件去调用托管类,事情就会出现问题,你也许会遇到加载器死锁问题:操作系统试图加载你的dll组件时,你的应用程序就会停下来,这就是被我们所知的下载器被锁。总的来说,当你的DLL正在加载时,你不能加载所需要的DLL组件。举个常见的例子:当你调用::MessageBox诊断DllMain或者静态对象构造函数时,这个问题就会发生。
为了避免下载器死锁,Visual Studio .NET 2003委托受控DLLs成为/NOENTRY DLLs(这种DLLs没有DllMain的入口点)。这就能保证下载器不会出现死锁的问题,但这时又会出现一个新的问题:现在静态变量不能初始化,因为Dll没有进行_DllMainCRTStartup,而由神奇的CRT函数来初始化他们。我在2005年二月份的栏目对此进行了详细地描述(msdn.microsoft.com/msdnmag/issues/05/02/CATWork)。没有静态对象是一个非常严重的问题,因为ATL 和MFC都要用到他们。没有DllMain,你就不能将受控代码和编写的代码混合封装成一个DLL组件,因为它又要用到ATL 和MFC.显然既然这是无法接受的,我们友好的Redmondtonians提供了一个特殊的文件<_vcclrit.h > ,这个文件包含了函数__crt_dll_initialize 和 __crt_dll_terminate(),通过调用这两个函数,你就可以初始化你的静态变量了。
这听起来像一个小题大做的方法,它的确是这样的。 当你得知Visual Studio 2005解决了加载器死锁的问题后,你将会很高兴。你就不需要_vcclrit.h文件和/NOENTRY了,因为你现在可以正常的编译混合代码的DLLs组件了。想详细了解,请看这篇文章《Initialization of Mixed Assemblies》,网址是:
msdn2.microsoft.com/ms173266.aspx.
引入新的操作符^
当你最终解决了这些漏洞的时候,是不是感觉非常良好。在非常成功的编译和运行了三个测试的程序(RegexTest, RegexForm, and WordMess)后,我十分高兴。为什么不去接受这个新的语法了?如果你的知识还是一直停留在过去的话,那么你现在就应该知道C++/CLI的核心在于一种新的类型的引入,这个新的类型叫做追踪句柄,它用符号^标识。因此我放弃了使用/clr:旧的语法(见图四),并且把ManWrap.h文件作了一个小的改变,即把* 替换成^。
#ifdef _MANAGED
# define GCHANDLE(T) gcroot<T^>
#else
# define GCHANDLE(T) intptr_t
#endif

图四:使用新的语法
在做了这个小的变化后,我便把ManWrap载入到编译器,并且把出现的编译错误都一个一个的记录。大多数就是把把* 替换成^。 当然,还有其他需要改变的。我下面就列出我载入ManWrap后的一些错误。对于非常熟悉C++/CLI的,她们便是很旧的东西了,如果你是专家的话,你就可以跳过下面。
•托管类必须用ref 或value进行声明,而不用__gc 或__value。总之,所有的__managed 关键字将由敏感的关键字代替。
•默认的索引器将被称为"default"而不是"Item"。所以
x = m->Item[name];
就应该写成
x = m->default[i];
当有多个索引器时,也是这么工作的,如Regex库里的MatchCollection。
MatchCollection* mc;
mc->default[0]; // int
mc->default["alpha"]; // string
•托管对象需要用关键字gcnew进行分配定义。在任何地方如果要给一个托管类分配空间,要用gcnew而不是new。
•有些变得明显了。看下面的程序片断:
// native entry
void Foo(LPCTSTR lpsz)
{
// managed ctor takes a String
Mumble *m = new Mumble(lpsz);
}
以前的受控扩充件(Managed Extensions)会自动把lpsz初始化为字符串型。但是在Visual Studio 2005下,你必须给lpsz分配一个字符串的空间,就像这样:
void Foo(LPCTSTR lpsz)
{
Mumble ^m = gcnew Mumble(gcnew String(lpsz));
}
这需要敲入更多的代码,但是这同时也使得代码很容易理解。我喜欢用gcnew,因为它促使让你知道你什么时候从堆里分配了空间。这种变化使得代码变得清晰和简单-看起来简单。C++是一个相对易学的语言(我认为这真是一件幸福的事情),因此我觉得让代码保持清晰,不要看起来很晦涩将会更好一些。既然RegexWrap在许多的方需要创建字符串类型,我就把这些创建字符串的代码用一个宏来保存起来。
#define S(s) (gcnew String(s))
对于受控字符串来说,这看起来像字符串的修正器,如字符串S是"Hello, world."那么我可以这么写:
Mumble ^m = gcnew Mumble(S(lpsz));
C++/CLI在数组方面也有一个新的语法变化。不能这么写:
ManagedType* myarray[];
而应该这么写:
array <ManagedType^>^ myarray;
开始的时候我没有写上第二个^。如果你没有的话,将会报这样的错误:"error C3149: cannot use this type here without a top-level '^'",这个错误看起来还是不太清楚,但是一旦你知道了规则,你就会觉得很有道理。既然编译器知道关键字"array"是来定义数组的,那么我不太确定为什么top-level^是必需的。但是这一点我是确信的,这么做肯定是由它的理由的,并且看起来还是很有意义的。许多地方都需要带上^。你需要用^来创建数组。举个例子:
// managed array of ints
array<int>^ foo;
•不用 Count,而用Length来获得数组的长度。
•如果你需要判断是否是空的指针,用nullptr,而不是NULL。
•模版能和托管类很好的组合在一起用。这是用C++/CLI的最重要的原因。因为每个托管类和本地的类都有他们各自的语法(并不是共用过多的*),这是模版产生器就能让这些类区分。
这里有更多的语法改变我还没有提到。毫无疑问你将自己会发现他们。要想察看全部,请看Stan Lippman的这篇文章《Hello C++/CLI》,这篇文章在msdn 杂志Visual Studio 2005专栏里面(网址是:msdn.microsoft.com/msdnmag/issues/06/00/PureC)我还推荐Stan的这篇文章《A Baker's Dozen: Thirteen Things You Should Know Before Porting Your Visual C++ .NET Programs to Visual Studio 2005》,网址是:(msdn.microsoft.com/library/en-us/dnvs05/html/BakerDozen.asp)。
别让新的语法吓着你了。一旦我把* 替换为^和把/clr:oldSyntax替换为/clr,剩下的工作仅仅是固化每个编译器的报错信息。一旦编译器让ManWrap成功的部署在里面,ManWrap就会运行的很好。Redmondtonians应该感到自豪,因为改善后的编译器能够保证它的正确性,就这一点来说,已经是一个成功了。* 到^的转换让我非常高兴,我正考虑把GCHANDLE重命名为MANHANDLE。
两个小的不爽
总之,从Visual Studio .NET 2003转变到Visual Studio 2005并不困难。我不想对IDE(集成编译环境)作过多的评论。我是一个纯粹的在文本编辑器下进行编程的高手。也就是说,在Visual Studio 2005下,我有两点不习惯的地方。第一个,它一直向“我的文档”里填充各种我不需要用到的文件。我能通过发现一些注册的关键字来重新存放我所有的文件,但有几个临时文件是看不到的,所以我也就忘了。如果按照我喜欢的方式来,我需要花很大的精力去整理我的文件,当程序需要移动时,更让我苦恼。
我另外一个要抱怨的地方是Visual Studio 2005不再支持语音方案。我并不是一个喜欢让应用程序发出吵杂声的爱好者,但声音还是非常有用的。在Visual Studio .NET 2003环境下,当我开始编译时,可以不离开,打开另外一个窗口。当编译完成时,我可通过发出的愉快悦耳声或是暴躁的巨响声来判断程序是否编译成功。在Visual Studio 2005环境下,我不得不读屏幕的信息,多么烦人啊!从这里可以得到一个教训了:别轻易删除你的特色。
除了这些小的改变和那个/NOENTRY带来的混乱,使用Visual Studio 2005 是一个很好的选择。如果你还没有做出这个改变,就去尝试吧——特别是那些写混编代码的人员。这些新的语法更好,只需要进行小的修改。但是一旦你熟悉了符号^,你就会很酷的。
你可以从MSDN杂志的网址上下载最新的ManWrap。下载的内容包含三个版本:Visual Studio .NET 2003下的最初的版本和Visual Studio 2005下用旧的语法和新的C++/CLI语法的版本。祝大家快乐编程!