在c ++中派生对象和基础对象之间有什么区别?

Mai*_*nID 3 c++ compiler-construction

在c ++中派生对象和基础对象之间有什么区别,

特别是,当班级中有虚拟功能时.

派生对象是否维护其他表来保存指针

功能?

Sla*_*ker 11

派生对象继承基类的所有数据和成员函数.根据继承的性质(公共,私有或受保护),这将影响这些数据和成员函数对类的客户端(用户)的可见性.

比如说,你私下从A继承了B,就像这样:

  class A
  {
    public:
      void MyPublicFunction();
  };

  class B : private A
  {
    public:
      void MyOtherPublicFunction();
  };
Run Code Online (Sandbox Code Playgroud)

即使A具有公共功能,B的用户也不会看到它,例如:

  B* pB = new B();
  pB->MyPublicFunction();       // This will not compile
  pB->MyOtherPublicFunction();  // This is OK
Run Code Online (Sandbox Code Playgroud)

由于私有继承,A的所有数据和成员函数虽然可用于B类中的B类,但对于仅使用B类实例的代码不可用.

如果您使用公共继承,即:

  class B : public A
  {
    ...
  };
Run Code Online (Sandbox Code Playgroud)

然后,所有A的数据和成员都将对B类用户可见.此访问仍然受到A的原始访问修饰符的限制,即A中的私有函数永远不会被B的用户访问(或者,对于B类本身的代码而言).此外,B可以重新声明与A中相同名称的函数,从而"隐藏"这些函数来自B类的用户.

至于虚函数,这取决于A是否具有虚函数.

例如:

  class A
  {
    public:
      int MyFn() { return 42; }
  };

  class B : public A
  {
    public:
      virtual int MyFn() { return 13; }
  };
Run Code Online (Sandbox Code Playgroud)

如果尝试MyFn()通过类型A*的指针调用B对象,则不会调用虚函数.

例如:

A* pB = new B();
pB->MyFn(); // Will return 42, because A::MyFn() is called.
Run Code Online (Sandbox Code Playgroud)

但是,假设我们将A更改为:

  class A
  {
    public:
      virtual void MyFn() { return 42; }
  };
Run Code Online (Sandbox Code Playgroud)

(注意A现在声明MyFn()虚拟)

然后这个结果:

A* pB = new B();
pB->MyFn(); // Will return 13, because B::MyFn() is called.
Run Code Online (Sandbox Code Playgroud)

这里MyFn()调用B版本,因为类A已声明MyFn()为虚拟,因此编译器知道在调用MyFn()A对象时它必须在对象中查找函数指针.或者它认为它是A的对象,就像在这种情况下,即使我们已经创建了一个B对象.

那么对于你的最后一个问题,虚拟函数存储在哪里?

这是编译器/系统相关的,但最常用的方法是对于具有任何虚函数(无论是直接声明,还是从基类继承)的类的实例,这样的对象中的第一个数据是'特殊'指针.此特殊指针指向" 虚函数指针表 ",或通常缩写为" vtable ".

编译器为它编译的每个具有虚函数的类创建vtable.所以对于我们的最后一个例子,编译器将生成两个vtable - 一个用于类A,一个用于类B.这些表的单个实例 - 对象的构造函数将在每个新创建的对象中设置vtable指针指向到正确的vtable块.

请记住,具有虚函数的对象中的第一个数据是指向vtable的指针,因此在给定需要调用虚函数的对象的情况下,编译器始终知道如何查找vtable.编译器所要做的就是查看任何给定对象中的第一个内存槽,并且它有一个指向该对象类的正确vtable的指针.

我们的情况非常简单 - 每个vtable都是一个条目长,所以它们看起来像这样:

A类的vtable:

+---------+--------------+
| 0: MyFn | -> A::MyFn() |
+---------+--------------+
Run Code Online (Sandbox Code Playgroud)

B类vtable:

+---------+--------------+
| 0: MyFn | -> B::MyFn() |
+---------+--------------+
Run Code Online (Sandbox Code Playgroud)

请注意,对于B类的vtable ,条目MyFn已被指针覆盖B::MyFn()- 这确保了当我们MyFn()甚至在类型的对象指针上调用虚函数时A*,正确调用BMyFn()是版本,而不是A::MyFn().

"0"数字表示表格中的条目位置.在这个简单的情况下,我们在每个vtable中只有一个条目,因此每个条目都在索引0处.

因此,要调用MyFn()对象(类型A或对象B),编译器将生成如下代码:

pB->__vtable[0]();
Run Code Online (Sandbox Code Playgroud)

(注意:这不会编译;它只是对编译器将生成的代码的解释.)

为了使它更明显,让我们说A声明另一个函数,MyAFn()它是虚拟的,B不会覆盖/重新实现.

所以代码是:

  class A
  {
    public:
      virtual void MyAFn() { return 17; }
      virtual void MyFn()  { return 42; }
  };

  class B : public A
  {
    public:
      virtual void MyFn() { return 13; }
  };
Run Code Online (Sandbox Code Playgroud)

然后B将具有函数MyAFn()MyFn()在其界面中,vtable现在将如下所示:

A类的vtable:

+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn  | -> A::MyFn()  |
+----------+---------------+
Run Code Online (Sandbox Code Playgroud)

B类vtable:

+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn  | -> B::MyFn()  |
+----------+---------------+
Run Code Online (Sandbox Code Playgroud)

所以在这种情况下,要调用MyFn(),编译器将生成如下代码:

pB->__vtable[1]();
Run Code Online (Sandbox Code Playgroud)

因为MyFn()在表中是第二个(因此在索引1处).

显然,调用MyAFn()会导致这样的代码:

pB->__vtable[0]();
Run Code Online (Sandbox Code Playgroud)

因为MyAFn()是在索引0.

应该强调的是,这是依赖于编译器的,并且iirc,编译器没有义务按照声明的顺序对vtable中的函数进行排序 - 这只取决于编译器使其全部工作.

在实践中,该方案被广泛使用,并且vtable中的函数排序是相当确定的,因此维护由不同C++编译器生成的代码之间的ABI,并且允许COM互操作和类似机制跨越由不同编译器生成的代码的边界.这绝不是保证.

幸运的是,你永远不必担心vtable,但是让你的心理模型变得有意义并且不会在将来给你带来任何惊喜绝对是有用的.