虚拟化概念

4

我很新于CPP,正在学习关于后期绑定多态性的知识。

根据我所读和了解的,virtual关键字用于实现后期绑定。在编译时会创建一个由vptr指向的虚函数表。举个例子,

class BASE{
public:
virtual void f1(){cout<<"BASE F1\n";}
virtual void f2(){cout<<"BASE F2\n";}
void f3(){cout <<"BASE F3\n";}
};

class D1:public BASE{
public: 
    virtual void f1(){cout<<"D1 F1\n";}
    void f2(){cout<<"D1 F2\n";}  
};

class DD1:public D1{
public:
    void f1(){cout<<"DD1 F1\n";}
    void f2(){cout <<"DD1 F2\n";}
};

在这里,基类将在其虚表中具有2个函数:

BASE::f1() 
BASE::f1()

D1是从BASE继承而来的,将会继承vtable:

D1::f1()
BASE::f1

DD1继承自D1,在其内部不会有任何vtable。

当我们创建一个对象时:

//case 1:
BASE *b = new D1(); 
b->f1();//will print "D1 F1"
b->BASE::f1();//will print "BASE F1"
b->f2();//will print "D1 F2"
//case 2:
BASE *b1 = new DD1();
b1->f1();//will print "DD1 F1"
b1->D1::f1();//will print "D1 F1"
b1->BASE::f1();//will print"BASE F1"

但是,在这种情况下: b1->D1::f1(); 会导致编译错误。
 error: ‘D1’ is not a base of ‘BASE’

问题:为什么不应该打印D1 F1作为其虚函数。

在查看fdump后,我发现了更有趣的事情,它有点令人困惑;

Vtable for BASE
BASE::_ZTV4BASE: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI4BASE)
16    (int (*)(...))BASE::f1
24    (int (*)(...))BASE::f2

Class BASE
   size=8 align=8
   base size=8 base align=8
BASE (0x7fbc3d2ff120) 0 nearly-empty
    vptr=((& BASE::_ZTV4BASE) + 16u)

Vtable for D1
D1::_ZTV2D1: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI2D1)
16    (int (*)(...))D1::f1
24    (int (*)(...))D1::f2

Class D1
   size=8 align=8
   base size=8 base align=8
D1 (0x7fbc3d31f2d8) 0 nearly-empty
    vptr=((& D1::_ZTV2D1) + 16u)
  BASE (0x7fbc3d2ff180) 0 nearly-empty
      primary-for D1 (0x7fbc3d31f2d8)

Vtable for DD1
DD1::_ZTV3DD1: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI3DD1)
16    (int (*)(...))DD1::f1
24    (int (*)(...))DD1::f2

Class DD1
   size=8 align=8
   base size=8 base align=8
DD1 (0x7fbc3d31f3a8) 0 nearly-empty
    vptr=((& DD1::_ZTV3DD1) + 16u)
  D1 (0x7fbc3d31f410) 0 nearly-empty
      primary-for DD1 (0x7fbc3d31f3a8)
    BASE (0x7fbc3d2ff1e0) 0 nearly-empty
        primary-for D1 (0x7fbc3d31f410)

问题:Class BASE的虚拟表是否会被Class D1继承,Class D1的虚拟表和Class BASE会被DD1继承吗?虚拟表的继承将如何进行?


如果在基类中声明了一个成员函数为虚函数,那么它始终是虚函数,并且在派生类中关键字是可选的。因此,在D1中的f2f1一样被覆盖,DD1中的两个函数也是如此。 - molbdnilo
5个回答

4
DD1继承自D1,将不会有任何自己的vtable。不对,错误的。它将拥有自己的vtable,因为它重写了虚函数(在基类中声明了virtual关键字,因此隐含了virtual关键字,这意味着一旦一个函数在基类中被声明为virtual,在所有地方都是virtual)。
问题:为什么?难道它不应该作为其虚函数打印D1 F1吗?
b1的静态类型是Base*,即使它的动态类型是DD1*。因此,您不能调用b1->D1::f1,因为该语法指示编译器在b1中静态解析调用到函数,在b1中静态不可用。如果您绝对想要执行此调用,并且知道b1的动态类型实际上是D1(或派生自D1),则可以将其转换以更改对象的静态类型:
static_cast<D1*>(b1)->D1::f1();

2
我建议您选择一些好书(我建议免费获取的《C++编程思想》),并阅读有关虚函数的章节,以便更清楚地了解这个令人困惑的主题。
话虽如此,您有几件事情搞错了。
引用:“从BASE继承的D1将继承vtable:”
D1::f1()

BASE::f1

实际上,在派生类选择覆盖基类虚函数的情况下,vtable内容会被替换。在您的情况下,您在D1中完成了这项工作。因此,D1的vtable将具有D1函数(是的,两个函数f1()和f2())。

