C++中编译器怎么处理类的内存分布

本文详细探讨了C++中各类类结构(如空类、普通类、单一及多重继承等)的内存布局特点,通过具体代码示例展示了编译器如何处理类的内存分布。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++特别适合用于开发压榨机器性能的程序,在资源有限的情况下,C++程序员不能放过机器的每一点计算力以及每一字节内存。当然了,要做到这一点,编程语言本身要提供精准的控制能力,C++语言作为一种强类型的编程语言,“精准控制”自然不在话下。这样看来,将机器性能发挥到极限的责任最后还是落在程序员身上了,要做到这一点,我们至少需要对各种数据类型使用的内存情况了然于胸。

C++是面向对象程序设计语言,自然支持面向对象的三大特性(继承、封装、多态)。在实现中,类的常见组织形式大致有以下几种,空类、普通类、单一继承(非虚继承)、多重继承(非虚继承)、单一继承(虚继承)、重复继承(虚继承)、重复继承(非虚继承),下面对这几种类的组织形式从内存分布方面来逐个学习分析,了解编译器是怎么处理类的内存分布的

Visual Studio IDE支持配置工程属性,来查看类的内存布局。先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置,定义一个类后,编译一下,就可以看到输出框里面有内存排布的输出。

一、空类

1. 声明空类

对于空类,在声明时,编译器不会生成任何成员函数,只会生成1个字节的占位符。有时可能会以为编译器会为空类生成默认构造函数等,事实上是不会的,编译器只会在需要的时候(比如定义类变量)生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。

// 声明
class A
{
 
};
class B
{
	virtual bool compare(int a,int b) = 0;
};
class C:public A,public B
{
 
};
class D:public A,public B
{
	virtual bool compare(int a, int b) = 0;
};
class E :virtual A, virtual B
{
 
};
class F :virtual A, virtual B
{
	virtual bool compare(int a, int b) = 0;
};

分析:

类A是空类,但空类同样可以被实例化,而每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以sizeof(A)的大小为1。

类B里面因有一个纯虚函数,故有一个指向虚函数的指针(vptr),32位系统分配给指针的大小为4个字节,所以sizeof(B)的大小为4。类C继承于A和B,编译器取消A的占位符,保留一虚函数表,故大小为4。类D继承于A和B,派生类基类共享一个虚表,故大小为4。类E虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。类F虚继承A和B,含有一个指向基类的指针(vftr)和一个指向虚函数的指针。

 

2. 定义空类

在定义时,会生成6个成员函数,分别是一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。

class Empty
{
};

等价于:

class Empty
{
  public:
    Empty();                            //缺省构造函数
    Empty(const Empty &rhs);            //拷贝构造函数
    ~Empty();                           //析构函数 
    Empty& operator=(const Empty &rhs); //赋值运算符
    Empty* operator&();                 //取址运算符
    const Empty* operator&() const;     //取址运算符(const版本)
};

使用时的调用情况:

Empty *e = new Empty();    //缺省构造函数
delete e;                  //析构函数
Empty e1;                  //缺省构造函数                               
Empty e2(e1);              //拷贝构造函数
e2 = e1;                   //赋值运算符
Empty *pe1 = &e1;          //取址运算符(非const)
const Empty *pe2 = &e2;    //取址运算符(const)

C++编译器对这些函数的实现,

inline Empty::Empty()                          //缺省构造函数
{
}
inline Empty::~Empty()                         //析构函数
{
}
inline Empty *Empty::operator&()               //取址运算符(非const)
{
  return this; 
}           
inline const Empty *Empty::operator&() const    //取址运算符(const)
{
  return this;
}
inline Empty::Empty(const Empty &rhs)           //拷贝构造函数
{
  //对类的非静态数据成员进行以"成员为单位"逐一拷贝构造
  //固定类型的对象拷贝构造是从源对象到目标对象的"逐位"拷贝
}
 
inline Empty& Empty::operator=(const Empty &rhs) //赋值运算符
{
  //对类的非静态数据成员进行以"成员为单位"逐一赋值
  //固定类型的对象赋值是从源对象到目标对象的"逐位"赋值。
}

二、普通类

普通类的内存布局,成员变量按声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间,不过通常得考虑内存对齐(字节对齐)的因素。

class Base
{
    int a;
    int b;
public:
    void CommonFunction();
};

这里不想花精力在内存对齐因素上,所以成员变量都设为int型。

三、单一继承(非虚继承)

 

可见以下几个方面:
1)虚函数指针在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。

代码(单一继承-非虚继承)实现如下,

#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;
 
