提问人:Riccardo Caiulo 提问时间:10/3/2023 最后编辑:Jan SchultkeRiccardo Caiulo 更新时间:11/9/2023 访问量:122
为什么纯虚拟成员函数必须是虚拟的?
Why does a pure virtual member function have to be virtual?
问:
我有一个关于 C++ 中纯虚函数声明的问题。我有 Java 背景,所以我认为纯虚函数是定义抽象类和接口思想的一种方式。我的问题很简单,当我们在 C++ 中定义一个纯虚函数时,我们必须写这样的东西:
virtual void function() =0
void 可以是任何类型,但我们必须包含等于 0 和 virtual 关键字。我把“等于 0”部分理解为用于定义纯函数,但我的问题是为什么它必须是虚拟的?我们不能在没有虚拟关键字的情况下定义它吗?这只是包含在纯虚函数定义中的 C++ 的一部分,还是有逻辑上的理由说明为什么“抽象方法”也必须是虚的?
答:
非虚拟纯函数没有多大意义。 从根本上说,函数是可以以某种方式调用的代码的命名部分。如果你让它变得纯粹,这意味着(可能)根本没有代码部分;只有名字。
这对于成员函数来说确实有意义,因为实现可以由其中一个派生类提供。即使不存在,呼叫也可能通过动态调度进行调用。但是,如果没有 ,就不存在这样的机制。virtual
Base::foo()
Base::foo()
Derived::foo()
virtual
同样,方法在 Java 中也没有多大意义。可以考虑 C++ 中的非虚拟成员函数,因为没有覆盖它的机制。abstract final
final
为什么它必须是虚拟的?我们不能在没有虚拟关键字的情况下定义它吗?
答案是否定的。虚拟成员函数和普通成员函数的机制有很大不同。
成员函数的工作原理
成员函数的地址由链接器在链接周期内确定。
检查此代码。在这段代码中,链接后,部分将被替换为真实的偏移地址,这意味着它是硬编码的。无论是否存在派生类,如果调用 的引用/指针,则确定要调用的函数。void Test::member(void)
call void Test::member(void)
member()
Test
然而,这并不是虚拟函数在 C++ 中的工作方式。
虚拟成员函数的工作原理
在 C++ 中,如果一个类具有虚拟函数,它将有一个虚拟表。TLDR,这里有一个小例子:
#include <iostream>
class Test {
virtual void test() = 0;
};
int main() {
std::cout << "sizeof(Test): " << sizeof(Test) << std::endl;
}
sizeof(Test): 8
虽然为空,但它包含指向虚拟表的指针,这使得多态性成为可能。Test
通过指定一个函数,只有编译器会将函数地址的地址放入虚拟表中,而不是用硬编码的函数地址替换调用语句。virtual
每当您声明一个虚拟函数时,都会在 vtable 中创建一个项目。在此示例中,一旦声明 ,第一项将存储 的函数地址,但由于它是一个纯虚函数,因此该项的内容可能是或其他任何内容,具体取决于编译器的实现。一旦你从 派生了一个类,假设 ,并在 中覆盖,第一项的内容将更新为 的地址。Test::test
Test::test
nullptr
Test
Derived
test
Derived
Derived::test
当你得到一个指针/引用,并尝试调用对象的函数时,程序将首先获取vtable中的第一项,获取的地址,然后调用该地址的函数。Test
test
<ObjectType>::test
下面是一个小例子:
#include <iostream>
class Test {
public:
virtual void test() = 0;
};
class Derived_A : public Test {
public:
virtual void test() override { std::cout << "A::" << __func__ << std::endl; }
};
class Derived_B : public Test {
public:
virtual void test() override { std::cout << "B::" << __func__ << std::endl; }
};
int main() {
std::cout << "sizeof(Test): " << sizeof(Test) << std::endl;
Derived_A a;
Derived_B b;
long *vtable = (long *)(&a); // get vtable address
long *first_item = (long *)(*vtable); // get the address of the first item
void (*func)() = (void (*)())(*first_item);
}
结果:
sizeof(Test): 8
A::test
由于我们得到了 的 vtable 的地址,因此我们用表的第一项调用的函数是 。然而,如果你使用相同的方法对 的 vtable 进行操作,你将调用 ,这就是多态性在 C++ 中的工作方式。Derived_A
Derived_A::test
Derived_B
Derived_B::test
上一个:在没有特定返回类型的情况下编写纯虚函数的正确方法是什么?
下一个:绑定纯虚法
评论
virtual
= 0
virtual
virtual
= 0
virtual
virtual
override
final