在C语言中提供了内存分配函数malloc()(或calloc()/realloc())和free(),这些函数在运行时从堆内存中分配存储单元。
然而,在C++中这些函数将不能很好地运行。因为构造函数不允许我们向它传递内存地址来进行初始化。
C++是如何保证正确的初始化和清理,又允许我们在堆上动态创建对象呢?
malloc()和free()都是库函数,因此不在编译器控制范围为内。然而,如果有一个完成动态内存分配及初始化组合动作的运算符和另一个完成清理及释放内存组合动作的运算符,编译器就可以保证所有对象的构造函数和析构函数都会被调用。
一、对象创建和销毁(new/delete) 对象创建(new)当创建一个C++对象时,将会发生两件事:
1)为对象分配内存;
2)调用构造函数来初始化那个内存。
C++解决方案是吧创建一个对象的所有动作都结合在new运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并为这块内存调用构造函数。
示例演示:
Type *ptr=new Type(2);
在运行时等价调用了malloc(sizeof(Type)),并使用2作为参数来为Type调用拷贝构造函数,this指针指向返回值的地址。在指针赋给ptr之前,它是不确定的、初始化的对象。它自动的被赋予正确的Type类型。
默认的new还进行检查以确保传递地址给构造函数之前内存分配完成。
对象销毁(delete)new表达式的反面就是delete表达式进行对象销毁。delete表达式首先会调用析构函数,然后释放内存。
示例演示:
delete ptr
delete只用于删除由new创建的对象。如果用delete删除由malloc()创建的对象,这个动作行为是未定义的。
如果正在删除的对象的指针是0(NULL/nullptr),将不会发生任何事情。所以经常在删除指针后立即将指针赋值为0以免对它进行多次删除。
内存管理的开销当在堆里创建对象需要一定的时间和空间开销。
首先会从堆上搜索一块足够大的内存来满足要求。这可以通过检查按某种方式排列的映射或目录来实现,映射和目录用来显示内存的使用情况。这个过程很快但可能要试探几次,所以它可能是不确定的,即每次运行malloc()并不是花费完全相同的时间。
在指向这块内存的指针返回之前,这块内存的大小和地址必须记录下来,这样以后调用malloc就不会使用它,而当调用free时,系统就知道释放多大的内存。
delete void*可能会内存泄漏对一个void* 类型指针进行delete *** 作,可能出错,因为它将不执行析构函数。
class Test { private: int* data; int size; public: Test(int len) :size(len) { data = new int[size]; cout << "constructor" << endl; } ~Test(){ delete []data; cout << "destructor" << endl; } }; int main() { Test* t = new Test(10); delete t; void* t2 = new Test(10); delete t2; }
由上图输出结果可知,指针t指向一个Test对象,所以析构函数会被调用,从而释放分配给data的内存。但是t2是通过void*类型的指针指向Test对象,只会释放Test对象的内存,而不会调用析构函数,也就不会释放data所指向的内存,这就造成内存泄漏。
二、用于数组的new和delete在堆上创建对象数组使用new[]。语法格式如下:
Test* t = new Test[10];
这样就可以在堆上为10个Test对象分配内存,并为每一个对象调用构造函数。指针t实际上是一个数组的起始地址。对于普通的指针ptr,delete ptr 释放内存是正确的,但是delete t将会出错,它只会调用一个析构函数,还有9个没有调用。
解决方案:给编译器一个信息,说明它是一个数组的起始地址。
语法格式如下:
delete []t;
[]告诉编译器产生代码,该代码的任务是从数组创建时存放在某处的对象按对应数量取回,并为数组的所有对象调用析构函数。
内存耗尽当运算符new()找不到足够大的连续内存块安排对象时,一个new-handler的特殊函数将会被调用。new-handler的默认动作是产生一个异常。new-handler的行为和new()绑在一起,如果已经重载了new(),new-handler将不会按默认调用,也需要被重载。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)