浅析C++内存管理(一):new和delete

静态存储区、栈区和堆区

首先看一个简单的内存分配,用new操作符申请一个长度为5的整型数组。

int* p = new int[5];

等式右边在堆区申请长度为5的整型数组,new操作符返回数组的地址,并赋值给整型指针p,作为局部变量存放在栈区。这样简单的一条语句涉及到堆区和栈区,除此之外还有C++还包含静态存储区。那么,它们分别用来存储什么数据呢?

静态存储区(static)用于存储静态变量,包括局部静态变量(local static)和类静态数据成员。函数体外定义的全局变量也存放在这一内存区域。栈区(stack)用于存储任何在函数体内定义的非静态变量,包括函数参数、返回值等。静态存储区的内存对象在使用前创建,在程序所运行的进程退出时销毁;而栈区对象在离开定义时其所在的代码块前销毁。静态存储区和栈区的对象的创建和销毁都由编译器(compiler)控制,在静态编译期间就已经确定。

另外还有一块内存被称作堆区(heap)或者自由存储区(free storage),用于在程序运行期间动态分配内存,使用malloc或new进行分配。程序员需要自己去控制这些对象的生命周期,在对象不再被需要时显式销毁这些对象来回收内存,否则会造成内存泄漏。

在c语言时代,程序员通过函数malloc和free分配内存和释放内存。在C++时代,则通过操作符new和delete进行内存管理。C++11还提供了智能指针(smart pointer),防止出现内存分配方面的一些常见错误,使内存管理进一步智能化、安全化。

malloc和free的使用

void * malloc(size_t size);

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

void free( void * memblock );

函数free能释放内存。如果p是null指针,那么free对p无论操作多少次都不会出问题。如果p不是null指针,那么free对p连续操作两次就会导致程序运行错误。

new和delete的使用

操作符new

操作符new相当于是c语言malloc函数的升级版本,它内置了sizeof、类型转换和类型安全检查功能。下面两条语句都是分配length长的int整型数组,new语句更简洁明了,

int *p1 = (int*) malloc(sizeof(int) * length);
int *p2 = new int[length];

在用new为自定义类型的对象申请内存时,会根据new()内形参来判断调用对应的构造函数。如果用new创建对象数组,那么对象的无参数构造函数会默认被调用,如果未定义无参数构造函数,编译不通过。而且,如果堆区内存空间不够导致new失败,程序会抛出bad_alloc异常。

操作符delete

操作符delete用于销毁对象并释放内存,对象的析构函数会被调用。传递给delete的指针那么是new生成的指向动态内存的指针,要么是null指针。delete同一个指针两次,会发生运行时错误。

delete数组时使用delete[] ptr。

new/delete陷阱

虽然操作符new比函数malloc使用起来要方便许多,但还是存在不少使用陷阱。典型的很容易犯的错误有:

1.内存泄漏。程序在使用完通过new动态申请的内存以后忘记了要用delete释放,或者因为出现exception而没有正确执行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
 }

2.delete两次。假若动态内存成功申请返回的指针被赋值给两个不同的指针变量,它们分别被传递给delete操作符,在第一个指针被delete时,所指向的内存被回收,另外一个指针被delete将导致程序运行错误。

3.悬挂指针(dangling pointer)。指向动态分配内存的指针被delete后,其指向的内存被回收,此时该指针指向无效的内存地址,相当于指针未被初始化。可以强制将悬挂指针赋值nullptr,指明该指针没有指向任何对象。

上述这些错误不止初级程序员会犯,在大型项目团队合作开发时,即使有经验的程序员也会遇到,而且一旦出现其中一个问题,如果发生在生产环境那损失可能是巨大的,比如程序在运行时崩溃。内存泄漏要求程序能够保证new和delete成对使用,即使中间出现exception,也要确保delete能够正确执行。C++语言机制中,构造函数和析构函数是保证会成对出现的,自然而然地想到可以把new放在构造函数中,而把delete放在析构函数中,那么就可以一劳永逸地避免内存泄漏的问题了。

delete两次这个问题,需要我们能够统计指针被引用的次数,在引用计数(reference count)为零时,主动销毁指针对象,从而避免被两次delete。引用计数同时解决了悬挂指针的问题。

C++11的智能指针实现的基本思想就是使用了析构函数以及引用计数。具体实现参见本系列第二篇《浅析C++内存管理(二):如何实现shared_ptr?》

你可能感兴趣的:(C++大杂烩,C++内存模型,new,heap)