众所周知,"只有在通过引用或指针进行调用时才能在运行时解析虚拟 ." 因此,当我发现取消引用运算符也保持动态绑定功能时,令我惊讶的是.
#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;
}
Run Code Online (Sandbox Code Playgroud)
输出是
Hello D
Run Code Online (Sandbox Code Playgroud)
问题:编译器处理解除引用运算符*的是什么?
我以为它是在编译时完成的.因此,当编译器使用指针p时,它应该假定p指向类型B的对象.例如,下面的代码
D temp = (*p);
Run Code Online (Sandbox Code Playgroud)
抱怨
error: no viable conversion from 'B' to 'D'
Run Code Online (Sandbox Code Playgroud)
从表面上看,这是一个有趣的问题,因为没有一元的重载*
,解除引用导致左值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()"
}
Run Code Online (Sandbox Code Playgroud)
(现场演示)
derefencing/indirection运算符*
本身并不执行任何操作.例如,当你只编写时,*p;
如果p
只是一个指针,编译器可能会忽略这一行.
它的*
作用是什么改变了读写的语义:
int i = 42;
int* p = &i;
*p = 0;
p = 0;
Run Code Online (Sandbox Code Playgroud)
该*p = 0
方法写入对象p
点.请注意,在C++中,对象 是存储区域.
同样的,
auto x = p; // copies the address
auto y = *p; // copies the value
Run Code Online (Sandbox Code Playgroud)
这里,读取*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;
Run Code Online (Sandbox Code Playgroud)
有点奇怪,但我认为允许的内存布局如下所示:
$$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指针存储在具有虚函数的最少派生类型中.它大致如本实现/模拟中所见,由派生类更改.
我们现在打电话
pb->say()
Run Code Online (Sandbox Code Playgroud)
它在C++标准中定义为
(*pb).say()
Run Code Online (Sandbox Code Playgroud)
编译器根据pb
(即Base*
)的类型知道我们称之为虚函数.因此,(*pb).say()
手段查找的条目say
在虚函数表
的对象pb
点,并调用它.对象pb
的一部分指向允许多态的内容.
另一方面,当我们复制
Base b = *pb;
Run Code Online (Sandbox Code Playgroud)
会发生什么是不复制vtable指针.这将是危险的,因为Derived::say
可能会尝试访问Derived::d
.但是这个数据成员在Base
我们当前正在创建的类型的对象中不可用(在复制文件中Base
).