在堆上申请空间,对于内置类型,new/delete和malloc/free的功能几乎相同,对于自定义类型,new/delete会多做一件事:调用该自定义类型的构造/析构函数。
class Stack
{
public:
Stack(int capacity=4)
{
cout << "Stack()" << endl;
_a = new int[capacity];
_size = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _a;
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int *_a;
int _size;
int _capacity;
};
void Test2()
{
Stack *ps = new Stack[5];
delete[] ps;
}
除此之外,new/delete和malloc/free的处理错误方式也不同,new/delete开辟空间失败会抛异常,而malloc/free开辟失败会返回一个空指针。
new/delete作为操作符,它本身无法开辟空间,C库里面的malloc函数才能做到开空间,最终开辟空间/释放空间这种事情还是要交给malloc/free。new/delete只是直接或间接封装了malloc/free,并对结果的处理方式做了一些优化:malloc开辟空间失败返回空指针不符合C++面向类和对象的需求,new/delete将它们封装并将处理结果改为抛异常
于是大胆猜测,malloc/free可能被封装到某个中间操作符里面,在这里对malloc开辟空间失败返回空指针的方式进行优化,申请空间的步骤完成,然后在调用析构/构造函数,再将这个中间操作符封装到new/delete里面,这样才是C++面向类和对象的编程。
下面将从汇编的层面理解这个问题:
![](https://i-blog.csdnimg.cn/blog_migrate/5eaf7a7c5551531c4c5a9ee505e6ca4a.png)
通过反汇编可以看到,第二行的operator new[]就是封装了malloc的操作符,而它里面是会去调用malloc开辟空间的。
同样,也可以通过反汇编看到new调用了构造函数(这里找起来比较麻烦,也是找了老半天)
![](https://i-blog.csdnimg.cn/blog_migrate/3060bee6e1a533e1d60ab96fda356b12.png)
![](https://i-blog.csdnimg.cn/blog_migrate/006bec228da4a4e16ce00161d50de78f.png)
现在比较好奇的是operator new[]里面是怎么实现的,只是调用了malloc函数这么简单吗?
进入operator new[]后发现,的确是在开空间,想必是开count个Stack大小的空间,也就是5*12=60个字节的空间
但是结果出人意料,打开监视窗口,它并不是60而是64个字节
![](https://i-blog.csdnimg.cn/blog_migrate/6013fed89c10a4d969432d82fd825d20.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0512812a2bf72c5e9987892bd8cb4b25.png)
为什么会多开4个字节,多开的4个字节里面又存的什么东西?
直接说结论,这四个字节就是存的该类的个数。
细心点可以发现,每次delete释放多个空间的时候都只需要一个[]即可,但后面还是需要调用析构函数,没有告诉次数编译器怎么知道要调用多少次析构函数 为了能正确调用析构函数的次数,保留这个数字是有必要的。
一般来说,申请和释放空间都要配对使用,malloc和free配对,new和delete配对,new[]和delete[]配对。
下面看来看几段有趣的代码:
class A
{
public:
A(int a=0)
{
cout << "A()" << endl;
_a = a;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void TestA()
{
A *pa = new A[5];
delete pa;
}
![](https://i-blog.csdnimg.cn/blog_migrate/86915dc328306f8fe8a21d65568314bd.png)
执行后程序崩溃了,但是简单的内存泄漏并不会导致程序崩溃。
其实这里崩溃的原因是空间释放不完全。
一样的,用new[]开辟空间会在前面多开四个字节来存储元素个数,并且那四个字节也是在堆上面申请的,用delete编译器就会想当然认为前面没有空间要释放,就会导致内存释放不完全。
而用delete[]就相当于你告诉了编译器这是多元素的内存需要释放,那编译器就知道前面还有四个字节空间需要释放。
在来看一种不带析构函数的自定义类型
class A
{
public:
A(int a = 0)
{
cout << "A()" << endl;
_a = a;
}
private:
int _a;
};
void TestA()
{
A *pa = new A[5];
delete pa;
}
这段代码却可以成功运行,也没有内存泄漏的问题。原因是自定义类型不带析构函数那么在delete释放空间时不会调用析构函数,那么它在new申请空间时就没必要多申请那四个字节的空间,它都没有析构函数了还存它的析构次数有何意义。
可以明确的说,即使是用free来释放,这里也不会有任何的问题。
一定情况下,delete[],delete,free都可以使用,但这只是少数情况,内存的申请和释放匹配使用不仅可读性更好,也更安全,保险起见,建议申请释放匹配使用。