浅析C++内存管理

来源:互联网 发布:本地数据库搭建 编辑:程序博客网 时间:2024/06/02 03:34

首先,了解一下C++内存是如何组织的。典型c++内存区域分为栈区、堆区和静态存储区。

a) 栈:存储局部变量和函数参数、返回地址等,内置于处理器的指令集中,效率很高,但是内存容量有限。
b) 堆:使用new进行分配,使用delete或delete[]释放。操作不当,会造成内存泄漏。程序结束时,由操作系统自动回收。
c) 静态存储区:存储全局变量和静态变量、常量。

看一个简单的内存分配的例子:
int* p = new int[5];
等式右边部分是在堆上申请长度为5个int的数组,发生在堆区;生成数组的地址,存储在指针p上,作为局部变量存放在栈区。

再来看C++有关内存管理的一个重要方面,就是函数参数传递。

函数参数传递机制

在C++中,函数若以对象的形式传参/返回,会得到对象的一个拷贝,造成内存开销的增大。假若函数以引用或指针的形式传参/返回,则不会产生一个拷贝,而是直接用它本身。可以内存资源,但是也会引发一些非常容易忽视的问题。
1、以引用或者指针的形式传参,函数对其的修改是双向的,假若该方法不能修改参数,为了程序的严谨,在参数前必须加const。
2、一定不能返回一个栈对象的引用。局部栈对象出了作用域之后就会被自动销毁,而外部程序再调用它是不合法的。有时这样调用或许不会出错,那是因为栈对象的内存地址还没有被写入其他数据,否则再调用它是会出错的。
3、谨慎使用返回一个堆对象的指针。因为堆对象是new出来的,那么它也必须通过delete显示的释放。假如你在函数里new了一个堆对象,然后又要返回该对象,这样返回时,返回的是堆对象本身,所以,销毁该堆对象就必须留给调用该方法的函数,如果调用函数没有delete堆对象,那么,这里就会出现内存泄露问题了。用一个while死循环来运行调用函数,电脑必定一下子就崩溃。
4、不要以引用的形式返回堆对象。局部堆对象是必须显示的delete来释放内存,而以引用的形式返回,则在调用函数里它是以引用的形式体现的。所以,应该是要自动销毁的,但是它本质上是一个堆对象的,是不能自动销毁的。所以,这样做更容易产生内存泄露。
(以上摘抄自 http://blog.csdn.net/lq_0402/article/details/4339926)

与Java的内存管理机制不同,C++给予程序员对内存的直接访问和控制,这样做的好处是可以实现效率好的程序,对内存管理更加紧凑的程序。那么,C++是如何进行直接的内存管理呢?主要是通过两对操作,malloc和free,new和delete,一定要配对使用。

malloc和free的使用
函数malloc:void * malloc(size_t size);
malloc返回值的类型是void *,所以在调用malloc时要显式地进行类型转换,将void * 转换成所需要的指针类型。malloc函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数,可以使用sizeof函数得到不同类型的总字节数。

函数free:void free( void * memblock );
语句free(p)能正确地释放内存。如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。

new和delete的使用
new内置了sizeof、类型转换和类型安全检查功能。使用起来比malloc要方便的多,下面两条语句都是分配length长的int整型数组,new语句更简洁明了,
1: int *p1 = (int *)malloc(sizeof(int) * length);
2: int *p2 = new int[length];

如果用new创建对象数组,那么对象的无参数构造函数会默认被调用,如果未定义无参数构造函数,编译不通过。数组的删除使用delete []。

了解完内存管理的基本知识后,我们接着谈谈内存管理需要遵循的原则:

谁负责申请内存,谁就负责释放。这条原则很好理解,但在现实编程中也很容易被遗忘。很多程序员,特别是初级C++程序员,很容易在用完资源以后忘记释放,导致内存泄漏,严重时甚至发生系统崩溃的情形,这个时候再回过头去查到底是哪里出现问题,就往往会花费大量debug的时间和精力。所以,最好的做法就是保证,负责申请内存的资源拥有者,在使用完资源后,一定要释放内存。一般地,我们会在构造函数中分配资源,在析构函数中释放资源。

单独处理。有些函数或代码块可能涉及到多种内存资源,比如同时需要处理文件以及网络连接,这种情况下需要确保,文件异常或是网络异常发生的时候,程序不会在完全释放两种资源之前返回异常,造成泄漏。可行的策略是将文件处理封装进一个类,将网络连接封装进另一个类,当离开作用域的时候,C++会保证两个类的析构函数被调用,从而释放资源。

智能指针

说了这么多,其实C++为我们提供了方便的机制可以使用,从而不需要太关注内存管理的细节,比如智能指针(smart pointer),它可以在被回收的时候自动释放资源。auto_ptr is a simple wrapper around a regular pointer. It forwards all meaningful operations to this pointer (dereferencing and indirection). Its smartness in the destructor: the destructor takes care of deleting the pointer.

如果通过常规指针分配内存,而且在执行delete之前发生异常,就不会自动释放内存,

void f(){    int *ip = ne int(42);   //dynamically allocate a new object                            //code that throws an exception that is not caugth inside f    delete ip;             //return the memory before exiting }

若使用auto_ptr对象来替代,将会自动释放内存,即使提早退出这个块,

void f(){    auto_ptr<int> ap(new int(42)); // allocate a new object    // code that throws an exception that is not caught inside f}   // auto_ptr freed automatically when function ends

支持的操作:
auto_ptr ap; 创建名为 ap 的未绑定的 auto_ptr 对象
auto_ptr ap(p); 创建名为 ap 的 auto_ptr 对象,ap 拥有指针 p 指向的对象。该构造函数为explicit
auto_ptr ap1(ap2); 创建名为 ap1 的 auto_ptr 对象,ap1 保存原来存储在ap2 中的指针。将所有权转给 ap1,ap2 成为未绑定的auto_ptr 对象
ap1 = ap2 将所有权 ap2 转给 ap1。删除 ap1 指向的对象并且使 ap1指向 ap2 指向的对象,使 ap2 成为未绑定的
~ap 析构函数。删除 ap 指向的对象
*ap 返回对 ap 所绑定的对象的引用
ap-> 返回 ap 保存的指针
ap.reset(p) 如果 p 与 ap 的值不同,则删除 ap 指向的对象并且将 ap绑定到 p
ap.release() 返回 ap 所保存的指针并且使 ap 成为未绑定的
ap.get() 返回 ap 保存的指针

智能指针省却了我们为内存的正确释放而额外地处理,但是这种方便还是有代价的:auto_ptr对象的赋值和复制是破坏性操作。

auto_ptr与普通指针的复制和赋值有区别。普通指针赋值或复制后两个指针指向同一对象,而auto_ptr对象复制或赋值后,将基础对象的所有权从原来的auto_ptr对象转给副本,原来的auto_ptr对象重置成为未绑定状态。

   1: auto_ptr<string> ap1(new string("stegosaurus"));   2: //after the copy ap1 is unbound   3: auto_ptr<string> ap2(ap1); //ownership transferred from ap1 to ap2

auto_ptr的复制和赋值改变右操作数,因此,auto_ptr赋值的左右操作数必须是可修改的左值。auto_ptr不能存储在标准容器中,因为标准库容器要求在复制或赋值后两对象相等,auto_ptr不满足這条件,如果将ap2赋值给ap1,则在赋值后ap1!=ap2,复制也类似。

除了将所有权从右操作数转给左操作数外,赋值还删除左操作数原来指向的对象–假如两个对象不同,通常自身赋值没有效果。

   1: auto_ptr<string> ap3(new string("pterodacty1"));   2: //object pointed to by ap3 is deleted and ownership transferred from ap2 to ap3;   3: ap3 = ap2; //after the assignment,ap2 is unbound

将ap2赋值给ap3后,1)删除了ap3指向的对象;2)将ap3置为指向ap2指向的对象;3)ap2是未绑定的auto_ptr对象。

还有,应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参。(原因:使用get成员初始化其他auto_ptr对象,违反了auto_ptr类的设计原则,在任意时刻只有一个auto_ptr对象保存给定指针,如果两个auto_ptr对象保存相同指针,则该指针会被delete两次!!!)

不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候,它只释放一个对象—它使用普通delete 操作符,而不用数组的 delete [] 操作符。

使用内存必须注意:
【规则1】用malloc或new申请内存之后,立即检查指针值是否为NULL。如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。
【规则2】不要忘记为数组和动态内存赋初值。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
【规则6】return语句不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。