内存布局和申请
1.在 VC 中,栈空间未初始化的字符默认是 -52,补码是 0xCC。两个 0xCC ,即
0xCCCC 在 GBK 编码中就是“烫” ;堆空间未初始化的字符默认是 -51,两个 -51 在 GBK
编码中就是“屯” 。 二者都是未初始化的内存
2.
代码区
数据区
堆区
栈区
代码区(Code Area)存放的是程序的执行代码 ;
数据区(Data Area)存放的是全局数据、常量、静态变量等 ;
堆区(Heap Area)存放的则是动态内存,供程序随机申请使用;
而栈区(Stack Area)则存放着程序中所用到的局部数据。这些数据
可以动态地反应程序中对函数的调用状态,通过其轨迹也可以
研究其函数机制。其中,除了代码区不是我们能在代码中直接
控制的,剩余三块都是我们编码过程中可以利用的。在 C++ 中,
数据区又被分成自由存储区、全局 / 静态存储区和常量存储区,再加上堆区、栈区,也就是
说内存被分成了 5 个区
栈(Stack)区
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存
储单元将自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是所分配的
内存容量有限。
堆(Heap)区
堆就是那些由 new 分配的内存块,其释放编译器不会管它,而是由我们的应用程序控制
它,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作
系统就会自动回收。
自由存储区
自由存储区是那些由 malloc 等分配的内存块,它和堆十分相似,不过它是用 free 来结
束自己生命的。
全局 / 静态存储区
全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始
化的和未初始化的,在 C++ 里面没有作此区分,它们共同占用同一块内存区。
常量存储区
这是一块比较特殊的存储区,里面存放的是常量,不允许修改。
堆与栈的区别:
管理方式不同
对于栈来讲,它是由编译器自动管理的,无须我们手工控制 ;对于堆来说,它的释放工
作由程序员控制,容易产生 memory leak。
空间大小不同
一般来讲在 32 位系统下,堆内存可以达到 4GB 的空间,从这个角度来看堆内存几乎是
没有什么限制的。但是对于栈来讲,一般都是有一定空间大小的。
碎片问题
对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而产生大量的碎片,
使程序效率降低。对于栈来讲,则不存在这个问题,其原因还要从栈的特殊数据结构说起。
栈是一个具有严明纪律的队列,其中的数据必须遵循先进后出的规则,相互之间紧密排列,
绝不会留给其他数据可插入之空隙,所以永远都不可能有一个内存块从栈中间弹出,它们必
须严格按照一定的顺序一一弹出。
生长方向
对于堆来讲,其生长方向是向上的,也就是向着内存地址增加的方向增长;对于栈来讲,
它的生长方向是向下的,是向着内存地址减小的方向增长的。
分配方式
堆都是动态分配的,没有静态分配的堆。栈有两种分配方式 :静态分配和动态分配。静
态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca 函数完成,但是栈的动态
分配和堆是不同的,它的动态分配是由编译器进行释放的,无须我们手工实现。
分配效率
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持 :它会分配专门的寄存器
存放栈的地址,而且压栈出栈都会有专门的指令来执行,这就决定了栈的效率比较高。堆则
是 C/C++ 函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算
法(具体的算法可以参考数据结构 / 操作系统)在堆内存中搜索可用的足够大小的空间,如
果没有足够大小的空间(可能是由于内存碎片太多) ,则可能调用系统功能去增加程序数据
段的内存空间,这样就有机会分到足够大小的内存了,然后返回。显然,堆的效率比栈要低
得多。
3.区分 new 的三种形态
new operator
operator new
placement new

