你真的明白“C++中的new”吗?
内存分配 |
内存释放 |
类型 |
是否可以重载 |
::new |
::delete |
表达式 |
否 |
new |
delete |
表达式 |
否 |
::new[] |
::delete[] |
表达式 |
否 |
new[] |
delete[] |
表达式 |
否 |
:: operator new |
::operator delete |
函数 |
是 |
:: operator new[] |
::operator new[] |
函数 |
是 |
operator new |
operator delete |
函数 |
是 |
operator new[] |
operator new[] |
函数 |
是 |
placement new |
placement delete |
函数 |
是 |
注意:
① ::operator new代指的是全局new操作符,即全局new函数,而operator new代指的是“我们一般在类类型中重载的new操作符,即属于类类型的new函数”;
② 其实placement new是全局new操作符的一种,可以在以构建的内存空间上初始化数据对象,也就是在我们已经构建的内存空间上放置数据;
③ new操作符,即new函数仅仅是用来申请内存空间,不进行其他任何操作,而且针对对象是void*数据类型的指针;
④ 我们知道了new函数的功能,new表达式的功能,也知道了placement new的功能,我们就可以很容易的得知operator new + placement new = new expression;
⑤ 总体来说,“new函数”可以概括为以下几类:
operator new是函数,分为三种形式(前2种不调用构造函数,这点区别于new operator):
⑴ void* operator new (std::size_t size) throw (std::bad_alloc);
⑵ void* operator new (std::size_t size, const std::nothrow_t& nothrow_constant) throw();
⑶ void* operator new (std::size_t size, void* ptr) throw();
第一种分配size个字节的存储空间,并将对象类型进行内存对齐。如果成功,返回一个非空的指针指向首地址。失败抛出bad_alloc异常。
第二种在分配失败时不抛出异常,它返回一个NULL指针。
第三种是placement new版本,它本质上是对operator new的重载,定义于#include <new>中。它不分配内存,调用合适的构造函数在ptr所指的地方构造一个对象,之后返回实参指针ptr。
这三种new 函数都可以进行重载,具体重载形式请详见下文。
不同类型的new和delete表达式搭配会引发异常
我们有时候说new和delete是一对,array new和array delete是一对,如果我们错误的使用会造成错误的发生,比如:
① new和delete[]搭配使用:
#include <iostream>
using namespace std;
class Base
{
private:
int obj;
public:
Base()
{
cout << "调用默认构造函数" << endl;
}
~Base()
{
cout << "调用析构函数" << endl;
}
};
int main()
{
Base* ptr = new Base; // new expression
delete[] ptr; // array delete expression
}
输出结果:
② new[]和delete搭配使用
#include <iostream>
using namespace std;
class Base
{
private:
int obj;
public:
Base()
{
cout << "调用默认构造函数" << endl;
}
~Base()
{
cout << "调用析构函数" << endl;
}
};
int main()
{
Base* ptr = new Base[2]; // array new expression
delete ptr; // delete expression
}
输出结果:
什么情况下不适当的使用delete会造成内存泄漏?
首先,内存泄漏是指的是“我无法通过有效途径回收已经使用的内存空间”。
① 在类类型的析构函数没有意义时,delete与否都不会造成内存泄漏:
#include <iostream>
using namespace std;
class Base
{
private:
int obj;
public:
Base()
{
cout << "调用默认构造函数" << endl;
}
~Base()
{
cout << "调用析构函数" << endl;
}
};
int main()
{
Base* ptr = new Base[2];
}
此时,析构函数没有意义,因为析构函数的实现中没有包含堆区内存的回收,当程序执行完main函数时,系统会将整个项目在Stack栈区内的内存全部释放掉,因此delete与否不会产生“main函数执行已完毕但是内存还遗留在堆区” 的现象。
② 当析构函数有意义时,不delete类对象会造成内存泄漏
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
Base* ptr = new Base[2];
delete[] ptr;
}
如果不调用delete[]去释放掉ptr指向的堆区内申请的内存空间,那么操作系统不会替你完成,操作系统只会在main函数执行完毕后释放掉项目文件所占用的Stack栈区内存,当操作系统释放了栈区的内存,我们就无法在寻找到我们在堆区申请内存所对应的地址。
注意:
造成内存泄露的原因是,在操作系统释放完栈区内存后,ptr这个main函数内的局部变量也会被释放掉,我们知道ptr相当于寻找到堆区内村的“GPS导航”,当我们丢失了这个GPS导航,我们再也无法找到回收堆区内存的途径,因此造成了内存泄漏。
③ 异常触发之前为调用delete释放内存也会造成内存泄漏:
为什么这样说呢?我前面有一个“异常处理的文章”里面详细解释了触发异常时的处理流程,但这在这里一点也不重要,我们只要清楚:触发异常时,系统会自动回收remodel函数已经执行部分所占的栈区内存,但是在这个函数内部申请的“堆区内存”却不会被回收,因此此时会导致内存泄漏。
当编译器释放了ps所在的栈区内存之后,我们就再也无法通过一个确定的地址去找到堆区动态申请的内存空间,找不到这片内存空间更别提释放它了,就相当于“你想回家吃饭但是你连家门都找不到还谈什么吃饭的事宜”。如下图示就是我想说明的情况:
为了防止这种情况的发生,我们可以使用智能指针。在《C++ Primer》中,智能指针的出现是有上述这个例子引出,为何要使用智能指针:
有了智能指针之后,自动回收内存将变为现实:
普通全局函数operator new和operator delete的底层实现
我们看到,在普通全局函数::operator new的实现源代码中:
① 首先,使用底层函数malloc去不断地申请size个字节的内存;
② 如果申请不到则返回的指针值=NULL(NULL=0),此时在while 函数中不断轮询等待,其中不断调用_callnewh(size_t)函数,这个函数是用来调用new函数句柄,这个函数句柄的触发时机是“电脑中已无内存可用或内存所剩无几根本不够size个字节”,C++很聪明,new函数句柄是在给我们一个机会——“当内存所剩无几/剩余内存已寥寥无几时,我们应该采取哪些操作来挽回这些局面,这是在给我们一个挽救的机会”;
③ 如果我们不充分利用C++提供的这个机会,即我们没有定义new函数句柄,那么低通会调用_CATCH(const exception& exp)宏去抛出异常bad_alloc。
含有new函数句柄的程序示例:
#include <iostream>
using namespace std;
void InsuffientMemory()
{
cout << "内存已不足,等待处理......" << endl;
}
int main()
{
new_handler InsuffientMemory_Handler = set_new_handler(InsuffientMemory);
void* ptr = ::operator new(1000000000); // 内存不足会调用new函数句柄
}
new expression与operator new的关系
new expression执行的流程包括三部分:
① 给void*类型指针分配内存;
② 将void*指针强制类型转换为指定类型的指针;
③ 调用指定类型的默认构造函数;
按照我们上述的流程一个完整的new expression实现代码如下:
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
void* temp = ::operator new(sizeof(Base));
Base* ptr = static_cast<Base*>(temp);
ptr->~Base();
}
但是事与愿违,出现的结果并不是我们预期的:
出现上述异常的原因就是“我们违规调用了默认构造函数”!默认构造函数我们是不可以调用的,因此我们想到了“placement new”,这个new操作符就比较牛逼了,我们虽然不可以直接调用默认构造函数进行初始化,但是我们可以利用placement new的 “在已有内存空间上放置数据” 的特性来实现“内存区域的申请和内存区域上数据的初始化”两个操作分别进行:
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
void* temp = ::operator new(sizeof(Base));
Base* ptr = static_cast<Base*>(temp);
new(ptr)Base();
}
这样的话就完全OK了,因此我在一开始就说:
在功能上,new expression = operator new + placement new。
placement new函数到底有何魅力?
operator new申请到的是“纯内存”,没有被任何构造函数初始化过,这些内存可以使得我们在上面建造各种类型的数据,不仅可以建造基本数据类型的数据,也可以建造类类型的对象,但是“我们必须使用placement new函数去操作”。基于“内存申请和内存空间的使用相互分离”,我们构建了“内存池”的概念,这就是为什么经常有人将“placement new”和“operator new”比作“构建内存池的两大件”。
placement new函数的功能举例:
如果有这样一个场景,我们需要大量的申请一块类似的内存空间,然后又释放掉,比如在在一个server中对于客户端的请求,每个客户端的每一次上行数据我们都需要为此申请一块内存,当我们处理完请求给客户端下行回复时释放掉该内存,表面上看者符合c++的内存管理要求,没有什么错误,但是仔细想想很不合理,为什么我们每个请求都要重新申请一块内存呢,要知道每一次内从的申请,系统都要在内存中找到一块合适大小的连续的内存空间,这个过程是很慢的(相对而言),极端情况下,如果当前系统中有大量的内存碎片,并且我们申请的空间很大,甚至有可能失败。为什么我们不能共用一块我们事先准备好的内存呢?可以的,我们可以使用placement new来构造对象,那么就会在我们指定的内存空间中构造对象。
delete expression和operator delete的关系
delete expression执行的流程:
① 利用指针调用类类型的析构函数;(基本数据类型无析构函数,因此不用调用)
② 释放掉该块内存空间。
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
void* temp = operator new(sizeof(Base));
Base* ptr = static_cast<Base*>(temp);
new(ptr)Base();
ptr->~Base();
operator delete(ptr, sizeof(Base));
}
我们看到:这里我是用是operator new,其实在我们没有重载operator new之前,operator new就是::operator new,但是重载后就不一样了:
operator delete与operator new原理一致,如下是重载“operator new/delete函数”的代码:
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
static void* operator new(size_t Size)
{
cout << "进行更加人性化的操作......" << endl;
return malloc(Size);
}
static void* operator new(size_t Size, void* ptr)
{
Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数
return temp;
}
static void operator delete(void* ptr, size_t Size)
{
cout << "调用自定义delete函数" << endl;
free(ptr);
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数
Base* ptr = static_cast<Base*>(temp);
new(ptr)Base();
ptr->~Base();
operator delete(ptr, sizeof(Base));
}
运行结果如下:
但是,我们注意到:编译器并不会调用我们自定义的delete函数,在以下两种方式中,placement delete function才会被调用:
① 我们重载的delete函数调用时机在我们显式的给delete指定作用域名时才会被调用:
#include <iostream>
using namespace std;
class Base
{
private:
char* ptr;
public:
Base()
{
ptr = new char[2]; // 在堆区内开辟一块空间
cout << "调用默认构造函数" << endl;
}
static void* operator new(size_t Size)
{
cout << "进行更加人性化的操作......" << endl;
return malloc(Size);
}
static void* operator new(size_t Size, void* ptr)
{
cout << "调用自定义placement new函数" << endl;
Base* temp = static_cast<Base*>(ptr); // 强制类型转换时已调用默认构造函数
return temp;
}
static void operator delete(void* ptr, size_t Size)
{
cout << "调用自定义delete函数" << endl;
free(ptr);
}
static void operator delete(void* ptr)
{
cout << "调用自定义delete函数" << endl;
free(ptr);
}
~Base()
{
if (ptr != nullptr)
{
delete[] ptr;
}
ptr = nullptr;
cout << "调用析构函数" << endl;
}
};
int main()
{
void* temp = Base::operator new(sizeof(Base)); // 切记要使用Base::去访问Base的静态类型成员函数
Base* ptr = static_cast<Base*>(temp);
new(ptr)Base();
Base::operator delete(ptr, sizeof(Base)); // 显式的指出Base::作用域名
}
运行结果:
注意:当我们重载operator new/operator delete函数时,(仅仅针对于重载局部operator new/operator delete函数)在类类型内部,被重载的operator new/operator delete函数默认被编译器当作static属性的成员函数,即operator new/operator delete作为类类型的静态成员变量而存在,因此我们使用重载版本时,需要使用class_name::来显示调用。
② 在placement new function出现异常时,即在分配内存时出现异常时,对应形式的placement delete function会被调用:
说有没有placement delete?placement delete expression是绝对没有的东西。但是有一个placement delete function,而不是placement delete expression。而这个placement delete function是你直接调用不到的东西。当placement new expression调用placement new function,如果构造函数函数构造的时候发生了异常,这个时候要防止内存泄露,那么要清理掉已分配的内存,就需要这个placement delete function。而这个东西的定义你可以打开<new>头文件看到。
但是,暴露出来的delete expression却只有两个出来:
也就是用于非数组与数组的delete。
接下来举一个例子来说明一下这个构造函数发生异常的时候,调用placement delete function的情况:
#include <cstdlib>
#include <iostream>
struct A {};
struct E {};
class T
{
public:
T() { throw E(); }
};
void * operator new (std::size_t, const A &)
{
void* nothing = 0;
std::cout << "Placement new called." << std::endl;
return nothing;
}
void operator delete (void *, const A &)
{
std::cout << "Placement delete called." << std::endl;
}
int main()
{
A a;
try {
T * p = new (a) T;
}
catch (E exp) { std::cout << "Exception caught." << std::endl; }
return 0;
}
运行结果:
以上是我copy的知乎一位朋友的讲解,但是我是用这个例子是真没调用出来这个结果,我在VS2017中运行结果如下:
我尝试了很多次,事实证明:在异常发生时。VS2017下的C++编译器在placement new function触发异常时不会调用自定义的placement delete function去释放内存,仅仅会调用全局的placement delete function去收拾这个”半成品”,然后会抛出异常触发异常机制。
但是为了保险起见,我们还是给每个operator new定义对应的operator delete,以便在内存分配时(构造对象时)触发异常后,调用相应的delete进行内存回收。我觉得调用结果因编译器不同而异吧。
使用new和delete构建的内存分配的途径
注意:
① 由于operator new/operator delete的底层是使用CRT(C Runtime library)中的malloc/free来实现的,因此operator new/operator delete是malloc/free的“进化版本”,而且malloc/free相较于operator new/operator delete来讲,malloc/free显著的区别在于:“malloc/free不可以重载”!
② 由于我们使用new expression和delete expression时,无论是否重载operator new/operator delete大概率都会调用到::operator new/::operator delete,因此重载“全局函数::operator new/::operator delete”不是一个最明智的选项!
③ 其实我们一般重载operator new/operator delete会使用“全局函数::operator new/delete”作为底层实现函数也可能会直接使用malloc/free来直接作为函数底层的实现:
④ 重载operator new/delete函数的本质是将“内存申请的控制权掌握到自己手中”以便后续内存处理时可以进行更加人性化的操作。