[英]in C++, what's the difference between an object and a pointer to an object?
[英]What's the difference between a derived object and a base object in 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
由于私有继承,A的所有数据和成员函数虽然可用于B类中的B类 ,但对于仅使用B类实例的代码将不可用。
如果您使用公共继承,即:
class B : public A
{
...
};
然后,所有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; }
};
如果尝试通过类型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()
声明为虚拟,因此编译器知道在A对象上调用MyFn()
时它必须在对象中查找函数指针。 或者它认为它是A的对象,就像在这种情况下,即使我们已经创建了一个B对象。
那么对于你的最后一个问题,虚拟函数存储在哪里?
这是编译器/系统相关的,但最常用的方法是对于具有任何虚函数(无论是直接声明,还是从基类继承)的类的实例,这样的对象中的第一个数据是'特殊'指针。 此特殊指针指向“ 虚函数指针表 ”,或通常缩写为“ vtable ”。
编译器为它编译的每个具有虚函数的类创建vtable。 所以对于我们的最后一个例子,编译器将生成两个vtable - 一个用于类A,一个用于类B.这些表的单个实例 - 对象的构造函数将在每个新创建的对象中设置vtable指针指向到正确的vtable块。
请记住,具有虚函数的对象中的第一个数据是指向vtable的指针,因此在给定需要调用虚函数的对象的情况下,编译器始终知道如何查找vtable。 编译器所要做的就是查看任何给定对象中的第一个内存槽,并且它有一个指向该对象类的正确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()
上调用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中的函数进行排序 - 这只取决于编译器使其全部工作。
在实践中,该方案被广泛使用,并且vtable中的函数排序是相当确定的,因此维护由不同C ++编译器生成的代码之间的ABI,并且允许COM互操作和类似机制跨越由不同编译器生成的代码的边界。 这绝不是保证。
幸运的是,你永远不必担心vtable,但是让你的心理模型变得有意义并且不会在将来给你带来任何惊喜绝对是有用的。
理论上,如果从另一个派生一个类,则有一个基类和一个派生类。 如果创建派生类的对象,则会有派生对象。 在C ++中,您可以多次从同一个类继承。 考虑:
struct A { };
struct B : A { };
struct C : A { };
struct D : B, C { };
D d;
在d
对象中,每个D
对象中有两个A
对象,称为“基类子对象”。 如果你尝试转换D
到A
,那么编译器会告诉你的转换是不明确的,因为它不知道到 A
要转换的对象:
A &a = d; // error: A object in B or A object in C?
如果您将A
的非静态成员命名为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
类子对象,即使你有两个子对象,这个对象包含在:subobject B
和sub-object 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
base-是您派生的对象。 derived - 是继承父亲的公共(和受保护)成员的对象。
派生对象可以覆盖(或在某些情况下必须覆盖)他父亲的一些方法,从而创建不同的行为
基础对象是从中导出其他对象的对象 。 通常它会有一些虚拟方法(甚至是纯虚拟方法),子类可以覆盖它们以进行特化。
基础对象的子类称为派生对象 。
派生对象派生自其基础对象。
你在询问各个对象在记忆中的表现吗?
基类和派生类都有一个指向其虚函数的指针表。 根据已覆盖的功能,该表中条目的值将更改。
如果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”),以便在运行时找到正确的函数。 您不能直接访问它,但它确实会影响内存中数据的大小和布局。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.