提问人:MainID 提问时间:11/9/2009 最后编辑:MainID 更新时间:11/12/2009 访问量:2918
c++ 中的派生对象和基本对象有什么区别?
What's the difference between a derived object and a base object in c++?
答:
Derived 是 Base,但 Base 不是 Derived
评论
class X {}; class Base : public X {}; class Derived : public Base {};
base- 是您从中派生的对象。 derived - 是继承他父亲的公共(和受保护)成员的对象。
派生对象可以覆盖(或在某些情况下必须覆盖)他父亲的某些方法,从而创建不同的行为
冒号。(我告诉过你C++很讨厌)public
class base { }
class derived : public 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”)的指针,以便在运行时可以找到正确的函数。您无法直接访问它,但它确实会影响内存中数据的大小和布局。
让我们有:
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)
派生对象继承基类的所有数据和成员函数。根据继承的性质(公共的、私有的或受保护的),这将影响这些数据和成员函数对类的客户端(用户)的可见性。
假设,你私下从 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
由于私有继承,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.
在这里,调用 的 B 版本是因为类 A 已声明为虚拟,因此编译器知道在调用 A 对象时它必须在对象中查找函数指针。或者一个对象,它认为它是一个 A,就像这个例子一样,即使我们已经创建了一个 B 对象。MyFn()
MyFn()
MyFn()
那么,对于最后一个问题,虚拟函数存储在哪里?
这依赖于编译器/系统,但最常用的方法是,对于具有任何虚函数(无论是直接声明的,还是从基类继承的)的类的实例,此类对象中的第一条数据是“特殊”指针。这个特殊的指针指向一个“虚拟函数指针表”,或者通常缩写为“vtable”。
编译器为其编译的每个具有虚函数的类创建 vtable。因此,对于我们的最后一个示例,编译器将生成两个 vtable——一个用于类 A,一个用于类 B。这些表有单个实例 - 对象的构造函数将在每个新创建的对象中设置 vtable 指针以指向正确的 vtable 块。
请记住,具有虚函数的对象中的第一条数据是指向 vtable 的指针,因此编译器始终知道如何找到 vtable,给定一个需要调用虚函数的对象。编译器所要做的就是查看任何给定对象中的第一个内存插槽,并且它有一个指向该对象类的正确 vtable 的指针。
我们的情况非常简单 - 每个 vtable 都有一个条目长,所以它们看起来像这样:
vtable 用于 A 类:
+---------+--------------+
| 0: MyFn | -> A::MyFn() |
+---------+--------------+
B类的vtable:
+---------+--------------+
| 0: MyFn | -> B::MyFn() |
+---------+--------------+
请注意,对于类的 vtable,条目已被指针覆盖 - 这确保了当我们调用虚函数时,即使在 类型的对象指针上,也会正确调用版本,而不是 .B
MyFn
B::MyFn()
MyFn()
A*
B
MyFn()
A::MyFn()
“0”数字表示表中的入场位置。在这个简单的例子中,我们在每个 vtable 中只有一个条目,所以每个条目都在索引 0 处。
因此,要调用一个对象(类型或 ),编译器将生成如下代码:MyFn()
A
B
pB->__vtable[0]();
(注意,这不会编译;它只是对编译器将生成的代码的解释。
为了更明显,假设声明了另一个函数,它是虚拟的,B 不会覆盖/重新实现它。A
MyAFn()
所以代码是:
class A
{
public:
virtual void MyAFn() { return 17; }
virtual void MyFn() { return 42; }
};
class B : public A
{
public:
virtual void MyFn() { return 13; }
};
然后 B 将在其界面中具有函数和,vtables 现在将如下所示:MyAFn()
MyFn()
vtable 用于 A 类:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> A::MyFn() |
+----------+---------------+
B类的vtable:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> B::MyFn() |
+----------+---------------+
所以在这种情况下,要调用 ,编译器将生成如下代码:MyFn()
pB->__vtable[1]();
因为是表中的第二个(在索引 1 处也是如此)。MyFn()
显然,调用会导致这样的代码:MyAFn()
pB->__vtable[0]();
因为位于索引 0 处。MyAFn()
应该强调的是,这是依赖于编译器的,而 iirc,编译器没有义务按照它们声明的顺序对 vtable 中的函数进行排序 - 它只是由编译器来使其在后台工作。
在实践中,这种方案被广泛使用,并且 vtables 中的函数排序是相当确定的,因此维护了不同 C++ 编译器生成的代码之间的 ABI,并允许 COM 互操作和类似机制跨不同编译器生成的代码边界工作。这绝不能保证。
幸运的是,您永远不必担心 vtables,但是让您对正在发生的事情的心理模型有意义并且不会在未来为您存储任何惊喜绝对有用。
评论
从理论上讲,如果从一个类派生另一个类,则有一个基类和一个派生类。如果创建派生类的对象,则具有派生对象。在 C++ 中,您可以多次从同一个类继承。考虑:
struct A { };
struct B : A { };
struct C : A { };
struct D : B, C { };
D d;
在对象中,每个对象中有两个对象,称为“基类子对象”。如果您尝试转换为 ,那么编译器会告诉您转换是不明确的,因为它不知道要转换为哪个对象:d
A
D
D
A
A
A &a = d; // error: A object in B or A object in C?
如果您命名 的非静态成员,也是如此: 编译器会告诉你一个歧义。在这种情况下,您可以通过转换为 or first 来规避它: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
从理论上讲,即使不考虑任何实现技术,其中一个或两个子对象现在也不再是连续的。两者都包含相同的 A 对象,但也不包含彼此。这意味着其中一个或两个对象必须被“拆分”,并且仅引用另一个对象的 A 对象,以便两个对象可以具有不同的地址。在线性内存中,这可能看起来像(假设所有对象的大小为 1 字节)B
C
B
C
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
评论