C/C++教程

关于C++对象的内存分配问题的思考与分析(下)

本文主要是介绍关于C++对象的内存分配问题的思考与分析(下),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

1.空类的对象占多大的内存?

2.空白基类最优化——单继承

3.空白基类最优化——多继承

4.一般带成员变量的类的对象所占的内存大小是多少?如何计算?

5.虚函数与对象内存之间的关系

6.函数代码(这里指的是变量常量之外的代码)占内存吗?

7.虚函数表在多继承时对内存的影响


1.空类的对象占多大的内存?

class EmptyClass
{

};

int _tmain(int argc, _TCHAR* argv[])
{
	cout << "空类的内存大小为:" << sizeof(EmptyClass) << endl << endl << endl;
	return 0;
}

输出结果为:

空类的内存大小为:1

EmptyClass中无任何数据成员,按理说它应该可以不占内存,但其实不是这样的。在C++中空类会占一个字节,编译器会给空类隐含加上一个字节,这是为了让对象的实例能够相互区别,并且每个实例在内存中都有独一无二的地址。既然要区分,为什么不是4或者8呢,因为1个字节就足以区分了,多了浪费空间。如果一个空类的大小是0,还会引发其它的问题,比如:数组是可以放对象的,那么若数组里面放的都是空类的对象,那么这个数组占多大内存呢?数组里有多少个元素呢?如何引用?所以“C++中空类会占一个字节”当成是一个规定就好了,不用过于纠结。

2.空白基类最优化——单继承

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优化对象布局。

3.空白基类最优化——多继承

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

空白基类最优化适用于单一继承,但对于多重继承需要看情况,该现象随着编译器不同可能不一样。

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的内存访问速度大大提升。

5.虚函数与对象内存之间的关系

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个,不会应为虚函数个数而改变。

6.函数代码(这里指的是变量常量之外的代码)占内存吗?

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求得函数代码所占的内存。而代码运行时所需要的内存,比如栈,堆等,则是在代码执行时才分配的。对于某个类的所有对象来说,类成员函数在内存中只有有一份,所有对象都共享一份成员函数的代码,同一个类的不同对象之间的差异是通过成员变量来体现的。

7.虚函数表在多继承时对内存的影响

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个指针所用的空间。

这篇关于关于C++对象的内存分配问题的思考与分析(下)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!