面向 C# 开发者的 C++ 2013 教程(一)
一、你好,世界
一致性是缺乏想象力的人最后的避难所。—奥斯卡·王尔德
自古以来,可以追溯到 Kernighan 和 Richie 关于 C 的书的发行,有一个传统,即打开一本关于 C 或其后代的书时,用一个简短的例子来说明显示“Hello World”是多么容易。本书也不例外。让我们并排检查一下“Hello World”的 C# 和 C++ 版本(见表 1-1 )。
表 1-1。
“Hello World” in C# and C++
| C# | C++ | | --- | --- | | `using System;` | `using namespace System;` | | `class HelloWorld` | | | `{` | | | `static void Main()` | `void main()` | | `{` | `{` | | `Console.WriteLine(“Hello World”);` | `Console::WriteLine(“Hello World”);` | | `}` | `}` | | `}` | |如表 1-1 所示,语言明显不同。另一方面,C# 和 C++ 就像法语和意大利语;尽管 C++ 的语法看起来像是外来的,但是它的意思是很清楚的。
以下是一些需要注意的事项:
- 在 C# 中,
Main()总是一个类的方法。在 C++/CLI(公共语言基础设施)中,main()不是类方法;这是一个全局函数。这很简单——只要记住全局函数没有类。 - 就像在任何 C# 程序中都有一个名为
Main()的唯一静态成员函数一样,在任何 C++ 程序中都有一个名为main()的唯一全局函数。在 C# 中,通过将多个Main()方法嵌入到不同的类中,可以避开这一要求。然后,您可以使用/main:<type>选项告诉编译器哪个类包含启动方法。这个技巧在标准 C++ 中不起作用,因为main()必须是一个全局函数,任何版本的main()在全局名称空间中都有相同的签名和冲突。 - C++ 使用
::(冒号-冒号)来分隔名称空间和类名,用一个点(.)来访问类成员;C# 对所有东西都用一个点。C++ 希望你对你正在做的事情更加明确。 - C++/CLI
using语句需要额外的关键字namespace。
Note
在 Microsoft Visual C++ 中,入口点可以是任何函数,只要它满足链接器文档中定义的某些限制。它可以是全局函数,也可以是成员函数。通过指定/entry: <function_name>链接器选项可以做到这一点。标准 C++ 需要一个名为 main 的唯一全局函数,它有一个整数返回值和一个可选的参数列表。参见 C++ 标准的第 3.61 节,ISO/IEC 14882:2003(E)。该标准的 PDF 版本可从 webstore 下载。安西。org 收取少量费用。</function_name>
启动 Visual Studio 2013 控制台
我打赌你一定很想试一试。“真正的程序员”使用命令行,所以让我们从那里开始。我们现在要构建一个控制台应用程序。
点击开始,打开 Visual Studio Tools 文件夹,如图 1-1 所示,然后双击 VS2013 的开发者命令提示符。
图 1-1。
Open the Visual Studio Tools folder
这产生了一个新的命令提示符,其环境变量设置为与 Visual Studio 2013 一起工作。所有 Visual Studio 编译器都可以从命令行运行,包括 Visual C++、Visual C# 和 Visual Basic。
检索源文件
要么弹出notepad.exe(肯定是你最喜欢的编辑器)并开始输入,要么从 Apress 网站的源代码部分获取源代码。去 www.apress.com ,用 ISBN 978-1-4302-6706-5 搜索这本书。
正在执行 HelloCpp.cpp
导航到这个第一章的样本目录,并转到HelloWorld子目录。这里是HelloCpp.cpp:
using namespace System;
void main()
{
Console::WriteLine(“Hello World”);
}
输入以下命令:
cl /nologo /clr HelloCpp.cpp
此命令指示 C++ 编译器针对公共语言运行时(CLR)编译此文件,并创建 C++/CLI 可执行文件。可执行文件是包含元数据和公共中间语言(CIL)的托管程序集,就像 C# 可执行文件一样。CIL 在 CLR 上也被称为 MSIL。
让我们执行这个例子。首先,键入
HelloCpp
接下来,按回车键。您应该看到以下内容:
Hello World
这是件好事。
Visual C++ IDE 快速浏览
在本节中,我们将介绍使用 Visual Studio 2013 C++ 集成开发环境(IDE)制作基本 C++/CLI 项目的步骤。这非常类似于创建一个 C# 项目。
Load Visual Studio 2013. From the File menu, select New Project. My system is set up with Visual C++ as the default language, so my New Project dialog box looks like the one shown in Figure 1-2.
图 1-2。
Creating a new HelloWorld project and solutionNavigate to the CLR project types under Visual C++. Select CLR Console Application. Enter HelloWorld in the Name text box. Click OK.
默认情况下,Visual Studio 2013 在 C:\ Users \ % USERNAME % \ Documents \ Visual Studio 2013 \ Projects 中创建新项目。如果您愿意,可以随意更改目录并将项目放在其他地方。单击确定。
了解项目和解决方案
Visual C++ CLR 控制台应用程序向导在也称为 HelloWorld 的解决方案中创建了一个名为 HelloWorld 的新项目。项目和解决方案有什么区别?
Visual Studio 中使用的基本范例是您创建一个解决方案,它是您正在处理的内容的容器。一个解决方案可以由几个项目组成,这些项目可以是类库或可执行文件。每个项目都是特定于语言的,尽管也可以使用定制的构建规则在一个项目中混合使用不同的语言。
在我们的例子中,我们需要一个 Visual C++ 项目来生成一个名为HelloWorld.exe的可执行文件,所以我们的解决方案只有一个项目。默认情况下,项目是在子目录中创建的,但是我们可以通过取消选择“为解决方案创建目录”来更改此行为。在本书的后面,我们将会有依赖于几个项目的更复杂的解决方案。
现在您应该看到两个平铺的窗口:Solution Explorer 和包含HelloWorld.cpp的编辑器窗口。看起来 Visual C++ 2013 已经费尽心思为我们编写了这个程序—现在这不是很好吗?
了解差异
我们的基本 HelloCpp 应用程序和由 Visual Studio C++ CLR 控制台应用程序向导创建的 HelloWorld 应用程序之间存在一些差异,如图 1-3 所示。最明显的区别是向导创建了几个额外的支持文件。
图 1-3。
The HelloWorld application as created by the CLR Console Application Wizard
让我们看看那些新文件。
资源
这些文件为您的应用程序配备了一个漂亮的小图标,并为将来的应用程序开发铺平了道路。Visual C++ 允许您在二进制文件中嵌入资源。它们可以是位图、图标、字符串和其他类型。有关更多信息,请参考 Visual C++ 文档。
resource.happ.icoapp.rc
预编译头
这些文件通过避免公共代码的多次编译来提高编译速度:
stdafx.hstdafx.cpp
本书中反复出现的一个主题是 C++ 中声明和定义的区别。与 C# 不同,类原型(称为声明)可以从类定义中分离到不同的文件中。这提高了编译速度,避免了循环依赖,并为复杂项目提供了面向对象的抽象层。在许多 C++ 项目中,通常只包含声明的文件,称为头文件,以扩展名.h结束,在每个源文件的开头被编译为一个单元。如果项目中的头文件是相同的,编译器最终会用每个源文件编译相同的代码块。Visual C++ 提供的一个优化是在所有其他编译之前,将stdafx.h文件中引用的头文件全部编译成二进制 PCH(预编译头文件)文件。这称为预编译头文件。只要头文件没有被修改,源文件的后续编译就会大大加快,因为预编译头文件是作为一个单元从磁盘加载的,而不是单独重新编译。Visual C++ 生成了两个文件stdafx.h和stdafx.cpp来辅助这种机制。有关更多信息,请参考 Visual C++ 文档。
可以通过更改项目属性来禁用预编译头。要修改项目设置,在解决方案资源管理器中右键单击HelloWorld项目。导航到配置属性,并单击三角形以展开列表。然后展开 C/C++ 旁边的三角形,并选择预编译头。如图 1-4 所示,属性页窗口出现在屏幕上,允许你在应用程序中配置预编译头文件。
图 1-4。
Configuration of precompiled headers from the Property Pages window
AssemblyInfo.cpp
文件AssemblyInfo.cpp包含组件的所有属性信息。这个和 C# 出品的AssemblyInfo.cs差不多。这包括但不限于版权、版本和基本程序集描述信息。默认值对于开发来说很好,但是您需要在发布之前填写一些信息,包括版权属性。图 1-5 显示了一个样本AssemblyInfo.cpp的摘录。
图 1-5。
An excerpt from AssemblyInfo.cpp
hello world . CPP . hello world . hello world . hello world . hello world . hello world . hello world . hello world
主源文件也有一些显著的不同,如图 1-6 所示:
图 1-6。
HelloWorld.cpp
main函数被定义为接受一个System::String的托管数组,相当于 C#Main(string[] Args)。这允许您访问命令行参数。- 包含预编译头文件
stdafx.h是为了支持预编译头文件的使用。 - 文字字符串“Hello World”前面加了一个
L来表示一个宽字符串。在本机 C++ 中,默认情况下,字符串是字节数组。编译 C++/CLI 时,编译器试图通过上下文来区分宽字符串和字节数组。不管你在这个上下文中有没有一个L,一个宽字符System::String被创建。
窗口布局
Visual Studio 的一个精心设计的功能是能够通过使用简单的鼠标移动来重新排列窗口,从而自定义 IDE 的外观。在本节中,我们将学习如何停靠和定位窗口。
停靠窗口
解决方案资源管理器自然出现在 Visual Studio 的左侧或右侧,这取决于默认情况下选择的设置。幸运的是,自定义重排既简单又直观。右键点击标题栏,弹出窗口如图 1-7 所示,可以停靠窗口,停靠为选项卡式文档,或者浮动在顶部。
图 1-7。
Right-clicking on the title bar reveals options for displaying the window
现在,当您单击并按住标题栏时,您会在光标悬停的框架中看到一个小指南针,以及每个其他窗口框架上的引用标记。指南针允许您根据您悬停的框架来指示窗口的位置。将窗口移到另一个框架上,指南针会跳到那个框架。
图 1-8。
Clicking and holding down the title bar reveals a compass
指南针的中心
指南针本身有方向标签(北、南、东、西)和一个中心框。如果在中心框上释放鼠标,窗口将在当前框架内变成选项卡式窗口。将它放在主框架上,在这里编辑文档。您现在可以看到,它与其他主窗口共享一个框架。
当您将鼠标悬停在其中一个指南针方向选项卡上时,目标框架的相应部分会变灰,以便您可以预览新的窗口排列。如果你把窗口放到了错误的地方,你总是可以把它撕下来或者手动把它设置为可停靠或者浮动,这取决于它的状态。
玩玩这个。在图 1-9 中,您可以在主窗口中看到作为选项卡式文档的解决方案窗口。
图 1-9。
Solution Explorer as a tabbed document in the main frame
构建、执行和调试
让我们在构建和测试 HelloWorld 时快速浏览一些关键的 Visual C++ IDE 命令(见表 1-2 )。
表 1-2。
Common IDE Commands Quick Reference
| C# | C++ | 说明 | | --- | --- | --- | | 第三子代 | 第三子代 | 查找下一个 | | F8 | 法乐四联症 | 转到源代码中的下一个编译错误 | | 移位-F8 | 移位-F4 | 转到源代码中的上一个编译错误 | | F5 | F5 | 调试时执行 | | Ctrl-F5 | Ctrl-F5 | 不调试就执行 | | F6 | F7 | 建设 | | F9 | F9 | 切换断点 | | F10 | F10 | 跨过 | | F11 | F11 | 进入 |构建程序
根据我们的键绑定,我们可以使用 F6 或 F7 来构建。如果有任何错误,它们会出现在屏幕底部的输出窗口中,您可以使用 F8 或 F4 来循环显示它们。
在 C++ 中,就像在 C# 中一样,多个编译错误经常是乱真的;编译器尝试在第一个检测到的问题之后进行编译,可能会丢失。这通常允许您看到两三个错误,并在一次编辑过程中修复它们。通常,额外的错误是编译器基于不正确的语法出去吃午饭的产物,修复一两个错误可能会使其余的错误消失。我建议经常建设。
执行 HelloWorld
F5 键是执行命令。因为这是一个控制台应用程序,所以执行会产生一个显示“Hello World”的命令窗口,然后很快关闭,这有点不令人满意。有几种方法可以解决这个问题。一种方法是创建另一个开发人员命令提示符,导航到创建可执行文件的调试目录,手动运行程序,就像我们前面所做的那样。另一种方法是将下面的调用添加到main()函数的末尾:
Console::ReadLine()
该方法要求用户输入一行内容,并保持控制台窗口打开,直到用户按下 Enter 键。
另一组解决方案通过利用内置的 Visual C++ 调试器而呈现出来。您可以使用 F9 命令在程序的最后一行设置断点,也可以一行一行地单步执行程序。无论哪种方式,您都可以切换到衍生的命令提示符来查看所需的输出。
让我们试着使用调试器。
使用 Visual C++ 2013 调试器
调试器集成在 Visual Studio 2013 中,因此启动调试非常简单。输入任何调试命令都会在调试器下启动应用程序。窗口布局肯定会改变,因为默认情况下,有几个状态窗口只有在调试时才可见。
Note
编辑和调试有不同的窗口配置。每个配置都必须单独定制。
基本的调试命令是 F5(带调试执行)、F9(切换断点)、F10(单步执行源代码行)和 F11(单步执行源代码行)。
单步执行代码
Step 命令执行程序中的一行代码。“单步执行”命令有两种:F10(单步执行)和 F11(单步执行)。这些是相似的,但是当应用于函数调用时,它们是不同的。F10 执行到函数调用后的一行,而 F11 在函数体的第一行停止执行。当然,使用 F11 总是取决于调试信息是否可用于该函数所来自的二进制文件。因为Console::WriteLine()的调试信息没有随 Visual C++ 2013 一起发布,所以 F10 和 F11 都跳过了该函数。
按 F10 开始用 Visual C++ 2013 调试 HelloWorld。标题栏更改为显示“HelloWorld(调试)”以指示调试模式。此外,在单独的窗口中会产生一个命令窗口。此时,它是空白的,因为 HelloWorld 尚未显示任何信息。
编辑器窗口的左边缘会出现一个黄色小箭头,指示当前正在执行的代码行。图 1-10 显示执行已经停止,调试器等待下一个命令。
图 1-10。
Debugging HelloWorld
箭头表示我们开始执行main()函数,下一个要执行的行包含Console::WriteLine()语句。
再次按下 F10。执行Console::WriteLine()函数调用,并且“Hello World”出现在单独的命令窗口中。
如果你敢多按几次 F10,你就会在屏幕上制造一场噩梦。第一次,你执行返回函数。下一次,您从 HelloWorld 代码返回到 C/C++ 运行时,或 CRT。此模块执行重要的任务,包括在 Windows 中初始化程序、打包程序的命令行参数以及处理程序退出 Windows。注意,这段代码通过名字显式地调用main(),这解释了为什么每个 C++ 程序都需要一个名为main()的全局函数。
完成执行
按一次 F5 执行退出代码的剩余部分,并返回到编辑器。如果HelloWorld.cpp不可见,您可以点击选项卡再次显示信号源。此时,调试已经完成,标题栏不再显示调试。
摘要
本章向您提供了如何从控制台创建简单的 C++/CLI 应用程序以及如何使用 IDE 创建更复杂的应用程序的基本概述。我还向您展示了如何使用集成调试器在 Visual C++ 2013 中执行基本调试。
在下一章,我们将看到如何从一个简单的 C++ 程序中调用 C#。
二、没有什么地方比得上家
我没有停止恐惧,但我不再让恐惧控制我。我已经接受了恐惧是生活的一部分,特别是对变化的恐惧,对未知的恐惧,尽管心里怦怦直跳,说着:回头,回头;如果你走得太远,你会死的。—埃里卡·琼
在这一章中,我们将介绍 C++ 的互操作性特性,并向您展示一种结合 C# 和 C++ 的快速方法。我们首先用 C# 开发一个洗牌类。接下来,我们添加一个使用 C# 类的 C++ 存根。在第四章中,我们更进一步,将整个应用程序迁移到 C++。我们将在第十九章中更详细地讨论语言集成和互操作性。
开发程序
假设你有一个非常好的 C# 类,你想把它和你的 C++ 代码一起使用。如果不得不抛弃这一切并用 C++ 重写,那就太可惜了,不是吗?
当我在开发。NET Reflector add-in for C++/CLI 时,我发现自己正处于这种情况。在我的开发过程中。NET Reflector,正处于改进反射器接口的过程中,结果删除了我需要的一个类。为了帮我,他给我发了一个 C# 文件,里面有被删除的代码。我没有被迫用 C++ 重写他的代码,而是在我的项目中添加了对他的类的引用,然后回去继续编写插件。
给我发牌
似乎面试的问题总是相关的,不管你在这个行业已经多少年了。它们可以发人深省并富有娱乐性。我最喜欢的一个游戏,洗牌,应该是有教育意义的。
从表面上看,这似乎是一个简单的问题,但是在您开始编码之前,有几种方法会让您陷入困境。
过程
面试开始出错的第一次是当你在洗牌之前试图找出如何表现这副牌的时候。噩梦会像这样展开:
- 你:套牌是什么样子的?
- 面试官:随机的。
- 你:我如何表示随机输入?你会给我一个输入状态的卡片列表吗?
让我说,在这一点上,面试官会退到洞穴里,以某种形式重复这个问题:
- 记者:给你一副任意的牌,你需要洗一副牌。我就说这么多。
他是这么说的,但他想的是“不雇佣”你需要在这里停下来想一想目标。目标是产生一副完全随机的洗牌牌。开始时牌的顺序并不重要。你可以选择任何你喜欢的顺序。
列举卡片
面试中的下一个障碍是通过四种不同的花色来表现王牌中的王牌。有一个更简单的方法:用一个从1到52的数字来标识每张牌。如果卡片从0到51编号,那么用 C++ 和 C# 编程就更容易了,因为在这些语言中数组是零索引的。
给花色分配一个任意的顺序,例如 0 到 3 之间的一个数。Bridge 采用字母顺序,为什么不效仿呢?
namespace CSharp
{
class Deck
{
enum Suit
{
Clubs = 0, Diamonds, Hearts, Spades
}
}
}
你可以对卡片本身使用同样的技巧:
namespace CSharp
{
class Deck
{
enum Card
{
Ace=0, Deuce, Trey, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King
}
}
}
因此,我们有两种类型的信息来分别表示:0和12之间的Card号,以及0和3之间的Suit号。这个问题的一个常见解决方案是使用以下公式将它们映射到一个数字:
Number = Suit*13+Card
由于Card小于13,很明显(int)(Card/13) ==0,所以两边除以 13 得到Suit,余数为Card。因此,我们已经导出了用于逆变换的以下方程:
Suit = Number/13
Card = Number%13
Number在Card和Suit都为0时达到最小值,在Card=12和Suit=3时达到最大值。
min(Number) = 0 * 13 + 0 = 0
max(Number) = 3 * 13 + 12 = 51
因此,我们将任意一张卡片(Suit,Card)映射到 0 到 51 之间的唯一数字。实际上,这个问题可以归结为 0 到 51 之间的随机数的随机化问题。你可能会认为这是一件容易的事情,但事实证明这并不简单,而且很容易出错。鉴于在线赌博的激增,这尤其令人不安。
Note
这里有一个诱人的算法,只是不工作。将卡片放在一个数组中,遍历它们,用随机位置的一张卡片交换每张卡片。事实上,这确实非常壮观地混淆了牌,但是它有利于某些牌的顺序并产生不均匀的分布。你能看出为什么吗?
每一次交换都有 52 分之一的机会与自己交换——一次微不足道的交换。你可能会想,如果洗牌的结果是一副未洗牌的牌,比如说,{0 1 2 3 4… . 51},那么一定有偶数的非平凡交换。现在这副牌{2 1 3 4… . 51}需要奇数个非平凡交换。这应该是一个危险信号,因为我们的算法总是精确地执行 52 次交换,这是偶数,所以这两副牌以相等的可能性生成似乎是可疑的。
洗牌算法
一个声音算法模仿你发牌时的动作。首先,你从 52 张牌中随机抽取一张,然后从剩下的 51 张中抽取一张,以此类推。在这个算法中,你得到一个均匀的分布,直到随机数发生器的随机性:
namespace CSharp
{
class Deck
{
void Shuffle()
{
for (uint u = 52; u > 0; --u)
{
Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
}
}
}
}
完整的 C# 程序
这个实现将一副牌洗牌,并“分发”出前五张牌供观看。我们可以断定这个游戏的名字是五牌梭哈。
using System;
namespace CSharp
{
public class Deck
{
uint[] Cards;
Random randomGenerator;
public enum Suit
{
Clubs = 0, Diamonds, Hearts, Spades
}
public enum Card
{
Ace = 0, Deuce, Trey, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King
}
Deck()
{
randomGenerator = new Random();
Cards = new uint[52];
for (uint u = 0; u < 52; ++u)
{
Cards[u] = u;
}
}
void Swap(ref uint u, ref uint v)
{
uint tmp;
tmp = u;
u = v;
v = tmp;
}
void Shuffle()
{
for (uint u = 52; u > 0; --u)
{
Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
}
}
uint RandomCard(uint Max)
{
return (uint)((double)Max * randomGenerator.NextDouble());
}
string CardToString(uint u)
{
Suit s = (Suit)(Cards[u] / 13);
Card c = (Card)(Cards[u] % 13);
return c.ToString() + " of " + s.ToString();
}
public static void Main()
{
Deck deck = new Deck();
deck.Shuffle();
for (uint u = 0; u < 5; ++u)
{
Console.WriteLine(deck.CardToString(u));
}
Console.ReadLine();
}
}
}
快速浏览一下代码
如同在每个 C# 应用程序中一样,代码以static Main()开始。在那里,我们创建一个新的Deck,在上面调用Shuffle(),然后显示前五张卡。因为WriteLine()不熟悉如何打印卡片,我们创建了一个将卡片转换成字符串的函数,然后用它的结果调用WriteLine()。函数CardToString(uint cardnumber)完成了这个任务。
项目和解决方案
首先让我们创建一个简单的 C# shuffle 项目。这个 C# 项目没有什么特别独特的地方。要创建它,请选择文件➤新➤项目。浏览新的项目树视图,创建一个名为 Shuffle 的 Visual C# 控制台应用程序。如果你的系统设置和我的一样,控制台应用程序会出现如图 2-1 所示。
图 2-1。
The C# Shuffle console application
C# 和 C++ 编译器都将元数据打包成模块和程序集。模块是程序集的构建块。程序集由一个或多个模块组成,是部署单元。程序集被部署为可执行文件或类库。在第一个版本中,Shuffle 项目是一个独立的可执行文件。在本章的后面,我们将把这个可执行文件变成一个类库,而不需要修改任何一行 C# 代码。
快速浏览
选择编辑➤概述➤折叠到定义。这给了你一个代码的鸟瞰图,如图 2-2 所示。
图 2-2。
A bird’s-eye view of the code
将光标放在任何包含省略号的框上都会弹出一个窗口,显示代码的折叠部分。
构建和执行项目
选择“生成➤生成解决方案”来生成项目。对于 Visual C++ 键绑定,这是 F7 键。对于 Visual C# 键绑定,这是 F6 键。在任一情况下,您都可以用 F5 键执行它。
您会看到类似如下的输出—您的手牌可能会有所不同:
Ten of Diamonds
Deuce of Clubs
Trey of Clubs
Jack of Hearts
Deuce of Spades
由于调用了Console.ReadLine(),命令窗口现在暂停,等待您按回车键。
嗯。一对 2——还不错,但还没好到可以打开。
绑定 C++
现在我们要从 C++ 中调用这个 C# 类。我们将利用 C++/CLI 程序以名为main()的全局函数开始的事实,而 C# 程序以名为Main()的静态函数开始。因为这些名字是截然不同的,所以它们并不冲突,我们可以将它们无缝地绑定在一起。
创建 C++ 项目
首先,我们将 C# 程序与 C++/CLI 合并。要创建一个 C++ 项目,选择文件➤添加➤新项目。在模板下,依次选择 Visual C++、CLR 和 CLR 控制台应用程序。将项目命名为 CardsCpp,从解决方案下拉列表中选择添加到解决方案,如图 2-3 所示。然后单击确定。
Note
您也可以使用解决方案资源管理器中的“添加项目”。这样,您就不会冒意外创建新解决方案的风险。
图 2-3。
Creating the C++/CLI project
设置启动项目和项目依赖项
您应该有一个名为 CardsCpp 的新项目。在解决方案资源管理器中按照下列步骤操作:
Right-click the CardsCpp project, and select Build Dependencies ➤ Project Dependencies. Check the box so that CardsCpp depends on Shuffle. This ensures that the C# project Shuffle is built before the C++ project CardsCpp. We want a dependency in this direction, because we will bring in the completed C# project as a class library DLL and the C++ project will be the master project. See Figure 2-4.
图 2-4。
Project Dependencies dialog boxRight-click the CardsCpp project again, and select Set as Startup Project.
使 C# 项目成为类库
现在,我们将变一点魔法,修改 C# 应用程序,以便它可以作为类库被 C++ 应用程序引用。在解决方案资源管理器中右击 Shuffle,然后选择 Properties。在应用选项卡中,将输出类型改为类库,如图 2-5 所示。
图 2-5。
Convert the C# project to a class library
添加对 C# 类库的引用
右键单击 CardsCpp 项目,并选择“添加➤引用”。然后单击“添加新引用”按钮。单击“项目”选项卡;洗牌项目应该已经被选中,如图 2-6 所示。单击 OK 向 C++ 项目添加对 Shuffle 的引用。
图 2-6。
Add a reference to the C# project
创建 C++/CLI 存根
对 C++ 源文件CardsCpp.cpp有一个小的改动。替换以下行:
Console::WriteLine(L"Hello World");
随着
CSharp::Deck::Main();
请注意,当您键入时,Visual C++ IntelliSense 会弹出一个窗口来帮助您。就像 C# IntelliSense 一样,它是一个上下文敏感的代码引擎,可以帮助您在键入时发现类成员和参数信息。如图 2-7 所示,智能感知揭示了CSharp::Deck类的方法和字段。它们是什么以及如何访问它们由名称左侧的小图标决定。较小的框添加了关于所选项的更多信息,以及 XML 文档注释(如果有的话)。
图 2-7。
IntelliSense helps you code
您的代码现在应该如图 2-8 所示,准备好使用 F5 执行。
图 2-8。
The finished C++/CLI stub
在没有 IDE 的情况下进行洗牌
在没有 IDE 的情况下,组合 C++ 和 C# 程序也很容易,尽管它不容易扩展到大型项目。IDE 为您提供了强大的处理能力,但也增加了一层复杂性。使用 IDE,您可以获得以下内容:
- 使用智能感知和浏览功能编辑帮助和代码信息
- 项目管理
- 构建管理
- 集成调试
基本命令行编译
因为这是一个小而简单的项目,所以我们不需要通过完整的 IDE 设置来展示我们的演示。
使用以下去掉预编译头文件的基本 C++ 程序。在与Program.cs:相同的目录中创建一个名为cardscpp1.cpp的文件
#using "shuffle.dll"
void main()
{
CSharp::Deck::Main();
}
打开 Visual Studio 2013 命令提示符并导航到此目录。编译并执行该程序,如下所示:
csc /target:library /out:shuffle.dll program.cs
cl /clr cardscpp1.cpp
cardscpp1
King of Diamonds
Trey of Clubs
Jack of Hearts
Deuce of Diamonds
Four of Hearts
看来这次我们该弃牌了!
使用模块
模块是比 DLL 更小的编译单元。使用模块,可以将几个模块组合成一个 DLL。下面是一个使用模块而不是 DLL 的例子。在这种情况下,使用模块和 DLL 没有什么区别。
在与shuffle.cs相同的目录下创建一个名为cardscpp2.cpp的文件:
#using "shuffle.netmodule"
void main()
{
CSharp::Deck::Main();
}
将 C# 编译成一个模块,使用 C++ 制作一个可执行文件,并运行它:
csc /target:module /out:shuffle.netmodule program.cs
cl /clr cardscpp2.cpp
cardscpp2
King of Clubs
Queen of Diamonds
Queen of Spades
Ten of Spades
Ace of Clubs
这是一手好牌!
摘要
在这一章中,我们开发了一个简单的 C# 程序。首先,我们从 IDE 中编译并独立运行它。然后,我们将它的输出类型更改为库,以便创建一个供 C++ 可执行文件使用的 DLL,既可以从 IDE 也可以从命令行使用。最后,我们给出了一个使用模块的例子。这应该给你一个很好的介绍,让你知道在. NET 下使用 C# 和 C++ 的各种方法。在第十九章的中,我们将重温这些主题,并讨论与本地代码的互操作性。但是我们不要想得太多;首先要涵盖许多基础知识,我们将在下一章探讨语法差异。
三、语法
纯粹而简单的真理很少是纯粹的,也从不简单。—奥斯卡·王尔德
前面的章节强调了 C# 和 C++/CLI 之间的相似之处。现在我们触及它们不同的主要领域,并开始理解为什么。这些包括附加的或不同的关键字、分隔符和运算符。
关键词和分隔符
在 C++ 中,当using是一个名称空间时,需要额外的关键字namespace(见表 3-1 )。
表 3-1。
Namespaces in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `using System.Threading;` | `using namespace System::Threading;` | | `System.Console.WriteLine("H");` | `System::Console::WriteLine("H");` |此外,在 C# 使用点作为通用分隔符的情况下,C++ 根据上下文以及被分隔项的含义使用几种不同的分隔符。这些分隔符中最常见的是冒号-冒号(::)和点(.)。冒号-冒号分隔符或范围解析运算符用于用命名空间、类、属性和事件限定标识符,以及访问静态字段和方法。在这两种语言中,点分隔符或成员访问运算符都用于访问类实例的成员。
C++ 的范例(不同上下文中的不同分隔符)和 C# 的范例(所有上下文中的单个分隔符)与每种语言的总体设计理念是一致的。C# 喜欢简单,而 C++ 需要更深层次的特异性来换取更大的灵活性。
表 3-2 显示了 C# 和 C++ 之间的分隔符差异。随着本书的进展,我将详细介绍所有这些分隔符。
表 3-2。
Separators in C++
| 分离器 | 名字 | 意义 | | --- | --- | --- | | `::` | 结肠-结肠 | 作用域解析操作符,当`::`左边的表达式是名称空间、类、属性或事件名称,而`::`右边的表达式是名称空间、类名或类的静态成员时使用。如果没有左表达式,右边的表达式就是一个全局变量。 | | `.` | 点 | 类成员访问运算符,当箭头左侧的表达式是类对象时使用 | | `->` | 箭 | 类成员访问运算符,当箭头左侧的表达式是指向类对象的指针或句柄时使用 | | `.*` | 圆点星 | 指向成员运算符的指针,当箭头左边的表达式是类对象,而箭头右边的表达式是指向同一类成员的指针时使用 | | `->*` | 箭头星 | 指向成员运算符的指针,当箭头左边的表达式是指向类对象的指针,而箭头右边的表达式是指向同一类成员的指针时使用 |C# 和 C++ 对类和结构的定义是不同的。除了一个明显的语法差异——c++ 要求在类型定义后面有一个分号——还有显著的语义差异。参见表 3-3 中比较 C# 和 C++ 中的类和结构的示例。
表 3-3。
Classes and Structures in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `class R {}` | `ref class R {};` | | 不适用的 | `ref struct R {};` | | `struct V {}` | `value class V {};` | | 不适用的 | `value struct V {};` | | `enum E {}` | `enum class E {};` | | 不适用的 | `enum struct E {};` | | 不适用的 | `class C {};` | | 不适用的 | `struct C{};` |在 C# 中,类和结构是实现 CLI 定义的引用类型和值类型的工具。在 C++ 中,类和结构定义了一个类型——一般来说,是字段、方法和子类型的相关集合。
C++/CLI 引入了两个类修饰符,ref和value,它们提供了一种在 C++ 中表示 CLI 类类型的方法。它们与class或struct关键字一起,由空格分隔,如在ref class中,它们形成一个新的关键字,恰当地称为空格关键字。
引用类型和值类型在中非常重要。NET 编程,在我们继续之前,最好先回顾一下这些类型。引用类型和值类型之间有许多实际差异,但主要差异与它们的分配方式有关。引用类型分为两部分。引用类型的数据在托管堆上分配,而该数据的单独句柄在堆栈上分配。值类型是在堆栈上自动分配的。
A C# class是引用类型;一个 C# string也是如此。A C# struct和大多数 C# 内置类型,包括int和char,都是值类型。引用类型中包含的值类型(通过装箱显式或隐式地)成为引用类型的元素,并在托管堆上分配。
C# 类(引用类型)
假设你有一个名为Hello的 C# class。使用分配实例
Hello h = new Hello();
从语法上看,似乎您已经创建了一个类型为Hello的统一实体。在幕后还有更多的事情在进行,因为数据是在堆栈和托管堆上分配的。在托管堆上分配了一个Hello对象的实例,这个实例的句柄存储在堆栈的变量h中。
C# 结构(值类型)
如果Hello被定义为 C#,那么就会发生完全不同的操作。Hello的整个实例在堆栈上分配,h代表这个对象的实例。
警告
当你给引用类型赋值时,引用类型在栈和堆之间划分的事实会产生一些有趣的和有些不直观的结果。将一种值类型分配给另一种值类型时,会将与该类型的一个实例关联的数据复制到另一个实例。将一个引用类型分配给另一个引用类型时,会用另一个实例的句柄覆盖一个实例的句柄。实例本身保持不变。
考虑以下 C# 代码:
class Hello
{
int i;
Hello(int number)
{
i=number;
}
static void Main()
{
Hello h = new Hello(1);
Hello j = new Hello(2);
j = h;
System.Console.WriteLine(j.i);
h.i = 3;
System.Console.WriteLine(j.i);
}
}
编译并运行这段代码后,我们得到
C:\>csc /nologo test.cs
C:\>test
1
3
在这个程序中,我们在托管堆上分配了两个类型为Hello的对象。这些类的句柄h和j被分配在堆栈上。我们用h和孤儿Hello(2)中的句柄覆盖j中的句柄。Hello(2)可以被垃圾收集器回收。h和j现在都引用了Hello(1)对象,使用h或j访问成员字段i没有区别。
换句话说,因为Hello是引用类型,h和j是指向托管堆上数据的句柄。当赋值j=h发生时,h和j都指向相同的数据。将3赋给h.i也会影响j.i,显示j.i会导致数字3。
对比
另一方面,如果Hello是值类型,您会看到不同的结果。将Hello的申报从class变更为struct:
struct Hello
{ /**/ }
编译和执行程序后,我们看到
C:\>csc /nologo test.cs
C:\>test
1
1
这次的结果不同,因为我们的对象都被分配到堆栈上,并且相互覆盖。
缺乏局部性
方法Main()的局部检查不足以确定程序的结果。你不能通过查看周围的代码来确定WriteLine将会产生什么结果。C# 要求你参考Hello的定义,发现Hello是class还是struct。
缺乏局部性是危险的,并且违背了 C++/CLI 的设计理念。在 C++/CLI 中,引用类型和值类型之间的区别要明显得多。程序员更精确地指定他或她想要做什么,这避免了混淆,并最终使代码更易于维护。代价是语法稍微难一点。
C++ 方法
在 C++/CLI 中,通常使用句柄标点符号^来标记句柄。它也被称为跟踪句柄,因为它指向一个在垃圾收集期间可能被移动的对象。
将前面的代码翻译成 C++/CLI,我们实现了以下内容:
private ref class Hello
{
private:
int i;
Hello(int number)
{
i=number;
}
public:
static void Main()
{
Hello ^h = gcnew Hello(1);
Hello ^j = gcnew Hello(2);
j = h;
System::Console::WriteLine(j->i);
h->i = 3;
System::Console::WriteLine(j->i);
}
};
void main()
{
Hello::Main();
}
编译和执行之后,我们得到
C:\>cl /nologo /clr:pure test.cpp
C:\>test
1
3
与 C# 版本有一些明显的语法差异。然而,我想先指出一个语义上的区别。在 C++/CLI 中,通过将空白关键字ref class更改为value class,将Hello从引用类型更改为值类型,不会在编译和执行上产生不同的结果。
将类型从引用类型更改为值类型会影响类型的分配位置,但这不会改变在前面的代码片段中我们将数据视为引用数据的事实。如果Hello变成值类型,那么编译器生成不同的 IL,这样h和j仍然是托管堆上数据的句柄,结果是一致的。在幕后,值类型是封闭的——我们将在第六章的中再次讨论。
成员访问运算符的类型
C++ 代码片段和 C# 代码片段的另一个重要区别是 C++ 句柄使用不同的类成员访问操作符。语法类似于 C++ 中的指针,因为句柄可以被认为是一种特殊的指针。如果您正在使用指向某个对象的句柄或指针,您可以使用箭头成员访问运算符(->)来访问该对象的成员。如果您正在处理对象本身的实例,您可以使用点成员访问操作符(.)。虽然有两种不同类型的成员访问操作符看起来更复杂,但一个好处是像我们前面的例子这样的代码总是做您期望它做的事情,因为您被迫在编写时注意您正在做的事情——这是一件好事。
关键词差异
在这一节中,我们将讨论 C# 和 C++ 之间的关键字差异。这些差异中的大部分是由于 C++ 语言的发展以及添加到 C++ 语法中的兼容性和歧义消除限制。
让我们从关键字foreach开始,如表 3-4 所示。
表 3-4。
foreach in C# and for each in C++/CLI
| C# | C++/CLI | | --- | --- | | `foreach` | `for each` |在 C++/CLI 中,关键字for each有一个空格,用法与 C# 中的foreach略有不同。转换后的代码出现在表 3-5 中。
表 3-5。
Examples of foreach in C# and for each in C++/CLI
| C# | C++/CLI | | --- | --- | | `using System;` | `using namespace System;` | | `using System.Collections;` | `using namespace System::Collections;` | | `class R` | `ref class R` | | `{` | `{` | | | `public:` | | `static void Main()` | `static void Main()` | | `{` | `{` | | `ArrayList list = new ArrayList(0);` | `ArrayList ^list = gcnew ArrayList(0);` | | `list.Add("hello");` | `list->Add("hello");` | | `list.Add("world");` | `list->Add("world");` | | `foreach (Object o in list)` | `for each (Object ^o in list)` | | `{` | `{` | | `Console.WriteLine(o);` | `Console::WriteLine(o);` | | `}` | `}` | | `}` | `}` | | `}` | `};` | | | `void main()` | | | `{` | | | `R::Main();` | | | `}` |回顾
让我们回顾一下到目前为止您所看到的内容。C# 和 C++/CLI 之间的差异包括:
- 使用了附加关键字
namespace。 - 名称空间由冒号-冒号(
::)而不是点(.)分隔。 - 用
ref class代替class。 - 标点符号
^用于声明句柄。 - 箭头(
->)用作句柄成员访问操作符,而不是点(.)。 for each包含一个空格。- 类定义以分号(
;)结束。 - C++/CLI 用名为
main()的全局函数开始程序。
现在让我们继续;可以看到 C++/CLI 在表 3-6 中使用了关键字nullptr而不是null。
表 3-6。
null and nullptr
| C# | C++/CLI | | --- | --- | | `null` | `nullptr` |这些关键字的使用如表 3-7 所示。
表 3-7。
Usage of null and nullptr
| C# | C++/CLI | | --- | --- | | `class R` | `ref class R` | | `{` | `{` | | `static void Main()` | `static void Main()` | | `{` | `{` | | `R r = null;` | `R ^r = nullptr;` | | `}` | `}` | | `}` | `};` |C# 和 C++ 中的switch和goto有明显的区别,如表 3-8 所介绍。
表 3-8。
switch, case, and goto in C# and C++
| C# | C++ | | --- | --- | | 不允许 case 语句失败 | 允许 case 语句失败 | | `goto`案例 _ 陈述 | 不适用的 | | `goto`标签 | `goto`标签 | | `switch(string s)` | 不适用的 |在 C# 中,如果一个非空的 case 语句中缺少一个break或goto,编译器会发出一个错误。在 C++ 中,据说执行是从一个案例到它下面的案例,然后继续下一个案例。
两种语言都支持用户自定义标签的关键字goto。C# 允许 case 语句显式地使用goto。没有与 C++ 等价的语言,原因很大程度上是历史原因。在 C 语言中,switch / case / break与其说是一个正式的分支,不如说是对goto的宏替换。案例不是不同的块,而是作为切换目标的标签。c 开关是模仿汇编语言跳转表设计的。C++ 保留了它的传统。C# 试图采用一种更正式的抽象,在这种抽象中,案例是真正不同且不相关的实体,所以 C# 自然不支持 fall through。这两种抽象都有各自的优缺点。
C++ 不支持 C# 构造switch(string)。在 C++ 中,你必须使用if和else来扩展你的switch语句。参见表 3-9 了解goto中switch的用法以及 C# 和 C++ 中的穿越案例。
表 3-9。
Usage of switch in C# and C++
| C# | C++ | | --- | --- | | `// switch on a System.String and goto case` | `// equivalent to switch on a System::String` | | `string s="1";` | `System::String ^s="1";` | | `switch(s)` | `if(s=="1")` | | `{` | `{` | | `case "1":` | `}` | | `goto case "2";` | `else if(s=="2")` | | `case "2":` | `{` | | `break;` | `}` | | `}` | | | `// fall through case not available` | `// fall through case` | | | `int i,j=0;` | | | `switch(i)` | | | `{` | | | `case 1:` | | | `j++;` | | | `// no break, so case 1 falls into case 2` | | | `case 2:` | | | `break;` | | | `}` |数组和函数
在 C++/CLI 中,托管数组的声明是不同的(参见表 3-10 )。
表 3-10。
Managed Arrays in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `reftype []` | `array^` | | `valuetype []` | `array^` | | `class R` | `ref class R {};` | | `{` | | | `static void Main()` | `void main()` | | `{` | `{` | | `R[] n = new R[5];` | `array ^n = gcnew array(5);` | | `int[] m = {1, 2, 3, 4};` | `array ^m = {1, 2, 3, 4};` | | `m[3]=0;` | `m[3]=0;` | | `}` | `}` | | `}` | |虽然它们都是使用System::Array实现的,但是 C++/CLI 使用伪模板语法来声明它们。托管阵列将在第七章的中详细解释。伪模板的语法与过去 C++ 语言中添加扩展的方式一致,比如 cast 操作符(见第十六章)。
在 C# 和 C++ 中,都可以将修饰符附加到函数参数上。C# 和 C++/CLI 传递参数数组、引用参数、out 参数不同,如表 3-11 所示。
表 3-11。
Function Argument Modifiers
| C# | C++/CLI | | --- | --- | | `params T[]` | `... array ^` | | `ref` | `%` | | `out` | `[System::Runtime::InteropServices::Out] %` |我们稍后将再次讨论这些内容。
转换运算符
C# 运算符is和as执行的操作可以由 C++ 伪模板转换运算符static_cast<>()和dynamic_cast<>()执行(见表 3-12 )。
表 3-12。
C# and C++/CLI Conversion Operators
| C# | C++/CLI | | --- | --- | | `as` | `dynamic_cast<>()` | | `as` | `static_cast<>()` | | `is` | `(dynamic_cast<>()!=nullptr)` |转换运算符将在第十六章的中详细解释。
存储器分配
在 C++ 中,new操作符表示本机堆上的分配。在 C++/CLI 中添加了gcnew操作符来指示托管堆上的分配。C# 也使用new操作符在堆栈上分配值类型。在 C++ 中,这是不必要的,因为用于分配用户定义值类型实例的 C++ 语法与用于内置类型(如int)的语法是相同的。请参见表 3-13 了解在托管堆上分配时使用的关键字列表。
表 3-13。
Allocation on the Managed Heap in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `new`(参考类型) | `gcnew` | | `new`(值类型) | 不需要操作员 |下面是 C++/CLI 中本机堆和托管堆内存分配的一个简短示例:
value struct V {}; //value type
ref struct R {}; //reference type
struct N {}; //native type
void main()
{
N n;
N *pN = new N();
R ^r = gcnew R();
V v;
}
存储器分配将在第六章的中详细讨论。
可达性和可见性
accessibility 和 visibility 关键字相似,但语法不同。表 3-14 中列出了关键字差异,语法差异将在第八章中详细解释。
表 3-14。
Basic Protection Mechanisms
| 类型属性 | C# | C++/CLI | | --- | --- | --- | | 公众 | `public` | `public:` | | 不公开 | `private` | `private:` | | 装配 | `internal` | `internal:` | | 家庭的 | `protected` | `protected:` | | 家庭或集会 | `internal protected` | `protected public:` | | family 和 Assembly | 不适用的 | `protected private:` |属性、事件和委托
在第十章的中,我们将讨论属性、事件和代表,但参见表 3-15 中的介绍。
表 3-15。
Simple Example of a Property in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `class R` | `ref class R` | | `{` | `{` | | | `private:` | | `private int V;` | `int V;` | | | `public:` | | `public int Value` | `property int Value` | | `{` | `{` | | `get` | `int get()` | | `{` | `{` | | `return V;` | `return V;` | | `}` | `}` | | `set` | `void set(int newV)` | | `{` | `{` | | `V = value;` | `V = newV;` | | `}` | `}` | | `}` | `}` | | `}` | `};` |无商标消费品
在第十四章到第十六章中,你将学习泛型和模板,但是请参见表 3-16 中的介绍。
表 3-16。
Simple Example of a Generic in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `public class R` | `generic public ref class R` | | `{` | `{` | | | `private:` | | `private T m_data;` | `T m_data;` | | | `public:` | | `public R(T data)` | `R(T data)` | | `{` | `{` | | `m_data = data;` | `m_data = data;` | | `System.Console.WriteLine(m_data);` | `System::Console::WriteLine(m_data);` | | `}` | `}` | | `}` | `};` | | `public class R1` | | | `{` | | | `static void Main()` | `int main()` | | `{` | `{` | | `R r = new R(3);` | `R ^r = gcnew R(3);` | | `}` | `}` | | `}` | |内置类型
C# 和 C++/CLI 映射到具有不同关键字的 CLI 类型,并且 C++/CLI 映射尽可能与原生 C++ 保持一致。在我们进入第六章的之前,请参见表 3-17 进行介绍。
表 3-17。
Built-in Types
| C# | C++/CLI | | --- | --- | | `byte` | `char` | | `sbyte` | `signed char` | | `short` | `short` | | `ushort` | `unsigned short` | | `int` | `int, long` | | `uint` | `unsigned int`,`unsigned long` | | `long` | `long long` | | `ulong` | `unsigned long long` | | `single` | `float` | | `double` | `double` | | `string` | `System::String^` | | `object` | `System::Object^` | | `decimal` | `System:Decimal` | | `char` | `wchar_t` | | `bool` | `bool` |摘要
虽然 C# 和 C++ 之间的巨大差异初看起来令人望而生畏,但过一会儿就会出现一种模式。每种语言都经过智能设计,内部一致,C++ 语法很快就会变得直观。在下一章,我们将通过逐行将 C# 程序转换成 C++/CLI 来应用我们所学的知识。
四、C# 到 C++/CLI
这种感觉似曾相识。—约吉·贝拉
在这一章中,我将向你展示如何将一个基本的 C# 应用程序转换成 C++/CLI。我将在后面的章节中更详细地介绍更高级的转换方法。
转换 C# 应用程序
让我先从第二章中的 C# 洗牌程序的元素开始,并详细说明必要的改变,一次一个。我将从代码中抽取有代表性的样本并展示最终产品,而不是一行一行地迂腐地重复代码。
接下来是第二章的代码,插入了行号,这样你就不用来回翻动了:
01 using System;
02 namespace CSharp
03 {
04 public class Deck
05 {
06 uint[] Cards;
07 Random randomGenerator;
08 public enum Suit
09 {
10 Clubs = 0, Diamonds, Hearts, Spades
11 }
12 public enum Card
13 {
14 Ace=0, Deuce, Trey, Four, Five, Six, Seven,
15 Eight, Nine, Ten, Jack, Queen, King
16 }
17 Deck()
18 {
19 randomGenerator = new Random();
20 Cards = new uint[52];
21
22 for (uint u = 0; u < 52; ++u)
23 {
24 Cards[u] = u;
25 }
26 }
27 void Swap(ref uint u, ref uint v)
28 {
29 uint tmp;
30 tmp = u;
31 u = v;
32 v = tmp;
33 }
34 void Shuffle()
35 {
36
37 for (uint u = 52; u > 0; --u)
38 {
39 Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
40 }
41 }
42 uint RandomCard(uint Max)
43 {
44 return (uint)((double)Max * randomGenerator.NextDouble());
45 }
46 string CardToString(uint u)
47 {
48 Suit s = (Suit)(Cards[u] / 13);
49 Card c = (Card)(Cards[u] % 13);
50 return c.ToString() + “ of “ + s.ToString();
51 }
52
53 public static void Main()
54 {
55 Deck deck = new Deck();
56 deck.Shuffle();
57 for (uint u = 0; u < 5; ++u)
58 {
59 Console.WriteLine(deck.CardToString(u));
60 }
61 Console.ReadLine();
62 }
63 }
64 }
我们将从最初的声明性块开始,并从那里开始构建。
使用后添加关键字名称空间
在第 1 行,通过更改以下 C# 代码,将关键字namespace添加到using语句中:
using System;
到这个 C++
using namespace System;
当应用于一个名称空间时,using语句将该名称空间中的所有符号带入using语句的范围。在本例中,我们想使用Console类将数据输出到屏幕上。如果没有using语句,我们就必须通过编写System::Console来明确地告诉编译器如何找到Console类,只要我们想使用它。这称为完全限定名称。在 C++/CLI 中,我们需要将关键字namespace添加到using声明中。
在引用类型声明中添加标点符号^
在第 7 行和第 55 行,通过更改以下 C# 代码来更改引用类型声明:
Random randomGenerator;
Deck deck
到这个 C++
Random ^randomGenerator;
Deck ^deck
如果你在。MSDN 上的. NET Framework 类库引用,你发现Random是引用类型。你怎么知道?
在 MSDN 页面的 C# 部分,您会发现Random被声明如下:
public class Random
在 C++/CLI 部分,您会发现Random被声明如下:
public ref class Random
这两个声明都指示引用类型。在这种情况下,randomGenerator实际上是在托管堆上分配的Random实例的句柄。在 C++/CLI 中,它是一个句柄的事实是用句柄标点符号^明确表示的,randomGenerator的类型是Random^。在许多方面,randomGenerator可以被认为是指向托管堆上的类型为Random的对象的指针,但是不要把这个类比得太远。C++ 中的句柄和指针有很大的不同,我们将在第九章中进一步详细讨论。
修改内置类型
在第 6、22、27、29、37、42、44、46 和 57 行,更改 C# 别名:
uint
string
到 C++
unsigned int
System::String^
因为为 CLR 编译的 C# 和 C++ 都面向 CLI,所以一种语言的托管类型和另一种语言的托管类型之间总是有直接的相似之处。C++ 没有缩写形式uint,要求你使用扩展的unsigned int。
C++ 也没有内置string的类型,所以需要使用System::String^。在 C# 中,可以在内置别名string和System::String之间选择;它们是等价的。类型string内置于语言中,而System.String是等价的,通常通过using System语句引入。
Note
为什么 C++ 没有普通内置类型的标准缩写形式,比如unsigned int?嗯,这是我在 1987 年向 ANSI 委员会提出的,但是被否决了,因为“为现有类型添加同义词会将标识符添加到保留名称集中,而不会增加功能。”这种哲学在今天已经不那么流行了,C# 有一个System.String的同义词string就是证明。这不会增加任何功能,只是方便而已。在 C++ 中,固定宽度的整数类型,比如 int32_t,最终成为了语言。从 C99 开始,它们就已经是 C 语言了。
那么如果string和String相同,实际区别是什么呢?考虑下面的 C# 程序,注意没有任何using语句:
public class Hello
{
static void Main()
{
String s;
}
}
现在尝试编译它:
C:\Users\deanwi\Documents>csc g1.cs
Microsoft (R) Visual C# Compiler version 12.0.21005.1
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
g1.cs(5,9): error CS0246: The type or namespace name 'String' could not be found (are you missing a using directive or an assembly reference?)
如你所见,没有using System语句,编译器不知道String是什么。如果我们将String改为System.String,它将会编译。它甚至可以简单地编译成string。
注意,由于System.String是一个引用类型,C++/CLI 中使用的是句柄标点,所以正确转换到 C++/CLI 就给了我们System::String^。既然我们已经有了using namespace System,用String^就够了。
更改数组声明和类型参数
在第 6 行,更改 C# 数组声明:
uint[] Cards;
到 C++
array<unsigned int>^ Cards;
在这种情况下,声明看起来如此不同,很难想象它们表示相同的东西。这里的目标是告诉编译器,我们希望Cards是一个长度未知的无符号整数的托管数组。在表 4-1 中,你可以看到托管数组语法是如何在 C# 和 C++/CLI 之间映射的。
表 4-1。
Managed Array Declaration in C# and C++/CLI
| C# 数组语法 | C++/CLI 数组语法 | | --- | --- | | `type[]`变量名`;` | `array< type >^`变量名`;` |第一个变化是数组声明中使用的实际语法。C++/CLI 语言使用一种称为伪模板格式的语法,这是因为它看起来像 C++ 模板,但并不具备模板的所有特征。它使用了<和>字符,就像模板声明、实例化和使用的情况一样。此外,托管数组存储在托管堆上,所以 variable-name 是一个句柄,它需要^标点符号。
第二个变化是使用 typename unsigned int而不是uint,如前所述。
更改枚举声明
在第 8 行和第 12 行,更改 C# 中的以下枚举声明:
public enum Suit {}
public enum Card {}
到 C++
public:
enum class Suit {};
enum class Card {};
为了使这个枚举声明的语法正确,我们必须做三个小的改动。请注意,为了让事情变得更有趣,我在我们的翻译问题中添加了可访问性。首先,在 C++/CLI 中,嵌套类型、类字段、方法的可访问性指示符和public一样,都不是项特定的;它们是上下文特定的。在 C# 版本中,关键字public表示特定的enum Suit具有公共可访问性。在 C++/CLI 版本中,类中的关键字public:表示从那时起所有类型、方法和字段都具有公共可访问性。如果我们忽略了将关键字public添加到在Suit之后声明的下一个enum中,它将获得 C# 类的默认可访问性,这是私有的。另一方面,在 C++/CLI 版本中,缺少 accessibility 关键字,后续的enum声明将获得上下文可访问性级别,在这种情况下,它是公共的,因为在enum之前显式使用了public:。当在全局范围内处理可访问性时,C++ 语言也有一个特定于项目的public关键字。
第二个变化是 C++/CLI 托管枚举(类似于 C# 枚举)需要额外的关键字class来区别于本机 C++ 枚举。
最后一个变化是我们以前见过几次的——c++/CLI 类型定义在右花括号后以分号结尾。枚举是类型;名称空间不是。记住这一点的简单方法是,如果你能做一个,你需要一个尾随的分号。您可以实例化类型,但不能实例化命名空间。
更改对象分配和实例化
在第 19、20、22、37 和 55 行,更改以下 C# 代码:
randomGenerator = new Random();
Cards = new uint[52];
Deck deck = new Deck();
到 C++
randomGenerator = gcnew Random()
Cards = gcnew array<unsigned int>(52);
Deck^ deck = gcnew Deck();
这些表达式很容易从 C# 映射到 C++/CLI,这实际上只是一个习惯差异的问题。主要区别在于,C++ 通过要求关键字gcnew而不是关键字new来区分本机堆上的分配和托管堆上的分配。如果你碰巧用错了,编译器通常会礼貌地发出一条错误消息来提醒你。
更改“通过引用传递”参数
在第 27 行和第 39 行,更改以下 C# 代码:
void Swap(ref uint u, ref uint v)
Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
到 C++
void Swap(unsigned int %u,unsigned int %v)
Swap(Cards[u - 1], Cards[RandomCard(u)]);
我们需要修改Swap()函数声明以及Swap()的所有用法。代替 C# 关键字ref,C++/CLI 使用%标点符号来表示跟踪引用。在 C++/CLI 中,标点符号只在函数声明中使用,不在函数用法中使用。
在表 4-2 中,我列出了 C# 和 C++ 的对应关系,包括 C# out关键字的 C++ 实现。
表 4-2。
Parameter Passing in C# and C++/CLI
| C# | C++/CLI | | --- | --- | | `ErrorCode GetData(ref int data)` | `using namespace System::Runtime::InteropServices;` | | `{` | `ErrorCode GetData(int %data)` | | | `{` | | `}` | `}` | | `ErrorCode GetResult(out int result)` | `ErrorCode GetResult ([Out] int %result)` | | `{` | `{` | | `return GetData(ref result);` | `return GetData(result);` | | `}` | `}` |C++ 没有out关键字,但是可以使用[System::Runtime::InteropServices::Out]属性复制它的行为。
更改句柄的类成员访问运算符
在第 44、56 和 59 行,我们修改了 C# 中的类成员访问操作符:
randomGenerator.NextDouble()
deck.Shuffle()
deck.CardToString(u)
到 C++
randomGenerator->NextDouble()
deck->Shuffle()
deck->CardToString(u)
句柄和指针使用->类成员访问操作符访问它们的成员。如前所述,randomGenerator是一个句柄,访问托管堆上的数据需要->类成员访问操作符。
更改命名空间和静态访问的分隔符
在第 59 行和第 61 行,我们更改了 C# 中的以下分隔符:
Console.WriteLine()
Console.ReadLine()
到 C++
Console::WriteLine()
Console::ReadLine()
.类成员访问操作符是为实例化保留的,这表明您是直接访问数据,而不是通过句柄或指针间接访问。::分隔符,即范围解析操作符,用于限定名称空间和静态成员。
更改类声明
在第 4 行,我们修改了类声明。
class
到 C++
ref class
In addition, add a semicolon to the end of the class at line 63.
在 C++ 中,struct与class的区别仅在于可访问性和继承保护机制。此外,两者都不对应于 CLI 引用和值类型。空白关键字ref class和value class(以及ref struct和value struct)被添加到 C++ 中,以便表示这些类型。
添加函数 main()
C++ 程序以一个名为main()的全局函数开始。C# 程序以一个名为Main()的公共静态成员函数开始。因为这些函数有不同的签名,所以它们可以共存于一个 C++ 程序中,我们可以添加一个全局的main()函数来调用 C# Main()方法。这是添加函数main()的最简单的方法,无需改变代码的其余部分:
void main()
{
CPP::Deck::Main();
}
In addition, change line 53 from:
public static void Main()
到 C++
public:
static void Main()
完整的程序如下:
using namespace System;
namespace CPP
{
public ref class Deck
{
array<unsigned int>^Cards;
Random^ randomGenerator;
public enum class Suit
{
Clubs = 0, Diamonds, Hearts, Spades
};
public enum class Card
{
Ace=0, Deuce, Trey, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King
};
Deck()
{
randomGenerator = gcnew Random();
Cards = gcnew array<unsigned int>(52);
for (unsigned int u = 0; u < 52; ++u)
{
Cards[u] = u;
}
}
void Swap(unsigned int %u,unsigned int %v)
{
unsigned int tmp;
tmp = u;
u = v;
v = tmp;
}
void Shuffle()
{
for (unsigned int u = 52; u > 0; --u)
{
Swap(Cards[u - 1],Cards[RandomCard(u)]);
}
}
unsigned int RandomCard(unsigned int Max)
{
return(unsigned int)((double)Max * randomGenerator->NextDouble());
}
String^ CardToString(unsigned int u)
{
Suit s = (Suit)(Cards[u] / 13);
Card c = (Card)(Cards[u] % 13);
return c.ToString() + " of " + s.ToString();
}
public:
static void Main()
{
Deck^ deck = gcnew Deck();
deck->Shuffle();
for (unsigned int u = 0; u < 5; ++u)
{
Console::WriteLine(deck->CardToString(u));
}
Console::ReadLine();
}
};
}
void main()
{
CPP::Deck::Main();
}
将这段代码放在名为cardsconverted.cpp的文件中,编译并运行:
C:\>cl /nologo /clr:pure cardsconverted.cpp
cardsconverted
C:\>cardsconverted
Four of Diamonds
Ten of Spades
Ace of Spades
Ace of Hearts
Trey of Spades
那是更开放的。下注时间;我没有虚张声势。
摘要
在这一章中,我们经历了第二章的简单洗牌程序,逐行拆开,转换成 C#。因为将不同的语言结合在一起非常容易。NET,你不需要经常这样做。您将能够用 C# 和 C++/CLI,甚至 Visual Basic 创建模块和类库,并将它们绑定在一起,而无需更改单独的源文件。尽管如此,实际经历转换步骤的经验还是很有价值的。
在下一章,我们将探索一些工具,帮助你更有效地用 C#、C++ 和. NET 编程。
五、工具
当其他方法都失败时,清洁你的工具。—罗伯特·皮尔西格
在这一章中,我们将让自己熟悉一些工具。NET 开发在 C# 和 C++ 中都更容易。
卢茨.罗德尔的。网状反射器
最强大的工具之一。今天可用的. NET 开发是 Lutz Roeder 的。网反射器,现在由红门公司销售。这个程序是理解的无价工具。NET 程序集。它允许你反编译。NET 可执行文件以及库转换成 IL、C#、C++/CLI 和其他语言。
罗德的。NET Reflector 实现了一种独立于。NET 框架。它旨在通过反编译将元数据和 CIL 转换到更高的抽象层次。这个范例略有不同,因为它不是从程序本身内部调用的,所以从技术上讲它不是一个镜像;这更像是一次穿越镜子的旅行。你发射。NET Reflector,指定要查看哪个程序集或可执行文件,然后在里面看看。而System::Reflection中的类库主要用于运行时分析或处理。NET Reflector 擅长在封装后检查程序集。
什么是反思?
a。NET 汇编不仅仅是一系列的执行指令。它包含关于程序集内容的描述和属性,统称为元数据。英寸用. NET 的说法,反射是程序在运行时读取和分析另一个程序集的元数据的能力。当程序读取自己的元数据时,有点像照镜子,所以“反射”这个术语似乎很合适。名称空间System::Reflection专用于。NET 的类库实现了主要的反射机制。还有另外两个:CCI,允许你访问System::Reflection不能访问的信息的公共编译器基础设施,和IMetadata API。所有这些机制相辅相成。
反射允许您发现关于一个类型的所有元数据信息,而不需要实例化它。这包括但不限于以下项目:
- 名字
- 菲尔茨
- 方法
- 能见度
- 属性
反射使你能够发现这些物品的各种特征。这些包括字段修饰符(initonly、literal等)。)、方法类型(generic与否)、属性和事件。反射甚至允许您使用Reflection::Emit动态创建类型。
解码基类库程序集
基类库(BCL)是我们称之为。NET 框架。这个程序适用于所有的 BCL。NET 程序集,因此您可以查看任何 Microsoft DLLs 内部以帮助您的编程工作。当我试图找出程序中哪些 dll 必须通过#using引用或解析才能正确编译时,我发现这特别有用。例如,System::Collections::Generic名称空间被分成两部分,一部分在mscorlib.dll中,另一部分在System.dll中。很容易发现哪些类是在哪些 dll 中实现的。网状反射器。例如,如果你正在使用System::Collections::Generic::List<T>,你不需要引用任何 DLL,因为mscorlib.dll在每一个 C++/CLI 程序中都被隐式和自动引用。另一方面,如果您使用的是System::Collections::Generic::Stack<T>,您需要在项目设置中添加对System.dll的引用,或者在代码中添加以下代码行:
#using "System.dll"
在这两种情况下,如果您想在没有显式名称空间限定的情况下引用Stack<T>或List<T>,也需要下面一行:
using namespace System::Collections::Generic
关键字#using和using不相同;它们在 C++/CLI 中有不同的用途。这有点令人困惑,但这就是这种语言的定义。
#using是一种编译器指令,意思是它指示编译器在编译时如何做一些事情。在#using的情况下,它指示它添加一个对它的引用。NET 汇编,比如在 C# 编译器的命令行上用/reference做的事情。using是语言的一部分,而using namespace将符号纳入范围。
查看元数据和 CIL,或者进入深渊
我提到过。NET Reflector“允许你窥视内部”程序和“解码程序集”,但我没有解释这到底是什么意思。程序集,无论是编译为可执行文件还是动态链接库,都包含远不止可执行代码。这个信息集合被称为元数据,理想情况下,它是一个自包含的包,描述了使用这个程序集所需要知道的一切。
几年前,程序本身仅仅是可执行代码和数据的组合。能够执行另一个程序的唯一程序是操作系统本身,它充当信息路由器,定义如何在专用组件之间传递信息,例如设计用于硬件组件的设备驱动程序和想要与这些设备驱动程序通信的高级程序。
随着时间的推移,操作系统和程序不断发展。程序被设计用来与其他程序交换信息。在 Windows 世界中,这最初采取剪贴板的形式用于被动交换,OLE 1 用于主动交换。程序进化到不仅仅包含可执行代码;程序被绑定到资源文件,其中包含本地化和全球化信息,因此它们不仅可以被翻译成其他语言,还可以处理外国字符集、不同的货币、处理时间的方式以及其他特定于文化的信息。
那个。NET Framework 代表了一种范式的转变,它真正地将这些责任卸给了操作系统,或者在本例中是。NET 框架,可以认为是操作系统的扩展。
将有关程序集的尽可能多的信息绑定到单个文件中,并将其智能地组织为多种类型的数据或元数据的集合,这是。NET 框架。
可扩展类浏览器
微软和。Visual Studio 附带的. NET Framework IL 反汇编程序(ildasm.exe)和 Dependency Walker ( Depends.exe)允许用户检查或理解元数据的各个方面。ILDasm 允许您查看通用中间语言(CIL)以及元数据。CIL 构成了组成程序的可执行指令。卢茨.罗德尔的。NET Reflector 是一个类浏览器,可以显示程序集中所有方法的 CIL。它还更进一步,能够将 CIL 反编译成半普通的 C#、Visual Basic 和 Delphi。
Note
这个程序的优点之一是它有一个定义良好的代码模型,并接受第三方插件。一个将 CIL 反编译成 C++/CLI 的插件是我和 Jason Shirk 写的。它可以在 github —上免费获得,关于它的更多信息,后来已经有几十个程序被编写出来,用它来做令人惊奇的事情。网状反射器。在 http://www.red-gate.com/products/dotnet-development/reflector/add-ins .找到他们
从 C# 到 C++/CLI
可以用鲁兹的。NET Reflector 作为学习 C++/CLI 语法的教育工具。也可以用这个作为从 C# 转换到 C++/CLI 的工具。这样做不太令人满意,因为 Reflector 将元数据中的内容反编译成高级格式;通常,CIL 中有一些工件是由语言本身的编译或语法便利性创造出来的。实现这一点的算法很简单,使用以下步骤:
Create a C# program. Compile the program. Load the program in .NET Reflector. View any class definition or procedure using the C++/CLI add-in.
安装和装载。NET 反射器和 C++/CLI 外接程序
第一步是导航到红门的网站并获得。网状反射器。
http://www.red-gate.com/products/dotnet-development/reflector/
它可以试用 14 天,也可以立即购买。安装软件,然后打开 Visual Studio。它既可以独立运行,也可以作为 Visual Studio 外接程序运行。如果安装正确,您会在屏幕顶部看到一个新菜单。网状反射器。
现在安装 CppCliReflector 加载项。该项目位于:
http://www.sandpapersoftware.com
目前在 GitHub 上有一个带源代码的版本,在砂纸软件页面上有一个内置的二进制文件。从源代码构建是有指导意义的,所以请从以下网址下载源代码:
https://github.com/lzybkr/CppCliReflector
下载项目并解压缩源代码后,打开解决方案 CppCliReflectorAddin.sln,可能会要求您为最新版本的 Visual C++ 更新解决方案或项目。去吧,那不是问题。
它可能无法编译,因为插件需要直接从 Reflector 可执行文件中获取代码引用。我们将把这看作是一个学习如何引用外部可执行文件的机会。
首先,导航到解决方案资源管理器中的 References 部分。你会发现对反射器的引用不准确,如图 5-1 所示。
图 5-1。
The reference to the .NET Reflector executable
现在让我们通过再次添加来更新它—右键单击引用,然后单击添加引用。使用导航到。NET Reflector 可执行文件,它可能位于以下文件的某个变体中:
C:\Program Files (x86)\Red Gate\.NET Reflector\Desktop 8.3
现在构建项目。
接下来,让我们设置项目来加载反射器。右键点击图 5-1 中的 CppCliReflectorAddin,选择属性。选择调试选项卡,选择启动外部程序单选按钮,如图 5-2 。
图 5-2。
Exception generated by the .NET Framework
进入 Reflector.exe 之路,这可能是:
C:\Program Files (x86)\Red Gate\.NET Reflector\Desktop 8.3\Reflector.exe
现在,从 Visual Studio 调试菜单中,选择开始调试。。净反射器应加载。
从。反射器工具菜单,选择加载项。使用+按钮添加加载项。导航到 CppCliReflectorAddin 解决方案目录,向下浏览项目和 bin 子目录,直到找到:
CppCliReflectorAddin.dll
然后单击确定。
么事儿啦在那里。
现在 C++/CLI 已被添加到语言下拉列表中。这允许您通过在两种语言之间进行切换来查看 C# 代码在 C++/CLI 中的外观。
正在执行。网状反射器
让我们用做一个示例。网状反射器。编译下面的 C# 程序:
class Program
{
public static void Main()
{
System.Console.WriteLine("Hello, World!");
}
}
C# 视图
使用。NET Reflector,使用文件➤打开打开可执行文件(参见图 5-3 )。点击加号导航至Main()程序。展开{},它对应于全局名称空间。然后展开Program,导航到Main(),双击。确保下拉窗口显示 C# 作为反编译视图。
图 5-3。
C# view of the sample code using .NET Reflector
C++/CLI 视图
现在将下拉视图更改为 C++/CLI。视图应该切换成如图 5-4 所示的样子。
图 5-4。
C++/CLI view of the sample code using .NET Reflector
可以看到,在 C++/CLI 中,值类型和引用类型的声明和初始化是不同的;这将在第六章中详细讨论。
C++ 即时
即时 C++ 2 是一个有用的 C# 到 C++/CLI 的翻译器,可从有形软件解决方案( www.tangiblesoftwaresolutions.com )获得。该公司为遗留 C# 项目提供了一个易于使用、价格合理的翻译器。该软件不仅将孤立的 C# 代码片段转换成 C++/CLI,还能翻译完整的项目。
例如,假设我们使用 Snippet 转换器转换前面的示例(参见图 5-5 )。
图 5-5。
Conversion from C# to C++/CLI using Instant C++
这个代码片段已经可以编译了,保存了所有 C++ 程序都需要的必要的全局main()。只需添加下面一行,程序就可以用 C++ 编译了:
void main() { Program::Main();}
Visual Studio 附带的工具
Visual Studio 附带了许多非常有用的工具。在这一节中,我将介绍两个我最喜欢的。
微软。NET Framework IL 反汇编程序(ILDasm)
ildasm.exe是一个元数据和 CIL 浏览器,很像。净反射器,在更基本的水平切割。要在 VS2013 的开发人员命令提示符下使用 ILDasm,只需输入以下内容:
ildasm <assembly name>
您也可以从 IDE 的“工具”菜单中启动它。无论哪种情况,你都会看到一个类似于图 5-6 所示的窗口。
图 5-6。
ILDasm’s view of the test executable
单击加号展开每个类别下的定义。
依赖沃克(依赖)
Depends.exe是 Windows 二进制文件的依赖遍历器。它适用于。NET 程序集以及本机 Win32 二进制文件。这是一个追踪丢失的 dll 以及解决清单问题的非常有价值的工具。
首先从以下网址下载:
然后使用这个直观的命令来调用它:
depends <binary name>
您将看到一个类似于图 5-7 所示的窗口。
图 5-7。
Dependency Walker
如你所见,我似乎对一些丢失的 dll 有某种依赖,我想知道是什么导致了它们?
更多 Visual Studio 工具
无论你有多聪明,你的生产力都会受到工具质量的限制。虽然向您介绍 Visual Studio 2005 附带的所有工具已经超出了本书的范围,但是这里有一些更值得研究的工具:
- 可移植的可执行验证器确定一个程序集是否满足可验证代码的要求。
SN.exe:强名称实用工具对程序集进行数字签名。- 这个实用程序处理全局程序集缓存,这是一个机器范围的代码缓存,用于在一台计算机上的多个应用程序之间共享的程序集。
NMake.exe:Make 实用程序从命令行执行构建过程。MT.exe:清单工具是用来处理清单的。RC.exe:资源编译器用于处理.rc文件。ResGen.exe:资源生成器在格式之间转换资源。CLRVer.exe:CLR 版本工具确定安装在机器上的 CLR 的版本。- 这个工具允许你使用一个环境变量来追踪一个文件。
- 这个工具可以让你直观地比较同一个文本文件的不同版本。
guidgen.exe:该工具生成唯一的全局标识符。TLBImp.exe:类型库转换工具用于从类型库中导入类。
摘要
现在我们有了深入研究 C++/CLI 所需的工具。NET 实现。事不宜迟,让我们在下一章讨论数据类型。
Footnotes 1
OLE 是 COM 的前身,代表对象链接和嵌入。
2
Instant C++ 的版权归有形软件解决方案所有。所有图片和参考资料均经许可使用。