众所周知,只有在通过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'
答案 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()"
}
答案 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”一书中摘录或转述):
(*p)
,返回p
指向的对象。D: public B
的对象在逻辑上具有两部分,一部分是B类的子对象,另一部分是D类成员。(这解释了“切片我过去支持此答案的virtual mechanism of C++
来自文章12.5 The Virtual Table。它令我信服至少。下图是概念性地显示了我们问题中代码的*__vptr
和VTable
。
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。因此,当它打电话
D
,say()
的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()
编译器,将执行以下操作:
答案 4 :(得分:-2)
*p
是对B
对象的引用。