string *pStr = new string("Memory Management");
int *pInt = new int(2011);
这里所使用的 new 是它的第一种形态 new operator。它与 sizeof 有几分类似,它是语言
内建的,不能重载,也不能改变其行为,无论何时何地它所做的有且只有以下三件事,如图
3-2 所示。
图 3-2 new operator 所完成的三件事
所以当写出“string *pStr = new string("Memory Management");”代码时,它其实做的就
是以下几件事:
// 为 string 对象分配 raw 内存
void *memory = operator new( sizeof(string) );
// 调用构造函数,初始化内存中的对象
call string::string()on memory;
// 获得对象指针
string *pStr = static_cast<string*>(memory);
当然,对于内置类型,第二步是被忽略的,即:
// 为 int 分配 raw 内存
void *memory = operator new( sizeof(int) );
// 获得对象指针
int *pInt = static_cast<int*>(memory);
其实 new operator 背后还藏着一个秘密,即它在执行过程中,与其余的两种形态都发生
了密切的关系 :第一步的内存申请是通过 operator new 完成的 ;而在第二步中,关于调用什
么构造函数,则由 new 的另外一种形态 placement new 来决定的。
对于 new 的第二种形态 — 内存申请中所调用的 operator new,它只是一个长着“明星
脸”的普通运算符,具有和加减乘除操作符一样的地位,因此它也是可以重载的。
operator new 在默认情况下首先会调用分配内存的代码,尝试从堆上得到一段空间,同
时它对事情的结果做了最充分的准备:如果成功则直接返回;否则,就转而去调用一个 new_
hander,然后继续重复前面过程,直到异常抛出为止。所以如果 operator new 要返回,必须
满足以下条件之一:
内存成功分配。
抛出 bad_alloc 异常。
通常,operator new 函数通过以下方式进行声明:
void* operator new(size_t size);
注意,这个函数的返回值类型是 void*,因为这个函数返回的是一个未经处理的指针,
是一块未初始化的内存,它像极了 C 库中的 malloc 函数。如果你对这个过程不满意,那么
可以通过重载 operator new 来进行必要的干预。例如:
class A
{
public:
A(int a);
~A();
void* operator new(size_t size);
...
};
void* A::operator new(size_t size)
{
cout<<"Our operator new...");
return ::operator new(size);
}
还有一点需要注意
的是,正像 new 与 delete 一一对应一样,operator new 和 operator delete 也是一一对应的 ;
如果重载了 operator new,那么也得重载对应的 operator delete。
placement new 是用来实现定位构造的
placement new 是标准 C++ 库的一部分,被声明在了头文件 <new> 中,所以只有包含了
这个文件,我们才能使用它。它在 <new> 文件中的函数定义很简单,如下所示:
#ifndef __PLACEMENT_NEW_INLINE
#define __PLACEMENT_NEW_INLINE
inline void *__CRTDECL operator new(size_t, void *_Where) _THROW0()
{ // construct array with placement at _Where
return (_Where);
}
inline void __CRTDECL operator delete(void *, void *) _THROW0()
{ // delete if placement new fails
}
#endif /* __PLACEMENT_NEW_INLINE */
这就是 placement new 需要完成的事。细心的你可能会发现,placement new 的定义与
operator new 声明之间的区别:placement new 的定义多一个 void* 参数。使用它有一个前提,
就是已经获得了指向内存的指针,因为只有这样我们才知道该把 placement new 初始化完成
的对象放在哪里。
在使用 placement new 的过程中,我们看到的却是 "new(p) A(2011)" 这样奇怪的调用形
式,它在特定的内存地址上用特定的构造函数实现了构造一个对象的功能,A(2011) 就是对
构造函数 A(int a) 的显式调用。当然,如果显式地调用 placement new,那么也得本着负责任
的态度显式地调用与之对应的 placement delete :p->~A();。这部分工作本来可以由编译器独
自完成的 :在使用 new operator 的时候,编译器会自动生成调用 placement new 的代码,相
应的,在调用 delete operator 时同样会生成调用析构函数的代码。所以,除非特别必要,不
要直接使用 placement new。但是要清楚,它是 new operator 的一个不可或缺的步骤。当默
认的 new operator 对内存的管理不能满足我们的需要,希望自己手动管理内存时,placement
new 就变得有用了。就像 STL 中的 allocator 一样,它借助 placement new 来实现更灵活有效
的内存管理。
placement new的好处:
1)在已分配好的内存上进行对象的构建,构建速度快。
2)已分配好的内存可以反复利用,有效的避免内存碎片问题。
关于Placement new的标准用法,网上有一篇文章讲得很清楚,我这里再转述一下。
class Foo
{
char cc;
float f;
public:
void print() { std::cout << "ADDR: " << this << std::endl; }
void set_f( float _f ) { std::cout << "set f val : " << _f << std::endl; f = _f; }
void get_f() { std::cout << "get f val : " << f << std::endl; }
};
1)分配内存
char* buff = new char[ sizeof(Foo) * N ];
memset( buff, 0, sizeof(Foo)*N );
2)构建对象
Foo* pfoo = new (buff)Foo;
3)使用对象
pfoo->print();
pfoo->set_f(1.0f);
pfoo->get_f();
4)析构对象,显式的调用类的析构函数。
pfoo->~Foo();
5)销毁内存
delete [] buff;
上面5个步骤是标准的placement new的使用方法。
对于buff这块内存可以反复使用,只要重复2)、3)、4)步骤即可。
placement new还可以解决的一个问题是建立带参数的构造函数对象数组。
代码示例如下:
class CPong { public: CPong( int m ) : v(m) { std::cout << "CPong ctor." << std::endl; } private: int v; }; char* pong = new char[ sizeof(CPong) * 10 ]; CPong* pp = (CPong*)pong; for ( int i=0; i<10; ++i ) { new (pp+i)CPong(i); } for ( int j=0; j<10; ++j ) { pp[j].~CPong(); } delete [] pong;
浙公网安备 33010602011771号