如有错漏,还望指摘!
又更新一下:
这几天工作涉及到了重载new、new[]、delete、delete[]操作符的问题,突然发现之前研究的这个还挺有用的,因此重新编辑了一下。
更新一下:
最近回过头来重新看这篇文章,发现这里有一点小问题:
C++标准规定,这些是不可以混用的,会导致未定义行为。而这篇文章所讨论的行为,只是当前的编译器(或者说我使用的编译器)为了实现功能而自己定义的行为。随着版本的更新,有可能会有改动(不过我觉得可能性不大)。因此,这篇文章的内容只是帮助理解编译器如何实现的new[]和delete[],并不作为混用的解决方案。
不建议混用!
不建议混用!
不建议混用!
另外,我当时使用的VS2017做的测试,不确定linux平台下是否有同样的行为,回头我测试一下再看是否需要更新一下。
正文:
在C中,不存在class的概念,分配内存只需要简单的malloc即可。
而到了C++中,出现了class以及class的初始化,因此相应的出现了新的操作符new和delete。
那么新的操作符new、new[]、delete、delete[]究竟和C中的malloc和free有什么区别呢?又是怎样实现的呢?各种教材都明确说明,这些分配和析构必须对应使用,不可混用,这又是为什么呢?
malloc和free
其实malloc和free非常简单,malloc就是向系统申请一块指定大小的内存,free则是向系统归还指定地址开始的一块内存。当然更底层的实现就比较复杂了,在这里先挖个坑,将来有机会写一篇malloc和free的底层原理。
不过可以理解为去向图书馆借书一样,管理员会记录下借给你的内存起始地址。归还时则拿着起始地址归还,管理员找到对应的记录,然后标注为已归还,这样就可以继续借给他人。
new和malloc的比较
C++中的new操作符其实分为两个步骤:
- operator new
- placement new
这其实很好理解,因为我们从功能上来说也可以明白,c++的new相比C中的malloc而言,其实是做了两件不同的事情,分别是:
- 分配内存;
- 调用构造函数。
这其实也就是上面两个步骤分别做的事情。
而其中第一步operator new允许重载,其默认实现,其实就是调用了malloc来实现内存分配的。因此C++中的new和C的malloc的区别其实就是,new在malloc的基础上多了一步调用构造函数。
delete和free的比较
我们知道new和malloc的区别之后,其实应该就比较好理解delete和free的区别:
delete比free多了调用析构函数的过程。
和new一样,delete的第一步operator delete同样可以重载。而相较于new的第二步有一个特殊的placement new不同,delete在第二部直接调用了析构函数(因为析构函数是可以被直接调用的)。
new[]和delete[]
理解了普通的new和delete之后,我们再来看new[]和delete[]有多出了什么样的功能呢?
从功能上来说,new[]是说分配一个连续的数组并调用相应次数的构造函数。从这里来看和new的区别只有调用构造函数的次数不同而已。
但是delete[]的功能是,调用相应次数的析构函数,并释放整体的空间。这里就出现了一个问题,delete[]如何知道需要调用多少次析构函数呢?因此就必须仰赖new[]在构建时,就需要在内存空间中存储一个次数的信息,以便delete[]在析构时,可以正确调用对应次数的析构函数。
一般编译器是这么做的:
new[]在分配时,如果类中显式定义了析构函数,new会在分配的时候,根据系统的位数额外分配对应的空间(32位系统分配32位空间,也就是4字节,64位系统分配64位空间,也就是8字节)。如对于32位系统,new[2]分配的空间应该如下:
在返回的指针之前,还有一个4字节的header,其中存储了分配的个数,如这里就应该是2。
总结
- new相较于malloc多了调用构造函数的步骤,称为placement new;
- delete相较于free多了调用析构函数的步骤;
- new[]相较于new,额外分配了一段空间用来存储数组的长度,而delete[]则根据这个长度来正确地调用对应次数的析构函数;
- 对于构造函数没有作用的类,因为不需要调用构造函数,new和malloc可以混用。
- 对于构造函数有作用的类,如果想混用,需要显式调用构造函数的逻辑实现。
- 对于没有显式定义析构函数的类,delete、delete[]和free可以混用。
- 对于显式定义析构函数的类,delete[]和new[]必须配套使用,delete和free如果想混用,free需要显式调用析构函数。
- 如果不按规则混用了,程序会崩溃。因为显然,比如使用delete来析构new[]申请的空间,那么delete析构时使用的地址,并不是向操作系统申请内存时获取的地址,而是向后挪动过的地址,操作系统将无法找到对应的内存块。其他混用同理。
补充
对于上面说的new,分配内存,即operator new这一步在c++中是允许全局重载和类内重载的,而第二步palcement new则不允许。operator delete同样允许重载。
这在很多时候非常有用,比如使用自定义的内存池时可以将new操作符中的分配内存替换成从内存池中获取空间;又比如分配内存时可以打印自己想要的信息。
这里重载更推荐类内重载,因为全局重载会污染整个项目,在操作不当或者疏忽的情况下可能导致不可预知的后果。
类内重载时,虽然operator new是静态的,但是子类依然可以继承父类的实现,也可以覆盖。
new运算符返回值要求必须是void*。参数只有申请空间的大小:
class DemoClass
{
public:
DemoClass(int id) id_(id) {}
void* operator new(size_t size);
void operator delete(void* ptr);
private:
int id_;
};
void* DemoClass::operator new(size_t size)
{
//使用自定义的allocator进行内存分配
std::cout << "Using my allocator" << std::endl;
return my_allocator::allocate(size);
}
void* DemoClass::operator delete(void* p)
{
// 使用自定义的allocator进行内存释放。
std::cout << "Using my deletor" << std::endl;
return my_allocator::free(p);
}
int main(){
// 此时在构造和析构Demo类时会自动调用重载后的new和delete实现内存的分配与释放。
auto ptr = new Demo(); // Using my allocator
delete ptr; // Using my deletor
}
调用placement new则需要引用new头文件并且调用方式为:
new (地址指针) 类名(参数);
比如:
#include <new>
class DemoClass
{
public:
DemoClass(int id) id_(id) {}
private:
int id_;
};
int main()
{
char* buffer = new char[100];
DemoClass* ptr = reinterpret_cast<DemoClass*>(buffer);
for(int i = 0; i < 5; ++i, ++ptr)
{
new (ptr) DemoClass(i);
}
}
混用代码:
//构造函数和析构函数都没有作用。可以随意混用。
class Test1{
public:
Test1(){}
}
int main(void){
/*
这里用任意方式分配的内存都可以用任意方式释放
*/
//Test1 *ptr = new Test1;
Test1 *ptr = new Test1[3];
//Test1 *ptr = (Test1*)malloc(sizeof(Test1));
//delete ptr;
//delete[] ptr;
free(ptr);
}
//构造函数有作用。
//没有显示定义析构函数,因此delete、delete[]、free可以混用
#include <new>
class Test2{
public:
Test2()
{
init();
}
void init()
{
cout << "constructor" << endl;
}
}
int main(void){
//Test2 *ptr = new Test2;
//Test2 *ptr = new Test2[3];
/*
这里如果想用malloc,必须显示调用构造函数的逻辑。
注:由于构造函数无法被显式调用,因此如果不想调用placement new的话必须另外实现一个inti函数。
*/
Test2 *ptr = (Test2*)malloc(sizeof(Test2));
new (ptr) Test2();
// ptr->init();
//delete ptr;
delete[] ptr;
//free(ptr);
}
//构造函数没有有作用。new、new[]、malloc可以混用
//显示定义析构函数
class Test3{
public:
Test3(){}
~Test3(){
cout << "destructor" << endl;
}
}
int main(void){
//Test3 *ptr = new Test3;
Test3 *ptr = (Test3*)malloc(sizeof(Test3));
//delete ptr;
/*
必须显示调用析构函数
*/
ptr->~Test3();
free(ptr);
/*
只要定义了析构函数,即使析构函数内是空的,delete[]和new[]也必须配套使用,否则会一直调用析构函数(因为将前面的几个字节乱码,当成了数组长度),并在最后释放空间时崩溃。
*/
//Test3 *ptr = new Test3[3];
//delete[] ptr;
}
//对于32位系统,new[]和delete[]的非配套使用。
//显示定义析构函数
#include <new>
class Test3{
public:
Test3(){}
~Test3(){
cout << "destructor" << endl;
}
}
int main(void){
int n = 3;
/*
new[]和free混用
*/
Test3 *ptr1 = new Test3[n];
//模拟delete[]
for(int i = 0; i < n; i++){
(ptr1 + i)->~test3();
}
free(((char*)ptr1) - sizeof(void*));
/*
malloc和delete[]
*/
//模拟new[]
void* buffer = malloc(sizeof(void*) + n * sizeof(Test3));
Test3* ptr2 = (Test3*)((char*)buffer + sizeof(void*));
if(sizeof(void*) == 4)
*((int32_t*)buffer) = n;
else if(sizeof(void*) == 8)
*((int64_t*)buffer) = n;
for(int i = 0; i < n; ++i){
new (ptr2 + i) Test3();
}
delete[] ptr2;
}