C++ 对象模型
C++中的数据成员有两种:static和non-static;函数成员有三种:static, non-static, virtual。各种类型的成员是如何在对象的内存空间中存放的?
- non-static成员变量:存放在对象的内存空间中。
- static成员变量:存放在对象的内存空间之外。
- static成员函数:存放在对象的内存空间之外。
- non-static成员函数:存放在对象的内存空间之外。
- virtual成员函数:对象的内存空间中存储一个虚函数表的指针,该指针指向一个虚函数表,虚函数表中存放虚函数的地址,虚表的第一项通常存储这个类对象的RTTI信息,及runtime type identification。
对于下面的这个Point类,其各种成员的在内存中的分布如下图所示:
class Point
{
public:
int x;
static int count;
public:
Point();
void Move();
static int GetCount();
virtual void Print();
virtual ~Point();
};
![](https://img-blog.csdnimg.cn/349e728883f24622bdaf51845e74e108.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAbTBfNjExMTU4NzI=,size_20,color_FFFFFF,t_70,g_se,x_16)
验证:通过clangg可以看到类对象的内存空间。(clang -Xclang -fdump-record-layouts Point.cc)
*** Dumping AST Record Layout
0 | class Point
0 | (Point vtable pointer)
8 | int x
| [sizeof=16, dsize=12, align=8,
| nvsize=12, nvalign=8]
可以看到,在内存中前8个字节存储的是虚表指针。
继承体系下的类内存空间
1. 派生类只有一个基类
当派生类只有一个基类的时候,派生类的前8个字节存储的是一个虚表指针,派生类的虚函数表与基类的虚函数表不是一个表,但是派生类的虚函数表会存有基类虚函数表的地址,如果派生类重写了基类的虚函数,则在派生类的虚函数表中,会用派生类的虚函数地址替换掉基类的虚函数地址。
#include <iostream>
using namespace std;
class Point
{
public:
virtual void Show() { cout << "Show in Point" << endl; }
virtual void Draw() { cout << "Draw in Point" << endl; }
int pos;
};
class Plane : public Point
{
public:
virtual void Draw() { cout << "Draw in Plane" << endl; }
virtual void Print() { cout << "Print in Plane" << endl; }
int area;
};
int main()
{
Point p;
Plane pl;
cout << "Address of vtable of Point " << *(long*)(&p) << endl;
cout << "Address of vtable of Plane " << *(long*)(&pl) << endl;
long pointShowAddr = *(long*)*(long*)(&p);
long pointDrawAddr = *((long*)*(long*)(&p)+1);
long planeShowAddr = *(long*)*(long*)(&pl);
long planeDrawAddr = *((long*)*(long*)(&pl)+1);
long planePrintAddr = *((long*)*(long*)(&pl)+2);
cout << "Address of function Show in class Point is " << pointShowAddr << endl;
cout << "Address of function Draw in class Point is " << pointDrawAddr << endl;
cout << "Address of function Show in class Plane is " << planeShowAddr << endl;
cout << "Address of function Draw in class Plane is " << planeDrawAddr << endl;
cout << "Address of function Print in class Plane is " << planePrintAddr << endl;
((void(*)(void))pointShowAddr)();
((void(*)(void))pointDrawAddr)();
((void(*)(void))planeShowAddr)();
((void(*)(void))planeDrawAddr)();
((void(*)(void))planePrintAddr)();
return 0;
}
// 以上代码的输出
/*
Address of vtable of Point 4351221832
Address of vtable of Plane 4351221880
Address of function Show in class Point is 4351217376
Address of function Draw in class Point is 4351217440
Address of function Show in class Plane is 4351217376
Address of function Draw in class Plane is 4351217568
Address of function Print in class Plane is 4351217632
Show in Point
Draw in Point
Show in Point
Draw in Plane
Print in Plane
*/
此时,类对象的内存空间如下图所示:
如果Point有一个private成员 int dos; 则此时在Plane的内存空间中也会有一个dos,其位置在pos的下面一个slot。虽然Plane对象的内存空间中存在这个private这个成员,但是其无法访问。
2. 当派生类继承两个基类时,此时派生类中会有两个虚函数表指针。
#include <iostream>
using namespace std;
class Point
{
public:
Point() : pos(99) {}
virtual void Show() { cout << "Show in Point" << endl; }
virtual void Draw() { cout << "Draw in Point" << endl; }
int pos;
};
class Line
{
public:
virtual void Extend() { cout << "Extend in Line" << endl; }
int len;
};
class Plane : public Point, public Line
{
public:
virtual void Draw() { cout << "Draw in Plane" << endl; }
virtual void Print() { cout << "Print in Plane" << endl; }
int area;
};
int main()
{
Point p;
Line l;
Plane pl;
cout << "Address of vtable of Point " << *(long*)(&p) << endl;
cout << "Address of vtable of Line " << *(long*)(&l) << endl;
cout << "Address of vtable of Plane " << *(long*)(&pl) << endl;
long pointShowAddr = *(long*)*(long*)(&p);
long pointDrawAddr = *((long*)*(long*)(&p)+1);
long lineExtendAddr = *(long*)*(long*)(&l);
long planeShowAddr = *(long*)*(long*)(&pl);
long planeDrawAddr = *((long*)*(long*)(&pl)+1);
long planePrintAddr = *((long*)*(long*)(&pl)+2);
long planeExtendAddr = *(long*)*((long*)(&pl)+2); // 两个虚表之间还有一个成员pos,所以这里指针要在pl地址的基础上移动两个单位
cout << "Address of function Show in class Point is " << pointShowAddr << endl;
cout << "Address of function Draw in class Point is " << pointDrawAddr << endl;
cout << "Address of function Extend in class Line is " << lineExtendAddr << endl;
cout << "Address of function Show in class Plane is " << planeShowAddr << endl;
cout << "Address of function Draw in class Plane is " << planeDrawAddr << endl;
cout << "Address of function Print in class Plane is " << planePrintAddr << endl;
cout << "Address of function Extend in class Plane is " << planeExtendAddr << endl;
((void(*)(void))pointShowAddr)();
((void(*)(void))pointDrawAddr)();
((void(*)(void))lineExtendAddr)();
((void(*)(void))planeShowAddr)();
((void(*)(void))planeDrawAddr)();
((void(*)(void))planePrintAddr)();
((void(*)(void))planeExtendAddr)();
return 0;
}
// 以上代码的输出为
/*
Address of vtable of Point 4417945680
Address of vtable of Line 4417945728
Address of vtable of Plane 4417945768
Address of function Show in class Point is 4417940976
Address of function Draw in class Point is 4417941040
Address of function Extend in class Line is 4417941136
Address of function Show in class Plane is 4417940976
Address of function Draw in class Plane is 4417941280
Address of function Print in class Plane is 4417941344
Address of function Extend in class Plane is 4417941136
Show in Point
Draw in Point
Extend in Line
Show in Point
Draw in Plane
Print in Plane
Extend in Line
*/
此时,类Plane 同时继承了Point和Line,那么此时在Plane中会有两个虚函数表指针,第一个虚表指针所指向的虚函数表存放了类Point和类Plane的虚函数的地址;而第二个虚函数表指针所指向的虚函数表存放的是类Line的虚函数地址(如果类Plane重写了类Line的虚函数,则重写后的虚函数地址存放在第二个虚表中)。此时类对象的内存空间如下:
也通过clang可以看到Plane的内存空间:
*** Dumping AST Record Layout
0 | class Plane
0 | class Point (primary base)
0 | (Point vtable pointer)
8 | int pos
16 | class Line (base)
16 | (Line vtable pointer)
24 | int len
28 | int area
| [sizeof=32, dsize=32, align=8,
| nvsize=32, nvalign=8]
为什么不让两个虚表指针紧挨着排列呢?或者合并成一个虚表呢?
猜测是为了进行上行转换时比较方便。基于这一现象,当一个派生类指针上行转化成基类指针时,通过该基类指针调用虚函数时,如果派生类重写了基类的虚函数,则调用的仍然是派生类的虚函数;如果派生类没有重写基类的虚函数,则调用的仍然是基类的虚函数。所以本质上来讲,c++的类型转换,并没有真正的转换,只是改变了指针能够访问内存的范围,而且该转换而来的基类指针可以访问基类的非虚函数。
本章节后面的内容作者继续探讨了指针相关的话题。无论什么类型的指针,指针与指针之间的本质并没有什么不同,指针就是一个地址,在64位机器上是8个字节。指针类型的作用在于告诉编译器(cpu)如何解析一块内存,及要解析的内存有多长。多态即是基于指针的这种特点来实现的。派生类-基类的转换,就是改变了指针的类型,从而改变了对内存的解析。
以上就是Inside the c++ object model第一章的大致内容及自己的实践。