为什么dereference运算符在C ++中保留多态(后期绑定)?

时间:2014-05-19 22:14:07

标签: c++ polymorphism

众所周知,只有在通过a进行呼叫时,才会在运行时解析虚拟 引用或指针。"。因此,当我发现取消引用运算符也保留了动态绑定功能时,我感到很惊讶。

#include <iostream>
using namespace std;

struct B {
  virtual void say() { cout << "Hello B" << endl; }
};

struct D : B {
  void say() override { cout << "Hello D" << endl; }
};

int main() {
    D *ptr = new D();
    B *p = ptr;
    (*p).say();
    return 0;
}

输出

Hello D

问题:编译器处理解除引用运算符*的内容是什么?

我认为它是在编译时完成的。因此,当编译器使用指针p时,它应该假定p指向类型B的对象。例如,以下代码

D temp = (*p);

抱怨

error: no viable conversion from 'B' to 'D'

5 个答案:

答案 0 :(得分:8)

从表面上看,这是一个有趣的问题,因为如果没有一元*的重载,则取消引用左值B ,而不是引用类型。然而,即使开始沿着这条推理线走下去也是一个红色的鲱鱼:表达式从不有引用类型,因为引用会立即被删除并确定值类别。从这个意义上讲,一元*运算符非常类似于返回引用的函数

事实上,答案是您的初始断言不正确:动态分派根本不依赖于引用或指针。它是引用和指针,使您能够阻止切片,但是一旦你有一些表达式引用你的多态对象,任何旧的函数调用都会这样做。

还要考虑:

#include <iostream>

struct Base
{
   virtual void foo() { std::cout << "Base::foo()\n"; }
   void bar() { foo(); }
};

struct Derived : Base
{
   virtual void foo() { std::cout << "Derived::foo()\n"; }
};

int main()
{
   Derived d;
   d.bar();    // output: "Derived::foo()"
}

live demo

答案 1 :(得分:5)

derefencing / indirection operator *本身并没有做任何事情。 例如,当您只编写*p;时,如果p只是一个指针,编译器可能会忽略此行。

*所做的是改变读写的语义:

int  i = 42;
int* p = &i;

*p = 0;
 p = 0;

*p = 0表示写入对象p指向。 请注意,在C ++中,对象 是存储区域。

类似地,

auto x =  p; // copies the address
auto y = *p; // copies the value

此处,*p的读取表示读取对象p的值指向

*p的值类别仅确定C ++语言允许的操作 关于*p形式的表达式。

引用实际上只是语法糖的指针。 因此,试图通过使用引用解释*p的作用是循环推理。


让我们考虑一下稍微改变的类:

class Base
{
private:
    int b = 21;
public:
    virtual void say() { std::cout << "Hello B(" <<b<< ")\n"; }
};

class Derived : public Base
{
private:
    int d = 1729;
public:
    virtual void say() { std::cout << "Hello D(" <<d<< ")\n"; }
};


Derived d;
Derived *pd = &d;
Base* pb = pd;

有点奇怪,但我认为允许的内存布局如下:

$$2d graphics mode$$

        +-Derived------------+
        |    +-Base---+----+ |
        | d  | vtable | b  | |
        |    +--------+----+ |
        +----^---------------+
        ^    | pb
        | pd


$$1d graphics mode$$

name    #    /../   |d       |vtable          |b       |
address #   /../    |0 1 2 3 |4 5 6 7 8 9 1011|12131415|16
                     ^        ^
                     | pd     | pb

pd == some address
pb == pd + 4 byte

当我们从Derived*转换为Base*时,编译器知道偏移量 Base对象中的Derived子对象, 并且可以计算该子对象的地址值。

存储vtable指针,用于单个非虚拟继承, 在具有虚函数的最少派生类型中。 派生类的变化大致与in this implemenation/simulation一样。

我们现在打电话

pb->say()

在C ++标准中定义为

(*pb).say()

编译器根据pb的类型(Base*)知道我们称之为虚函数。 因此,(*pb).say()表示在vtable中查找say的条目 对象pb指向,并将其称为。 对象pb部分指向是允许多态的。

另一方面,当我们复制时

Base b = *pb;

发生的事情是vtable指针被复制。 这会很危险,因为Derived::say可能会尝试访问Derived::d。 但是此数据成员在Base类型的对象中不可用, 我们目前正在创建(在Base的副本中)。

答案 2 :(得分:2)

在做了一些研究之后,我想我有一个合理的(至少对我来说)这个问题的答案。

假设(摘自“C ++ Primer 5th”一书中摘录或转述):

  1. 指针p上的解引用运算符*,即(*p),返回p指向的对象。
  2. 派生类D: public B 的对象在逻辑上具有两部分,一部分是B类的子对象,另一部分是D类成员。(这解释了“切片
  3. 我过去支持此答案的virtual mechanism of C++来自文章12.5 The Virtual Table。它令我信服至少。下图是概念性地显示了我们问题中代码的*__vptrVTableenter image description here

    我的解释。

    D obj_d;
    D* ptr = &obj_d; // ptr is a pointer to type D,
                     // and points to obj_d, an object of type D
    
    B* p = ptr;      // p is a pointer to type B and p points to the B subobject of obj_d.
    
    (*p).say();
    

    由于p是指向B类型的指针,(*p)会返回B类型的对象, 即(*ptr)的子对象。将此B类型的对象命名为obj_b

    但是,*__vptr的{​​{1}}指向obj_b的VTable。因此,当它打电话 Dsay()的VTable中say()的函数指针指向打印的方法 D

    支持我解释的实验。

    "Hello D"

    进一步注意

    在调用对象 (&(*p))->say(); // outputs "Hello D" 的方法期间,是否发生多态(类成员的动态绑定)取决于它所指向的对象x的哪个VTable。

    如果我们写*__vptr,则输出为“Hello B”。这是因为obj_x是一个完全新构造的B类对象,它使用struct B的合成拷贝构造函数。因此,obj_x的B obj_x(*p); (&obj_x)->say();指向B的VTable。

    感谢dyp的帮助,我们有simulation of the virtual dispatch of this question。如果Coiliru删除了网页,我存储了代码here

答案 3 :(得分:1)

这里你不用

调用(虚拟)功能
p->say();

但是

(*p).say();

同样的,只是不同的符号。您调用virtual函数,然后解析dynamicaly。

编辑:

对于(*p).say()编译器,将执行以下操作:

  • 来自变量p load value(某事物的地址,在本例中为B或子类的对象)
  • 获取虚拟表指针的地址(相对于obj地址)。
  • 转到表并调用say()函数(act中的多态)。

答案 4 :(得分:-2)

*p是对B对象的引用。