C++/CLI所支持的基本类型,例如int、double、bool等,在某些方面可以说是沿袭了ISO-C++中的类型——同样的用法会在C++/CLI中得到同样的结果,例如加法或者赋值操作。但是C++/CLI也为这些基本类型引入了一些新的东西。
在通用类型系统(CTS)中,每一个基本类型都在System命名空间中存在一个对应的类(见表1)。例如int实际上完全等价于System::Int32。我们可以使用二者中的任何一个来声明一个整数:
int ival = 0;
Int32 ival2 = 0;
出于移植性的考虑,在使用这些基本类型时,我们推荐大家使用内建的要害词,而非System命名空间中的类名。
对于System命名空间中类的公有静态成员,我们既可以通过内建的要害字,也可以通过System命名空间中的类名来访问。例如,为了获取一个数值类型的取值范围,我们可以直接使用内建的要害字来访问其静态属性MaxValue和MinValue。
int imaxval = int::MaxValue;
int iminval = Int32::MinValue;
每个数值类型都支持一个名为Parse的成员函数,用以将一个字符串转化为其所表示的数值。例如,给定下面的字符串:
String^ bonus = "$ 12,000.79";
更多内容请看C/C++技术专题专题,或
调用Parse会将myBonus初始化为12000.79:
double myBonus = double::Parse( bonus, ns );
其中ns表示对一些NumberStyles枚举类型取位或(bitwise or)运算的结果。NumberStyles是位于System::Globalization命名空间中的一个枚举类型,用于表征对空白、货币符号、小数点或者逗号等的处理。看下面的代码:
using namespace System;
using namespace System::Globalization;
double bonusString( String^ bonus )
{
NumberStyles ns = NumberStyles::AllowLeadingWhite;
ns = NumberStyles::AllowCurrencySymbol;
ns = NumberStyles::AllowThousands;
ns = NumberStyles::AllowDecimalPoint;
return double::Parse( bonus, ns );
}
我们也可以使用转型符号来在类型间进行显式的转换。
int ival = ( int ) myBonus;
或者使用System::Convert类的一些转换方法,例如ToDouble(), ToInt32(), ToDateTime()等:
int ival2 = Convert::ToInt32( myBonus );
两种转换方法采用的策略有所不同:显式转型会直接对小数部分进行截断,而Convert的成员函数则采用的是舍入算法。例如上面的例子中ival赋值后的结果为12000,而ival2赋值后的结果为12001。
我们还可以直接使用字面常量(literal)来调用其对应类型的成员函数,虽然这乍看起来有些怪异。例如,我们可以编写如下代码:
Console::Write( "{0} : ", ( 5 ).ToString() );
其中( 5 ).ToString()返回的是字面常量整数5的字符串表示。注重5外面的圆括号是必须的,因为它会使得编译器将后面的成员选择操作符点号绑定到整数5上,而不是将'5.'解析为一个double类型的字面常量——那样的话,后面的ToString()将变得不合法。为什么我们有时候需要这样做呢?一种可能的情况是将一个字符串传递给Console的成员函数要比传递实际的数值来的更加高效。
对于字符以及字符串这样的字面常量,我们也可以像上面的整数一样调用它们的成员函数,但是它们的行为有一点点晦涩。例如,下面的代码:
Console::WriteLine(( 'a' ).ToString() );
将在控制台上打印出97,而非'a'这个字符。要将字符'a'打印出来,我们需要将其首先转型为System::Char:
Console::WriteLine(((wchar_t)'a').ToString() );
C++/CLI对字符串字面常量采取了非凡的处理策略。从某种程度上来讲,字符串字面常量在C++/CLI中的类型更接近System::String,而非C风格的字符串指针。显然,这将对重载函数的辨析产生影响。例如:
public ref class R {
public:
void foo( System::String^ ); // (1)
void foo( std::string ); // (2)
void foo( const char* ); // (3)
};
void bar( R^ r )
{
// 调用哪一个foo呢?
r->foo( "Pooh" );
}
更多内容请看C/C++技术专题专题,或
在ISO-C++中,这将被辨析为第3个foo(),因为字符串字面常量更接近const char*,而非ISO-C++标准库中的string类型。但是,在C++/CLI中,上面的调用将被辨析为第1个foo(),因为现在字符串字面常量被认为更接近System::String,而非字符指针。
要理解其中的缘由,让我们往后退两步,先来看看ISO-C++和C++/CLI如何辨析一个重载函数,然后再来看ISO-C++和C++/CLI如何辨析一个字符串字面常量。
一个重载函数的辨析过程通常包含以下三个步骤:
1.选择候选函数集合。候选函数是指那些从词法范畴来看与所调用函数名相匹配的函数。例如,由于我们上面是在R的一个实例上调用foo(),所以所有名称为foo但却不是R或者其基类的成员的那些函数将不被认为是候选函数。这样看来,我们现在有三个候选函数,即R中三个名称为foo的成员函数。假如这个阶段得到的候选函数集合为空,那么调用将告失败。
2.从候选函数集合中选择可用函数集合。可用函数是指函数声明时的参数个数和它们的类型与调用时所指定的相匹配的那些函数。在我们上面的例子中,三个候选函数都是可用函数。假如这个阶段得到的可用函数集合为空,那么调用也将失败。
3.从可用函数集合中选择最匹配的函数。这个阶段将会对实际传递的参数和可用函数所声明的参数之间的转换进行一个排名。对于只含一个参数的函数来说,这个过程比较简单。但是对于含有多个参数的函数来说,这个过程就变得相对有些复杂。假如没有一个最佳的匹配函数胜出,那么调用将告失败。也就是说各个可用函数的参数类型到实际参数类型之间的转换被认为一样的好,换言之多个调用之间产生了混淆。
那么现在摆在我们面前有两个问题:(1)我们实际传递的参数"Pooh"到底是什么类型?(2)在判定类型转换的优劣时采用的是什么算法?
在ISO-C++中,字符串字面常量"Pooh"的类型为const char[5]——注重,在字符串字面常量后面有一个隐含的截断字符null。在上面的例子中显然不存在这样的精确匹配,因此必须应用某种形式的类型转换。这样,两个ISO-C++候选函数(2)和(3)将进行竞争:
void foo( std::string ); // (2)
void foo( const char* ); // (3)
那么编译器如何从中判定可用函数呢?C++语言对类型转换按照优先顺序定义有一个层级结构,在这个结构中,假如一种转换优于另一种转换,那么它将被排在前面。在C++/CLI中,我们将CLI类型的行为也集成到了ISO-C++的标准类型转换层级结构中。下面是对集成之后的层级结构的一个描述:
1)精确匹配是最好的。需要注重的是精确匹配并不意味着实际传递的参数类型和函数声明的形式参数类型完全匹配。它们只需要“足够接近”就可以了。我们下面将会看到,“足够接近”对于ISO-C++和C++/CLI中的字符串字面常量有着一些不同的含义。
2)在标准转换中,拓宽转换要优于非拓宽转换。例如,将short拓宽为int要优于将int转换为double。
3)标准转换优于装箱(boxing)转换。例如,将int转换为double优于将int装箱为Object。
4)装箱转换优于用户自定义的隐式转换。
5)用户自定义的隐式转换优于没有任何转换!
6)否则,用户必须使用显式的转型符号来表示期望的转换。
对于上面两个ISO-C++下的候选函数,将字符串字面常量转换为一个std::string属于上面第5条,即隐式调用string的构造器来创建一个临时string对象。而将字符串字面常量转换为一个const char* 属于上面第1条。第1条优于第5条,因此,参数为const char*的那个函数在这场竞争中胜出。
这种归属在第1条“精确匹配”下的trivial conversions实际上在技术的定义上是很严格的。总共有4种这样的trivial conversions可以被归为精确匹配。即使在这4种trivial conversions中,为了规范语言对类型的选择,它们也有一个优先级的排序。
大多数读者和程序员可能对于这样的细节没有多大爱好,并且通常情况下我们也无须深入到这些细节的地方。但是假如我们要得到一个直观的语言行为、并且确保它们在不同的实现上表现相同,这些规则的存在就很有必要。这是因为作为一门编程语言,它的行为一般要具有某种程度的“类型感知”能力,从而答应程序员忽略这些细节。 下面让我们来对这4种trivial conversions做一简单的了解。其中3种被称为左值转换(lvalue transformation)。左值(lvalue)是一个可寻址的,可被执行写操作的程序实体。第4种为限定性转换(qualification conversion),例如,在一个类型声明上加一个const修饰符就属于这种转换。其中3种左值转换优于限定性转换。
在我们上面的例子中,由本地数组到指针的转换,即由const char [5]到const char *,就是一种左值转换。
在大多数情况下,我们甚至不将这看作一种转换。
这种形式的左值转换在C++/CLI中仍然适用,但是在我们将System::String类引入之后,字符串字面常量到const char*的转换就不再是最好的匹配了。实际上,在C++/CLI中,"Pooh"这样的字符串字面常量的类型既是const char[5](C++/CLI对本地类型系统保留后的结果),同时也是System::String(C++/CLI中的托管类型)。这样,在C++/CLI中,字符串字面常量和System::String类型之间是一个精确的匹配,它优于由字符串字面常量到const char*的trivial conversion。
有些朋友看到这里,可能会不兴奋,“为什么要这样做?难道ISO-C++对字符串字面常量的处理不能满足C++/CLI的绑定需要吗?”C++/CLI这样做的理由在于字符串字面常量是我们程序中的一个基本元素,而ISO-C++的行为在很多情况下显得并不直观。实际上,这些规则在我们现在看到的结果之前的一年中被来往返回改了3次之多。
这反映了ISO-C++和C++/CLI在对待各自的类型系统时存在的一个基础性差异。在ISO-C++中,除非是显式存在于一个类库中,否则类型就是独立的。因此,在字符串字面常量和std::string之间并没有隐含的类型关系,虽然它们共享着同一个抽象域(domain)。
但是在C++/CLI中,我们支持统一的类型系统。每一个类型,包括字面常量值,都是一个Object的子类。这也是我们为什么可以在一个字面常量值,或者内建类型的对象上直接调用方法的原因。整数字面常量5的类型为Int32,字符串字面常量"Pooh"的类型为String。认为字符串字面常量更接近C风格的字符串,或者就把它看作C风格的字符串是不合适的。
集成后的类型转换层级结构使得一个正常运行的ISO-C++程序在使用/clr编译器开关重新编译后仍能展现同样的行为,但是使用CLI类型的新的C++/CLI程序在处理字符串字面常量时将会体现新的类型优先排序规则。这段讨论的长度相对于这个主题的重要性而言可能并不合适,但是它却向大家揭示了我们到底在将CLI类型系统和ISO-C++语义框架集成在一起的时候,做了哪些工作,以及如何在必要的时候调整在集成过程中所出现的各个情况的优先级。同时,这也提醒大家在将一个本地类重新构造为一个CLI类的过程中需要注重的一些问题。比如有些情况下我们最好要对那些接受字符串字面常量的成员函数进行新的设计,而不是简单地将一个参数为String的函数添加到这些重载函数集合中了事。
另外需要注重的是,String表示的是Unicode字符集。和ASCII字符集不同,这需要两个字节来表示一个字符。虽然在C++/CLI中字符串字面常量的类型为String,但这并不意味着在C++/CLI中,一个字符串字面常量必然会被解析为双字节的字符流。在本地C++中,我们要在字符串字面常量前加一个L来告诉编译器将其看作一个双字节的字符流。在C++/CLI中,我们仍然需要这么做。
更多内容请看C/C++技术专题专题,或