C#和本机 C++ 代码的互用性--(较好的解释了_gc 和 c++/CLR 的关系问题)
http://www.vckbase.com/document/viewdoc/?id=1532
C++ At Work 专栏... 下载源代码:CAtWork0509.exe (276KB) 我有一个简单的 C++ 问题。我想让我的拷贝构造函数和赋值操作做同样的事情。你能告诉我最佳实现方法吗? Shadi Hani
乍一看,这似乎是一个答案简单的简单问题:写一个调用 operator= 的构造函数不就行了:
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 ctorCMainClass: copy-ctorCMainClass: 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
我有一个用 C#(用户界面)和经典的 C++(业务逻辑)写的应用程序。现在我需要从某个用 C++ 写的 DLL中调用一个函数(或方法),该函数在一个用 Visual C++ .NET 编写的 DLL 中。而这个 Visual C++ .NET DLL 又要调用另一个用 C# 写的 DLL。Visual C++ .NET DLL 相当于一个代理。这样做可行吗?我能用 LoadLibrary 调用 Visual C++ .NET DLL 输出的函数,可以得到返回值,但当我试图向 Visual C++ .NET DLL 中的函数传递参数时,我遇到如下错误:
Run-Time Error Check Failure #0—The value of ESP was not properly savedacross a function call. This is usually a result of calling a functiondeclared with one calling convention with a function pointer declaredwith a different calling convention. 我如何解决这个问题? Giuseppe Dattilo
我得到许多关于 .NET 框架和本机 C++ 之间的互操作问题,所以我不介意再次复习这个(well-covered)主题。有两条路可走:从 C++ 中调用框架;或者从框架调用 C++。我不打算在此涉及 COM 的互用性,我把它放在以后单独的一期专栏里讨论。 让我先从最简单的一种开始:从 C++ 调用框架。从 C++ 程序中调用框架最简单,最轻松的方法是使用托管扩展(Managed Extensions)。这是微软专用的 C++ 语言扩展,它被设计专门用来调用框架,只要包含两个头文件即可,然后象使用 C++ 类一样来使用它们。下面是一个非常简单的调用框架 Console 类的 C++ 程序: #using <mscorlib.dll>#using <System.dll> // impliedusing 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 objectpublic:Widget() { m_pObj = new CWidget; }~Widget() { delete m_pObj; }int Method(int n) { return m_pObj->Method(n); }// etc.}; 任何类都是这种模式:
你不必包装所有的成员函数,仅仅包装那些打算暴露给托管环境的函数即可。 // C# clientMPerson.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 stringString^ 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 期刊,可通过当地报摊获得,或者最好是 订阅 |