一、基本概念
①程序如何使用内存区:全局/静态数据区(全局/静态变量,包括全局静态变量、局部静态变量)
常量数据区(常量字符串等)
代码区(代码)
栈(自动变量或局部变量)
堆(用户程序控制的存储区,动态产生的数据)
注意内存对齐:内存对齐后CPU运行比较快。C/C++程序中的sruct、union、class在编译时会对成员变量进行对齐处理,可通过#pragma pack()或者编译器的编译选项控制对齐方式或者关闭对齐
②堆和栈需要注意的问题:int *pNew = new int[5];
char *pMalloc = (char*)malloc(1);
程序结束后,pNew 和pMalloc 会被栈释放,但它们指向的内存是堆上的。虽然它们的指针已经不存在,但它们不会被自动释放,因此造成了内存泄露。(delete:释放指针指向的内存 赋NULL:清空指针存储的地址)
一个程序使用栈的大小是固定的,由编译器决定,可以通过编译选项指定栈的大小;堆的大小一般受限于系统有效的虚拟内存大小,因此可用来分配一些占用内存比较大的对象或数据
③C++对象需要注意的问题:如果一个对象通过定义在某个函数内的变量或者实现需要的临时变量来创建时,它是栈上的一个对象;如果一个对象是定义在全局范围内的变量,则它存储在全局/静态数据区;如果一个对象通过new创建,它是堆上的对象
二、对象的生命周期
①通过定义变量实现:变量作用域决定对象的生命周期,作用域由{}决定,并不一定是整个函数,直至程序退出
int foo()
{
int nVal=0;
...
if(nVal>0)
{
A a;//一旦退出条件语句a就被销毁
}
}
②
通过new操作符创建对象:即使对象指针被销毁,但还没有调用delete,该对象就一直存在
int main(void)
{
A *pA=createA();//第一个对象被创建
pA=createA();//第二个对象被创建
delete(pA);//只销毁了第2个对象
return 0;
}
③
通过实现创建对象:一般是中间临时变量的创建和销毁,一般由拷贝构造函数创建。如果需要创建大量临时对象,且这些对象在构造时需要分配很多资源,就会造成资源在短时间内被频繁地分配和释放(假设资源在析构时正确地释放),甚至有可能造成大量的内存泄露(假设资源在析构时没有被释放)
三、C++对象的内存布局
①简单对象:
class simpleClass//sizeof(simpleClass)的结果为12
{
public:
static int nCount;//静态数据成员存储在全局/静态存储区,并不作为对象占据的内存的一部分
int nValue;
char c;
simpleClass();
virtual ~simpleClass();
int getValue();
virtual void foo();
static void addCount();
}
一个simpleClass对象的内存布局如图
非静态数据成员是影响对象占据内存大小的主要因素,随着对象数目增加,非静态数据成员占据的内存会相应增加。
静态成员函数和非静态成员函数不会影响对象内存的大小,虽然其会占据相应的内存空间,同样也不会随着对象数目的增加而增加。
如果对象中包含虚函数,会增加4字节的空间,不论有多少虚函数。
虚函数表指针指向的虚函数表中不必完全是指向虚函数实现的指针。当指定编译器打开RTTI开关时,虚函数表中的第一个指针指向的是一个typeinfo的结构。
②单继承对象:
派生类与其基类使用的是同一个虚函数表。或者说,派生类在构造是,不会在创建一个新的虚函数表,而应该在基类的虚函数中增加或修改。
上图表示了单继承情况下对象内存布局的情况,在派生类实例的头部存在一个基类的实例,派生类的实例使用的是在创建基类实例时创建的虚函数表。但需要注意的是,虚函数表的内容在建立派生类的实例时发生了变化。虚函数表的virtual void foo(void)的位置存储的是指向derivedClass::foo()的实现,而非其基类simpleClass中的foo()的实现。
③多继承对象:
最简单的多继承布局:
(图中derivedClass重新实现了simpleClass2的fool函数)多继承中,每一个派生类的实例中包含所有基类的实例,每个基类的实例都有自己的虚函数表。
“菱形继承”:
其内存布局如图
为了解决“菱形继承”带来的浪费内存、二义性问题,C++提供了虚拟继承,当使用虚拟继承时,公共的基类只存在一个实例。
class midClass1:<strong>virtual</strong> public baseClass
....
class midClass2:<strong>virtual</strong> public baseClass
....
VC++通过添加一个虚基类表的指针来实现虚拟继承,因此造成空间变大。对具有虚拟继承基类的对象进行类型转换时,也会有比较大的开销,这些都是在使用虚拟继承时需要考虑的地方。
虚拟继承内存布局:
四、构造与析构
C++标准中规定每个类都必须有构造函数、析构函数、拷贝构造函数。没有则编译器自动产生。注意这里的拷贝函数执行的是位拷贝,即按照对象的内存空间逐个字节进行复制,有时这种默认的拷贝构造函数会带来隐含的内存问题。如果不想通过赋值或者拷贝构造函数,可以将拷贝构造函数定义在private区域,这样simpleClass b=a就会在编译时出错