C ++如果我将派生对象向上转换为其基类型,虚函数是否仍然有用

时间:2014-03-19 11:32:19

标签: c++

请考虑以下事项:

  • B继承自A并覆盖打印功能。
  • A有一个静态函数,它取一个void *,将它强制转换为A并调用虚拟打印函数。
  • 如果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被覆盖的打印功能。原因不是立即清楚。

有人可以向我解释这是否是我可以依赖的行为,或者它是否只是一些有效的因素,因为它是一个小例子(比如如何返回对局部变量的引用并不总是在小程序中出现段错误)。

4 个答案:

答案 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;每个功能的地址。

编译后,编译器将为每个类(AB)创建Vtables: A的每个实例都有vtable = <Address of A Vtable>,而B的每个实例都有vtable = <Address of B Vtable>

在运行时,如果调用虚函数,程序将查找&#34; real&#34;来自Vtable的函数的地址,该地址存储在AB

的每个对象的第一个元素的地址处

以下代码是非标准且不健全的 ......但它可能会给你一个想法......

#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

https://ideone.com/iKyBB3

注意:无论您是通过A*还是B*访问B对象,您都会首先找到相同的vtable地址,并且您将找到包含在也是如此。

答案 3 :(得分:0)

如果你考虑在C ++中如何实现多重继承,那么

reinterpret_cast是行不通的。

基本上,当在具有多重继承的相关类型之间进行转换时,强制转换可能涉及添加偏移量。因此,在不知道源和目标类型的情况下,编译器无法发出正确的指令。

因此,对于至少这个用例,重新解释强制转换是可撤消的,因此它们被定义为未定义。

这里的危险部分,即使你不进行多重继承,现代编译器开始将这种“未定义”行为解释为意味着它们可以优化事物,它包含块等等。这几乎肯定是C ++标准有效(未定义意味着什么都没关系),但对开发人员来说可能是一个惊喜,开发人员倾向于将“未定义”理解为“代码输出可能无法正常工作”。