目录
1.空类的对象占多大的内存?
2.空白基类最优化——单继承
3.空白基类最优化——多继承
4.一般带成员变量的类的对象所占的内存大小是多少?如何计算?
5.虚函数与对象内存之间的关系
6.函数代码(这里指的是变量常量之外的代码)占内存吗?
7.虚函数表在多继承时对内存的影响
class EmptyClass { }; int _tmain(int argc, _TCHAR* argv[]) { cout << "空类的内存大小为:" << sizeof(EmptyClass) << endl << endl << endl; return 0; }
输出结果为:
空类的内存大小为:1
EmptyClass中无任何数据成员,按理说它应该可以不占内存,但其实不是这样的。在C++中空类会占一个字节,编译器会给空类隐含加上一个字节,这是为了让对象的实例能够相互区别,并且每个实例在内存中都有独一无二的地址。既然要区分,为什么不是4或者8呢,因为1个字节就足以区分了,多了浪费空间。如果一个空类的大小是0,还会引发其它的问题,比如:数组是可以放对象的,那么若数组里面放的都是空类的对象,那么这个数组占多大内存呢?数组里有多少个元素呢?如何引用?所以“C++中空类会占一个字节”当成是一个规定就好了,不用过于纠结。
class EmptyClass { }; class TestA { EmptyClass e; int a; }; class TestB : public EmptyClass { int b; }; int _tmain(int argc, _TCHAR* argv[]) { TestA a; TestB b; cout << "TestA的内存大小为:" << sizeof(a) << endl << endl; cout << "TestB的内存大小为:" << sizeof(b) << endl << endl; return 0; }
输入结果为:
TestA的内存大小为:8
TestB的内存大小为:4
为什么TestB的内存大小是4,不应该是5吗?
在上例中,对于大部分编译器而言,sizeof(TestB)的结果是4,而不是8。这就是所谓的空白基类最优化在(empty base optimization-EBO 或 empty base class optimization-EBCO)。在空基类被继承后由于没有任何数据成员,所以子类优化掉基类所占的1 byte。EBO并不是c++标准所规定必须的,但是大部分编译器都会这么做。由于空基类优化技术节省了对象不必要的空间,提高了运行效率,因此成为某些强大技术的基石,基于类型定义类如stl中的binary_function、unary_function、iterator、iterator_traits的实现复用;基于策略类如内存管理、多线程安全同步的实现复用。当某个类存在空类类型的数据成员时,也可考虑借助EBO优化对象布局。
class A { }; class B { }; class C : public A { public: int a; }; class D : public A, public B { public: int a; }; int _tmain(int argc, _TCHAR* argv[]) { A a1; C c1; D d1; cout << "sizeof(a1): " << sizeof(a1) << endl; cout << "sizeof(c1): " << sizeof(c1) << endl; cout << "sizeof(d1): " << sizeof(d1) << endl; return 0; }
输出结果为:
vs下是:
sizeof(a1): 1
sizeof(c1): 4
sizeof(d1): 8
gcc编译器是:
sizeof(a1): 1
sizeof(c1): 4
sizeof(d1): 4
空白基类最优化适用于单一继承,但对于多重继承需要看情况,该现象随着编译器不同可能不一样。
class FruitA { private: int m_iWeight; char m_bufName[5]; short m_id; }; class FruitB { private: short m_id; int m_iWeight; char m_bufName[5]; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"FruitA的内存大小为:"<<sizeof(FruitA)<<endl; cout<<"FruitB的内存大小为:"<<sizeof(FruitB)<<endl<<endl<<endl; return 0; }
输出结果为:
FruitA的内存大小为:12
FruitB的内存大小为:16
对齐规则:
(1)对于class(struct/union)的各个成员,第一个成员位于偏移为0的位置,以后每个数据成员的偏移量必须是“#pragma pack()指定的数”和“该数据成员的自身长度”这二者中较小数值的倍数,不够的话要补填充字节。
(2)在数据成员完成各自对齐之后,class(struct/union)本身长度要保证是“#pragma pack()指定的数”和“class(struct/union)中最大数据成员长度”这二者中较小值的整数倍,不够的话要补填充字节。
int m_iWeight; char m_bufName[5]; short m_id;
short m_id; int m_iWeight; char m_bufName[5];
为什么要内存对齐
(1)硬件限制:不是所有的硬件平台都能访问任意地址上的数据;某些硬件平台只能在某些地址处取特定类型的数据,否则抛出硬件异常。
(2)提高性能:内存8字节对齐(注:cpu访问byte、WORD、DWORD、__int64类型的数据时,只用一次内存访问)后,CPU的内存访问速度大大提升。
class FruitC { private: int m_iWeight; char m_bufName[5]; short m_id; public: }; class FruitD { private: int m_iWeight; char m_bufName[5]; short m_id; public: virtual void SetName(char *bufName) { } }; class FruitE { private: int m_iWeight; char m_bufName[5]; short m_id; public: virtual void SetName(char *bufName) { } virtual void SetWeight(int weight) { } }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"FruitC的内存大小为:"<<sizeof(FruitC)<<endl<<endl; cout<<"FruitD的内存大小为:"<<sizeof(FruitD)<<endl<<endl; cout<<"FruitE的内存大小为:"<<sizeof(FruitE)<<endl<<endl; return 0; }
输出结果为:
FruitC的内存大小为:12
FruitD的内存大小为:16
FruitE的内存大小为:16
FruitC的内存大小为:12,这个很好理解,按照对齐规则计算出来就是这么多
FruitD的内存大小为:16 ,FruitC和FruitD的区别只有一点,就是FruitD多了一个虚函数。每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着该虚函数表的指针,虚函数表是编译器生成的,程序运行时被载入内存。一个类的虚函数表中列出了该类的全部虚函数地址,对象存储空间的最前端,存放的是虚函数表的地址。FruitD比FruitC多了4个字节,就是因为虚函数表的指针多占了4个字节。
FruitE的内存大小为:16,FruitE和FruitD的大小都是16,别看FruitE有2个虚函数,但其实虚函数表只有1个虚函数表指针也只有1个,不会应为虚函数个数而改变。
class Test { public: void Func1() { } void Func2() { } }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"Test的内存大小为:"<<sizeof(Test)<<endl<<endl; return 0; }
内存的分区:
代码区:存放程序的代码,即CPU执行的机器指令,并且是只读的。
常量区:存放常量(程序在运行的期间不能够被改变的量,例如: 10,字符串常量”abcde”, 数组的名字等)
静态区(全局区):静态变量和全局变量的存储区域是一起的,静态区的内存直到程序全部结束之后才会被释放
堆区:由程序员调用malloc/new函数来主动申请的,需使用free/delete函数来释放内存,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏
栈区:存放函数内的局部变量,形参和函数返回值。栈区之中的数据的作用范围过了之后,系统就会自动回收栈区的内存,不需要开发人员来手动管理。
函数代码存放在代码区,它也是占内存的,不过它是从程序加载时,就已经由系统分配好了内存用于存放这些代码,不能直接用sizeof求得函数代码所占的内存。而代码运行时所需要的内存,比如栈,堆等,则是在代码执行时才分配的。对于某个类的所有对象来说,类成员函数在内存中只有有一份,所有对象都共享一份成员函数的代码,同一个类的不同对象之间的差异是通过成员变量来体现的。
class BaseA { virtual void FuncA() { } }; class BaseB { virtual void FuncB() { } }; class TestC : public BaseA, public BaseB { }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"TestC的内存大小为:"<<sizeof(TestC)<<endl<<endl; return 0; }
输出结果为:
TestC的内存大小为:8
同时继承N个基类时,若基类中都有虚函数,则派生类中会有N个虚函数表指针,此时,派生类所占内存中会包含这N个指针所占用的空间。
class BaseA { virtual void FuncA() { } }; class BaseB : public BaseA { virtual void FuncB() { } }; class TestC : public BaseB { }; int _tmain(int argc, _TCHAR* argv[]) { cout<<"TestC的内存大小为:"<<sizeof(TestC)<<endl<<endl; return 0; }
输出结果为:
TestC的内存大小为:4
多次单继承时,派生类中只会有1个虚函数表指针,此时派生类所占内存中会包含这1个指针所用的空间。