前言,C中的类型转换
在C语言中你可能会看到这样的操作,
int i = 0;
double d = 2.22;
i = d;
我们把这种操作叫做类型转换,因为变量d和i的类型不相同,当你编译时,编译器会提醒你可能会发生数据丢失,因为相比int,double是更高的精度,但仅仅是个warning,谁关心呢?但如果你写出下面这样的代码,那么编译器就不敢苟同了:
int a = 0;
int* ap = a; // int型数据给指针变量赋值 (1)
const int b = 0;
int* bp = &b; // 非const指针指向const类型数据 (2)
同样是类型转换,下面的两种为什么编译器给出了error呢?因为这是两种相差较大的类型(这种相差较大是程序员认为的,更是编译器认为的),但是在C中我们也有方法,加上强制类型转换就可以,
int a = 0;
int* ap = (int*)a; // int型数据给指针变量赋值 (1)
const int b = 0;
int* bp = (int*)&b; // 非const指针指向const类型数据 (2)
我们把这种类型转换成为强制类型转换,而最上面那种叫做隐式类型转换,在C中如果你不想类型转换报错或者警告,直接强制转换。
int i = 0;
double d = 2.22;
i = (int)d; //这行不会警告,因为我强转了
但是在C++中我们有了一套新玩法。
一、static_cast
C++中引入的每一个feature都有它的目的,引入新的类型转换目的就是为了规范代码,当遇到类型转换,每个C++程序员都这样写,就会统一起来。C++引入了四个类型转换,我们来逐一介绍。
static_cast是很简单的类型转换,对应C中的隐式类型转换,即相差不大的两种类型。
int i = 0;
double d = 2.22;
i = d; // C写法,会警告
i = static_cast<int>(d); //C++写法,不会警告
类型转换的用法类型生成一个匿名对象,4个类型转换的用法相同。
二、reinterpret_cast
我觉得,比起如何使用,记住并正确写出reinterpret_cast更困难一点。
reinterpret_cast为两种相差较大的类型提供桥梁,这对应了C中的强制类型转换(不包括const),而强制类型转换是C/C++非常灵活的一个栗子,怎样转换取决于程序员而非编译器,同样的,我们也应该为代码负责。
int a = 0;
int* ap1 = (int*)a;
int* ap2 = reinterpret<int*>(a);
这个栗子有点做作,因为我想不到谁会在实际中这么写。
三、const_cast
首先一点,const类型和非const类型是相差较大的类型!!这也许与我们的认知不符,但事实如此。const_cast的目的是为了去掉const的修饰,使得非const的指针或者引用可以指向const对象。
const int a = 0;
int* p = const_cast<int*>(&a); //这可以
这里还有一个题目,
const int a = 0;
int* p = const_cast<int*>(&a);
*p = 1;
cout << a << endl;
cout << *p << endl;
上面的两个输出是多少呢?都是1吗?运行过后我们发现是0和1.实际上,因为a是const类型,你已经对编译器下了保证a是不可变的,a就会被放在寄存器中,直到你的代码用到a的地址,它才会为a分配内存,而cout的输出是从寄存器里面取出数据,所以a在寄存器里面就是0,而*p是内存中的a,已经被改变,是1.
四、dynamic_cast
C语言中的类型转换都处理完毕,但是C++又新增了一个类型转换。dynamic_cast是为了处理继承中的多态问题,父类必须有虚函数,而子类可以没有虚函数。
我们都知道,子类对象,指针或者是引用可以传给父类的对象,指针或者引用,这是天经地义的(至少编译器是这样认为的),我们把这种叫做切片。那么反过来,父类的对象,指针or引用能不能用于子类的呢?
对象的拷贝显然不行。指针和引用有时候可以。像这样,
class A
{
public:
virtual void Fun() //A类写了虚函数
{}
protected:
int _a;
};
class B : public A //B类继承A类
{
private:
int _b;
};
A a;
B b;
A* ap1 = &b; // 父类指针指向子类对象,切片,可以
B* bp1 = (B*)ap1; // ap虽然是父类指针,但是指向子类对象,赋值给子类指针可以。
A* ap2 = &a; // 父类指针指向父类对象
B* bp2 = (B*)ap2; // 此时父类指针想赋值给子类指针,不行。
dynamic_cast就是为了处理上面两种情况应运而生的类型转换,如果是第一种情况,dynamic_cast允许它发生,如果是第二种情况,dynamic_cast会返回nullptr。
void dynamic_cast_test(A* ap)
{
B* bp = dynamic_cast<B*>(ap);
if(bp)
{
cout << "传入的指针指向子类对象" << endl;
}
else
{
cout << "传入的指针指向父类对象" << endl;
}
}
dynamic_cast_test(ap1);
dynamic_cast_test(ap2);
输出结果是第一个输出指向子类对象,第二个输出指向父类对象。
相比到这里,有一个疑惑会盘旋在各位脑海中(也许没有),那就是我一开始提出的,为什么父类一定要有虚函数呢?我们都知道,有了虚函数就会有虚表和虚表指针,编译器是如何通过dynamic_cast判断指针是指向子类还是父类呢?实际上编译器会在虚表上面的位置存入对象的类型信息,编译器就是有了虚表才能找到指针的指向对象的类型。
五,explicit
explicit它的名字一样,保证了构造不支持隐式类型转换,像下面这样,
class C
{
public:
C(int a = 0)
:_a(a)
{}
private:
int _a;
};
C c = 1; //这实际上是隐式类型转换,编译器会
//先用1构造一个C的临时对象,再用临时对象拷贝构造c
如果你不想让上面这种事情发生,你可以这样,
explicit C(const int a = 0)
:_a(a)
{}
在C的构造函数前面加上关键字explicit。在EffectiveC++一书中,作者在一开始也提醒我们如果没有什么充分的理由不加上explicit,我们最好还是使用它。在C++11中,explicit也可以避免多参数的隐式类型转换。
class D
{
public:
explicit D(const int a = 0, const int b = 0)
:_a(a)
,_b(b)
{}
private:
int _a;
int _b;
};
D d = {1,2}; // C++11的初始化列表,但是由于explicit,无法成功。
(全文完)