class Parent {
public:
	int iparent;
	Parent() :iparent(10) {}
	virtual void f() { cout << "Parent::f()" << endl; }
	virtual void g() { cout << "Parent::g()" << endl; }
	virtual void h() { cout << "Parent::h()" << endl; }
};
 
 
class Child : public Parent {
public:
	int ichild;
	Child() :ichild(100) {}
	virtual void f() { cout << "Child::f()" << endl; }
	virtual void g_child() { cout << "Child::g_child()" << endl; }
	virtual void h_child() { cout << "Child::h_child()" << endl; }
};
 
class GrandChild : public Child {
public:
	int igrandchild;
	GrandChild() :igrandchild(1000) {}
	virtual void f() { cout << "GrandChild::f()" << endl; }
	virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
	virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};


//https://blog.youkuaiyun.com/a3192048/article/details/82259966 

//typedef void(*Fun)(void);
// https://blog.youkuaiyun.com/u014221279/article/details/50978204
// 需要知道:对于一个函数glFun(),它的函数名 glFun 其实是一个指针,指向该函数在内存中存的地方 
// typedef  返回类型(*新类型)(参数表)
// typedef的功能是定义新的类型。第一句就是定义了一种 Fun 的类型,
// 并定义这种类型为指向某种函数的指针,这种函数以一个void为参数并返回void类型。

// intptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。 
#define intptr_t long long
int main()
{
	typedef void(*Fun)(void);
	GrandChild gc;
	Fun pFun = NULL;
 
	// 运行的电脑是64位的,因此指针是64位的, intptr_t*其实就是long long* 
	printf("%p , %x\n", &gc, &gc); // %p输出完整的地址,以16进制输出时,会自动省略前面的0 
	// 在64位系统下, intptr_t*指针占8个字节,64位 
	cout << &gc << " <- 指针指向的地址,也是虚表指针地址" << endl;
	// 但是int类型只占4个字节,32位 
	cout << &gc.iparent << " <- 变量iparent地址" << endl;
	cout << &gc.ichild << " <- 变量ichild地址" << endl;
	cout << &gc.igrandchild << " <- 变量igrandchild地址" << endl;
	cout << endl;
	
	intptr_t *Vptr = (intptr_t *)&gc; // Vptr就为指针的起始地址
	
	cout << "[0] GrandChild::_vptr->                " << Vptr << endl;
	intptr_t *pVtab = (intptr_t*) *(Vptr + 0); //*(Vptr + 0), 解引用后为 虚表指针指向的值,也就是虚函数表地址 
	for (int i = 0; ; i++){
		pFun = (Fun)(*(pVtab + i));		//虚函数地址的指针(pVtab + i),指向的地址*(pVtab + i) 
//		pFun = (Fun)(pVtab[i]); // pFun = (Fun)(*(pVtab + i)) 的另一种写法 
		cout << "    [" << i << "] ";
		if (pFun == NULL){
			cout << pFun << endl;
			break;
		}
		pFun();
	}
	
	//在64位系统下,对于 intptr_t*类型的指针,intptr_t为long long,进行+1后,增加的是8个字节
	// 但是int类型是4个字节的,所以在 虚表指针地址+1后,将指针转换为 int*,这样+1的时候,增加的是4个字节 
	int* pV = (int*)(Vptr + 1);
	cout << "[1] Parent.iparent = " <<  *(pV + 0) << "                " << (pV + 0) << endl;
	cout << "[2] Child.ichild = " << *(pV + 1)  << "                 " << (pV + 1) << endl;
	cout << "[3] GrandChild.igrandchild = " << *(pV + 2)  << "      " << (pV + 2) << endl;	 
 	
 	cout << endl << endl;
	
//	int* pVtab2 = (int*)&gc; 
//	intptr_t* pVtab3 = (intptr_t*)&gc; 
//	printf("%p\n%p\n", pVtab2, pVtab3);
//	cout << "[0] GrandChild::_vptr      = " << &pVtab2 << endl; 
//	cout << "[1] offset + 1             = " << &pVtab2[1] << " value:" << *(int*)(&pVtab2[1]) << endl;
//	cout << "[2] offset + 2             = " << &pVtab2[2] << " value:" << *(int*)(&pVtab2[2]) << endl;
//	cout << "[3] offset + 3             = " << &pVtab2[3] << endl; 

 
	return 0;
}

四、多重继承(非虚继承)

 

 (注:图中有误:Derive::Base3中[1]应为Base3::g(),[2]应为Base3::h())

