编译的C ++类怎么样? [英] How does a compiled C++ class look like?

查看:159
本文介绍了编译的C ++类怎么样?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有了汇编指令和C程序的一些背景知识,我可以想象一个编译的函数是什么样子,但很有趣,我从来没有仔细想过一个编译的C ++类是什么样子。

  bash $ cat class.cpp 
#include< iostream>
class Base
{
int i;
float f;
};

bash $ g ++ -c class.cpp

p>

  bash $ objdump -d class.o 
bash $ readelf -a class.o

但我得到的是我很难理解。

Jagrati

/ p>

解决方案

类(或多或少)构造为常规结构。方法是(或多或少)转换成第一个参数是this的函数。对类变量的引用作为对this的偏移量来完成。



就继承而言,我们可以从C ++常见问题解答LITE中引用,这里反映了 http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.4 。本章展示了如何在真正的硬件中调用虚拟函数(在机器代码中编译什么)。






一个例子假设Base类有5个虚函数: virt0() virt4()

  //原来的C ++源代码
class Base {
public:
virtual arbitrary_return_type virt0任意参数...);
virtual arbitrary_return_type virt1(... arbitrary params ...);
virtual arbitrary_return_type virt2(... arbitrary params ...);
virtual arbitrary_return_type virt3 (... arbitrary params ...);
virtual arbitrary_return_type virt4(... arbitrary params ...);
...
};



步骤#1 :编译器构建一个包含5个函数指针的静态表,许多(不是所有)编译器在编译定义Base的第一个非内联虚函数的.cpp时定义这个表。我们称该表为v表;让我们假装它的技术​​名称是 Base :: __ vtable 。如果函数指针符合目标硬件平台上的一个机器字, Base :: __ vtable 将最终占用5个隐藏的内存字。不是每个实例5个,每个函数不是5个;它可能看起来像下面的伪代码:

  //伪代码(不是C ++,对于在文件Base.cpp中定义的静态表

// Pretend FunctionPtr是一个通用成员函数的指针
//(记住:这是伪代码,而不是C ++代码)
FunctionPtr Base :: __ vtable [5] = {
& Base :: virt0,& Base :: virt1,& Base :: virt2,& Base :: virt3,& :virt4
};

步骤2 :编译器添加一个隐藏指针机器字)到类Base的每个对象。这被称为v指针。将此隐藏指针视为隐藏的数据成员,如同编译器将您的类重写为如下所示:

  //您的原来的C ++源代码
class Base {
public:
...
FunctionPtr * __vptr; ←由编译器提供,从程序员隐藏
...
};

步骤#3 :编译器初始化 - > __ vptr 。这个想法是让每个对象的v指针指向它的类的v表,好像它在每个构造函数的init-list中添加了以下指令:

  Base :: Base(... arbitrary params ...)
:__vptr(& Base :: __ vtable [0])←由编译器提供, $ b ...
{
...
}

现在让我们计算一个派生类。假设你的C ++代码定义了继承自Base类的类Der。编译器重复步骤#1和#3(但不是#2)。在步骤#1中,编译器创建隐藏的v表,保留与 Base :: __ vtable 中相同的函数指针,但替换对应于覆盖的那些槽。例如,如果Der重写 virt0() virt2()并继承其他原样,Der的v-表可能看起来像这样(假设Der不添加任何新的虚拟):

  //伪代码,不是C)为文件中定义的静态表Der.cpp 

// Pretend FunctionPtr是一个通用成员函数的指针
//(记住:这是伪代码,不是C ++代码)
FunctionPtr Der :: __ vtable [5] = {
& Der :: virt0,& Der :: virt1,& Der :: virt2,& Base :: virt3, & Base :: virt4
}; ^^^^ ---------- ^^^^ ---按原样继承为

在步骤#3中,编译器在每个Der的构造函数的开头添加一个类似的指针赋值。这个想法是改变每个Der对象的v指针,使它指向它的类的v表。 (这不是第二个v指针;它是在基类Base中定义的相同的v指针;记住,编译器不会重复类Der中的第#2步)



最后,让我们看看编译器如何实现对虚函数的调用。您的代码可能如下所示:

  //您的原始C ++代码
void mycode(Base * p)
{
p-> virt3();
}

编译器不知道这是否会调用 Base :: virt3() Der :: virt3()或者可能是 virt3()另一个派生类的方法,甚至不存在。它只知道你确实调用 virt3()这恰恰是v表的槽#3中的函数。它将该调用重写为如下所示:

  //编译器从您的C ++生成的伪代码

void mycode(Base * p)
{
p-> __ vptr [3](p);
}






开发者阅读常见问题。它可能需要几个星期(因为它很难阅读和长期),但它会教会你很多关于C + +以及可以做什么。


With some background in assemble instructions and C programs, I can visualize how a compiled function would look like, but it's funny I have never so carefully thought about how a compiled C++ class would look like.

bash$ cat class.cpp
#include<iostream>
class Base
{
int i;
float f;
};

bash$ g++ -c class.cpp

I ran:

bash$objdump -d class.o
bash$readelf -a class.o

but what I get is hard for me to understand. Could somebody please explain me or suggest some good starting points.

Thanks,

Jagrati

解决方案

The classes are (more or less) constructed as regular structs. The methods are (more or less...) converted into functions which first parameter is "this". References to the class variables are done as an offset to "this".

As far as inheritance, lets quote from the C++ FAQ LITE, which is mirrored here http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.4 . This chapter shows how Virtual functions are called in the real hardware (what does the compile make in machine code.


Let's work an example. Suppose class Base has 5 virtual functions: virt0() through virt4().

 // Your original C++ source code
 class Base {
 public:
   virtual arbitrary_return_type virt0(...arbitrary params...);
   virtual arbitrary_return_type virt1(...arbitrary params...);
   virtual arbitrary_return_type virt2(...arbitrary params...);
   virtual arbitrary_return_type virt3(...arbitrary params...);
   virtual arbitrary_return_type virt4(...arbitrary params...);
   ...
 };

Step #1: the compiler builds a static table containing 5 function-pointers, burying that table into static memory somewhere. Many (not all) compilers define this table while compiling the .cpp that defines Base's first non-inline virtual function. We call that table the v-table; let's pretend its technical name is Base::__vtable. If a function pointer fits into one machine word on the target hardware platform, Base::__vtable will end up consuming 5 hidden words of memory. Not 5 per instance, not 5 per function; just 5. It might look something like the following pseudo-code:

 // Pseudo-code (not C++, not C) for a static table defined within file Base.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Base::__vtable[5] = {
   &Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
 };

Step #2: the compiler adds a hidden pointer (typically also a machine-word) to each object of class Base. This is called the v-pointer. Think of this hidden pointer as a hidden data member, as if the compiler rewrites your class to something like this:

 // Your original C++ source code
 class Base {
 public:
   ...
   FunctionPtr* __vptr;  ← supplied by the compiler, hidden from the programmer
   ...
 };

Step #3: the compiler initializes this->__vptr within each constructor. The idea is to cause each object's v-pointer to point at its class's v-table, as if it adds the following instruction in each constructor's init-list:

 Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
   ...
 {
   ...
 }

Now let's work out a derived class. Suppose your C++ code defines class Der that inherits from class Base. The compiler repeats steps #1 and #3 (but not #2). In step #1, the compiler creates a hidden v-table, keeping the same function-pointers as in Base::__vtable but replacing those slots that correspond to overrides. For instance, if Der overrides virt0() through virt2() and inherits the others as-is, Der's v-table might look something like this (pretend Der doesn't add any new virtuals):

 // Pseudo-code (not C++, not C) for a static table defined within file Der.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Der::__vtable[5] = {
   &Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
 };                                        ^^^^----------^^^^---inherited as-is

In step #3, the compiler adds a similar pointer-assignment at the beginning of each of Der's constructors. The idea is to change each Der object's v-pointer so it points at its class's v-table. (This is not a second v-pointer; it's the same v-pointer that was defined in the base class, Base; remember, the compiler does not repeat step #2 in class Der.)

Finally, let's see how the compiler implements a call to a virtual function. Your code might look like this:

 // Your original C++ code
 void mycode(Base* p)
 {
   p->virt3();
 }

The compiler has no idea whether this is going to call Base::virt3() or Der::virt3() or perhaps the virt3() method of another derived class that doesn't even exist yet. It only knows for sure that you are calling virt3() which happens to be the function in slot #3 of the v-table. It rewrites that call into something like this:

 // Pseudo-code that the compiler generates from your C++

 void mycode(Base* p)
 {
   p->__vptr[3](p);
 } 


I strongly recommend every C++ developer to read the FAQ. It might take several weeks (as it's hard to read and long) but it will teach you a lot about C++ and what can be done with it.

这篇关于编译的C ++类怎么样?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