C++ 强制类型转换

C++ 强制类型转换

在类型不同的情况下,会进行类型转换;C++主要提供了两种类型转换:

  1. 隐式类型转换。
  2. 显示类型转换。

下面我们针对这两种类型转换做基本的分析。

1. 隐式类型转换

隐式类型转换是编译器自动执行的,当编译器发现类型不一致的时候,就会自动隐式类型转换(当时是在能执行的时候),例如:

void TypeCast()
{
	short a = 100;
	int b = a;
}

编译器将自动进行类型转换,如下:

	int b = a;
00CB16B7  movsx       eax,word ptr [a]  
00CB16BB  mov         dword ptr [b],eax  

我们把这种基础类型的转换称之为标准转换。但是对于目前的编译器,从长的数据转换到短的编译器会给出相关警告。

对于类的隐式转换,存在三种情况:

  1. 单个参数的构造函数。
  2. 赋值操作。
  3. 类型转换函数。

例如:

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中类型转换。

  1. C语言式的类型转换。
  2. static_cast类型转换。
  3. const_cast类型转换。
  4. dynamic_cast类型转换。
  5. 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 )		

这个转换主要使用如下场景:

  1. 隐式转换(包括C++类的隐式转换),这种转换和普通的C转换没有太大的区别。
  2. downcast,这个的意思是将基类的指针或者引用转换为子类的指针或者引用。
  3. 右值类型转换,可以强制将左值转换为右值,例如std::move
  4. 如果转换为void类型,那么这个表达式将值评估expression。
  5. 枚举类型可以转换为整数或者浮点类型。
  6. 浮点和整数也可以转换为枚举类型。
  7. void的指针可以转换为其他类型的指针。

值得注意的地方是:

  1. static_cast不能转换掉表达式的constvolitale、或者__unaligned属性。
  2. static_cast会做编译时的类型检测,如果类型不匹配将无法转换。
  3. 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]

从这里看到:

  1. b,c都计算了偏移地址,直接跳过了001bfc34 00988b34 CPPDemo!CLevel1::vftable
  2. d转换的时候,直接使用地址,导致使用了001bfc34 00988b34 CPPDemo!CLevel1::vftable数据错误。

2.3 const_cast 类型转换

这个的使用语法是:

const_cast < new_type > ( expression )		

转换规则为:

  1. 通类型可以去掉const属性。
  2. 这个是需要类型匹配。

例如:

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);
}

如果我们此时需要转换ObjCCat的时候,希望能够转换不成功,那么此时就需要使用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转换有两个不同地方:

  1. 对于指针,如果转换是否,返回null。
  2. 如果对于引用,将会抛出异常(因为引用无法指向空)。

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++的强制类型转换,大致可以总结如下:

  1. 默认类型的转换,使用static_cast,没有虚函数的基类指针转换为子类的指针也使用static_cast
  2. const属性去除,使用const_cast
  3. 函数虚函数的基类指针或者引用转换为子类的指针或者引用使用dynamic_cast
  4. 在原始的地址或者值上面强制进行类型转换,使用reinterpret_cast(这个不进行类型检测)。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xdesk

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值