【发布时间】:2009-11-08 16:17:34
【问题描述】:
c++中派生对象和基础对象有什么区别,
尤其是当类中有虚函数时。
派生对象是否维护额外的表来保存指针
函数?
【问题讨论】:
c++中派生对象和基础对象有什么区别,
尤其是当类中有虚函数时。
派生对象是否维护额外的表来保存指针
函数?
【问题讨论】:
派生对象继承了基类的所有数据和成员函数。根据继承的性质(公共、私有或受保护),这将影响这些数据和成员函数对您的类的客户端(用户)的可见性。
说,你从 A 私下继承了 B,像这样:
class A
{
public:
void MyPublicFunction();
};
class B : private A
{
public:
void MyOtherPublicFunction();
};
虽然 A 有一个公共函数,但它对 B 的用户是不可见的,例如:
B* pB = new B();
pB->MyPublicFunction(); // This will not compile
pB->MyOtherPublicFunction(); // This is OK
由于 private 继承,A 的所有数据和成员函数虽然对 B 类在 B 类中 可用,但对简单的代码将不可用使用 B 类的实例。
如果您使用公共继承,即:
class B : public A
{
...
};
那么 A 的所有数据和成员将对 B 类的用户可见。这种访问仍然受到 A 的原始访问修饰符的限制,即 B 的用户永远无法访问 A 中的私有函数(或者说,B 类本身的代码)。此外,B 可能会重新声明与 A 中的函数同名的函数,从而对 B 类的用户“隐藏”这些函数。
至于虚函数,那要看A有没有虚函数。
例如:
class A
{
public:
int MyFn() { return 42; }
};
class B : public A
{
public:
virtual int MyFn() { return 13; }
};
如果你试图通过 A* 类型的指针在 B 对象上调用 MyFn(),那么虚函数将不会被调用。
例如:
A* pB = new B();
pB->MyFn(); // Will return 42, because A::MyFn() is called.
但是假设我们将 A 更改为:
class A
{
public:
virtual void MyFn() { return 42; }
};
(通知 A 现在将MyFn() 声明为虚拟)
那么这个结果:
A* pB = new B();
pB->MyFn(); // Will return 13, because B::MyFn() is called.
这里调用MyFn()的B版本是因为A类已经将MyFn()声明为virtual,所以编译器知道在A对象上调用MyFn()时必须在对象中查找函数指针.或者它认为是 A 的对象,就像在本例中一样,即使我们已经创建了 B 对象。
那么对于最后一个问题,虚函数存储在哪里?
这取决于编译器/系统,但最常用的方法是对于具有任何虚函数(无论是直接声明还是从基类继承)的类的实例,在此类object 是一个“特殊”指针。这个特殊的指针指向一个'虚函数指针表',或者通常缩写为'vtable'。
编译器为它编译的每个具有虚函数的类创建 vtables。因此,对于我们的最后一个示例,编译器将生成两个 vtable - 一个用于 A 类,一个用于 B 类。这些表有单个实例 - 对象的构造函数将在每个新创建的对象中设置 vtable-pointer 以指向到正确的 vtable 块。
请记住,具有虚函数的对象中的第一条数据是指向虚表的 this 指针,因此编译器总是知道如何找到虚表,给定一个需要调用虚函数的对象。编译器所要做的就是查看任何给定对象中的第一个内存槽,它有一个指向该对象类的正确 vtable 的指针。
我们的例子非常简单——每个 vtable 都是一个条目,所以它们看起来像这样:
A类的vtable:
+---------+--------------+
| 0: MyFn | -> A::MyFn() |
+---------+--------------+
B类的vtable:
+---------+--------------+
| 0: MyFn | -> B::MyFn() |
+---------+--------------+
请注意,对于 B 类的 vtable,MyFn 的条目已被指向 B::MyFn() 的指针覆盖 - 这确保了当我们调用虚函数 MyFn() 时,即使是在对象指针键入A*,正确调用MyFn() 的B 版本,而不是A::MyFn()。
“0”数字表示表中的条目位置。在这个简单的例子中,我们在每个 vtable 中只有一个条目,因此每个条目的索引为 0。
因此,要在对象(A 或 B 类型)上调用 MyFn(),编译器将生成如下代码:
pB->__vtable[0]();
(注意。这不会编译;它只是对编译器将生成的代码的解释。)
为了更清楚,假设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; }
};
那么 B 在其界面中将具有函数MyAFn() 和MyFn(),vtables 现在看起来像这样:
A类的vtable:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> A::MyFn() |
+----------+---------------+
B类的vtable:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> B::MyFn() |
+----------+---------------+
所以在这种情况下,调用MyFn(),编译器会生成如下代码:
pB->__vtable[1]();
因为MyFn() 在表中排名第二(因此在索引 1 处)。
显然,调用MyAFn() 会导致如下代码:
pB->__vtable[0]();
因为MyAFn() 在索引 0 处。
需要强调的是,这是依赖于编译器的,而 iirc 编译器没有义务按照声明的顺序对 vtable 中的函数进行排序 - 只需要编译器让它们在引擎盖。
在实践中,这种方案被广泛使用,并且 vtables 中的函数排序具有相当的确定性,因此不同 C++ 编译器生成的代码之间的 ABI 得到维护,并允许 COM 互操作和类似机制跨不同编译器生成的代码边界工作. 这无法保证。
幸运的是,您永远不必担心 vtables,但是让您对正在发生的事情的心智模型变得有意义并且不会在未来为您储存任何惊喜,这绝对是有用的。
【讨论】:
从理论上讲,如果您从另一个类派生一个类,那么您就有一个基类和一个派生类。如果你创建一个派生类的对象,你就有一个派生对象。在 C++ 中,您可以多次从同一个类继承。考虑:
struct A { };
struct B : A { };
struct C : A { };
struct D : B, C { };
D d;
在d 对象中,每个D 对象中有两个A 对象,称为“基类子对象”。如果您尝试将D 转换为A,那么编译器会告诉您转换是模棱两可的,因为它不知道要转换哪个 A 对象: p>
A &a = d; // error: A object in B or A object in C?
如果你命名A 的一个非静态成员也是如此:编译器会告诉你一个歧义。在这种情况下,您可以先转换为B 或C 来规避它:
A &a = static_cast<B&>(d); // A object in B
对象d 被称为“最派生对象”,因为它不是另一个类类型对象的子对象。为避免上述歧义,您可以虚拟继承
struct A { };
struct B : virtual A { };
struct C : virtual A { };
struct D : B, C { };
现在,只有 一个 类型为 A 的子对象,即使您有两个子对象包含该对象:子对象 B 和子对象 C。将D 对象转换为A 现在是明确的,因为在B 和C 路径上的转换将产生相同的A 子对象。
这里出现了上述问题:理论上,即使不考虑任何实现技术,B 和 C 子对象中的一个或两个现在都不再连续。两者都包含相同的 A 对象,但两者也不包含彼此。这意味着其中一个或两个必须“拆分”并仅引用另一个的 A 对象,以便 B 和 C 对象可以具有不同的地址。在线性内存中,这可能看起来像(假设所有对象的大小都是 1 字节)
C: [1 byte [A: refer to 0xABC [B: 1byte [A: one byte at 0xABC]]]]
[CCCCCCC[ [BBBBBBBBBBCBCBCBCBCBCBCBCBCBCB]]]]
CB 是 C 和 B 子对象包含的内容。现在,如您所见,C 子对象将被拆分,没有办法,因为B 不包含在C 中,反之亦然。编译器要使用C 函数中的代码访问某些成员,不能只使用偏移量,因为C 函数中的代码不知道它是否包含为子对象,或者 -当它不是抽象的 - 它是否是一个最衍生的对象,因此它旁边有 A 对象。
【讨论】:
public 冒号。 (我告诉过你 C++ 很讨厌)
class base { }
class derived : public base { }
【讨论】:
让我们来:
class Base {
virtual void f();
};
class Derived : public Base {
void f();
}
没有 f 是虚拟的(在伪“c”中实现):
struct {
BaseAttributes;
} Base;
struct {
BaseAttributes;
DerivedAttributes;
} Derived;
带有虚函数:
struct {
vfptr = Base_vfptr,
BaseAttributes;
} Base;
struct {
vfptr = Derived_vfptr,
BaseAttributes;
DerivedAttributes;
} Derived;
struct {
&Base::f
} Base_vfptr
struct {
&Derived::f
} Base_vfptr
对于多重继承,事情变得更加复杂:o)
【讨论】:
Derived 是 Base,但 Base 不是 Derived
【讨论】:
class X {}; class Base : public X {}; class Derived : public Base {};
base- 是您从中派生的对象。 派生 - 是继承他父亲的公共(和受保护)成员的对象。
派生对象可以覆盖(或在某些情况下必须覆盖)他父亲的某些方法,从而创建不同的行为
【讨论】:
基础对象是其他对象的派生对象。通常它会有一些虚方法(甚至是纯虚方法),子类可以重写以进行专门化。
基础对象的子类称为派生对象。
【讨论】:
派生对象派生自其基础对象。
【讨论】:
您是在询问各个对象在内存中的表示形式吗?
基类和派生类都有一个指向它们的虚函数的指针表。根据被覆盖的函数,该表中条目的值会发生变化。
如果 B 添加更多不在基类中的虚函数,则 B 的虚方法表会更大(或者可能有单独的表,具体取决于编译器实现)。
【讨论】:
c++中派生对象和基础对象有什么区别,
派生对象可以用来代替基础对象;它拥有基础对象的所有成员,可能还有更多自己的成员。所以,给定一个函数引用(或指针)到基类:
void Function(Base &);
您可以传递对派生类实例的引用:
class Derived : public Base {};
Derived derived;
Function(derived);
尤其是当类中有虚函数时。
如果派生类覆盖了一个虚函数,那么被覆盖的函数将始终在该类的对象上被调用,即使是通过对基类的引用。
class Base
{
public:
virtual void Virtual() {cout << "Base::Virtual" << endl;}
void NonVirtual() {cout << "Base::NonVirtual" << endl;}
};
class Derived : public Base
{
public:
virtual void Virtual() {cout << "Derived::Virtual" << endl;}
void NonVirtual() {cout << "Derived::NonVirtual" << endl;}
};
Derived derived;
Base &base = derived;
base.Virtual(); // prints "Derived::Virtual"
base.NonVirtual(); // prints "Base::NonVirtual"
derived.Virtual(); // prints "Derived::Virtual"
derived.NonVirtual();// prints "Derived::NonVirtual"
派生对象是否维护额外的表来保存指向函数的指针?
是的 - 两个类都包含一个指向虚函数表(称为“vtable”)的指针,因此可以在运行时找到正确的函数。您不能直接访问它,但它确实会影响内存中数据的大小和布局。
【讨论】: