文章目录
C++ 强制类型转换
在类型不同的情况下,会进行类型转换;C++主要提供了两种类型转换:
- 隐式类型转换。
- 显示类型转换。
下面我们针对这两种类型转换做基本的分析。
1. 隐式类型转换
隐式类型转换是编译器自动执行的,当编译器发现类型不一致的时候,就会自动隐式类型转换(当时是在能执行的时候),例如:
void TypeCast()
{
short a = 100;
int b = a;
}
编译器将自动进行类型转换,如下:
int b = a;
00CB16B7 movsx eax,word ptr [a]
00CB16BB mov dword ptr [b],eax
我们把这种基础类型的转换称之为标准转换。但是对于目前的编译器,从长的数据转换到短的编译器会给出相关警告。
对于类的隐式转换,存在三种情况:
- 单个参数的构造函数。
- 赋值操作。
- 类型转换函数。
例如:
class CData
{
public:
CData() {}
CData(int d) : m_iData(d) {}
CData& operator=(int d)
{
m_iData = d;
}
operator int()
{
return m_iData;
}
private:
int m_iData = 100;
};
void TypeCast()
{
CData d = 200; //构造函数
d = 1000; //赋值函数
int a = d; //类型转换函数
}
只要类支持了这三类函数,那么相应的隐式转换就会编译支持。
2. 强制类型转换
对于不同类型不能支持隐式类型的转换,C++提供了强制类型转换,默认情况下C++提供了5中类型转换。
- C语言式的类型转换。
static_cast
类型转换。const_cast
类型转换。dynamic_cast
类型转换。reinterpret_cast
类型转换。
2.1 C式强制类型转换
这种类型转换是C语言使用的类型转换,这种转换编译器不做任何判断,保障能够转换成功,例如:
void TypeCast()
{
int* a = (int*)'a';
int* b = (int*)100;
char* c = (char*)NULL;
a = (int*)c;
int* d = (int*)&std::cout;
}
编译器对于这种转换并不会给出任何的提示,全部转换成功;这就要求编码者对于数据类型非常了解,C++主要是为了简化编程,所以C++不支持使用上述的编码形式,对于这种类型转换应当慎用。更加麻烦的是,如果对于右虚函数继承关系的对象,使用强制转换会导致直接的错误(关于虚函数的内存地址布局,参考C++虚函数内存对象模型),因此我们必须使用C++的强制类型转换表达式。
2.2 static_cast类型转换
这个表达式的基本使用语法如下:
static_cast < new_type > ( expression )
这个转换主要使用如下场景:
- 隐式转换(包括C++类的隐式转换),这种转换和普通的C转换没有太大的区别。
downcast
,这个的意思是将基类的指针或者引用转换为子类的指针或者引用。- 右值类型转换,可以强制将左值转换为右值,例如
std::move
。 - 如果转换为void类型,那么这个表达式将值评估expression。
- 枚举类型可以转换为整数或者浮点类型。
- 浮点和整数也可以转换为枚举类型。
- void的指针可以转换为其他类型的指针。
值得注意的地方是:
static_cast
不能转换掉表达式的const
、volitale
、或者__unaligned
属性。static_cast
会做编译时的类型检测,如果类型不匹配将无法转换。static_cast
的 downcast或者upercast将会计算偏移获取真实地址(C式强转也一样)。
例如:
class CLevel0
{
public:
CLevel0() {}
~CLevel0() {}
private:
int m_Data0 = 10;
};
class CLevel1 : public CLevel0
{
public:
CLevel1() {}
virtual ~CLevel1() {}
virtual void Fun1()
{
std::cout << "CLevel1 Fun1" << std::endl;
}
private:
int m_Data1 = 100;
};
例如编译时类型检查:
void ClassObj()
{
char a = 'c';
int* b = static_cast<int*>(&a); //error C2440: 'static_cast': cannot convert from 'char *' to 'int *'
}
向下转换:
void ClassObj()
{
CLevel1 a;
CLevel0* b = &a;
CLevel0* c = static_cast<CLevel0*>(&a);
CLevel0* d = reinterpret_cast<CLevel0*>(&a);
}
//内部结果如下:
0:000> dv
d = 0x001bfc34
c = 0x001bfc38
b = 0x001bfc38
a = class CLevel1
0:000> dx -r1 (*((CPPDemo!CLevel1 *)0x1bfc34))
(*((CPPDemo!CLevel1 *)0x1bfc34)) [Type: CLevel1]
[+0x004] m_Data0 : 10 [Type: int]
[+0x008] m_Data1 : 100 [Type: int]
0:000> dds 0x1bfc34 L3
001bfc34 00988b34 CPPDemo!CLevel1::`vftable'
001bfc38 0000000a
001bfc3c 00000064
0:000> dx -r1 ((CPPDemo!CLevel0 *)0x1bfc34)
((CPPDemo!CLevel0 *)0x1bfc34) : 0x1bfc34 [Type: CLevel0 *]
[+0x000] m_Data0 : 9997108 [Type: int]
从这里看到:
- b,c都计算了偏移地址,直接跳过了
001bfc34 00988b34 CPPDemo!CLevel1::vftable
。 - d转换的时候,直接使用地址,导致使用了
001bfc34 00988b34 CPPDemo!CLevel1::vftable
数据错误。
2.3 const_cast 类型转换
这个的使用语法是:
const_cast < new_type > ( expression )
转换规则为:
- 通类型可以去掉const属性。
- 这个是需要类型匹配。
例如:
void ClassObj()
{
const char* a = "hello";
char* b = const_cast<char*>(a);
*const_cast<char*>(a) = 'w'; //这个禁止使用,因为地址为不可写入地址,会导致异常行为
std::cout << a << std::endl;
}
2.4 dynamic_cast类型转换
这个主要是用来从多态情况下的类型转换,还是使用之前的例子:
class CLevel0
{
public:
CLevel0() {}
virtual ~CLevel0() {}
private:
int m_Data0 = 10;
};
class CLevel1 : public CLevel0
{
public:
CLevel1() {}
virtual ~CLevel1() {}
virtual void Fun1()
{
std::cout << "CLevel1 Fun1" << std::endl;
}
private:
int m_Data1 = 100;
};
对于如下情况的转换:
void ClassObj()
{
CLevel0* a = new CLevel0;
CLevel1* b = static_cast<CLevel1*>(a);
delete a;
}
很明显这个代码是很有问题的,但是编译器可以直接编译通过,编译后的结果如下:
0:000> dv
b = 0x015cada4
a = 0x015cada8
0:000> dx -r1 ((CPPDemo!CLevel0 *)0x15cada8)
((CPPDemo!CLevel0 *)0x15cada8) : 0x15cada8 [Type: CLevel0 *]
[+0x000] m_Data0 : 10 [Type: int]
0:000> dx -r1 ((CPPDemo!CLevel1 *)0x15cada4)
((CPPDemo!CLevel1 *)0x15cada4) : 0x15cada4 [Type: CLevel1 *]
[+0x004] m_Data0 : 10 [Type: int]
[+0x008] m_Data1 : -33686019 [Type: int]
可以看到b的类型很明显是错误的,那么有么有办法识别这种错误呢?这就是需要涉及到类型识别了,我们可以使用dynamic_cast
来解决这个问题,如下:
void ClassObj()
{
CLevel0* a = new CLevel0;
CLevel1* b = dynamic_cast<CLevel1*>(a);
delete a;
}
转换后的结果如下:
0:000> dv
b = 0x00000000
a = 0x00e65960
0:000> dx -r1 ((CPPDemo!CLevel0 *)0xe65960)
((CPPDemo!CLevel0 *)0xe65960) : 0xe65960 [Type: CLevel0 *]
[+0x004] m_Data0 : 10 [Type: int]
进行了运行时的判断,判断类型不匹配没有转换成功,也许你会好奇,什么情况下会将基类指针转换为子类指针呢?请示场景很多的,例如如下:
class CAnimal
{
};
class CDog : public CAnimal
{
};
class CCat : public CAnimal
{
};
CAnimal* CreatorFactory(int type)
{
switch (type)
{
case 0:
return new CDog();
break;
case 1:
return new CCat();
break;
default:
return nullptr;
}
}
void Test()
{
CAnimal* Obj = CreatorFactory(0);
}
如果我们此时需要转换Obj
到CCat
的时候,希望能够转换不成功,那么此时就需要使用dynamic_cast
了。那么dynamic_cast
底层是怎么实现的呢?
CLevel1* b = dynamic_cast<CLevel1*>(a);
00F65470 push 0
00F65472 push offset CLevel1 `RTTI Type Descriptor' (0F6A120h)
00F65477 push offset CLevel0 `RTTI Type Descriptor' (0F6A13Ch)
00F6547C push 0
00F6547E mov eax,dword ptr [a]
00F65481 push eax
00F65482 call ___RTDynamicCast (0F6150Ah)
00F65487 add esp,14h
00F6548A mov dword ptr [b],eax
主要是做了RTTI类型识别操作。RTTI保存在虚函数表中,相关结构信息如下:
例如上述代码中:
0:000> dv
a = 0x006fad28
0:000> dds 0x006fad28 L1
006fad28 00f68b34 CPPDemo!CLevel0::`vftable' //虚函数表
0:000> dd 00f68b34-4 L1 //虚函数表的前面
00f68b30 00f68f30
0:000> dd 00f68f30 L4 //
00f68f30 00000000 00000000 00000000 00f6a13c //type_info
00F65477 push offset CLevel0 RTTI Type Descriptor' (0F6A13Ch)
就是最后一个地址,然后调用___RTDynamicCast
匹配类型是否可以真实转换。
对于dynamic_cast
转换有两个不同地方:
- 对于指针,如果转换是否,返回null。
- 如果对于引用,将会抛出异常(因为引用无法指向空)。
2.5 reinterpret_cast类型转换
这个是真实意义上面的强制转换,可以转换所有类型,并且无论什么操作,不用重新计算内存布局信息,直接强制类型转换,例如:
class CLevel0
{
public:
CLevel0() {}
~CLevel0() {}
private:
int m_Data0 = 10;
};
class CLevel1 : public CLevel0
{
public:
CLevel1() {}
virtual ~CLevel1() {}
virtual void Fun1()
{
std::cout << "CLevel1 Fun1" << std::endl;
}
private:
int m_Data1 = 100;
};
void ClassObj()
{
CLevel1 a;
CLevel0* d = reinterpret_cast<CLevel0*>(&a);
}
结果如下:
0:000> dv
d = 0x001bfc34
a = class CLevel1
0:000> dx -r1 (*((CPPDemo!CLevel1 *)0x1bfc34)) //a
(*((CPPDemo!CLevel1 *)0x1bfc34)) [Type: CLevel1]
[+0x004] m_Data0 : 10 [Type: int]
[+0x008] m_Data1 : 100 [Type: int]
0:000> dds 0x1bfc34 L3
001bfc34 00988b34 CPPDemo!CLevel1::`vftable'
001bfc38 0000000a
001bfc3c 00000064
0:000> dx -r1 ((CPPDemo!CLevel0 *)0x1bfc34) //d
((CPPDemo!CLevel0 *)0x1bfc34) : 0x1bfc34 [Type: CLevel0 *]
[+0x000] m_Data0 : 9997108 [Type: int]
从这里看,d实际完全使用了a的地址,并没有根据内存布局信息来调整d指针的偏移,导致d的m_Data0
实际变成了虚表指针。
3. 总结
对于C++的强制类型转换,大致可以总结如下:
- 默认类型的转换,使用
static_cast
,没有虚函数的基类指针转换为子类的指针也使用static_cast
。 - const属性去除,使用
const_cast
。 - 函数虚函数的基类指针或者引用转换为子类的指针或者引用使用
dynamic_cast
。 - 在原始的地址或者值上面强制进行类型转换,使用
reinterpret_cast
(这个不进行类型检测)。