我们可以看到:
1) 每个父类都有自己的虚表指针。
2) 子类的成员函数被放到了第一个父类的虚表指针指向的地址中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。

代码(多重继承-非虚继承)实现如下,

#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;
 
class Base1 {
public:
	int ibase1, int_a;
	char char_b;
	Base1() :ibase1(10), int_a(11), char_b('1') {}
	virtual void f() { cout << "Base1::f()" << endl; }
	virtual void g() { cout << "Base1::g()" << endl; }
	virtual void h() { cout << "Base1::h()" << endl; }
};
 
class Base2 {
public:
	int ibase2;
	Base2() :ibase2(20) {}
	virtual void f() { cout << "Base2::f()" << endl; }
	virtual void g() { cout << "Base2::g()" << endl; }
	virtual void h() { cout << "Base2::h()" << endl; }
};
 
class Base3 {
public:
	int ibase3;
	Base3() :ibase3(30) {}
	virtual void f() { cout << "Base3::f()" << endl; }
	virtual void g() { cout << "Base3::g()" << endl; }
	virtual void h() { cout << "Base3::h()" << endl; }
};
 
class Derive : public Base1, public Base2, public Base3 {
public:
	int iderive;
	Derive() :iderive(100) {}
	virtual void f() { cout << "Derive::f()" << endl; }
	virtual void g1() { cout << "Derive::g1()" << endl; }
};
//https://blog.youkuaiyun.com/a3192048/article/details/82259966
int main()
{
	typedef void(*Fun)(void);
	Derive d;
	Fun pFun = NULL;
 
	intptr_t *Vptr = (intptr_t *)&d; // Vptr就为指针的起始地址
	intptr_t *pVtab = NULL;
	
	cout << "[0] Base1::_vptr->          " << Vptr + 0 << endl;
	pVtab = (intptr_t*) *(Vptr + 0);
	for (int i = 0; i < 4; i++){
		pFun = (Fun)(pVtab[i]);
		cout << "    [" << i << "] ";
		pFun();
	}
	int* pV = (int*)(Vptr + 1);
	cout << "[1] Base1.ibase1 = " << *(pV + 0) << "       " << (pV + 0) << endl;
	cout << "    Base1.int_a  = " << *(pV + 1) << "       " << (pV + 1) << endl;
	cout << "    Base1.char_b = " << *(pV + 2) - '0' << "        " << (pV + 2) << endl;
	
	cout << "[2] Base2::_vptr->          " << Vptr + 3 << endl; 
	// 这里为什么 Vptr + 3呢?这就涉及到内存对齐了,这里默认的对齐字节为8字节  https://www.cnblogs.com/zrtqsk/p/4371773.html 
	pVtab = (intptr_t*) *(Vptr + 3);
	for (int i = 0; i < 3; i++){
		pFun = (Fun)(pVtab[i]);
		cout << "    [" << i << "] ";
		pFun();
	}
	cout << "[3] Base2.ibase2 = " << *(Vptr + 4) << "       " << (Vptr + 4) << endl;

	cout << "[4] Base3::_vptr->          " << Vptr + 5 << endl;
	pVtab = (intptr_t*) *(Vptr + 5);
	for (int i = 0; i < 3; i++){
		pFun = (Fun)(pVtab[i]);
		cout << "    [" << i << "] ";
		pFun();
	}
	pV = (int*)(Vptr + 6);
	cout << "[5] Base3.ibase3 = " << *(pV + 0) << "       " << (pV + 0) << endl;
	cout << "[6] Derive.iderive = " << *(pV + 1) << "    " << (pV + 1) << endl;

	
	cout << sizeof(d) << endl << endl;  // 输出 56 
	
	d.f();
	d.g1();
//	d.g();	// 没有重写,而且因为有多个父类都有g()函数,不能明确用哪个 
	Base3 *p = &d;
	p->g();
//	p->g1();  // Base3没有g1()
	Base1 *pp = &d; 
//	pp->g1(); // Base1也没有g1(),子类新增加的,父类是不能调用的 


	return 0;
}

五、单一继承(虚继承)

 

 

六、重复继承(非虚继承)

重复继承,也叫菱形继承,或钻石继承。

 运行在32位系统中,指针占4个字节。

 

1)其中第一个vfptr指向的虚表地址是B1和D共享的,因此其中的函数接口应该覆盖了B1和D的虚函数,而第二个则只有B2的虚函数。
2)从图中可以看出,在派生类D中,存在着两份基类B的成员变量ib和cb,一份是B1继承而来的,另一份是B2继承而来的,这样可能会出现二义性编译错误。我们可以指定类作用域符::进行限定来消除二义性:

