内存分配(勿在浮沙筑高台)
C++的内存区域分为几种:常量数据、栈、自由存储、堆、全局或静态。
栈分配
学过数据结构的人都知道,栈是一种先进后出的连续内存块。C++中每个函数执行时,都会创建一个私有栈,然后将参数依次放入栈中(通常由右向左),在这个函数中分配的自动变量,都会存入栈中,当函数返回时,栈中的数据会以入栈的相反顺序弹出。
int main(int argc, _TCHAR* argv[])
{
int i=3;
int j=9;
return 0;
}
在上面的main函数中,会存在一个属于该函数的栈,除了参数外,变量i会先于j获得栈中的内存并初始化,当main返回时,main的私有栈会将栈里存放的东西逐个弹出,j会先于i被清除。
由于栈是一块大小固定的预先分配好的内存,所以在栈中分配一块内存给变量i仅仅是移动一个栈顶的指针,所以分配速度要比自由存储和堆中快的多。
在C++中,栈是无处不在的,每个函数有,每个对象也有,栈的基本原理构成了智能指针的理论依据。
所有的函数处在一个运行时栈中。
(下面摘自http://blog.programfan.com/article.asp?id=16454)
可以把运行时栈看成是一叠卡片,最上面的卡片表示程序的当前作用域,这往往就是当前正在执行的函数。当前函数中声明的所有变量都置于栈顶帧中,即占用栈顶帧的内存,这就相当于一叠卡片中最上面的一张卡片。如果当前函数调用了另一个函数,举例来说,一开始一叠卡片位于最底的卡片是main()函数,main()函数调用了foo()函数,则相当于在这一叠卡片上加了另一张卡片,这样foo()函数就有了自己的栈帧(就是指一块内存空间)以供使用。从main()传递到foo()的所有参数都会从main()栈帧复制到foo()栈帧中。 然后foo()函数又调用了bar()函数,则在这一叠卡片上又加了一张卡片,这样bar()就有了自己的栈帧(stack frame)以供使用,从foo()传递到bar()的参数就会从foo()栈帧复制到bar()栈帧中。 图片如下: foo()函数声明了一个整数值。
栈帧很有意义,因为栈帧可以为每个函数提供一个独立的内存工作区。如果一个变量是在foo()栈帧中声明的,那么调用bar()函数不会对它带来改变,除非你专门要求修改这个变量。另外,foo()函数运行结束时,栈帧既消失,该函数中声明的所有变量就不会再占用内存了。
现在流行的性能分析工具多数是采用了间隔一段时间就来查看运行时栈的顶部是那个函数在运行,因此就可以统计出程序生存期间函数的调用次数和运行时间。
栈的大小总是4字节的倍数,可以通过编译时指定链接器的参数来设定。如果没有指定,有些系统默认大小为64K。在windows平台下,线程和进程的创建函数CreateProcess/CreateThread可以通过参数指定栈的大小。
在堆上分配内存
void *malloc(size_t size);
标准C风格的内存分配方法,以字节为基本单位,返回分配的一块内存的起始地址(如果成功的话),无类型概念。如果没有足够的内存,会返回NULL。
凡是malloc分配的内存,在不用的时候,需要使用free释放。
#include <malloc.h>
int main(int argc, _TCHAR* argv[])
{
size_t length=10;
void* p=malloc(length);
free(p);
return 0;
}
malloc分配内存时遍历一个链表,链表中各个元素均指向某块内存,通常从小到大排列,找到足够大的内存块后,该内存快将被拆分开来,同时链表上相应节点上的指针进行调整。malloc/freee比较适合用于分配大中型对象(数百个或者数千个bytes),但是并没有对分配小内存做优化。
所以.Net的内存分配机制通常要快于C的malloc,因为他通过各种手段把内存造成一个连绵不绝没有尽头的连续内存块,分配内存只是移动一下指针。但是.Net也有自己的问题,必须依靠垃圾回收器回收不用的内存,然后重新整理内存,保证内存块总是够用,而垃圾回收器一旦工作,整个应用程序处于停止状态。
在自由存储区域分配内存
new/delete的幕后
通常我们这样写代码:
int* p=new int;
...
delete p;
new int语句幕后发生了什么。首先调用标准c++提供的void* operator new(std::size_t _Count) throw(bad_alloc)函数分配sizeof(int)大小的内存,int类型的大小通常和机器内存总线大小相同,所以32位机器上,sizeof(int)==4字节。
然后要看我们分配的是什么类型,如果是预定义类型,则什么都不做,直接返回一分配的内存块的指针,如果是一个c++的class,则会调用默认构造函数在已分配的内存上对对象初始化,然后返回指针。
那么,int*p =new int()又代表什么呢?因为int是预定义类型,所以分配完内存后,初始化内存值为0。所以不用再写下面的代码了:
int*p =new int
*p=0;
如果类型是一个class,则int* p=new string和int* p=new string()是等价的。
现在我们明白了,平时我们用new int的时候,这个new是c++提供的一个基本操作符,和operator new函数不是一回事。
当我们调用delete p的时候,如果p指向string,则先调用string的析构函数,然后调用operator delete回收内存。如果p指向的是预定义类型如int,则直接调用operator delete回收内存。
幕后就这么多么?不是。实际上,每次使用new分配一块内存的时候,c++编译器会分配一块内存(4-32字节)用于管理,可能保存了要分配内存的大小,这样delete才能保证正确释放掉合适大小的内存。
这是一个很麻烦的问题,如果你只分配了4字节,结果管理这4字节的内存耗用了16字节,天哪!Andrei说,老练的c++程序员都不会在这种情况下使用标准new。那么用什么呢?你可以使用Loki::SmallObject、Loki::SmallValueObject或者boost的pool库,ACE的ACE_Local_Memory_Pool类。他们使用了各自的策略,避免了这些额外的管理开销,以及频繁的new和delete。通常都是先分配一大块内存,然后慢慢用。
operator new/delete 函数
void* operator new(std::size_t _Count)throw(bad_alloc);
void* operator new(std::size_t _Count,const std::nothrow_t&) throw( );
void* operator new(std::size_t _Count, void* _Ptr) throw( );
很多编译器在operator new的实现代码中使用了malloc,但是c++标准并没有规定事情一定会是这样,c++标准只规定了malloc实现代码一定不会调用operator new。为了不把我们的代码基于一个今后可能会变化的假设上,逻辑上我们应该认为operator new/delete和malloc/free并没有必然的联系,所以我们不能使用free来释放operator new分配的内存,或者调用delete来释放malloc分配的内存。 由于malloc天生的缺陷,我希望看到将来的operator new不用malloc实现。
第一种形式的operator new如果分配内存失败会抛出bad_alloc异常对象,请不要检查返回的指针是否为NULL,那是过时的做法,不是标准做法。
void* operator new(std::size_t _Count,const std::nothrow_t&) throw( );这种形式保证分配内存即使失败也不会抛出异常,只会返回NULL。
void* p=::operator new(sizeofe(string),std::nothrow);
if(p!=NULL)
{
....
}
为什么要提供这种重载形式,内存分配错误是个十分严重的错误,因此用异常报告错误是十分合适的。但是如果你打算在一个循环里面分配内存,或者在一个时间要求极高的场合下分配内存,异常对象的传递还是慢了点。除此之外,还是尽量使用第一种重载形式比较好。
第三种形式其实不分配内存,而是你提供一块内存,然后它帮助你初始化。
void* p=::operator new(sizeof(string));//使用operator new的第一种形式分配内存
new (p) string; //在已分配的内存上初始化一个string对象
与operator new相对应的,也有三种重载的operator delete。
void operator delete(void* _Ptr) throw( );
void operator delete(void *, void *) throw( );
void operator delete(void* _Ptr,const std::nothrow_t&) throw( );
operator new[]/delete[] 函数
void *operator new[](std::size_t _Count)throw(std::bad_alloc);
void *operator new[](std::size_t _Count,const std::nothrow_t&) throw( );
void *operator new[](std::size_t _Count, void* _Ptr) throw( );
上面第一个new[]函数负责分配数组空间,
char* p=new char[10]; //如果是基本预定义类型,只分配数组空间不初始化
char* p=new char[10](); //如果是基本预定义类型,先分配数组空间然后每个元素初始化为0
string* p=new string[10];// 如果是c++类,则不仅分配数组空间而且调用默认构造函数初始化每个元素。
注意,请使用delete[] 销毁用new[] 创建的内存,如果是预定义类型,则也可以使用delete。
这三个new[]和上面三个的区别就是分配数组和分配单个对象,其余几乎相同。
void operator delete[](void* _Ptr) throw( );
void operator delete[](void *, void *) throw( );
void operator delete[](void* _Ptr, const std::nothrow_t&) throw( );
delete的多种用法,可以参考http://bright-li.spaces.live.com/blog/cns!64A26545E8622B86!460.entry
出错处理
void* operator new(std::size_t _Count)throw(bad_alloc);
当我们调用上面形式的operator new分配内存时,函数最后面throw(bad_alloc)异常规格说明代表一旦分配内存出错,将抛出一个bad_alloc异常对象。我们该怎么处理?
看看C++标准委员会主席Herb Sutter怎么阐述这个问题?他的观点概括为:
1)在绝大多情况下,你都不需要关心,因为new通常都会成功
好极了,我们的生活压力将因为这句话大大减轻。这个观点有几个理由:
有些操作系统只有当内存真正使用的时候,才会真正的分配内存,也就是说new总是工作得很好,等你用那块内存的时候,才有可能因为分配不到内存报错,所以检查new是否成功无意义;
在一个虚拟内存系统上,new失败的可能性很小;
即便new真的失败了,你又能怎么样呢?让程序崩溃实际上是一个很好的办法。
2)在某些特定情况下,检查new是否成功有意义:
如果能够预见到应用程序将使用多少内存,一开始就分配好所有需要的内存,如果要失败,就在一开始失败巴;
如果试图分配一个巨大的内存块,new失败后,也许你要做的事情就是再次调用new,只不过不要那么贪婪,少要点内存。
如果你真的有足够的理由要检查并处理new的错误,那就需要了解operator new内部错误处理流程。
当operator new的第一种重载形式在内部分配内存失败,他会通过指向new-handler函数的指针调用new-handler函数,如果new-handler函数指针为NULL,则抛出异常,如果指针有效,就在一个循环里面调用函数new-handler函数,直到new_handler函数1)释放了一定的内存,使得循环内部下一次分配内存成功,则满意退出2)当前这个new-handler函数认为自己没有办法,只好设置另一个函数为new-handler函数,循环会在下一次分配内存失败使调用新的new-handler函数3)如果认为没有办法继续下去,new-handler内部可以抛出bad_alloc异常或者将当前的new-handler设置为NULL,最终导致退出循环,客户代码收到异常对象。
如何编码的细节,我们将在讲述class专有operator new的时候描述。这里你们需要知道,因为默认情况下new-handler函数的指针为NULL,所以总是在错误的时候抛出异常。
常量区域
顾名思义,只有常量才能存在的区域,该区域内的变量将存在直到整个程序结束,并且不能被修改。
全局或静态区域
程序启动时,全局变量或者静态变量将被分配内存并初始化,直到整个程序结束才销毁。
如果几个全局或者静态变量在多个C++文件中使用,他们的初始化顺序是不确定的,所以他们之间最好不要有依赖关系。这也是Singletom模式遇到的烦恼。
注意,在常量、全局或者静态区域定义的预定义变量通常不仅分配了内存,而且会自动初始化,比如int变量会初始化为0,而栈中创建的预定义变量不会自动初始化。