Standard ECMA-372 1st Edition / December 2005
C++/CLI Language Specification
C++/CLI 语言详述
译者:Enzo Yang
第八章:语言概览
关于这个章节(clause)的信息.
这个C++版本是标准C++规范的一个超集.这一章描述了这个C++版本的重要特征, 致力于简单完整地把这些特征阐明, 在后面的章节会把这些特征展开说明. 这个章节的目的是提供给读者一些对早期程序有用的关于语言的介绍, 以及让大家更方便阅读以后的章节.
8.1 开始
标准的”hello,world”程序在这里是这样写的:
int main() {
System::Console::WriteLine(“hello, world”);
}
C++/CLI的源文件一般是放在一个或几个扩展名为.cpp的文件中,比如说hello.cpp. 使用命令行编译器(比微软的是cl),这个hello.cpp可以用如下命令编译
cl hello.cpp
这样就产生了一个名为hello.exe的程序. 这个程序运行时会输出:
hello,word
在输出的结尾WriteLine函数会自动地加上一个换行.
CLI库是被组织成好多个名字空间(namespace)的,最常用的是System名字空间. 那个名字空间含有一个引用类(ref class)叫做Console, 它提供了一组控制台的I/O函数. 其中一个就叫做WriteLine, 当传递一个字符串参数给该函数,那个函数就会输出那个字符串和一个空行到控制台上.(从这个例子可以认为System名字空间是一个using声明(using-declaration)的对象.)
8.2 类型(types)
值类型(value class type)与句柄类型(handle class type)的不同在于值类型的变量直接包含他们的数据而句柄类型的变量保存对象的句柄(store handles to object). 使用句柄类型,我们能够使两个变量引用同一个CLI对象, 因此可以用两个变量操作同一个对象. 使用值类型, 每个变量都有自己的对象, 因此不可以通过对一个变量所代表的对象的操作而影响另一个变量所代表的对象.
比如说
ref class Class1 {
public:
int Value;
Class1() {
Value = 0;
};
int main() {
int val1 = 0;
int val2 = val1;
val2 = 123;
Class1^ ref1 = gcnew Class1;
Class1^ ref2 = ref1;
ref2->Value = 123;
Console::WriteLine(“Value: {0}, {1}”, val1, val2);
Console::WriteLine(“Refs: {0}, {1}”,ref1->Value, ref2->Value);
}
显示出它们的不同点.输出是:
Values: 0, 123
Refs: 123, 123
对局部变量(local variable)val2的赋值不影响本地变量val1,这是因为这两个局部变量是原始类型(primitive types)(原始类型也是值类型), 每个原始类型的局部变量都有自己的储存空间(storage). 相反, ref2->Value = 123;这个赋值影响了ref1和ref2这两个引用指向的CLI对象.
这行代码:
Console::WriteLine(“Value: {0}, {1}”, val1, val2);
Console::WriteLine(“Refs: {0}, {1}”,ref1->Value, ref2->Value);
值得深入解释一下, 正如它们所演示的一些字符格式化行为那样, Console::WriteLine可以容纳不定数量的参数. 第一个参数是一个字符串, 这个字符串可以包含一些像{0}和{1}这样的占位符(placeholder), 每个占位符都代表着后面的一个参数(argument),比如{0}代表第二个参数, {1}代表第三个参数. 在输出到控制台之前, 每个占位符都会被与那些参数对应的字符串值代替.
开发者可以通过enum和value class定义新的值类型.
下面是如何定义每种类型(kind)的类(type)的例子.后面的章节会详细介绍.
public enum class Color {
Red, Blue, Green
};
public value struct Point {
int x, y;
};
public interface class IBase {
void F();
};
public interface class IDerived : IBase {
void G();
};
public ref class A {
protected:
virtual void H() {
Console::WriteLine(“A.H”);
}
};
public ref class B : A , IDerived {
public:
virtual void F() {
Console::WriteLine(“B::F, implementation of IBase::F”);
}
virtual void G() {
Console::WriteLine(“B::G, implementation of IDerived::G”);
}
protected:
virtual void H() override {
Console::WriteLine(“B::H, override of A::H”);
}
};
public delegate void MyDelegate();
像上面Color, Point和Ibase这样没有定义在其它类型里面的类型(或者说是最高层类型(top-level types)), 可以在它们前面加上public或者priviate可见性修饰关键字(visibility specifier). 上面例子的public是用来指明该类型是能被它上一层assembly之外见到的. 相反, private指名该类型是不可以被它上一层assembly以外看到. 默认最高层类型的可见性是private.
8.2.1 基本类型和CLI
每一个基本类型都有编译器(implementation)提供的一个对应的值类型, 这种对应是编译器定义的(implementation-defined). 比如说, 一个编译器可能会指定int对应System::Int32, 而另一个编译器指定int对应System::Int64. 关键字名有一般标准C++的意义, CLI名则指明特定的CLI平台类型. [example: int代表编译器定义的自然(natural)整型,而Int32代表在CLI平台上一个正好32位的整型. end example]
下面的表格列出了基本类型和它们对应的CLI类型(不同编译器可能会有不同的对应,这里只是其中一种)
类 | 描述 | 对应的CLI类型 |
bool | 布尔类型; 一个bool 不是true就是false | System::Boolean |
char | 8位 有符号或无符号整数类型 | System::SByte (有选项指定有无符号) |
signed char | 8位整数类型 | System::SByte |
unsigned char | 8位无符号整数类型 | System::Byte |
short | 16位有符号整数类型 | System::Int16 |
unsigned short | 16位无符号整数类型 | System::UInt16 |
int | 32位有符号整数类型 | System::Int32 |
unsigned int | 32位无符号整数类型 | System::UInt32 |
long | 32位有符号整数类型 | System::Int32 |
unsigned long | 32位无符号整数类型 | System::UInt32 |
long long int | 64位有符号整数类型 | System::Int64 |
unsigned long long int | 64位无符号整数类型 | System::UInt64 |
float | 单精度浮点数类型 | System::Single |
double | 双精度浮点数类型 | System::Double |
long double | 扩展精度浮点数类型 | System::Double(选择IsLong) |
wchar_t | 16位Unicode单元 | System::Char |
另外还有三个CLI库提供的类型值得提一下,虽然它们不是基本类型:
System::Object 是所有值类型和句柄类型的基类
System::String 是一个Unicode字符串
System::Decimal 一个精确的支持最少28位有效数字的十进制数类
C++/CLI没有关键字与它们对应.
8.2.2 转换
CLI定义了一系列新的转换. 包括句柄和参数数组转换和其他.(注:这里没有详细描述)
8.2.3 CLI数组类型
CLI数组和本机数组不同(C++ Standard &8.3.4). CLI的数组是分配在CLI堆的,可以有多于一列(rank). 列确定了与数组元素相关的数字指标. CLI数组的列也就是通常说的维数. 一个只有一列的数组称为CLI一维数组(single-dimensional CLI array), 多于一列的CLI数组称为多维数组(multi-dimensional CLI array).
在这本书定义的标准中, CLI数组指CLI的数组. 本地数组(native array)或者数组(array)指C++风格的数组.
一个CLI数组类型是用一个固定的(built-in)仿模板(pseudo-template)的句柄类语法定义的:
namespace cli {
template<typename T, int rank = 1>
ref calss array : System::Array {
};
}
下面是使用这个仿模板的例子:
int main() {
array<int> ^ arr1D = gcnew array<int>(4) {10, 42, 30, 12};
Console::Write(“The {0} elements are:”, arrID->Length);
for each (int i in arrID) {
Console::Write(“{0,3}”,i);
}
Console::WriteLine();
array<int,3>^ arr3D = gcnew array<int, 3>(10, 20, 30);
}
输出是:
The 4 elements are: 10 42 30 12
arr1D句柄可以指向任何一维int数组. 在例子中它指向了一个含有四个元素的数组. 数组的只读属性Array::Length包含了元素的个数. arr3D可以指向任意的三维int数组. 例子中它指向一个10X20X30的数组,每个元素都会默认为0.
8.2.4 统一的类型系统(Type system unification)
C++/CLI 提供了一个统一的类型系统. 所有的值类型和句柄类型都继承自System::Object. 这样就有可能在向调用所有值的实例的成员函数,甚至是像int这样的基本类型.
int main() {
Console::WriteLine((3).Tostring());
}
这个例子调用了System::Int32里的ToString函数,返回一个字符串”3”用于输出.(注意,那两个看上去多余的括着3的括号不是多余的,这里3被装箱了;他们需要取得符号”3”和”.”而不是取得”3.”.)
int main() {
int i = 123;
Object^ o = i; //装箱(boxing)
int j = safe_cast<int>(o); //拆箱(unboxing)
}
这个例子更有趣. 一个int值可以转换成System::Object^然后转回int. 这个例子展示了装箱和拆箱. 当一个值类型要转换成一个句柄类型时, System::Object这个箱(box)分配出空间来储存那个值,那个值被复制到箱里. 拆箱则相反. 当System::Object箱句柄被转换回原来的值类型, 那个值被复制出箱子到适合的存储空间.
这种统一的类型系统提供了值类型和基类Object的好处是显而易见的.(这句话抄自大便天下的狗窝). 当程序不希望int值表现得像CLI 对象时, int值就是简单的32位值. 当希望int值表现得像CLI对象时, 它又根据需要转换成一个对象. 这种可以把一个值看成CLI对象的能力在好多语言都存在值类型和引用类型的鸿沟上架设了桥梁. 比如, 一个Stack类可以提供Push和Pop函数来压入或弹出Object^.
public ref class Stack {
public:
Object^ Pop() {…}
Void Push(Object^ o) {…}
};
因为C++/CLI拥有统一的类型系统, 这个Stack类可以用来存储所有类型包括int的数据.
8.2.5 指针,句柄和null
标准C++支持指针类型和空指针常数. C++/CLI添加了句柄类型和空值. 为了整合句柄, 和拥有一个广义上的null, C++/CLI定义了一个叫做nullptr的关键字. 这个关键字代表一种没有类型(null type)的常量. nullptr属于空值常量(null value constant). (空类型是不可以实例化的,唯一取得一个空值常量的途径就是使用nullptr关键字.)
有了nullptr空指针常量的定义加强了(之前标准C++需要编译时把这个常量转换成0). nullptr可以隐式转换成任何指针或句柄类型. 这样nullptr就可以在关系,条件,赋值等表达式上面使用.
Object^ obj1 = nullptr; // handle obj1 has the null value
String^ str1 = nullptr; // handle str1 has the null value
if (obj1 == 0); //false (0 is boxed, the two handles differ)
if (obj1 == 0L); //false (0 is boxed, the two handles differ)
if (obj1 == nullptr); //true
char* pc1 = nullptr; //pc1 is the null pointer value
if (pc1 == 0); //true as zero is a null pointer value
if (pc1 == 0L); //true
if (pc1 == nullptr); //true as nullptr is a null pointer constant
int n1 = 0;
n1 = nullptr; //error, no implicit conversion to int
if (n1 == 0); //true, performs integer comparison
if (n1 == 0L); //true, performs integer comparison
if (n1 == nullptr); //error, no implicit conversion to int
if (nullptr); //error
if (nullptr == 0); //error, no implicit conversion to int
if (nullptr == 0L); //error, no implicit conversion to int
nullptr = 0; //error, nullptr is not an lvalue
nullptr + 2; //error, nullptr can’t take part in arithmetic
Object^ obj2 = 0; //obj2 is a handle to boxed zero
Object^ obj3 = 0L; //obj3
String^ str2 = 0; //error, no conversion from int to String^
String^ str3 = 0L; //
char* pc2 = 0; //pc2 is the null pointer value
char* pc3 = 0L; //pc3
Object^ obj4 = expr ? nullptr : nullptr; //obj4 is the null value
Object^ obj5 = expr ? 0 : nullptr; //error, no composite type
char* pc4 = expr ? nullptr : nullptr; //pc4 is the null pointer value
char* pc5 = expr ? 0 : nullptr; //error, no composite type
int n2 = expr ? nullptr : nullptr; //error, no implicit conversion to int
int n3 = expr ? 0 : nullptr; //error, no composite type
sizeof(nullptr); //error, the null type has no size
typeid(nullptr); //error
throw nullptr; //erro
void f (Object^); //1
void f (String^); //2
void f (char*); //3
void f (int); //4
f (nullptr); //error, ambiguous (1 ,2 ,3 posible)
f (0); //calls f(int)
void g (Object^, Object^); //1
void g (Object^, char*); //2
void g (Object^, int); //3
g(nullptr, nullptr); //error, ambiguous (1, 2 possible)
g(nullptr, 0); //calls g(Object^, int)
g(0, nullptr); //error, ambiguous (1, 2 possible)
void h (Object^, int);
void h (char*, Object^);
h (nullptr, nullptr); //calls h(char*, Object^);
h (nullptr, 2); // calls h(Object^, int);
template<typename T> void k(T t);
k(0); //specializes k, T = int
k(nullptr); //error, can’t instantiate null type
k((Object ^)nullptr); //specializes k, T = Object^
k<int *>(nullptr); //specializes k, T = int*
如果对象被分配在本地堆中,那么它不会移动, 指向和引用这个对象不需要跟踪这个对象的位置. 但是, 被分配在CLI堆中的对象会移动, 所以需要跟踪它们. 在CLI堆中, 本地指针和引用不能做到跟踪的. 为了在CLI堆中跟踪对象, C++/CLI定义了句柄(使用符号^)和跟踪引用(使用符号%).
N* pn = new N; //allocate on native heap
N& rn = *pn; //bind ordinary reference to native object
R^ hr = gcnew R; //allocate on CLI heap
R% rr = *hr // bind tracking reference to gc-lvalue
一般请况下, 符号%相对于^等于符号&相对于*
就像标准C++有一个一元操作符&那样, C++/CLI提供了一个一元操作符%. &t产生一个T*或者一个interior_ptr<T>(详细看下文), %t则产生一个T^.
右值(rvalue)和左值(lvalue)延续标准C++的意义, 适用于下面的规则:
# 一个定义为T*类(即是本地指针)的实体, 指向一个左值
# 在一个本地指针前添加 * 操作符, 解引用这个本地指针, 产生一个左值
# 一个定义为T&类(即是本地引用)的实体, 是一个左值
# 表达式---&左值---产生一个T*
# 表达式---%左值---产生一个T^
(注:这里的左值都是不是gc-lvalue)
一个gc-lvalue是一个指向可能存在于CLI堆中的对象或该对象的值成员的表达式. 下面的规则适用于gc-lvalue:
# 存在从”cv-qualified of type T”到 “cv-qualified gc-lvalue of type T” 以及从”cv-qualified gc-lvalue of type T”到”cv-qualified rvalue of type T”的标准转换.(注:这里的cv-qualified的意思是被const或violate修饰的,加粗的rvalue是按照原文抄的, 不知道是否正确)
# 一个定义为T^的实体(一个指向T的句柄)指向一个gc-lvalue.
# 在定义为T^的实体前加上*,解引用T^,产生一个gc-lvalue.
# 一个定义为T%的实体(一个T的跟踪引用),是一个gc-lvalue.
# 表达式---&gc-lvalue---产生一个interior_ptr<T>
# 表达式---%gc-lvalue---产生一个T^
垃圾收集器可以移动存放在CLI堆中的对象. 为了使指针正确地指向那些对象, 在运行时刻需要更新指针指向的对象的位置. 一个内部指针(用interior_ptr来定义)就是一个满足这个需要的指针.