本篇随笔讨论一个比较冷门的知识,继承结构中内存对齐的问题,如今内存越来越大也越来越便宜,大部分人都已经不再关注内存对齐的问题了。但是作为一个有追求的技术人员,实现功能永远都是最基本的要求,把代码优化到自己想要的样子才能从中找到真正的愉悦感。这便是我们追求细节的意义。
声明:以下例子,以x86_64 64bit编译器编译的结果作为参考,32位编译器会有不同结果,这里不讨论。
目录 |
引子-内存对齐示例与规则 |
进阶-继承体系中的内存对齐 |
引子-内存对齐示例与规则:
讨论内存对齐,就要牵涉到#pragma pack(n)中定义n的大小。C语言中针对结构体提出了内存对齐的概念。下面请看代码:
#include <iostream>
using namespace std;
#pragma pack(8)
struct Ethanol{
char ch;
short sh;
int it;
};
struct Ether{
char ch;
int it;
short sh;
};
int main()
{
cout<<"Ethanol:"<<sizeof(Ethanol)<<endl;
cout<<"Ether :"<<sizeof(Ether)<<endl;
return 0;
}
运行结果:
如果不清楚上面的数据如何产生,请对照下面内村对齐的规则:
x86(linux 默认#pragma pack(4), window 默认#pragma pack(8))。linux 最大支持 4 字节对齐。
1 )取 pack(n)的值(n= 1 2 4 8--),取结构体中类型最大值 m。两者取小即为外对齐大小 Y= (m<n?m:n)。
2 )将每一个结构体的成员大小与 Y 比较取小者为 X,作为内对齐大小.
3 )所谓按 X 对齐,即为地址(设起始地址为 0)能被 x 整除的地方开始存放数据。
4 )外部对齐原则是依据 Y 的值(Y 的最小整数倍),进行补空操作。
原则:先内后外。
结构体内元素不同的排列组合方式大概可以用化学中的同分异构体来比喻,比如乙醇和甲醚,他们有着完全相同的成分,但是化学性质却不同。所以上面我用了Ethanol和Ether两个名字来命名结构体。
过渡到C++中,类与结构提不仅可以通过组合的方式构成新类型,还可以通过继承的方式来实现代码的重用
由结构体延伸到类看看包含关系中的类大小:
#include <iostream>
using namespace std;
#pragma pack(8)
class P1
{
public:
P1(){}
virtual void printP1(){ }
protected:
int p1;
};class Son
{
public:
Son(){}
private:
int son;
P1 P1;
};
int main()
{
cout<<"P1_Size :"<<sizeof(P1)<<endl;
cout<<"Son_Size:"<<sizeof(Son)<<endl;
}
运行结果:
以上运行结果,如果对C++了解,那么应该知道含有虚函数的类,除了成员变量大小外,虚函数表指针排在最前面。所以虚函数表指针的大小也要算进去。对照上面的内存对齐规则,我们计算一下上面的结果是怎么来的:
#pragma pack(n) ,n = 8。系统和编译器均为为64位,所以指针的大小是8Byte。
1)取P1中的最大类型大小m(虚函数表指针大小)与n作比较,m==n,所以外对齐Y==8。
2)结构体内每一个元素与Y做比较,取小者做内对其。所以,排列为下图:
所谓对齐,就是以0为起始地址,对元素进行排列,使用一个来能够整除内对齐的地址来安放后一个数据元素。
内对齐8+4=12
3)但是12不是外对齐的倍数,要用外对齐的最小整数倍来补齐8X2=16正好是这个类的大小。
进阶-继承体系中的内存对齐
继承现象:
上面用足组合的关系来计算一个类的大小是符合内存对齐规则的,这种包含关系与内存结构体中的对齐没有任何区别,那如果改为继承关系呢?
#include <iostream>
using namespace std;
#pragma pack(8)
class P1
{
public:
P1(){
cout<<"P1() :"<<(long long)this<<endl;
}
virtual void printP1(){ }
protected:
int p1;
};
class Son:public P1
{
public:
Son()
{
}
private:
int son;
// P1 P1;
};
int main()
{
cout<<"P1_Size :"<<sizeof(P1)<<endl;
cout<<"Son_Size:"<<sizeof(Son)<<endl;
}
运行结果:
仿佛有4个字节丢失了。。。。。。
查看内存排列:
我们在派生类的构造函数看一下son元素的地址,此地址即使son类型的起始地址,也是基类类型P1的结束地址:
#include <iostream> using namespace std; #pragma pack(8) class P1 { public: P1() { cout<<"Base_addr:"<<(long long)this<<endl; } virtual void printP1(){ } protected: int p1; }; class Son:public P1 { public: Son() { cout<<"son_addr:"<<(long long)&son<<endl;
cout<<"Base_inherit_size:"<<(long long)&son - (long long)this<<endl;
}
private: int son; // P1 P1;
};
int main()
{
cout<<"P1_Size :"<<sizeof(P1)<<endl;
cout<<"Son_Size:"<<sizeof(Son)<<endl;
Son son;
}
运行结果:
由此结果我们得到的结论是派生类继承并非完全继承了基类的大小,却继承了基类未作外对齐的大小,由此运行结果我们可以看出派生类(Son)类只继承了基类(P1)12个字节。对此感到疑惑的朋友可以继续使用其它示例来检验这个结果。
由此我们给出以下总结
继承体系中内存对齐的实质:
所谓继承关系实质上是派生类继承了基类中的元素(虚函数表和成员变量),而非继承已经内存对齐固化的基类结构,基类中的元素被继承到派生类中与派生类中新添加的元素需要重新按着元素的排列组合内存对齐。所以就有了上面包含关系与继承关系完全不一样的大小的现象。
原文:https://www.cnblogs.com/wangkeqin/p/12861852.html