C++ Programmer's Cookbook

{C++ 基础} {C++ 高级} {C#界面,C++核心算法} {设计模式} {C#基础}

C/C++ 应用程序路线图(msdn)

发布日期: 11/26/2004 | 更新日期: 11/26/2004

Kate Gregory
Gregory Consulting

适用于:
Microsoft Visual C++
Microsoft.NET 公共语言运行库 (CLR)
C++/CLI 扩展
Microsoft .NET Framework
WinFX

摘要: 本文针对(至少到目前为止)在新的和现有的应用程序中还没有以Microsoft .NET Framework 为目标开发平台的 C++ 程序员。 文中为如何转向 .NET Framework 而且避免抛弃现有代码中的投资提供了一些指导,并阐述了为什么应该考虑转向 .NET Framework ,这不仅适用于新的开发工作,对现有应用程序亦然。

*
本页内容
为什么要使用公共语言运行库? 为什么要使用公共语言运行库?
相关语言回顾 相关语言回顾
理解 .NET 上的 C++ 理解 .NET 上的 C++
托管和非托管类型 托管和非托管类型
依然是 C++ 依然是 C++
混合托管和本机开发 混合托管和本机开发
.NET 路线图 .NET 路线图
小结 小结

为什么要使用公共语言运行库?

Microsoft 总是支持程序员从他们自己的代码中访问操作系统功能。 在 Windows 的早期,我们从 C 语言程序中使用 Windows API ,进行函数调用(比如 GetMessage(), TranslateMessage(), 等等)以达到目的。 随着时间变迁, Windows 功能开始使用 COM 组件比如 Shell 来公开。 为了充分利用 Windows 完整的功能,程序员学会了 COM 的概念,创建了许多 COM 应用程序。 这种演变仍在继续,如今,为了充分发挥 WinFX 的功能,你的应用程序应该使用公共语言运行库,也称 CLR 了。 WinFX 是一种托管 API ,设计成从为 CLR 编写的托管应用程序中调用,通过 WinFX ,你的应用程序能够利用 Avalon 和 Indigo 这样的新技术的功能。 WinFX 是基于 .NET 对象模型的。

访问操作系统功能当然不是 CLR 的唯一优势: .NET Framework 提供了比 COM 和 DCOM 更佳而且更简单的组件模型。 .NET Framework 中包含的托管代码库所能提供的功能,远远超出了操作系统本身,这使我们能够将精力放在应用程序中专门解决特定问题的部分,而用不着再去处理许多人已经解决过的问题。 同样,可以构建一个基于组件的解决方案,而不需涉及以前与 COM 和 DCOM 部署相关的那些困难。

相关语言回顾

这是否意味着你应该重新编写所有应用程序呢? 当然不是。 如果原来的应用程序是用 C++ 编写的,现在不对代码做任何改变就可以针对 CLR 进行编译。 重新编写会带来巨大风险,因为在将程序移植到另一种语言比如 C# 时,可能在能够工作的代码中引入错误。 重新编写和转换代码所能期望的最佳值,也不过是实现与以前同样的功能。 如果想以 CLR 为目标平台根本不需要这样做。 相反,你的时间和精力可以花在使用新的系统功能和扩展应用程序的功能上。

自 .NET Framework, CLR 和 C# 发布以来的几年中,许多开发人员都对 Microsoft 的 C++ 计划感到惊奇。 有些人推测 C# 将取代 C++ ,事实上当然并非如此。 C# 是一种比 C++ 更容易学习的语言,它提供了访问 CLR 功能的途径。 对于已经了解 C++ 的人来说,要访问 CLR 的功能,无需学习其他语言, C++ 具备 C# 中没有的功能,因此转向 C# 实际上将会丧失一些能力。

Microsoft Visual C++ 的每个版本与标准的兼容性都比前一版更好,当前版本 Visual C++ .NET 2003 ,大约与 ISO C++ 标准达到了 98% 兼容。 这一版本中访问 CLR 功能的关键字都以双下划线开始,因此不会干扰与标准的兼容性。 虽然这种方式可行,但是很笨拙,而且不够直观。 Visual C++ 2005 中将包含一个新的 C++ 到 .NET 的绑定,该绑定正在以 C++/CLI 的名称进行标准化。 这一修订包括当前 C++ 标准中没有的关键字,但是同样不会干扰符合标准的 C++ 程序,因为它们遵守 ISO C++ 的标准扩展机制。 C++/CLI 扩展的国际标准正在由 ECMA 制定,最终将提交给 ISO 。 与今天的 C# 一样, C++/CLI 将被标准化,因此 Microsoft 将不会是 C++/CLI 编译器的唯一来源。 Visual C++ 2005 现在已经处于 beta 版,因此我们可以立即探讨这些新的扩展。 在这篇白皮书中,我们将在代码示例中使用新的 C++/CLI 语法。 ( CLI 代表的是公共语言基础结构,是 .NET Framework 的标准化部分,包括 .NET 公共语言运行库。)

理解 .NET 上的 C++

当你编写在 CLR 上运行的代码时,你所编写的就是托管代码。 标准 C++ 代码,也就是能够在任何符合标准的 C++ 编译器上编译的代码,可以编译成非托管(本机)代码或者编译成 MSIL : 只需使用一个编译器开关即可。 指定 /clr 选项,编译器就会生成 MSIL ,一个在 CLR 上运行的程序集。 使你的代码成为托管的就是使用 编译器选项。 不需要使用任何特殊的关键字或者太多改变代码(如果需要改变的话),就能够通过 /clr 选项干净利索地进行编译。

编写了托管代码之后,就可以(如果你愿意)使用 CLR 功能了,比如基类库: 这是能够实现 XML 操作、加密解密、数据访问等等功能的强大的类库。 非托管代码,即没有使用 /clr 选项编译的代码,就无法声明托管类的实例,并按托管代码的方式直接调用它们的方法。 可以通过 .NET Interop 从非托管代码中访问托管代码,该技术能够使 .NET 对象看上去像是一个 COM 组件。 这种方式与将代码编译成托管的,并直接调用托管代码相比,肯定要慢。

无论是否使用基类库(和其他托管库),你仍然可以用 C++ 编写程序,仍然拥有 C++ 赋予你的所有功能和灵活性。 可以使用模板,编写操作符重载,创建内联函数,等等。 编译为 MSIL 并不会阻止你使用任何 C++ 功能。 比如说,多重继承被排除在外不是因为代码编译为 MSIL 了,而只是因为你编写的是托管代码而已。

托管和非托管类型

一个普通的 C++ 类,就是编程语言入门课程中教授的那种,将定义一个非托管类型:

class A
{
private:
   int x;
public:
   A(int xx): x(xx) {}
};

无论是否带 /clr 选项编译代码(托管还是非托管代码),这都是一个非托管类型,也俗称为非托管类或者非托管数据。 这个类的实例可以分配在堆栈中,这也是编程语言入门课程中教授的内容:

A something(3);

它们还可以在本机或者非托管堆中创建:

A* otherthing = new A(4);

程序员然后还必须记住清除非托管堆中的对象,使用 delete 操作符:

delete otherthing;

无论是哪一种方式,都不会涉及垃圾回收器,即使代码编译为 MSIL ,而且应用程序运行在运行库上。

但是你也可能需要编写托管类型(也称托管类或者托管数据)。 这些类型可以从其他程序集中调用,其他运行在运行库上的托管代码,无论其他程序集是用什么语言编写的。 用 C# 、用 Visual Basic .NET 或者用你没有听说过但是碰巧能编译成 MSIL 的语言编写的代码,都可以使用你的托管类型。 这种交互是由运行库托管的,而且在大多数情况下,你的代码和其他程序集所创建的那些类型实例的生存期也是由运行库托管的。

这些托管类型是第一级的 .NET 对象。 用 C++/CLI 创建托管类型,可以采取一种自然的语法,与传统 C++ 区别不大:

ref class R
{
private:
   int x;
public:
   R(int xx): x(xx) {}
};

这个类定义使用了一个空格关键字——仅包含一个空格的关键字。 从技术上说, C++/CLI 中并没有 ref 关键字,而只有 ref class 关键字。 这意味着你可以使用名为 ref 的变量而不会引起冲突。 而且这个类 R 可以被用 C# 或者 Visual Basic .NET 或者其他支持 .NET 的语言编写的代码所使用。 可以用 C++/CLI 编写一个类库,使用你作为一位 C++ 程序员多年形成的技术和技巧,然后将这个库用在根本没有使用 C++ 编写的应用程序中。 它们只需要是运行在 CLR 上的应用程序即可。

依然是 C++

转向 CLR 并不意味着要转向 C# 。 许多 C++ 开发人员在 C# 发布时转向了 C# 。 其原因多种多样: C# 的向导和设计器支持更好,而且管理层经常会支持新语言,仅仅是因为它新,而一些开发人员并没有认识到托管应用程序也能够用 C++ 创建。 但是许多开发人员都拒绝这种转向,甚至某种程度上完全拒绝转向 CLR 。 拒绝的理由中常见的主题是: “我喜欢 C++ 。” C++ 具有其他语言不具备的功能,比如真正的确定性析构和模板。 选择用 C++ 编写托管代码时,你可以获得所有 C++ 的功能和所有 CLR 功能: 可谓鱼与熊掌兼得。

确定性析构

在其他支持 .NET 的语言比如 Visual Basic .NET, C# , 或者 Visual C++ .NET 2003 C++ 托管扩展中,实例的位置取决于要创建的类型。 如果创建的是一个托管类型的实例,它将创建在托管堆中:

Dim o as new R(3)  ' VB.NET
R o = new R(3); // C#
R* o = new R(3); // managed extensions for C++

实例(在所有这些例子中都是 o )使用的内存是由运行库托管的,可以被垃圾回收器清除或者重新组织。

相反,如果你创建的是一个值类型的实例,在所有三种语言中,实例都是在堆栈中创建的:

Dim i As Int32 = 7  ' VB.NET
int i = 7; // C#
int i = 7; // managed extensions for C++

只有在 C++/CLI 中你能够获得自由,自行决定在哪里创建对象,是否想让实例的内存由运行库托管。 你可以在托管堆中创建一个引用类的实例(即前面用 ref class 关键字定义的那个实例),如下:

R^ o = gcnew R(3);  // C++/CLI

如果愿意,可以在堆栈中创建实例:

R os(3);

o 和 os 之间的区别在它们的生存期上,或者说得更加具体一些,是对它们生存期的控制力。 如果编写的是托管代码,你可能不会介意放弃对内存的控制权,反而愿意信任运行库和垃圾回收器为你管理内存。 但是开发人员仍然需要操心与内存无关的清除工作: 比如关闭文件或者连接。 垃圾回收本身不足以处理你在应用程序中使用的所有资源。 在 C++ 中,这种与内存无关的清除通常是在析构函数中进行的。

托管堆中的对象是通过句柄 o 访问的,当控制达到带有 gcnew 的那一行时对象就开始存在。 未来某个时候, o 将超出控制范围。 可能控制已经超过用 return 或者 exit 语句声明它的代码块,可能代码块是 if, for, 或者 while 语句的谓词而且控制已经以通常的方式离开,或者出现了异常。 无论原因如何, o 都将超出范围。 这时候,事情变得有些复杂。 如果任何代码都有句柄的副本,副本将到处都是,然后只要范围中有句柄,对象就将继续在托管堆中存在。 如果句柄对象应该回收了,但是回收的准确时间并不知道,因此何时运行析构函数是未知的。 这取决于应用程序施加的内存压力数量等等因素。

对于堆栈中的对象 os ,情况就大大不同了。 在超出范围后(按照使 o 超出范围的同样情况),对象的一切就结束了。 它的析构函数,如果有的话,将在 os 离开范围后立即运行。 你可以准确地知道与内存无关的清除何时发生,而且能够尽快发生。 这就是所谓确定性析构。

顺便提及, os 实例(我们认为它在堆栈中)实际上使用的是托管堆上的内存(依然是由垃圾回收器托管的)。 析构函数并不回收该实例使用的内存;它关心的是与内存无关的清除。 引用类型只能模拟为在堆栈中。 如果你已经习惯不管内存管理,并信任垃圾回收器处理一切,这种模拟是非常理想的。

C# 中的 using 构造提供了类似的能力,但是 C++ 中的自动范围更简单: 编写的代码更少,而且不会忘记。 在 C++ 中的析构函数和可以用其他语言编写的托管类型中的 Dispose() 方法之间有很好的对应关系: 实际上它们是相同的。 当 C# 代码使用你的托管类型并调用 Dispose() 时,实际上运行的就是析构函数。 如果 C++/CLI 代码使用一个不是用 C++ 编写的托管类型,并在堆栈中创建它,当实例超出范围时,就不会运行一个 C++ 析构函数,而是运行一个 Dispose() 方法。 对于 C++ 开发人员而言,这就是确定性析构。 这意味着我可以这样编写 C# 代码:

{
   using( System::Data::SqlClient::SqlConnection conn
     = new System::Data::SqlClient::SqlConnection(connString) ) {
      // work with the connection in some way
      // including code that might throw an exception
      using( System::Data::SqlClient::SqlCommand cmd
        = new System::Data::SqlClient::SqlCommand(
              queryString, conn) ) {
         // work with the command
      // must write "using"s to call Dispose or Close
      }
   }
}

在 C++ 中,我可以编写这样的代码:

{
   System::Data::SqlClient::SqlConnection conn(connString);
   // work with the connection in some way
   // including code that might throw an exception
   System::Data::SqlClient::SqlCommand cmd(queryString, %conn);
   // work with the command
   // don't call Dispose or Close explicitly
}

SqlConnection 和 SqlCommand 对象实现了 IDisposable ,但是 C++/CLI 程序员不需要记着调用 Dispose() 。 编写代码更少,不会遗忘,都是 C++ 中析构机制的自然优点。 使用 CLR 上的这一机制非常自然和直观,而无需要求库用 C++ 编写或者实现析构函数。

模板

C++/CLI 中使我们能够在堆栈中创建托管类型实例的因素,也使我们能够在传统 C++ 模板中使用托管类型:

set<String^>^ SetofStrings;
. . .
String^ s = "Hello World";
SetofStrings->insert( s );

这意味着使用托管类型(无论是你自己的还是来自基类库)时, STL 中的一切都可以访问。 C++/CLI 带有一些辅助模板,包括 auto_close<> (这是一个智能指针的 Variant ,能够调用它所包装的实例的 Close 方法),和 marshal_as<> ,能够转换相关的类型比如 System::String 和 std::string 。

混合托管和本机开发

如果你还在犹豫是否用 /clr 选项重新编译标准非托管 C++ 代码,请不要烦恼! 在一个应用程序中混合和匹配托管和本机(非托管)代码是非常直接的。

从托管代码,你可以调用任何现有的非托管代码,就像非托管到非托管似的。 包含头文件,与库连接之后,然后就一切就绪了。 你是调用一个老的 C 语言库,一个 C++ 库还是一个 COM API 都无关紧要。 你所调用的代码使用了什么库也无关紧要。 只需 #include 并如常继续即可。 你的代码可以在托管呼叫代码和非托管目标代码之间过渡,然后为托管代码带来最高性能的回报。 其他支持 .NET 的语言都没有这样的选择。 这种功能,以前被称为 It Just Works interop ,现在称为 C++ Interop 。

从非托管代码可以访问所有托管类型,不过需要包装为 COM 类型。 regasm 实用工具为托管类型生成和注册类型库,可以使用编译器支持(比如 #import )从本机代码中访问组件。 这一选择会影响性能: 可以改而选择用 /clr 编译呼叫代码,并直接通过运行库访问托管类型。 为了保持 C++ 的理念,选择由你决定。

.NET 路线图

C++/CLI 为你提供了极大的余地,可以选择如何访问由 CLR (以及由 WinFX )提供的功能。 如果你要编写一个新的应用程序,用 C++/CLI 编写以 CLR 为目标平台所具有的优点是不可抗拒的。 可以拥有可靠性,可以访问重要的托管 API ,从托管库中获得极高的开发人员生产率,而又无需放弃访问本机库或者任何 C++ 范型。 进行这种决策简直是再清楚不过了: 用 C++/CLI 为 Windows 编写一个托管应用程序。

那么现有的应用程序怎么办呢? 有些应用程序已经到了它们生存周期的末期: 你已经不想增加功能或者进行明显改动了。 用户不再指望它与其他应用程序集成。 像这样的应用程序无需转向 .NET 。 它们将在未来运行在 Microsoft Windows 代号 "Longhorn" 之上,因此可以不去管它们。

剩下的就是还没有到生存期末期的现有应用程序了。 你想维护和增强这些应用程序,可能还计划在增强中使用托管库。 或者你可能愿意以 CLR 为目标平台以减少大型分布式应用程序部署时的麻烦。 基本上,有三种选择: 重写,集成或者迁移。

重写是风险最大的一种方式。 必须完整地阅读代码,找出 .NET 世界中有等效物的库和构造。 例如,原来可能使用 MFC 构建 Windows 应用程序的用户界面: 而今天 .NET 中的等效物是 Microsoft Windows 窗体,在 "Longhorn" 时代是 Avalon 。 可以使用 ADO 或者 MFC 数据访问类处理数据: 而 .NET 的等效物是 Microsoft ADO.NET ,使用 DataSet 或者 DataReader 类。 寻找这些库和构造的工作量是非常大的。 然后还要重新编写应用程序的大部分,在此过程中还可能引入错误,所以还必须进行全面测试。 这都需要时间和成本,但是当工作全部完成,你将拥有一个现代的应用程序,全部用 C++/CLI 编写,尽可能使用托管库。

如果当前代码基础比较老,而且已经更新多次,或者你的开发团队并不能很好地理解,但是它又必须以某种方式改进或者更新,那么重新编写可能是最适合你的方式。 如果必须创建可以验证的程序集以用于部分信任情况,可能也需要采取一些重新编写的方式。 虽然对于大多数开发人员来说这一方式是第一计划,但是实际上它应该是最后的选择。 如果现在你的代码很简洁,文档齐全,性能很好,应该尽可能拒绝进行重新编写。

集成方式是将所有 .NET 库(基类库, WinFX, Indigo, Avalon, 等等)都视为要使用的新库。 现有的代码基础保持不变,用使用这些新库的新模块扩展它。 可以将应用程序的主干代码重新编译为 MSIL ,直接调用运行库中的新库,并使用 C++ Interop 访问老库。 或者,可以将主干代码保留为本机(非托管)代码,使用 COM 可调用包装来包装托管类型,将它们公开给旧代码。 进行一些性能测试有助于进行决策。 可以根据选择用 /clr 编译部分代码基础。

迁移方式处于上面两个极端之间。 在你的应用程序已经分成多个组件或者层时最适合。 (巨大的单块应用程序是非常难于维护的,因此无论如何都应该将应用程序组件化,但是这种重构也是一种形式的重新编写,会带来风险。) 然后可以将每个组件用公开托管类型的 C++/CLI 代码包装起来。 新的主干应用程序可以使用这些托管类型,新的托管类型也可以与 .NET 库交互。 随着时间推移,在包装中的本机“核心”可以重新编写为完全的托管代码,只使用托管库,如果有重要的性能原因或者代码安全原因需要如此的话。

小结

无论进行何种开发: 从头编写一个新的应用程序,维护我们无需做太多工作的现有应用程序,或者使一个历尽沧桑的老应用程序重新焕发青春, .NET Framework 都可以使你的工作更加简单。 使用 C++/CLI 将应用程序与 CLR 集成,或者将应用程序迁移到 CLR ,都比将应用程序移植到 C# 更加可行。 C++/CLI 为我们提供了CLR 上 C++ 的功能和灵活性,提供了真正的确定性析构(即使是托管类型),提供了性能最高的交互操作。 对于任何要以 CLR 为目标平台的开发应用程序的 C++ 程序员而言,这都是一种非常自然的选择。

作者简介

Kate Gregory 是一位 Microsoft 地区经理, Visual C++ MVP ,以及《 Microsoft Visual C++ .NET 2003 Kick Start 》一书的作者。 Gregory 咨询公司为全北美提供咨询和开发服务,专门利用最新技术从事软件开发、集成项目、技术写作、指导和培训。

转到原英文页面

posted on 2005-12-15 09:23 梦在天涯 阅读(1587) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


公告

EMail:itech001#126.com

导航

统计

  • 随笔 - 461
  • 文章 - 4
  • 评论 - 746
  • 引用 - 0

常用链接

随笔分类

随笔档案

收藏夹

Blogs

c#(csharp)

C++(cpp)

Enlish

Forums(bbs)

My self

Often go

Useful Webs

Xml/Uml/html

搜索

  •  

积分与排名

  • 积分 - 1785089
  • 排名 - 5

最新评论

阅读排行榜