因此,D1的VTable如下:

D1::f1()
D1::f2()

基类函数在 D1 vTable 中消失或被覆盖。

DD1 vtable 中包含了 DD1 的函数,而不是 D1 的。

关于您看到的错误,答案已经发布了。


2

您的一些期望可能是错误的:

//案例2:
BASE *b1 = new DD1();
b1->f1();//将打印“DD1 F1”

可以想象是正确的。也许它应该产生“D1 F1”,语言律师,请帮忙。

b1->D1::f1();//将打印“D1 F1”

为什么要那样做?b1的类型是BASE *,编译器没有理由相信对象实际上是D1,如果您想断言,您需要一个显式转换。

b1->BASE::f1();//将打印“BASE F1”

是的。


1
编程语言专家在此:它必须生成“DD1 F1”。DD1 :: f1是虚拟的。 - Konrad Rudolph
那么 DD1::f1 是虚函数,即使它没有被声明为虚函数吗?如果我接着重写创建另一个派生类 D31,并尝试通过指向 DD1 的指针调用该方法,哪个方法会被调用? - Adrian Ratnapala
2
一旦虚拟,永远虚拟。它在基类中被声明为虚拟的。重写一个函数并不会重新声明它,只是替换它的实现。或许不幸的是,C++允许在重写函数时省略“virtual”关键字,但其效果与写上该关键字相同。 - Konrad Rudolph
好的,我相信它。我也认为这很不幸。 - Adrian Ratnapala
我认为“一旦虚拟,永远虚拟”是一个重要且不明显的事情。这就是为什么我编辑了你下面(或上面)的评论,以使其明确。也许你应该用自己的话来编辑它。 - Adrian Ratnapala
感谢您的编辑。实际上我不小心拒绝了它(抱歉!),然后自己进行了编辑。 - Konrad Rudolph

1

"为什么它不打印D1 F1作为虚函数?"

因为指针b1的类型是BASE。你实际上试图从一个BASE对象中访问D1类,编译器不允许这样做。你需要告诉编译器,b1指针是有效的D1对象,像这样:

dynamic_cast<D1*> (b1) -> f1()

1
除非您实际检查转换是否成功,否则不需要使用dynamic_cast - Konrad Rudolph
1
好观点。https://dev59.com/7W445IYBdhLWcg3w5OEH - Gearoid Murphy
1
这个例子将调用DD1而不是D1,请查看我的答案以获取正确的调用(FQN)。 - Iuri Covalisin
3
ж‚Ёеџғжњ¬дёЉиҮҮи§Әдғ†dynamic_castе’Њstatic_castзљ„е·ӨдҢњеҺџзђ†гЂ‚иҮ·еЏ‚и§ЃдёЉйқұиҮ„и®ғдё­й“ңжҺӨзљ„и§Әй‡ЉгЂ‚ - Konrad Rudolph
1
@Iuri,我并没有针对这个答案错误的部分发表评论,我特别是在回应你评论中的其他部分。而你所断言的“每当你编写真实代码时,你永远无法确定向下转换”是完全错误的。有许多这样的情况,事实上,在我的经验中,它们占主导地位。在这种情况下使用dynamic_cast就像是空洞式编程,就像在调用new之后检查nullptr一样(除非使用std::nothrow调用new,否则new永远不会返回nullptr)。 - Konrad Rudolph
显示剩余3条评论

0

在基类中无法进行完全限定名称(FQN)方法调用,因为它对于类D1一无所知。

这里是解决方案 - 向下转型为DD1或D1,然后进行FQN调用:

(dynamic_cast<DD1*>(b1))->D1::f1();//will print "D1 F1"

1
与Gearoid的回答相同:只有在实际检查转换是否成功(并且存在不成功的可能性)时,dynamic_cast才有意义。否则请使用static_cast - Konrad Rudolph
静态转换也无法识别问题 - 这个例子仍然可以编译和运行。BASE *b1 = new D1(); b1->f1();//将打印 "DD1 F1" (static_cast(b1))->D1::f1();//将打印 "D1 F1" - Iuri Covalisin
我不理解这个注释。在这里,static_cast 的作用与 dynamic_cast 完全相同,只是后者意味着不同的用法(它的含义是“我不确定这个转换是否成功,因此我需要检查它的返回值)。 - Konrad Rudolph
1
这完全是错误的。在 OP 的示例中,我们确实确定了类型。事实上,在许多(我会说是大多数)向下转换场景中,我们都确定了类型。无论如何,你的代码使用了 dynamic_cast 错误,因为它没有检查返回值是否为 nullptr - Konrad Rudolph
Konrad是正确的。如果您不检查成功,则执行动态转换没有任何意义。如果您可以证明它会成功,那么请使用static_cast。这看起来像是人们使用“最佳实践”而不是实际处理错误的示例。 - Adrian Ratnapala
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接