假设我有
class A { public: void print(){cout<<"A"; }};
class B: public A { public: void print(){cout<<"B"; }};
class C: public A { };
如何在内存级别实现继承?
C
是否将print()
代码复制到自身,或者它是否指向指向A
部分代码中某处的指针?
当我们覆盖之前的定义时会发生同样的事情,例如在B
(在内存级别)?
答案 0 :(得分:7)
允许编译器执行此操作但是他们选择。但他们通常遵循CFront的旧实施。
考虑:
#include <iostream>
class A {
void foo()
{
std::cout << "foo\n";
}
static int bar()
{
return 42;
}
};
A a;
a.foo();
A::bar();
编译器将最后三行更改为类似于:
的内容struct A a = <compiler-generated constructor>;
A_foo(a); // the "a" parameter is the "this" pointer, there are not objects as far as
// assembly code is concerned, instead member functions (i.e., methods) are
// simply functions that take a hidden this pointer
A_bar(); // since bar() is static, there is no need to pass the this pointer
曾几何时,我会猜到这是在创建的每个A
对象中使用指针到函数来处理的。但是,这种方法意味着每个A
对象将包含相同的信息(指向同一函数的指针),这会浪费大量空间。编译器很容易处理这些细节。
当然,那不是你问的那个。但是我们可以将它扩展到继承,这是你所期望的:
class B : public A {
void blarg()
{
// who knows, something goes here
}
int bar()
{
return 5;
}
};
B b;
b.blarg();
b.foo();
b.bar();
编译器将最后四行转换为:
struct B b = <compiler-generated constructor>
B_blarg(b);
A_foo(b.A_portion_of_object);
B_bar(b);
当你谈论virtual
方法时,事情变得有点棘手。在这种情况下,每个类都会获得一个特定于类的指针到函数的数组,每个virtual
函数都有一个这样的指针。该数组称为vtable(“虚拟表”),每个创建的对象都有一个指向相关vtable的指针。通过查找要在vtable中调用的正确函数来解析对virtual
函数的调用。
答案 1 :(得分:3)
如果您对内存布局有任何疑问,请查看the C++ ABI。它被标记为“Itanium C ++ ABI”,但它已成为大多数编译器实现的C ++标准ABI。
答案 2 :(得分:3)
我认为标准不做任何保证。编译器可以选择制作多个函数副本,组合碰巧在完全不同的类型上访问相同内存偏移的副本等。内联只是其中一个更明显的例子。
但是大多数编译器都不会生成A :: print代码的副本,以便在通过C实例调用时使用。在C的编译器内部符号表中可能有一个指向A的指针,但在运行时,您很可能会看到:
A a; C c; a.print(); c.print();
已经变成了以下几点:
A a;
C c;
ECX = &a; /* set up 'this' pointer */
call A::print;
ECX = up_cast<A*>(&c); /* set up 'this' pointer */
call A::print;
两个调用指令都跳转到代码存储器中的完全相同的地址。
当然,既然您已经要求编译器内联A::print
,那么代码很可能会被复制到每个调用站点(但是因为它取代了call A::print
,所以它实际上并没有增加太多内容。程序大小)。
答案 3 :(得分:1)
对象中不存储任何信息来描述成员函数。
aobject.print();
bobject.print();
cobject.print();
编译器只会将上面的语句转换为直接调用函数print,基本上没有任何东西存储在对象中。
伪汇编指令将如下所示
00B5A2C3 call print(006de180)
由于print是成员函数,因此您将拥有一个额外的参数;这个指针。这将作为函数的每个其他参数传递。
答案 4 :(得分:1)
在你的例子中,没有任何复制品。通常一个对象不知道它在运行时的类 - 当程序编译时会发生什么,编译器说“嘿,这个变量属于C类,让我们看看是否有一个C :: print()。不,好吧,A :: print()怎么样?是的?好的,打电话给你!“
虚方法的工作方式不同,因为指向正确函数的指针存储在对象中引用的“vtable” * 中。如果您直接使用C,那仍然无关紧要,因为它仍然遵循上述步骤。但对于指针,它可能会说“哦,C :: print()?地址是vtable中的第一个条目。”并且编译器插入指令以在运行时获取该地址并调用它。
*从技术上讲,这不是必须的。我很确定你不会在“vtable”的标准中找到任何提及;它的定义是特定于实现的。它恰好是第一个C ++编译器使用的方法,并且碰巧比其他方法更好地工作,因此几乎每个C ++编译器都使用它。