http://www.vckbase.com/document/viewdoc/?id=1532
C++ At Work 专栏... 下载源代码:CAtWork0509.exe (276KB)
Shadi Hani
![]() CFoo::CFoo(const CFoo& obj) { *this = obj; } 或者,写一个公用的拷贝方法,拷贝构造函数和 operator= 都调用这个方法也行。就像这样: CFoo::CFoo(const CFoo& obj) { CopyObj(obj); } CFoo& CFoo::operator=(const CFoo& rhs) { CopyObj(rhs); return *this; } 对于大多数类来说,这是行得通的,但还有些特殊情况需要考虑。如果你的类包含有数据成员是另一个类的实例会怎样呢?为了弄清楚这个问题,我写了一个测试程序如 Figure 1 所示。它有一个主类 CMainClass,它包含另一个类 CMember 的实例。两个类都有拷贝构造函数和赋值操作,用 CMainClass 的拷贝构造函数调用 operator=,如下面的代码段所示。代码中使用 printf 语句是为了显示何时调用了哪个方法。为了运行构造函数,cctest 程序首先用缺省构造函数创建 CMainClass 实例,然后用拷贝构造函数创建另一个实例: CMainClass obj1; CMainClass obj2(obj1); 如果你编译并运行 cctest,当构造 obj2 时,你会看到下面的 printf 信息: CMember: default ctor CMainClass: copy-ctor CMainClass: operator= CMember: operator= 成员对象 m_obj 被初始化了两次!第一次是缺省构造,第二次是赋值时再次被初始化。嘿,这是怎么回事? CFoo::CFoo() { m_obj = DEFAULT; } 与下面代码相对: CFoo::CFoo() : m_obj(DEFAULT) { } 使用赋值方式,m_obj 被初始化两次,而用初始化例程语法,m_obj 只被初始化一次。所以,要如何避免拷贝构造期间额外的初始化呢?当它与你的代码重用初衷相抵触时, 最好的解决俄u方法就是分开实现拷贝构造和赋值操作,即便它们做同样的事情。从拷贝构造中调用 operator= 肯定能行得通,但不是最有效率的实现。我对初始化的建议是: CFoo::CFoo(const CFoo& rhs) : m_obj(rhs.m_obj) {} 现在,主拷贝构造用初始化例程调用成员对象的拷贝构造,并且 m_obj 只被其拷贝构造初始化一次。通常情况下,拷贝构造应该调用其成员的拷贝构造。赋值也是如此。并且,它也同样适用于基类:派生类的拷贝构造和赋值操作应该调用对应的基类方法。当然,有时因为一些具体情况,可能你的做法会有所不同——这里我所描述的是通用规则,只有在你遇到强制性原因时才会破坏这个规则。如果你要在基本对象被初始化之后完成一些公共任务,可以将它们放到一个公共的初始化方法中,并在构造函数和 operator= 中调用。
Sunil Peddi
![]() Run-Time Error Check Failure #0—The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention. 我如何解决这个问题?
Giuseppe Dattilo
![]() 让我先从最简单的一种开始:从 C++ 调用框架。从 C++ 程序中调用框架最简单,最轻松的方法是使用托管扩展(Managed Extensions)。这是微软专用的 C++ 语言扩展,它被设计专门用来调用框架,只要包含两个头文件即可,然后象使用 C++ 类一样来使用它们。下面是一个非常简单的调用框架 Console 类的 C++ 程序: #using <mscorlib.dll> #using <System.dll> // implied using namespace System; void main() { Console::WriteLine("Hello, world"); } 为了使用托管扩展,你只需引入 <mscorlib.dll> 和你打算使用的框架类所附着的程序集。不要忘了用 /clr 编译。 cl /clr hello.cpp 你的 C++ 代码可以或多或少地使用托管类,就像普通的 C++ 类一样。例如,你可以用操作符 new 创建框架对象,并用 C++ 指针语法存取它们,象下面这样: DateTime d = DateTime::Now; String* s = String::Format("The date is {0}/n", d.ToString()); Console::WriteLine(s); Console::WriteLine(s->Length); 这里,String s 被声明为 String 指针,因为 String::Format 返回一个新的 String 对象。 public class Win32 { [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int MessageBox(IntPtr hWnd, String text, String caption, int type); } 这段代码告诉编译器 MessageBox 是 user32.dll 中的一个函数,参数是 IntPtr (HWND),两个 String 和一个 int。这样你便可以在 C# 程序中调用: Win32.MessageBox(0, "Hello World", "Platform Invoke Sample", 0); 当然,使用 MessageBox 你不必通过 P/Invoke,因为 .NET 框架已经具备一个 MessageBox 类,但是大量的 API 函数框架是不直接支持的,调用这些函数时需要 P/Invoke。并且,你还可以用 P/Invoke 调用自己 DLL中输出的 C 函数。尽管在例子中我用的是 C#,但 P/Invoke 支持任何基于 .NET 的语言,如:Visual Basic .NET 或 JScript.NET。函数名称都相同,只是语法有差别。 public __gc class Widget { private: CWidget* m_pObj; // ptr to native object public: Widget() { m_pObj = new CWidget; } ~Widget() { delete m_pObj; } int Method(int n) { return m_pObj->Method(n); } // etc. }; 任何类都是这种模式:
你不必包装所有的成员函数,仅仅包装那些打算暴露给托管环境的函数即可。 // C# client MPerson.Person p = new MPerson.Person("Fred"); String name = p.Name; p.Name = "Freddie"; 用不用属性纯粹是编程风格问题,我完全可以照搬本地 C++ 类的做法也输出两个方法:GetName 和 SetName。但属性给人的感觉更像 .NET。包装器类就是一个程序集,只不过与本地 DLL 链接。这是托管扩展一个很酷的特性之一:你可以直接与本地 C/C++ 代码链接。如果你下载并编译我的 CPerson 例子源代码,你会发现 makefile 产生两个单独的 DLLs:person.dll 和 mperson.dll,前者实现常规的本地 DLL,后者是包装前者的托管程序集。还有两个测试程序:testcpp.exe,此为调用 person.dll 的本地 C++ 程序;testcs.exe,此为用 C# 编写的程序,它调用托管包装器 mperson.dll(它又调用本地 person.dll)。 // PtrToStringChars, from vcclr.h // get an interior gc pointer to the first character contained in a // System::String object // inline const System::Char * PtrToStringChars(const System::String *s) { const System::Byte *bp = reinterpret_cast<const System::Byte *>(s); if( bp != 0 ) { unsigned offset = System::Runtime::CompilerServices:: RuntimeHelpers::OffsetToStringData; bp += offset; } return reinterpret_cast<const System::Char*>(bp); }我在 MPerson 中使用 PtrToStringChars 来设置 Name,详细代码参见 Figure 3。 指针固定并不是你将遇到的仅有的互用性问题。如果你要处理数组,引用,结构和回调,或者存取某个对象中的嵌入对象,还会碰到其它的问题。这是一些将来要讨论的更高级的技术,如:StructLayout,框入/框出(boxing),__value 类型等等。你还需要专门的代码来处理异常(本地或托管)以及回调/委托。但不要让这些户用性细节遮住了大方向。首先确定你的调用方式(是从托管调用本地还是从本地调用托管),如果你是从托管调用本地,是使用 P/Invoke 还是包装器。 Visual Studio 2005 中(有些人已经开始用beta版了),托管扩展已更名并升级到 C++/CLI。你可以把 C++/CLI 看成是 Managed Extensions Version 2,或者是 Managed Extensions 演变成的一个什么。这个改变几乎都是语法上的,虽然也有一些重要的语义变化。总体上讲,C++/CLI 是设计用来突出而不是模糊托管和本地对象间的差异。使用托管对象的指针语法是明智的想法,但最终也许做的有些过于明智,因为它淡化了托管和本地对象之间的重要差异。C++/CLI 引入了一个处理托管对象的关键概念,CLI 处理托管对象时使用 ^(读作 hat)来代替 C 语言的指针语法: // handle to managed string String^ s = gcnew String; 正像你已经明确注意到的,还有一个 gcnew 操作符用以来表示你是在托管堆中分配对象,而不是在本地分配。这样做有一个额外的好处是 gcnew 不会与 C++ 的 new 发生冲突,它能被重载或者甚至被重定义成一个宏。C++/CLI 有许多其它很棒的特性,专门用来使互用性尽可能简单明了。 |
![]() Paul DiLascia 是一名自由作家,顾问和 Web/UI 设计者。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。通过 http://www.dilascia.com 可以获得更多了解。 |
本文出自 MSDN Magazine 的 September 2005 期刊,可通过当地报摊获得,或者最好是 订阅 |