我们在编程的过程中经常遇到需要转换变量的类型,以期得到我们想要的数据类型。所以,要想成为一个好的程序员必须对类型变换有一个深入的理解。那么,就让我们一起看看C++中的类型转换都是怎么回事吧!
1、隐形类型转换
先看下面的例子:
short a=2000;
int b;
b=a;
这段代码是我们经常遇见的,它将数据类型为short 形的a变量提升为int形。但是,我们并没有使用任何显式的类型转换声明操作。这就是标准的数据类型转换。这类类型转化主要在常规数据类型中使用,像short->int,int->double ,double->float,double->int,bool类型转换及一些指针类型的转换。
我们从上面的类型转换中可以看出,作为提升形的类型转换基本没有问题,但是对于一些算数类型之间的转换并不总是能够得到精准的结果。例如:
(1)有符号类型向无符号类型转换时,因为是所有位数2密数的和相加,而有符号类的最高有效位并不表示实际的数值,所以像-1转化为无符号数就是最大值等,可能并不是我们想要的结果。
(2)bool类型的转化,false转化为数值类型时,就是0,转化为指针类型时就是NULL;true转化为数值类型时,是1,所有其它数据类型都是true。
(3)float型向int型转化时,小数部分就会被舍弃。这可能造成未定义的错误。
(4)相同数据类型(例如整数到整数,浮点数到浮点数)之间转换是合法的,但是因为指定的平台的关系,可能无法移植。
当然了,现在的编译器对于任何可能造成精度误差等都会发出warning,这些warning可以通过显式的类型转换避免。
对于非基本的类型转化,数组和函数转化为指针,下面这些操作是允许的:
(1)NULL可以转换为任何类型的指针;
(2)指向任何类型的指针都可以转化为void类型指针;
(3)指针向上转换:派生类指针可以转化为它可以访问的,明确的基类指针,而不必修改它的const或者volatile限定符。
2、类的隐形转换
在类的世界里,可以通过三个成员函数方法控制隐形转换:
(1) 单参数构造函数:
允许从一个特定类型隐式转换从而初始化一个对象。
(2) 赋值操作符:
允许对赋值操作实现隐式转换。
(3) 类型转换操作符:
允许对一个特定类型的隐式转换。
// 类的隐式转换:
#include <iostream>
using namespace std;
class A {};
class B
{
public:
// 从A转换而来 (构造函数):
B(const A& x) {}
// 从A转换而来 (赋值):
B& operator= (const A& x) {return *this;}
// 从A转换而来 (类型转换):
operator A() {return A();}
};
int main (void)
{
A foo;
B bar = foo; // 调用构造函数
bar = foo; // 调用赋值操作
foo = bar; // 调用类型转换
return 0;
}
类型操作符使用了一个特殊的语法:
它使用operator关键字,后面紧跟目标类型和一对空括号()。注意,返回的是目标类型,因而不需要在operator关键字之前指定返回类型。
3、关键字explicit
对于函数调用,C++允许每一个参数都可以发生隐式转换。这会在某种程度上为类带来麻烦,因为它可能并不总是如设计想得那样。例如,我们对上面的代码添加下面的函数:
void fn (B arg) {}
这个函数带有一个参数,是一个类型为B的参数,但是它也可以使用A对象作为参数进行调用,如下:
fn (foo);
这可能不是设计的初衷。但是,好在我们可以使用关键字explicit标记这个有影响的构造函数:
// explicit:
#include <iostream>
using namespace std;
class A {};
class B {
public:
explicit B (const A& x) {}
B& operator= (const A& x) {return *this;}
operator A() {return A();}
};
void fn(B x) {}
int main(void)
{
A foo;
B bar(foo);
bar = foo;
foo = bar;
//fn (foo); // not allowed for explicit ctor.
fn (bar);
return 0;
}
如上面的例子所示,我们就无法使用A调用fn()这个函数了。
另外,被用explicit标记的构造函数不能使用像赋值那样的语法进行调用了;在上面的例子中,bar不能使用下面的语法进行构造。
B bar = foo;
类型转换函数(前面段落中描述的那些)也能够使用explicit指定。作用也与前面的相同。
4、类型转换
C++是一种强类型语言。许多类型转换,尤其是那些隐含着不同值解释的转换,要求显式转换,如我们已经知晓的类型转换操作。这里有两种类C风格的类型转换:
double x = 10.3;
int y;
y = int (x); // 函数标识法
y = (int) x; // 类C转换标识法
对于基本数据类型来说,这些通用类型转换格式就已经足够了。但是,如果这些操作不假思索地用在类或者类指针上,会导致语法上正确,而运行时会产生错误。例如,我们看下面的代码(下面的代码编译就没有错误):
// 类的类型转换
#include <iostream>
using namespace std;
class Dummy {
double i,j;
};
class Addition {
int x,y;
public:
Addition (int a, int b) { x=a; y=b; }
int result() { return x+y;}
};
int main () {
Dummy d;
Addition *padd;
padd = (Addition*) &d;
cout << padd->result();
return 0;
}
程序首先声明了一个Addition类型的指针,然后显式的调用类型转换强行把一个不相关类型的引用对象赋值给它:
padd = (Addition*) &d;
不加限制的显式类型转换,允许任何类型之间的转换,而不管它们指向的类型不同。后续对成员的调用有可能会造成运行时错误,也有可能造成不可预知的错误。
为了控制这些类的类型转换,我们设计了四种专用的类型转换操作符:dynamic_cast,reinterpret_cast,static_cast,const_cast:
(1)dynamic_cast <新类型> (表达式)
(2)reinterpret_cast <新类型> (表达式)
(3)static_cast <新类型> (表达式)
(4)const_cast <新类型> (表达式)
传统的类型转换等价于这些表达式:
(新类型) 表达式
新类型(表达式)
5、dynamic_cast
dynamic_cast只可作用于指针和类引用(或者使用void*)。它的目的是,保证类型转换的结果指向正确的、完整的目标指针所指向类型的对象。
This naturally includes pointer upcast (converting from pointer-to-derived to pointer-to-base), in the same way as allowed as an implicit conversion.
这自然包含了指针向上转换-update(指向派生类的指针向基类指针转换),
但是dynamic_cast也能够多态类向下转换(基类指针转换为派生类指针)这里的多态类就是那些具有虚成员的类。这里的前提条件是,如果(也只能是)所指向的对象是一个合法完整的目标类型的对象。例如,下面的例子:
// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;
class Base { virtual void dummy() {} };
class Derived: public Base { int a; };
int main ()
{
try {
Base *pba = new Derived;
Base *pbb = new Base;
Derived *pd;
pd = dynamic_cast<Derived*>(pba);
if (pd==0)
cout << "Null pointer on first type-cast.\n";
pd = dynamic_cast<Derived*>(pbb);
if (pd==0)
cout << "Null pointer on second type-cast.\n";
}
catch (exception& e) {
cout << "Exception: " << e.what();
}
return 0;
}
运行结果:
Null pointer on second type-cast.
兼容性提示:
dynamic_cast类型返回Run-Time类型信息(RTTI),以便能够追踪dynamic类型。一些支持该功能的编译器,默认是禁止该功能的。在使用dynamic_cast时,需要使能runtime type checking功能。
上面的代码试图对两个类型的指针对象实现动态类型转换,Base 和Derived ,但是只有第一个成功了。现在我们分别看它们的初始化:
Base *pba = new Derived;
Base *pbb = new Base;
尽管都是Base *类型指针,事实上,pba指向了Derived类型的对象,与此同时,pbb指向了一个Base类型的对象。因此,当对它们分别执行dynamic_cast类型转换操作的时候,pba正指向了一个完整的Derived类,而pbb指向了Base类型的对象,Base类型的对象是一个不完整的Derived类对象。
当所要求的类不是一个完整的对象时,dynamic_cast 是不能对指针进行类型转换的。正如前面的例子中那样,第二种类型转换的过程中,它返回了一个null指针,表明失败。如果dynamic_cast被用来转换一个引用类型,且这个转换是不可能的,类型为bad_cast的异常就会被抛出。
dynamic_cast也可以对指针执行其它允许的类型转换:把指针类型转换为null指针;或把任何指针类型转换为void*指针。
6、static_cast
static_cast能够执行相关类指针的类型转换,不仅仅是向上转换,也可以是向下转换。在程序执行的时候,不做目标对象是否为一个完整对象的类型检查,直接执行。因此,必须由程序员自己保证转换是安全的。另一方面,它不会产生dynamic_cast转换时执行安全检查带来的麻烦。
class Base {};
class Derived: public Base {};
Base * a = new Base;
Derived * b = static_cast<Derived*>(a);
这是合法的代码,尽管由于b指向了一个不完整的对象,导致解引用时发生运行时错误。
static_cast所能做的操作是:
(1)从void*类型向任何类型转变。在这种情况下,如果保证要转换的类型是转换为void*的指针类型指向的值,转换结果就是正确的。
(2)转换整数,浮点数和枚举类型到枚举类型。
另外,static_cast 也能够执行下面这些情况:
(1)单参数构造函数或转换操作符的显式调用。
(2)转换为返回值引用。
(3)转换枚举类型值到整数值或者浮点数值。
(4)转换任何类型为void, 评估和抛弃某一个值。
7、reinterpret_cast
reinterpret_cast 能够转换任何指针类型为另一种指针类型,即使不相关的类。转换的结果是,从一个指针到另一个的简单的二进制的拷贝。所有的指针转换都是允许的:既不会对指向的内容做检查也不会对指针类型作检查。
也能够对整数类型的指针作转换。唯一需要保证就是把指针转换为足够大的整数类型,以便能够包含完整的指针值,这样做的目的就是能够把该值还能够完整的转化为一个合法的指针。
class A { /* ... */ };
class B { /* ... */ };
A * a = new A;
B * b = reinterpret_cast<B*>(a);
这个代码会编译,尽管没有什么意义,因为现在b指向了不相关类型的对象,而且很可能是不兼容的类。解引用b的时候是非常不安全的。
8、const_cast
const_cast能够控制指针所指向对象的常量性,不论是设置还是移除。例如,为了传递给一个带有non-const参数的函数一个const指针时,就需要这个类型转换,如下所示:
// const_cast
#include <iostream>
using namespace std;
void print (char * str)
{
cout << str << '\n';
}
int main () {
const char * c = "sample text";
print ( const_cast<char *> (c) );
return 0;
}
运行结果:
sample text
上面的代码能够保证工作,是因为函数print()不会写到对象中去。请注意,移除要被写入对象的常量性会造成未定义的行为。
9、typeid
typeid允许检查一个表达式的类型:
typeid (表达式)
必须包含头文件。在这里要注意typeid是一个操作符。看下面的例子:
// typeid
#include <iostream>
#include <typeinfo>
using namespace std;
int main () {
int * a,b;
a=0; b=0;
if (typeid(a) != typeid(b))
{
cout << "a and b are of different types:\n";
cout << "a is: " << typeid(a).name() << '\n';
cout << "b is: " << typeid(b).name() << '\n';
}
return 0;
}
运行结果:
a and b are of different types:
a is: Pi
b is: i
当对类使用typeid操作符时,typeid使用RTTI保持对动态对象类型的追踪。当typeid被应用到一个多态类时,返回的结果是最终被派生的完整对象的类型。
// typeid, polymorphic class
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;
class Base { virtual void f(){} };
class Derived : public Base {};
int main () {
try {
Base* a = new Base;
Base* b = new Derived;
cout << "a is: " << typeid(a).name() << '\n';
cout << "b is: " << typeid(b).name() << '\n';
cout << "*a is: " << typeid(*a).name() << '\n';
cout << "*b is: " << typeid(*b).name() << '\n';
} catch (exception& e) { cout << "Exception: " << e.what() << '\n'; }
return 0;
}
运行结果
a is: P4Base
b is: P4Base
*a is: 4Base
*b is: 7Derived
注意:type_info的成员名称返回的字符串依赖于你的编译器和库的实现。上面的代码的返回类型就是在Ubuntu12.04上运行的g++编译器运行结果。
如果typeid要求值的类型是一个指针,这个指针通过(*)号进行解引用,恰巧这个指针是null指针,typeid会抛出一个bad_typeid异常。