从今日开始,将前期学习《Visual C++ 2005入门经典》(Ivor Horton著 清华大学出版社出版)的相关笔记整理到随笔中,希望能和C++/CLI爱好者分享学习过程中的心得。文中主要内容和例子摘自原书相关章节,如有侵权,请留言或来信告知。

相比于ISO/ANSI C++而言,C++/CLI进行了大量的扩充,并且提供了大量的附加功能。主要包括:

  • 在C++/CLI程序中,所有ISO/ANSI基本数据类型都可以使用,但在一些特殊的上下文环境中,它们具有一些额外属性;
  • 在控制台程序中,C++/CLI对键盘和命令行输出提供了自己的机制;
  • C++/CLI中引入了safe_cast运算符,确保强制类型转换操作能够生成可检验的代码;
  • C++/CLI提供了另外一种基于类的枚举功能,其灵活性超过了ISO/ANSI C++中的enum声明。

一、基本数据类型

C++/CLI中包括了所有ISO/ASNI C++中的基本数据类型,算术运算也和本地C++完全一样。除此之外,C++/CLI中还定义了2种整数类型,如表1所示:

表1:C++/CLI新增基本数据类型

类型

字节

值域

long long

8

从-9223372036854775808到9223372036854775807

Unsigned long long

8

  从0到18446744073709551615

指定long long数据类型时,需要在整数数值后面加LL或小写字母ll,如

longlong big = 123456789LL;

指定unsinged long long类型时,需要在整数数值后面加ULL或小写字母ull,如

unsigned long long huge = 123456789LL;

在C++/CLI中,每一个ISO/ANSI C++基本类型名称都映射到System命名空间中定义的值类类型。在C++/CLI程序中,ISO/ANSI C++基本类型名称都是CLI中对应值类类型的简略形式。表2给出了基本类型、占用内存以及对应的值类类型。

表2:基本类型与CLI值类型

基本类型

字节

CLI值类类型

bool
char
singed char
unsigned char
short
unsigned short
int
unsigned int
long
unsigned long
long long
unsigned long long
float
double
long double
wchar_t

1
1
1
1
2
2
4
4
4
4
8
8
4
8
8
2

System::Boolean
System::SByte
System::SByte
System::Byte
System::Int16
System::UInt16
System::Int32
System::UInt32
System::Int32
System::UInt32
System::Int64
System::UInt64
System::Single
System::Double
System::Double
System::Char

默认情况下,char类型被视为singed char,因此其关联的值类类型为System::SByte。如果编译选项/J,则char 默认为unsigned char,此时关联为System::Byte。System为根命名空间名,C++/CLI的值类类型在这个空间中定义。此外System空间中还定义了许多其他类型,如表示字符串的String类型、精确存储的十进制小数类型Decimal等等。

在C++/CLI中,关联的值类类型为基本类型添加了重要的附加功能。编译器在需要时,将安排原值与关联类型之间的自动转换,其中从原值转换为关联类型成为装箱(boxing),反之称为拆箱(unboxing)。根据上下文环境,这些变量将表现为简单的值或者对象。

由于ISO/ANSI C++基本类型的名称是C++/CLI程序中值类类型名称的别名,所以原则上C++/CLI代码中可用任何一种名称。

int count = 10; 
double value = 2.5;

与下面的代码是等价的

System::Int32 count = 10; 
System::Double value = 2.5;

上面2种代码是完全合法的,但应尽量使用基本类型名称,如int和double,而不是System::Int32和System::Double。这是因为上面描述的这种映射关系仅适用于Visual C++ 2005及以上版本的编译器,其他版本编译器未必实现这种映射关系。

将基本类型转换为值类类型是C++/CLI的一个重要特征。在ISO/ANSI C++中基本类型与类类型完全不同,而在C++/CLI中,所有数据都以类类型的形式存储,包括值类型(存储在堆栈上)和引用类型(存储在堆上)2种。

例子:Fruit CLR控制台项目

在Visual Studio 2005中创建CLR Console Application项目,输入名称Ex2_12,将生成如下文件

// Ex2_12.cpp : main project file. 
#include "stdafx.h" 
using namespace System; 
int main(array<System::String ^> ^args) 
{ 
    Console::WriteLine(L"Hello World"); 
    return 0; 
}

main函数后的参数为命令行参数。然后按如下方式改写代码

- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::开始==>> [Ex_12.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Ex2_12.cpp : main project file. 
#include "stdafx.h" 
using namespace System; 
int main(array<System::String ^> ^args) 
{ 
    int apples, oranges; 
    int fruit; 

    apples = 5; 
    oranges = 6; 
    fruit = apples + oranges; 
    Console::WriteLine(L"\nOranges are not the only fruit ..."); 
    Console::Write(L"- and we have "); 
    Console::Write(fruit); 
    Console::Write(L" fruit in all.\n"); 

    return 0; 
}
- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::结束==>> [Ex_12.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

编译后执行得到如下输出:

Oranges are not the only fruit?- 
- and we have 11 fuit in all. 

与ISO/ANSI C++版本比较,变量的类型int将成为C++/CLI类型System::Int32。如果用System::Int32替换代码中的int,然后重新编译运行,结果将没有变化。

WriteLine()函数为C++/CLI函数,定义在System命名空间的Console类中。Console表示标准输入输出流。Write()函数为该类的另一个输出函数,不会自动换行。下面专门讨论C++/CLI的控制台输入输出。     

二、控制台输出

C++/CLI中特有控制台格式化输出功能,例如可用下面的代码来输出字符串与变量混合文本。

Console::WriteLine(L"There are {0} fruit.", fruit);

Console::WriteLine()的第一个参数是L”There are {0} fruit.”,其中{0}为格式化占位符,表示在此处插入第二个参数的值,如果有更多需要插入的参数,则该参数对应的占位符编号继续增加:{1}、{2}、{3}…。在第一个字符串参数中,编号的顺序可以颠倒,如

Console::WriteLine(L"There are {1} packages weighting {0} pounds", packageWeight, packageCount);

格式化占位符还可以控制显示的格式,如{1:F2}表示第2个参数显示成有2位小数的浮点数,冒号后的为格式规范

Console::WriteLine(L"There are {0} packages weighting {1:F2} pounds ", packageCount, packageWeight);

输出为

There are 25 packages weighting 7.50 pounds.       

一般说来,可以编写格式为{n,w:Axx}的格式规范,其中n为索引值,用于选择逗号后的第几个参数; A为单个字母,表示如何对变量格式化;xx为1个或2个数字,指定参数的精度;w为有符号整数,表示可选的字段宽度范围,如果w为+,则字段右对齐,如果w为-,则左对齐。如果数值的位置数小于w指定的位置数,则多出来的用空格填充,如果数值的位置数大于w指定的位置数,则忽略w的限定:

Console::WriteLine(L"Packages: {0,3} Weight: {1,5£ºF2} pounds ", packageCount, packageWeight);

输出为(下划线表示空格填充位):

Packages: _25 Weight: __7.50 pounds

可选的格式说明符如表3所示

表3:格式说明符

格式说明符

说明

C或c

把值作为货币量输出

D或d

把整数作为十进制值输出。如果指定的精度大于位数,则在数值的坐标填充0

E或e

按照科学技术法输出浮点值

F或f

把浮点数作为±####.##的定点数输出

G或g

以最紧凑的形式输出,取决于是否指定了精度值,如果没有则适用默认精度

N或n

把值作为定点十进制值输出,必要时以3位一组用逗号分隔

X或x

把整数作为十六进制值输出。根据X或x,以大写或小写输出。

例子:计算地毯价格,演示CLR控制台程序的格式化输出

- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::开始==>> [Ex_13.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Ex2_13.cpp : main project file.
#include "stdafx.h"
using namespace System;
int main(array<System::String ^> ^args)
{
	double carpetPriceSqYd = 27.95;
	double roomWidth = 13.5;			// in feet
	double roomLength = 24.75;			// in feet
	const int feetPerYard = 3;
	double roomWidthYard = roomWidth/feetPerYard;
	double roomLengthYard = roomLength/feetPerYard;
	double carpetPrice = roomWidthYard*roomLengthYard*carpetPriceSqYd;

	Console::WriteLine(L"Room is {0:F2} yards by {1:F2} yards",
		roomWidthYard, roomLengthYard);
	Console::WriteLine(L"Room area is {0:F2} square yards", 
		roomWidthYard*roomLengthYard);
	Console::WriteLine(L"Carpet price is ${0:F2}", carpetPrice);
	return 0;
}
- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::结束==>> [Ex_13.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

三、控制台输入

.Net Framework的控制台键盘输入功能有限,可以适用Console::ReadLine()函数把整行输入作为字符串读取,或者使用Console::Read()读取单个字符,还可以适用Console::ReadKey()读取按键。ReadLine()的例子如下:

ReadLine()用于将整行文本存入字符串中,按下Enter键时,文本结束。变量line为String^型,表示String数据类型的引用,line为Console::ReadLine()函数读入字符串的引用。

String^ line = Console::ReadLine();

Read()用于逐字符的读入输入数据,并将其转换成对应的数字值。ReadKey的例子如下:

char ch = Console::Read();

ReadKey()用于读取按键值,并返回ConsoleKeyInfo对象,该为对象定义在System命名空间中的值类型。参数true表示按键不在命令行上显示出来,false则表示显示按键回显。按键对应的字符可用ConsoleKeyInfo对象的KeyChar得到。

ConsoleKeyInfo keyPressed = Console::ReadKey(true);
Console::WriteLine(L"The key press corresponds to the character: {0}", keyPress.KeyChar);

尽管C++/CLI控制台程序中不能格式化输入,但输入一般都通过窗口组件得到,因此这仅仅是一个小缺陷。

四、强制类型转换safe_cast

在CLR环境中safe_cast用于显示的强制类型转换。safe_cast用于将一种类型转换为另一种类型,在不成功时能够抛出异常,因此在C++/CLI中使用safe_cast是比较好的选择。其用法和static_cast一样:

double value1 = 10.5; 
double value2 = 15.5; 
int whole_number = safe_cast<int>(value1) + safe_cast<int>(value2);

五、枚举

C++/CLI的枚举与ISO/ANSI C++有较大的区别。下例为C++/CLI中的一个枚举类型:该语句定义了一个枚举类型Suit,该类型的变量只能被赋值枚举定义中的值,且必须用枚举类型名称限定枚举常数。

enum class Suit {Clubs, Diamonds, Hearts, Spades};
Suit suit = Suit::Diamonds;

注意class关键字跟在enum之后。说明该枚举类型为C++/CLI,该关键字还表明在定义中规定的常量: Clubs\Diamonds\Hearts\Spades都是类对象而非ISO/ANSI C++中的基本类型(整型)值。实际上,默认情况下这些都是Int32类型的对象。

由于C++/CLI枚举定义中的变量都是类对象,因此不能在函数内部定义。

(一)指定枚举常量的类型

枚举中常量的类型可以是下表中任一基本类型:

short

int

long

long long

signed char

char

unsigned short

unsigned int

unsigned long

unsigned long long

unsigned char

bool

要指定一个枚举常量的类型,可以在枚举类型名称之后写入常量类型名称(要用冒号隔开),下例枚举类型中的常量为Char类型,对应的基本类型为char。其中第一个常量默认情况下对应于代码值0,后面的依次递增。

enum class Face:char { Ace,Two,Three,Four,Five,Six,Seven,Eight,Nine,Ten,Jack,Queen,King};

(二)指定枚举常量的值

可以赋予枚举类型定义中的一个或全部常数对应的值,下例使得Ace获得1,Two获得2,其余依此类推,直到King=13。

enum class Face:char { Ace=1,Two,Three,Four,Five,Six,Seven,Eight,Nine,Ten,Jack,Queen,King};

如果想让Ace获得最大值,则可以如下定义:

enum class Face:Char { Ace=14,Two=2,Three,Four,Five,Six,Seven,Eight,Nine,Ten,Jack,Queen,King};

例子:使用枚举类型

- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::开始==>> [Ex_14.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Ex2_14.cpp : main project file.
#include "stdafx.h"
using namespace System;

enum class Suit {Clubs, Diamonds, Hearts, Spades};

int main(array<System::String ^> ^args)
{
	Suit suit = Suit::Clubs;
	int value = safe_cast<int>(suit);

	Console::WriteLine(L"Suit is {0} and the value is {1}", suit, value);
	suit = Suit::Diamonds;
	value = safe_cast<int>(suit);
	Console::WriteLine(L"Suit is {0} and the value is {1}", suit, value);
	suit = Suit::Hearts;
	value = safe_cast<int>(suit);
	Console::WriteLine(L"Suit is {0} and the value is {1}", suit, value);
	suit = Suit::Spades;
	value = safe_cast<int>(suit);
	Console::WriteLine(L"Suit is {0} and the value is {1}", suit, value);
    return 0;
}
- - - - - - - - - - - - - - - - <<== 华丽的分割线 ::结束==>> [Ex_14.cpp] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

该例子的输出为

Suit is Clubs and the value is 0 
Suit is Diamonds and the value is 1 
Suit is Hearts and the value is 2 
Suit is Spades and the value is 3 

例子说明

  • Suit为枚举类型,不能在函数main()内部定义,因此只能定义为全局作用域内。
  • 必须用类型名称Suit限定枚举常量,如Suit::Clubs,否则编译器将无法识别。
  • 变量suit的值为类对象,要获取其值必须显示的将其转换成int类型。