【C++】 类的内存对齐、虚函数表

  本文分为以下几个部分内容:

  • 什么是内存对齐,为什么要内存对齐
  • C++的空类,以及没有虚函数和非静态变量的类
  • C++类的内存分布(成员变量)
  • C++类的内存分布(虚函数)
    • 一个类的情况
    • 继承关系中的情况
一、什么是内存对齐,为什么要内存对齐
1.1 什么是内存对齐:

  内存对齐是从硬件层面出现的概念。可执行程序是由一系列CPU指令构成的,其中有一些指令是需要访问内存的。在很多CPU架构下,这些指令都要求操作的内存地址(更准确的说,操作内存的起始地址)能够被操作的内存大小整除,满足这个要求的内存访问叫做对齐内存的访问aligned memory access),否则就是未对齐内存的访问unaligned memory access)。如果访问未对齐的内存会出现什么结果呢?这个要看CPU。

  • 有些CPU架构可以访问未对齐的内存,但是会有性能上的影响。典型的就是 x86 架构CPU
  • 有些CPU会抛出异常
  • 有些CPU不会抛出任何异常,会静默地访问错误的地址
  • 近几年也有些CPU的一部分指令可以正常访问未对齐的内存,同时不会有性能影响

  因为每个CPU对未对齐内存的访问的处理方式都不一样,所以访问未对齐的内存是要尽量避免的。所以就出现了 C/C++ 的内存对齐机制。

1.2 为什么要内存对齐:
  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
二、C++的空类,以及没有虚函数和非静态变量的类
2.1 C++ 空类大小:
class A{
};

int main() {
	cout << sizeof(A) << endl;	// 1
}

  对于一个什么都没有的空类,实际并不是空的,因为有默认的函数,具体可以参考 (待填入网址),大小是 1,这是因为需要有一个地址,C++ 不允许两个不同的对象有相同的地址,所以 C++ 中空的类和结构体大小都是 1。

2.1 加入成员函数、静态成员函数、静态成员变量:

  当我们显示加入了新的成员函数、静态成员函数、静态成员变量后,类的大小还是 1:

class A{
	A(){}
	~A(){}
	void print() { printf("print()\n"); }
	void foo() { printf("print()\n"); }

	static void sprint() { printf("sprint()\n"); }
};

int main() {
	cout << sizeof(A) << endl;	// 1
}

  也就是成员函数、静态成员函数、静态成员变量都是不占用类的内存的,这是因为这些东西都不是类的,而不是每个对象分别存储。static 变量就是存储在全局静态区。

三、C++类的内存分布(变量)

  C++ 中会影响一个类的对象的大小的,就是非静态成员变量虚函数
  在 C++ 中每个类型都有两个属性,一个是大小(size),还有一个就是对齐要求(alignment requirement),或称之为对齐量(alignment)。C++标准并没有规定每个类型的对齐量,但是一般都会有这样的规律:

  • 所有基础类型的对齐量等于这个类型的大小。
  • struct, class, union 类型的对齐量等于其中非静态成员变量最大的对齐量
  • 标准规定所有的对齐量必须是 2 的幂次。

  编译器在给一个变量分配内存时,都要算出并满足这个类型的对齐要求。struct 和 class 类型的非静态成员变量的字节数偏移(offset)也要满足各自类型的对齐要求。
  从下边的例子中我们就可以看到:

class A {		// size		pos range
public:
	int i;		// 4		0 - 3
	double d;	// 8		8 - 15
	short s;	// 2		16 - 17
};

class B {
public:
	int i;		// 4		0 - 3
	short s;	// 2		4 - 5
	double d;	// 8		8 - 15
};

class C {
public:
	short s1;	// 2		0 - 1
	int i;		// 4		4 - 7
	short s2;	// 2		8 - 9
	double d;	// 8		16 - 23
};

int main() {
	cout << sizeof(A) << endl;	// 24
	cout << sizeof(B) << endl;	// 16
	cout << sizeof(C) << endl;	// 24
	cout << endl;

	A a;
	cout << "&a.i\t  &a.d\t  &a.s" << endl;
	cout << &a.i << "  " << &a.d << "  " << &a.s << endl << endl;
	B b;
	cout << "&b.i\t  &b.s\t  &b.d" << endl;
	cout << &b.i << "  " << &b.s << "  " << &b.d << endl << endl;
	C c;
	cout << "&c.s1\t  &c.i\t  &c.s2\t  &c.d" << endl;
	cout << &c.s1 << "  " << &c.i << "  " << &c.s2 << "  " << &c.d << endl;
}

  上述代码输出为:

24
16
24

&a.i    &a.d    &a.s
010FF8F4 010FF8FC  010FF904

&b.i     &b.s    &b.d
010FF8DC 010FF8E0 010FF8E4

&c.s1    &c.i    &c.s2    &c.d
010FF8BC 010FF8C0 010FF8C4 010FF8CC

  类 A 、类 B、类C 的对齐量都是 sizeof(double) = 8就好比这个类都是 8 大小的盒子,每个变量都是按声明的前后顺序往盒子里放,当前盒子放不下,就放下一个全新的空盒子中。所以上边的类 A 中 int a; 占了第一个盒子的一半,double b; 发现只有 4 大小的盒子放不下,就往下一盒子盒子中放了(全新的盒子一定放的下,因为盒子大小是所有变量中最大的!),而类 B 中 int a; 放在第一个盒子后,short c; 只需要 2 的大小,所以还是可以和 int a; 放在一个盒子中(所以类 B 中的 short c; 换成 int c; 不会影响类的大小,因为第一个盒子还是放得下)。
  类C 的作用在于看到一个盒子里边是怎么存放的,也就是一个变量存放一定是按照他自身大小的倍数存放,int 就一定是 4 。看懂了 C (尤其是 c.i 和 c.d)就明白了了。

四、C++类的内存分布(虚函数)
4.1 一个类中有虚函数时内存分布

  C++ 的类中,没有除了虚函数以外的所有函数,都是不占类的内存的,但是如果类有了虚函数,类内就会有一个虚函数表的指针 _vptr,指向自己的虚函数表,vptr 一般都是在类的最前边,如下所示。

  由于只是存一个指向虚函数表的指针,所以不管有多少个虚函数,都是 4 字节大小(32位下,任何指针大小都是 4,64位下,任何指针大小都是 8),比如下边这个类 A,size 就是 4:

4.2 继承关系中的有虚函数时的内存分布

  用下边这段代码看,内存分布如图所示:

class A {
public:
	A(){}
	virtual ~A(){}
	virtual void foo(){}
	virtual void print() {}
};

class B : public A {
	double d;
	void print() override { cout << "B print()" << endl; }
};

int main() {
	A a;	// sizeof(A) = 4  (_vptr: 4)
	B b;	// sizeof(B) = 16 (_vptr: 4 + 空: 4 + double: 8)
}

  最关键的一个点就是,对于没有 override 的虚函数,基类和子类中 _vptr 指向的虚函数表中,这个虚函数的地址是一样的,也就是上边的 foo() 函数,而对于重写了的或者默认重写的析构函数来说,_vptr 指向的虚函数表中,函数地址是不一样的(当然两个类的 _vptr 地址也是不一样的,这是肯定的),这就能窥探到多态的实现了。

  • 2
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值