C++的指针类型的强制转换何解

作者:知乎用户
链接:https://www.zhihu.com/question/64761793/answer/224490320
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
 

因为所有指针类型都是 void* 类型,所以不需要写任何转换关键字就可以完成了。转换到 void* ,也就是说把指针指向的数据类型信息给去掉。这样,编译器就不知道被指向的数据是什么东西,那么当然编译器也无法对 void* 的指针 p 做 p [ i ], p += x; 这样的操作。如果再把 void* 转换到其他具体类型时,编译器也就无法根据对象模型进行指针值的偏移调整。

顺便,我这里讲一下 C++ 语言里的这几个 cast:

(1)dynamic_cast:

upcast 时,等效于 static_cast。(c++ 支持隐式 upcast ,所以 upcast 不需要写任何东西)

downcast 时,在运行时进行类型检查,如果你“欺骗”编译器(实际指向的对象不是预期的类型),将会得到 NULL 指针。如果你没有欺骗编译器,效果等同于 static_cast ,但显然运行时成本高,效率比 static_cast 低。转换不一定会成功,因此你应该检查返回值(在早期也可能是要你捕获异常)。

即使两者之间明显不搭界,驴唇不对马嘴,编译器也不会有任何抱怨,一切交给运行时处理,无所谓。

有很小的运行时成本。

转换前后,指针的值可能发生变化。

(2)static_cast:

在编译时转换。如果指针为 NULL,转换结果为 NULL。否则,根据源对象和目标对象,在对象模型上的偏移关系,在编译时“调整”指针的值(可能对指针的值进行偏移)。如果编译器发现两者之间明显不搭界,驴唇不对马嘴,那么在编译时就会拒绝你。效率高。编译器将信任你,如果你欺骗编译器,可能引发严重后果(可能导致程序崩溃)。

可认为没有运行时成本。

转换前后,指针的值可能发生变化。

(3)const_cast:

用于把 const 指针的 const 修饰去掉。显然,这种做法有些粗鲁、粗暴,你应该尽可能避免。

(4)reinterpret_cast:

对指向的内存数据进行“重新解释”,也就是说你将得到一个新的指针,直接指向你提供的地址,并且把那里的数据视作“你想要的数据类型“。转换前后的指针的值保持不变。这和 C 语言的强制类型转换的实现结果一致。(因为 C 语言里没有“继承”这种概念)。编译器将信任你,如果你欺骗编译器,可能引发严重后果(例如进程崩溃)。

不能去除 const 修饰。

没有运行时成本。

转换前后,指针的值不变。

(5)T* pT = (T*) (XXX); (C 语言模式里的强制类型转换)

在 C++ 里面,

如果两者类型实际存在转换关系,则等价于 static_cast。

如果两者之间明显不搭界,驴唇不对马嘴,则等价于 reinterpret_cast。

可认为没有运行时成本。

转换前后,指针的值可能发生变化。

====

总结,由于 C++ 支持隐式 upcast(好比说,狗是动物,这句话当然是对的),所以 upcast 不用写任何转换。所以 static_cast 和 dynamic_cast 主要用于 downcast,把基类指针转换成”更详细“”更具体“的子类指针。

static_cast (编译时转换)效率更高,但你要对转换结果负责,你必须确保你告诉编译器的情况属实,如果你不了解情况进行蛮干,那么将来运行时出了什么偏差你是要负责任的。

dynamic_cast 交给运行时进行类型检查,如果情况属实,就会给你转换。如果情况不符,就返回给你 NULL。所以,当你不了解情况,你也尽可以尝试一下看能不能转。但有运行时成本,效率低。

reinterpret_cast 是 C 语言里强制类型转换的实现等价物,因为如果你在 C++ 里写 C 语言模式的强制转换,会被编译器当作 static_cast 处理。如果你不想要编译器在背后插手搞任何小动作,想要得到 C 语言强制转换的实现效果,那么你要使用这个转换。它是对一块 bits 模式的数据进行重解释。

在 C++ 理念和哲学里,你应该把 const_cast 和 reinterpret_cast 视作最后的考虑和选择。

===

最后附上我写的测试和研究代码:

#include <stdio.h>
#include <string.h>

//base class 1
class HasName
{
protected:
	char m_name[12];
public:
	HasName() { m_name[0] = 0; }
	virtual ~HasName() { }
	virtual void TellName() { printf("My name is %s.\n", m_name); }
	void SetName(const char* name) { strcpy(m_name, name); }
};

//base class 2
class HasHobby {
protected:
	char m_hobby[12];
public:
	HasHobby() { m_hobby[0] = 0; }
	virtual ~HasHobby(){ }
	virtual void TellHobby() { printf("I like %s.", m_hobby); }
	void SetHobby(const char* hobby) { strcpy(m_hobby, hobby); }
};

class Tom : public HasName, public HasHobby {
private:
	char m_gf[16];
public:
	Tom() { m_gf[0] = 0; }
	virtual ~Tom() { }
	virtual void TellGf() {	printf("My gf is %s.\n", m_gf); }
	void SetGf(const char* gf) { strcpy(m_gf, gf); }
};

//打酱油的
class John : public HasName {
public:
	char m_padding[16]; //HasHobby 的占位
	char m_gf[16]; //和 Tom 的成员保持一致
public:
	John() {
		strcpy(m_name, "John");
		strcpy(m_gf, "Marry");
	}
	~John() { }
};

int main(int argc, char* argv[]) {
	Tom tom; //用这个对象得到 Tom 的虚函数表地址
	John john; //打酱油的一个人 
	//拷贝 Tom 的 HasHobby 虚函数表指针
	memcpy(john.m_padding, (char*)&tom + 16, 4);
	strcpy(&john.m_padding[4], "Drawing"); //为 john 设置 hobby。
	Tom* pTom = (Tom*)&john; //C style cast; 完全不搭界的两种类型
	pTom->TellName(); //该调用是安全的
	pTom->TellHobby(); //拷贝了 tom 的虚函数表后,该调用是安全的
	pTom->TellGf(); //Will corrupt at run-time !!!
	//Because pTom is using John's vftable.
	return 0;
}

上面代码中只包含了 C Style 的强制类型转换,在代码中假设虚函数表指针占用 4 bytes,成员数据紧随其后(需要面向 x86 编译和对应的对齐)。对象 UML 类图和 C++ 对象模型如下:

 

在上图中,可以看到 Tom* 和 HasHobby* 之间存在 0x10 的字节距离。Tom* 和 HasName* 是重叠的,因为 HasName 是 Tom 的第一个基类,换句话说,如果一直是 single base 的单继承

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值