请考虑以下事项:
如果void *最初是B,它会调用A :: print或B :: print?
#include <iostream>
class A {
public:
static void w(void *p) {
A *a = reinterpret_cast<A*>(p);
a->print();
}
virtual void print() {
std::cout << "A" << std::endl;
}
};
class B : public A {
public:
void print() {
std::cout << "B" << std::endl;
}
};
int main () {
B b;
A::w(&b);
}
这为我打印B.
似乎已经转换为A的void *仍然知道B被覆盖的打印功能。原因不是立即清楚。
有人可以向我解释这是否是我可以依赖的行为,或者它是否只是一些有效的因素,因为它是一个小例子(比如如何返回对局部变量的引用并不总是在小程序中出现段错误)。
答案 0 :(得分:8)
您的代码有未定义的行为
§5.2.10重新解释演员
7将“指向T1的指针”类型的prvalue转换为 键入“指向T2的指针”(其中T1和T2是对象类型,T2的对齐要求不比T1更严格)并返回其原始类型会产生原始指针值。 未指定任何其他此类指针转换的结果。
答案 1 :(得分:5)
虚函数通常由隐式vtable
解析。这基本上是类层次结构中每个虚函数的函数指针数组。编译器将其添加为&#34;隐藏成员&#34;到你的班级。调用虚函数时,调用vtable中的相应条目。
现在,当您创建类型为B
的类时,它隐式地将B-vtable存储在对象中。强制转换不会影响此表。
因此,当您将void *
投射到A
时,会显示原始vtable(类B
),其指向B::print
。
请注意,这是实现定义的行为,标准不保证这一点。但大多数编译器都会这样做
答案 2 :(得分:2)
首先,您的reinterpret_cast
未定义。
如果您将A*
传递给w
,则会定义。
A * p = new B;
A::w(p);
delete p;
如果始终使用static_cast<A*>(p)
调用w
,我建议使用A*
。
如果你有void*
定义的强制转换,那么内存地址保持不变。
因此,如果您首先将有效的a
传递给w
,则A*
内的A*
将是有效的w
。
程序知道如何处理呼叫的问题与称为“虚拟表”的机制有关。
注意:对于不同的编译器,这可能会有所不同。我将谈谈Visual Studio如何处理它。
为简单的继承提供一些粗略的想法:
编译器将在您的代码中编译2个print
函数:
A::print
(,即地址X
)和B::print
(,即地址Y
)。
包含虚函数的类的实际内存占用量(即
struct A
{
void print (void);
size_t x;
};
struct B : A
{
void print (void);
size_t y;
};
)有点像
struct Real_A
{
void * vtable;
size_t x;
};
struct Real_B : Real_A
{
size_t y;
};
此外,将有两个所谓的虚拟表,每个类包含一个包含虚函数或具有虚函数的基类。
您可以将vtable视为一个持有&#34;真实&#34;每个功能的地址。
编译后,编译器将为每个类(A
和B
)创建Vtables:
A的每个实例都有vtable = <Address of A Vtable>
,而B
的每个实例都有vtable = <Address of B Vtable>
。
在运行时,如果调用虚函数,程序将查找&#34; real&#34;来自Vtable的函数的地址,该地址存储在A
或B
以下代码是非标准且不健全的 ......但它可能会给你一个想法......
#include <iostream>
struct A
{
virtual void print (void) { std::cout << "A called." << std::endl; }
size_t x;
};
struct B : A
{
void print (void) { std::cout << "B called." << std::endl; }
};
// "Real" memory layout of A
struct Real_A
{
void * vtable;
size_t x_value;
};
// "Real" memory layout of B
struct Real_B : Real_A
{
size_t y_value;
};
// "Pseudo virtual table structure for classes with 1 virtual function"
struct VT
{
void * func_addr;
};
int main (void)
{
A * pa = new A;
pa->x = 15;
B * pb = new B;
pb->x = 20;
A * pa_b = new B;
pa_b->x = 25;
// reinterpret addrress of A and B objects as Real_A and Real_B
Real_A& ra(*(Real_A*)pa);
Real_B& rb(*(Real_B*)pb);
// reinterpret addrress of B object through pointer to A as Real_B
Real_B& rb_a(*(Real_B*)pa_b);
// Print x_values to know whether we meet the class layout
std::cout << "Value of ra.x_value = " << ra.x_value << std::endl;
std::cout << "Value of rb.x_value = " << rb.x_value << std::endl;
std::cout << "Value of rb.x_value = " << rb_a.x_value << std::endl;
// Print vtable addresses
std::cout << "VT of A through A*: " << ra.vtable << std::endl;
std::cout << "VT of B through B*: " << rb.vtable << std::endl;
std::cout << "VT of B through A*: " << rb_a.vtable << std::endl;
// Reinterpret memory pointed to by the vtable address as VT objects
VT& va(*(VT*)ra.vtable);
VT& vb(*(VT*)rb.vtable);
VT& vb_a(*(VT*)rb_a.vtable);
// Print addresses of functions in the vtable
std::cout << "FA of A through A*: " << va.func_addr << std::endl;
std::cout << "FA of B through B*: " << vb.func_addr << std::endl;
std::cout << "FA of B through A*: " << vb_a.func_addr << std::endl;
delete pa;
delete pb;
delete pa_b;
return 0;
}
Visual Studio 2013输出:
Value of ra.x_value = 15 Value of rb.x_value = 20 Value of rb.x_value = 25 VT of A through A*: 00D9DC80 VT of B through B*: 00D9DCA0 VT of B through A*: 00D9DCA0 FA of A through A*: 00D914B0 FA of B through B*: 00D914AB FA of B through A*: 00D914AB
gcc-4.8.1输出:
Value of ra.x_value = 15 Value of rb.x_value = 20 Value of rb.x_value = 25 VT of A through A*: 0x8048f38 VT of B through B*: 0x8048f48 VT of B through A*: 0x8048f48 FA of A through A*: 0x8048d40 FA of B through B*: 0x8048cc0 FA of B through A*: 0x8048cc0
注意:无论您是通过A*
还是B*
访问B对象,您都会首先找到相同的vtable地址,并且您将找到包含在也是如此。
答案 3 :(得分:0)
reinterpret_cast是行不通的。
基本上,当在具有多重继承的相关类型之间进行转换时,强制转换可能涉及添加偏移量。因此,在不知道源和目标类型的情况下,编译器无法发出正确的指令。
因此,对于至少这个用例,重新解释强制转换是可撤消的,因此它们被定义为未定义。
这里的危险部分,即使你不进行多重继承,现代编译器开始将这种“未定义”行为解释为意味着它们可以优化事物,它包含块等等。这几乎肯定是C ++标准有效(未定义意味着什么都没关系),但对开发人员来说可能是一个惊喜,开发人员倾向于将“未定义”理解为“代码输出可能无法正常工作”。