第16章 ANSI C++标准语法补充
大多数C++语言系统都支持ANSI/ISO C++标准。本章介绍该标准中的部分内容,作为前面各章节的语法补充。本章将介绍逻辑型bool、命名空间namespace、两个修饰符explicit和mutable、运行时刻类型信息RTTI和typeid运算符、以及4种新型的强制类型转换运算符。本章各部分之间相对,相互之间没有严格次序。
16.1 逻辑型bool
C语句中没有逻辑类型,而C++标准有逻辑型bool。逻辑型也被称为布尔型。逻辑值只有真true和假false两个值,而且只能进行逻辑运算。C语言用整数int来表示逻辑值,0值表示false,非0为true。整数表示逻辑值的缺点是算术运算的结果可直接作为逻辑值,而且“逻辑值”也可进行算术运算,这不符合高级编程语言的要求。
C++可定义bool类型变量,可直接赋值true或false,可作为关系表达式和逻辑表达式的计算结果。bool类型变量支持逻辑运算非!、与&&、或||运算。一个bool值在内存中占1字节,故此sizeof(bool)为1。但内部采用整数值0表示false,1表示true。
例16-1逻辑型bool的例子。
#include #include using namespace std; void main(){ bool boolean = false; int x = 0; cout<<"boolean is "< cin >> x; cout<<"integer " < <<" and interpreted as "; if (x) cout<<"true\\n"; else cout<<"false\\n"; bool b = true; boolean = false && b; cout<<"boolean is "< < boolean = x * 3 > 10; if (boolean) cout<<" x * 3 > 10"< cout<<" x * 3 <= 10"< 执行程序,输入45,输出结果如下: boolean is 0 Enter an integer:45 integer 45 is nonzero and interpreted as true boolean is 0 boolean output with boolalpha manipulator is false sizeof(boolean)=1 x * 3 > 10 本质上bool类型仍然是一个整型值。在cout<<输出时,仍然输出0或1,而不是false或true,除非用boolalpha作为格式控制符。另一个需要注意的是bool类型变量仍然可以进行算术运算,例如: bool b2 = true, b3 = false; bool b4 = b2 - b3; //bool型变量之间不应该允许算术运算。 对上面语句,编译器仅给出警告而不是错误。 但无论如何,引入bool类型本身就是一种改进,建议在C++程序中尽可能采用bool类型。 16.2 命名空间namespace 命名空间(namespace)是解决大程序中多个元素(如类、全局函数和全局变量)命名冲突的一种机制。当我们要把几个部分(往往来自不同的人员或团队)合并成为一个大程序时,往往就会出现命名冲突的问题:类名、全局函数名、全局变量名都可能重名。解决的方法就是把这些名字放在不同的命名空间中,在访问这些名字时使用各自的命名空间作为限定符。 16.2.1 命名空间的定义 命名空间类似文件系统中的目录,空间中的成员类似目录中的文件。全局空间相当根目录,一个目录名作为其中多个文件的命名空间,子目录作为嵌套空间,文件作为空间中的成员。同时一个命名空间也是一个作用域。 命名空间的基本规则如下: ●一个程序所用的多个命名空间在相同层次上不重名; ●在同一个命名空间中的所有成员名字不重复; ●在一个命名空间中可以嵌套定义其内层的多个子空间。 定义命名空间的语法格式如下: namespace [<空间名>]{一组成员} 其中,namespace是关键字,后面给出一个空间的名字标识符。后面用花括号括起来一组成员,可以是一组类、一组函数、一组变量。如果空间名缺省则为无名空间。无名空间中的元素类似于全局变量,只是在本文件中访问,而全局空间中的成员可以被其它文件访问。 命名空间可嵌套说明,就如同目录与子目录之间的关系。 例如下面代码: int myInt = 98; //全局空间中的变量 namespace Example{ //说明了一个Example空间 const double PI = 3.14159; //Example中的变量成员 const double E = 2.71828; //Example中的变量成员 int myInt = 8; //Example中的变量成员,隐藏了全局同名变量 void printValues(); //Example中的函数成员的原型 namespace Inner{ //嵌套空间,名为Inner enum Years{FISCAL1 = 2005, FISCAL2, FISCAL3}; //嵌套空间中的成员 int h = ::myInt; //用全局变量进行初始化,98 int g = myInt; //用外层成员进行初始化,8 } } namespace{ //无名空间 double d = 88.22; //无名空间中的变量成员 int myInt = 45; //无名空间中的变量成员 } 图16.1 空间的结构 图16.1表示了上面例子所说明的空间的结构。 在描述成员的全称时,要使用作用域运算符“::”。例如,Example是一个空间,其中各成员的全称为: Example::PI; Example::E; Example::myInt; Example::printValues(); Example::Inner是一个嵌套空间,Example被称为Inner的外层空间,嵌套空间中的成员的全称为: Example::Inner::Years Example::Inner::h Example::Inner::g 16.2.2 空间中成员的访问 如何访问某空间中的成员?这与访问文件系统中的文件相似。访问文件可用相对路径,也可用全路径。全路径就是把全部路径名作为文件名的限定符。 如何查找一个成员?在当前空间中对一个名字k的访问需要查找过程,在编译时确定被访问的实体。有以下3种形式: 1、限定名形式:“空间名::k”。先在当前空间的限定嵌套空间中查找k,相当于一个相对路径。在嵌套空间中如果未找到成员k,就将该空间名作为全路径的空间名再查找成员k,如果仍未找到,就给出错误信息。这种访问形式对相对路径的空间名优先。 2、全局限定形式:“::k”。在全局空间中查找,如果未找到就出错。 3、无限定形式:“k”。按局部优先原则,先在当前空间中查找。如果未找到,就向外层空间找,直到全局空间。如果未找到,就从导入空间中查找,如果找到一个就确定,如果在全局空间和导入空间中找到多于一个就指出二义性错误。如果仍未找到k就是一个错误名字。 如果要多次访问某个空间中的成员,你可能觉得成员名前面总是挂上一串空间名很麻烦。有一个简单方法就是用using namespace语句来导入(import)一个命名空间。格式如下: using namespace [::] [空间名::] 空间名; 其中,using和namespace是关键字,然后指定一个空间名,或者一个嵌套空间名。例如: using namespace std; 就是导入空间名“std”,所有标准C++库都定义在命名空间std中,也包括标准模板库STL,因此这条语句经常使用。 导入空间语句仅在当前文件中有效。在一个文件中可以有多条导入语句,来导入多个空间名。 实际上,一个无名空间的定义都隐含着以下两条语句: namespace unique{...} using namespace unique; 其中unique是系统隐含的一个唯一的空间名。导入命令使得无名空间中的成员仅能在本文件中访问,因此无名空间就是一种缺省导入的空间。 如果只想导入某空间中的某个成员,而不是全部成员,可按下面格式说明: using [::]空间名::成员名; 例16-2 命名空间的定义和访问。 #include using namespace std; int myInt = 98; namespace Example{ const double PI = 3.14159; const double E = 2.71828; int myInt = 8; void printValues(); namespace Inner{ enum Years{FISCAL1 = 2005, FISCAL2, FISCAL3}; int h = ::myInt; int g = myInt; } } namespace{ double d = 88.22; int myInt = 45; } void main(){ cout<<"In main"; cout<<"\\n(global) myInt = "<<::myInt; //A cout<<"\\nd = "< } void Example::printValues(){ cout<<"\\nIn Example::printValues:\\n" <<"myInt = "< <<"\\nInner::FISCAL3 = "< 执行程序,输出结果如下: In main (global) myInt = 98 d = 88.22 Example::PI = 3.14159 Example::E = 2.71828 Example::myInt = 8 Example::Inner::FISCAL3 = 2007 Example::Inner::h = 98 Example::Inner::g = 8 In Example::printValues: myInt = 8 d = 88.22 PI = 3.14159 E = 2.71828 ::myInt = 98 Inner::FISCAL3 = 2007 Inner::h = 98 Example::Inner::g = 8 函数main是全局函数,其中代码要访问某个空间中的成员就要给出绝对路径全称。注意到在全局空间和无名空间中都定义了myInt成员,因此在全局空间中如果直接用“myInt”来访问,就会造成二义性。假如A行用“myInt”来访问,就会产生二义性。用“::myInt”是就访问全局变量。B行用“d”来访问无名空间中的成员,此时无二义性。 函数printvalues是Example空间中的成员,其中代码可用绝对路径,也可用相对路径来访问成员。C行用“myInt”访问当前空间中的成员,尽管在全局空间和无名空间中都有同名成员,但当前命名空间中的成员具有优先权,故此无二义性。D行用“::myInt”限定全局变量。E行和F行采用了相对路径,而G行采用了绝对路径,给出了全称。 对于命名空间的使用,应注意以下要点: ●尽量使全局空间中的成员最少,这样发生冲突的可能性就会减少。 ●空间的命名应尽可能地表示自然结构,而不仅仅是为了避免命名冲突。 ●在一个源文件中应尽可能避免说明多个平行的空间。 ●二义性往往发生在全局空间和导入空间中含有同名元素,而程序中用无限定的形式来访问该元素。 16.3 修饰符explicit 我们在第10章介绍过,在一个类中,如果说明了单参构造函数,就可用于隐式的类型转换。这种隐式的类型转换可能会被误用,尤其是在函数调用时,实参到形参的隐式转换,可能在不经意之间就创建了对象。 例如,如果一个函数的形参是一个对象或对象引用,而该对象类型恰好有单参构造函数,那么调用方就可利用此形参隐式创建新对象,再传给函数做实参。用explicit修饰符就能避免这种无意的创建对象。 如果将一个单参构造函数修饰为explicit,该函数就不能用于隐式的类型转换,而只能用于显式地创建对象。 例16-3 分析下面例子。 #include #include class IntArray{ //一个整数数组类 int size; int *ptr; friend ostream &operator<<(ostream &, const IntArray &); //运算符<< public: IntArray(int = 10); //A 单参构造函数,同时也是缺省构造函数 ~IntArray(); }; IntArray::IntArray(int arraySize){ //单参构造函数 size = (arraySize>0 ? arraySize : 10); ptr = new int[size]; assert(ptr != 0); //用断言检查点 for (int i = 0; i < size; i++) //置缺省值 ptr[i] = 0; } IntArray::~IntArray(){delete[] ptr;} //析构函数 ostream &operator<<(ostream &output, const IntArray &a){ //运算符<<重载函数 for (int i = 0; i < a.size; i++) //输出各元素 output< } void outputArray(const IntArray &a){ //B全局函数,调用<<输出各元素 cout<<"The array:\\n"<} void main(){ IntArray integers1(7); //说明一个整数数组 outputArray(integers1); //调用全局函数,输出个元素 outputArray(15); //C 隐式创建对象 } A行说明了一个单参构造函数,形参为一个int,这意味着可以从一个int值来创建一个IntArray对象。例如:IntArray a1= 15; 在需要一个IntArray对象的地方,提供一个int值就能自动创建一个IntArray对象,再提供给它。B行函数形参恰好就是IntArray对象,那么C行调用函数时提供了一个int值15,实际上转换为 outputArray(IntArray(15)); 所以语法上没有错误。但实际上可能不是你想要的。你只是无意中犯了一个错误,所以希望C行能给出编译错误,而不想自动创建一个IntArray对象。 此时修饰符explicit就有用了。只要对类中构造函数原型添加这样一个修饰,如下: explicit IntArray(int =10); 就可以避免这种隐式的创建对象,此时C行代码编译报错。 对于一些“比较大”的类,比如STL容器类(如vector、deque、list等),它们的单参构造函数都用explicit修饰,目的就是避免误建对象。 16.4 修饰符mutable 我们在第10章介绍过,用const修饰的成员函数不能改变对象的状态,即不能改变数据成员的值。但有个例外,用mutable修饰的数据成员可以被改变。 例16-4 分析下面例子。 class TestMutable{ mutable int value; //A 用mutable修饰的数据成员 public: TestMutable(int v = 0){value = v;} void modifyValue() const {value++;} //B const函数中可改变mutable成员 int getValue() const {return value;} }; void main(){ const TestMutable t(99); cout<<"Initial value is "< cout<<"\\nModified value is "< A行用mutable说明了一个数据成员。B行是一个const函数,但能改变这个数据成员。 修饰符mutable的用途就是说明一个数据成员是可变的,在任何成员函数中都可改变。 注意,mutable与修饰符volatile的区别。volatile修饰符说明一个数据成员是易变的,随时可能被程序外部其它东西所改变,如操作系统、硬件、并发线程等。它要求编译器对它的访问不能进行优化。volatile对于一般的编程没有直接影响。而mutable对于编程是有影响的,const成员函数中不能改变数据成员的值,但除了mutable修饰的成员。 16.5 RTTI与typeid RTTI是Run-Time Type Information运行期类型信息的简写。在运行时刻我们常常要利用RTTI动态掌握有关类型的信息,尤其是对于抽象类编程和模板编程。在抽象层面上可根据具体类型做出不同处理,更方便通用性编程。 typeid是一个关键字,就像sizeof一样,以函数的形式,在运行时刻获得指定对象或表达式的类型信息。typeid有下面两种形式: const type_info& typeid( type-id ) //求类型type-id的具体类型 const type_info& typeid( expression ) //求表达式的具体类型 返回值的类型type_info是一个对象类,定义在typeinfo.h文件中: class type_info { public: virtual ~type_info(); int operator==(const type_info& rhs) const; int operator!=(const type_info& rhs) const; int before(const type_info& rhs) const; const char* name() const; //读取类型的名字 const char* raw_name() const; }; 可以用“typeid(<表达式>).name()”来获取<表达式>在运行时刻的类型名称。对于基本类型就直用其名,如“int”、“double”。对于类名,要加前缀“class”。 例16-5 运行期类型信息的例子。 #include #include #include using namespace std; template T maxmum(T value1, T value2, T value3){ T max = value1; if (value2 > max) max = value2; if (value3 > max) max = value3; const char * dataType = typeid(T).name(); //A cout< } class A{ public: void f(){ cout<<"f1 this is "< void f()const{ cout<<"f2 this is "< }; void main(){ int a = 2,b = 3, c= 1; double d = 4.3, e = 7.7, f= 2.3; string s1 = "one", s2 = "two", s3 = "three"; cout< a1.f(); const A a2; a2.f(); const char * str1 = "const string"; //D cout< cout< 执行程序,输出结果如下: ints are compared. Largest int is 3 doubles are compared. Largest double is 7.7 class std::basic_string char> >s are compared. Largest class std::basic_string locator f1 this is class A * f2 this is class A const * char const * char const * char [6] type of a > b is bool A行读取模板形参T的运行时刻具体类型的名称,下面再显示出来。A行中也可对某个形参变量来处理,如...=typeid(value1).name(),得到一样结果。 可看到string的运行时刻的具体类型,“class std::basic_string B行和C行分别显示在非const成员函数和const成员函数中的this的类型。可以看到,在非const成员函数中的this类型为class A *,而在const成员函数中则为class A const *。实际上,我们习惯将后者表示为class const A *,将const放在类型名之前,与放在类型名与*之间是一样的。因此D行与E行描述的两个字符串类型是一样的。 对于字符串字面常量的类型,也可以看到,F行显示char [6]。 关系表达式的类型是bool型,而不是int,G行显示可以验证。 16.6 强制类型转换 我们前面介绍了强制类型转换cast,作用于基本类型的变量或表达式,转换为另一种类型。在面向对象编程中也介绍了强制类型转换,将一个基类的指针或引用强制转换为其派生类的指针或引用。一般称为“向下转换”或者“窄化”。传统类型转换形式为“(类型名)变量名/表达式”,存在的问题是过于笼统、功能过强而易失控、不够安全。 C++标准中引入了4个新的强制类型转换运算符,以取代传统的强制类型转换,好处是更具体、功能弱化、相对安全。表16.1给出了这4种新型类型转换的特点。 表16.1 新型类型转换 16.6.1 static_cast运算符 静态转换在编译时刻进行类型检查。对于非法的转换将给出错误信息。例如,将const类型转换为非const类型、把基类转换到非公有继承的派生类、从一个类型转换到不具有特定构造函数或转换函数的类型等。对于基本类型,采用静态转换替代传统转换是比较安全的。 语法格式如下: static_cast<目标类型名>(变量名/表达式) 例16-6 静态转换的例子。 #include class BaseClass{ public: void f()const{cout<<"BASE\\n";} //A 非虚函数 }; class DerivedClass: public BaseClass{ public: void f()const{cout<<"Derived\\n";} //B }; void test(BaseClass * basePtr){ DerivedClass *derivedPtr; derivedPtr = static_cast derivedPtr = (DerivedClass *)basePtr; //D derivedPtr->f(); } void main(){ double d = 8.22; int x = d; //E warning: possible loss of data int y = (int)d; //F int z = static_cast double d2 = y; //H cout<<"x is "< void * vp = str1; //I char * str2 = (char *)(vp); //J char * str3 = static_cast if (str2 == str3) cout<<"str2 == str3"< test(&base); } 执行程序,输出结果如下: x is 8 y is 8 z is 8 d2 is 8 str2 == str3 C++ C++ Derived A行定义的函数是非虚函数,B行派生类定义的同名同参函数并非改写override。 C行使用了静态转换,效果与D行的传统转换一样。 E行将一个double赋给一个int,导致一个警告。 F行是传统转换,与G行的静态转换效果一样。 H行将一个int赋给一个double,不会导致警告。 I行将一个char*赋给一个void*,不会导致警告。 J行使用传统转换,与K行的静态转换效果一样。 可以看出,多数传统的类型转换都可用静态转换来实现,只是静态转换的类型检查更严格。如果将公有继承改变为缺省的私有继承,静态转换的C行将报错,而传统转换的D行却不报错。 16.6.2 dynamic_cast运算符 这种转换被称为“动态转换”。在运行时刻进行类型检查,一般作用于类的指针或引用,也包括void指针。对于指针进行动态转换之后,应立即判断新的指针值是否为0,如果为0,表示转换失败,如果非0,表示转换成功,然后才能通过该指针进行操作。 语法格式如下: dynamic_cast<目标类型名>(变量名/表达式) 例16-7 动态转换的例子。 #include #include class B{ public: virtual void foo(){cout<<"Base"< class D:public B{ public: void foo(){cout<<"Drived"< void func(B *pb){ if (pb == 0) return; cout< pd1->foo(); D *pd2 = dynamic_cast if (pd2 != 0) pd2->foo(); else cout<<"pd2 == 0"< void main(){ D d; func(&d); //F B b; func(&b); //G } 在VC++6环境中编译上面程序时,要求改变项目选项,否则就会对C和D行给出警告,而且会造成运行错误。方法如下:菜单“Project”下拉选择“Settings…”菜单,在对话框中选择“C/C++”,然后在下方“Project Options”中添加“/GR”,按“OK”。 执行程序,输出结果如下: class D object is provided Drived Drived class B object is provided Base pd2 == 0 A行在基类中说明一个虚函数,使A类成为多态性基类。派生类D在B行改写了这个虚函数。C行用typeid获得对象的实际类型,再用name函数读取名字并显示出来。 D行用静态转换将形参B* pb转换为派生类指针D*pd1。这对于F行调用是安全的,因为实参就是派生类的对象。但对于G行调用就出错了,因为实参是基类对象,而用派生类指针来操作,虽然能调用虚函数,但仍执行基类的函数,而不是派生类的函数。 E行用动态转换形参B* pb转换为派生类指针D*pd2。如果转换成功,指针为非0;如果失败,指针为0。对于0指针不能进行任何操作。因此对于类的指针的转换,使用动态转换是比较安全的。 16.6.3 const_cast运算符 这种转换被称为“常量转换”,专门对const或volatile修饰的变量进行转换。 语法格式如下: const_cast<目标类型名>(变量名) 例16-8 常量转换的例子。 #include class ConstCastClass{ int number; public: void setNumber(int num){number = num;} int getNumber()const{return number;} void printNumber() const{ //A ConstCastClass* newThis; newThis = const_cast newThis = (ConstCastClass* const)this; //C newThis->number--; //D cout<<"\\nNumber after modification:"< }; void main(){ ConstCastClass x; x.setNumber(8); cout<<"Initial value of number is "< } 执行程序,输出结果如下: Initial value of number is 8 Number after modification:7 A行定义成员函数为const函数,那么该函数中就不能改变数据成员number的值。这是利用this指针的const性质来实现的。 在非const成员函数中this指针的类型为“class 类名*const”,不能改变this的值使其指向另一个对象,但能改变当前对象的值。 在const成员函数中this指针的类型为“const class 类名*const”,不能改变this的值,也不能通过this指针来改变数据成员的值,也不能通过this指针调用非const函数。 B行用常量转换将this的类型由“const ConstCastClass *const”转换为“ConstCastClass* const”,这样通过该指针就能改变数据成员number的值。这个效果与C行传统转换一样。最终结果是在一个const函数中改变了一个数据成员的值。使用mutable来修饰一个数据成员也能达到同样目的,但常量转换后能改变所有的数据成员,而不限一个。 可以看出,使用常量转换会破坏函数的约定,建议慎用。 16.6.4 reinterpret_cast运算符 这种转换被称为“重释转换”,只能对指针进行转换或者转换到指针,最具技巧性,也最危险。 语法格式如下: reinterpret_cast<目标类型名>(变量名/表达式) 例16-9 分析下面例子。 #include void main(){ unsigned x = 22, *unsignedPtr; void * voidPtr = &x; //A char * charPtr = "C++"; unsignedPtr = reinterpret_cast cout<<"*unsignedPtr is "<<*unsignedPtr <<"\\ncharPtr is "< <<(x = reinterpret_cast cout<<"\\nunsigned to char * result in:" < double *d = reinterpret_cast cout<<*d< 执行程序,输出结果如下: *unsignedPtr is 22 charPtr is C++ char * to unsigned result in:4350096 unsigned to char * result in:C++ 2.07234e-307 A行使一个void*指针指向一个unsigned变量,B行用重释转换将这个void*指针转换为unsigned*指针,使unsignedPtr指针指向x。然后用cout语句通过该指针访问x的值,没有错误。 C行用重释转换将一个char*指针转换为一个unsigned值,赋给x并输出,可以看到该指针的值,就是字符串"C++"的存储地址。此时你可以随便改变x的值。D又用重释转换将unsigned变量x转换为一个char*,并输出。 E行用重释转换将一个int*转换为一个double*,而得到与原值不相干的结果。 可以看出,重释转换可以将指针随意转换到其它类型,或将其它类型转换到指针。具危险性,建议读者慎用。 小 结 ●C++支持逻辑型bool,可直接赋值true或false,也可作为关系表达式或逻辑表达式的结果。内存中占1字节。bool类型变量支持逻辑运算非!、与&&、或||运算。 ●命名空间(namespace)是解决大程序中多个元素(如类、全局函数和全局变量等)命名冲突问题的一种机制。 ●一个程序所用的多个命名空间在相同层次上不重名;在同一个命名空间中的所有成员名字不重复;一个命名空间可以嵌套定义其内层的多个不重名的空间。 ●使用namespace关键字来定义命名空间,及其成员。嵌套空间也如此定义。 ●用作用域运算符“::”来分隔空间名以及成员名。 ●使用using namespace来导入空间,以方便访问特定空间中的成员。 ●当用一个名字k来访问成员时,按局部优先原则,先在当前空间中查找。如果未找到,就向外层空间找,直到全局空间。如果未找到,就从导入空间中查找。如果在全局空间和导入空间中找到多于一个就是二义性错误。 ●修饰符explicit用来单参构造函数,避免隐式地创建对象,对于大对象类有用。 ●修饰符mutable使类的数据成员可改变,而不管是否在const成员函数之中。const成员函数中不能改变数据成员的值,但除了mutable修饰的成员。 ●运行期类型信息RTTI,Run-Time Type Information在运行时刻掌握类型信息对于通用性编程,对于复杂程序的分析都有用。typeid是关键词,需要包含typeinfo.h文件,常用“typeid(<表达式>).name()”来获取<表达式>在运行时刻的类型名称。 ●传统的强制类型转换存在的问题是过于笼统、功能过强而易失控、不够安全。标准C++引入了4个新的强制类型转换运算符,以取代传统的强制类型转换,好处是更具体、功能弱化、相对安全。下载本文
下面介绍这4种运算符。类型转换 语法 (注意尖括号不能缺少) 特点 静态转换 static_cast<目标类型名>(变量名/表达式) 编译时检查合法性。比较安全 动态转换 dynamic_cast<目标类型名>(变量名/表达式) 运行时刻检查合法性,适用于指针转换。转换之后应判断指针是否为0,若为0则转换失败,若非0则转换成功。比较安全 常量转换 const_cast<目标类型名>(变量名) 专门对const或volatile修饰的变量进行转换。会破坏既有的const约定,建议慎用。 重释转换 reinterpret_cast<目标类型名>(变量名/表达式) 专门处理指针转换。指针之间的转换,也可将指针随意转换到其它类型,或将其它类型转换到指针。技巧性和危险性并存。