作者:知乎用户
链接: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 的单继承