D d;
d.ib = 0;               //二义性错误
d.B1::ib = 1;           //正确
d.B2::ib = 2;           //正确

 也可以在语言层面利用虚继承机制来解决。

七、重复继承(虚继承)

 运行在32位系统中,指针占4个字节。

从上图可以看出,重复继承时用虚继承,派生类D对象在内存中占有56字节,比之前不用虚继承多了8个字节(之前为48字节),主要是因为多了“基类虚表指针”“vtordisp域”

注意和非虚多重继承不同的是,
1)第一个虚表指针指向的内容只记录了B1和D的虚函数(不包括B);
2)第二个虚标指针指向的内容只记录了B2和D的虚函数(不包括B);
3)B的虚函数在最后的基类虚表中;
4)B1和B2部分各增加了一个基类虚表指针vbptr,指向后面的基类。

从图中可以看出,VC++编译器在实现虚拟继承时,在派生类的对象中安插了两个vbptr指针。因此,对每个继承自虚基类的类实例,将增加一个隐藏的“基类虚表指针”(vbptr)成员变量,从而达到间接计算基类虚表位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
由上可以看出,B1虚基类表指针vbptr与虚基类B之间的偏移量是40字节,B2虚基类表指针vbptr与虚基类B之间的偏移量是24字节。第一项中-4的含义:表示的是vfptr和vbptr的距离,如果B1中没有虚函数的定义,这个地方就会是0。

注意到在虚拟继承的C++对象内存布局中,还有一个4个字节的vtordisp字段。如果虚继承中派生类重写了基类的虚函数,并且在构造函数或者析构函数中使用指向基类的指针调用了该函数,编译器会为虚基类添加vtordisp域。

 八、改变虚函数表代码

代码如下,

#include <iostream>
#include <stdio.h>
#include <stdint.h>
using namespace std;

class animal
{
protected:
  int age_;
  animal(int age): age_(age) { }

public:
  virtual void print_age(void) = 0;
  virtual void print_kind() = 0;
  virtual void print_status() = 0;
};

class dog : public animal
{
public:
  dog(): animal(2) { }
  ~dog() { }

  virtual void print_age(void) {
    cout << "Woof, my age = " << age_ << endl;
  }

  virtual void print_kind() {
    cout << "I'm a dog" << endl;
  }

  virtual void print_status() {
    cout << "I'm barking" << endl;
  }
};

class cat : public animal
{
public:
  cat(): animal(1) { }
  ~cat() { }

  virtual void print_age(void) {
    cout << "Meow, my age = " << age_ << endl;
  }

  virtual void print_kind() {
    cout << "I'm a cat" << endl;
  }

  virtual void print_status() {
    cout << "I'm sleeping" << endl;
  }
};

void print_random_message(void* something) {
  cout << "I'm crazy" << endl;
}
//https://www.zhihu.com/question/29256578/answer/43725188 
int main(void)
{
  cat kitty;
  dog puppy;
  animal* pa = &kitty;
  
  intptr_t* cat_vptr = *((intptr_t**)(&kitty));
  intptr_t* dog_vptr = *((intptr_t**)(&puppy));

  intptr_t fake_vtable[] = {
    dog_vptr[0],         // for dog::print_age
    cat_vptr[1],         // for cat::print_kind
    (intptr_t) print_random_message
  };
  // ((intptr_t**) pa) 是虚表指针,  *解引用后,为 虚函数表 地址 
  // 改变了指向的虚函数表 
  *((intptr_t**) pa) = fake_vtable;

  pa->print_age();    // Woof, my age = 1
  pa->print_kind();   // I'm a cat
  pa->print_status(); // I'm crazy

  return 0;
}

参考:

C++虚继承(一) --- vtordisp字段_H-KING的博客-优快云博客

vfptr和vbptr_temporg的博客-优快云博客_vfptr和vbptr

虚基类指针vbptr和虚函数指针vfptr_程序媛_婷的博客-优快云博客_虚基类指针

C++继承的内存布局_Waves___的博客-优快云博客_继承的内存布局

C++中对象的内存布局(三)_zhuoya_的博客-优快云博客

c++继承中的内存布局 - findumars - 博客园

钻石型继承模型的内存分布_CodeWill的博客-优快云博客

C++ 虚函数表解析---陈皓改进版_啊大1号的博客-优快云博客

C++ 对象的内存布局(上)---陈皓改进版_啊大1号的博客-优快云博客

C++ 对象的内存布局(下)---陈皓改进版_啊大1号的博客-优快云博客

Have Fun

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值