new和delete都是C++的关键字,不可重载。其底层的行为可以看作多个函数的组合。
一、自己实现new与delete的功能
#include <iostream>
using namespace std;
class Student{
private:
int age{24};
public:
Student(){
cout<<"start"<<endl;
}
~Student(){
cout<<"end"<<endl;
}
void f(){
cout<<"age = "<<age<<endl;
}
};
int main(void) {
Student * p = (Student *)operator new(sizeof(Student)); //自己实现new
new(p) Student;
p->f();
p->~Student(); //自己实现delete
operator delete(p);
return 0;
}
第一行:
Student * p = (Student *)operator new(sizeof(Student));
operator new是C++自带的函数,可以重载。准确调用方法是:
::operator new(sizeof(Student));
::表示全局命名空间,注意不是std::标准命名空间!
底层调用的是malloc函数,实际上返回的是void * 指针。参数表示要申请的字节数。
第二行:
new§ Student;
表示在给定的地址(堆上地址)执行构造函数。
对应delete的操作:
p->~Student();表示在某个地址上执行析构函数。
operator delete§;
调用的是C++自带的函数,同样可以重载。底层调用的是free()函数。
二、operator new和operator delete重载
operator new和operator delete是全局空间里面的两个函数,前者用于申请空间,后者用于释放空间。操作符new的行为可以拆解为三步,第一步就是调用operator new()函数申请内存空间。对于operator new()函数,我们可以重载自己的版本。
operator new()函数重载规则如下:
(1)可以重载全局版本,但是非常不推荐,会影响所有类的new操作。可以重载某个类的版本。重载的时候将operator new定义为成员函数即可。operator new函数默认是静态函数,即使你不加static,编译器也会将其置为static函数。因为operator new的作用是构造对象,调用它时候一般而言还没有将对象构造出来,如果是非static函数就不可用了。
(2)operator new的标准格式为void* operator new(size_t size);
重载的时候也可以定义多个参数,但是第一个参数必须是size_t size。
(3)当用户对这个类进行new操作的时候,会自动调用operator new函数。划重点:
一般new是不带参数的,但是如果重载的operator new版本有多个参数,那么new中需要传入除了size_t size外的其它参数。例如:
void* operator new(size_t size, int test);
那么new的时候:new(123) ClassName;123就是传给形参test的值。
(4)系统自带两个重载的operator new函数,如果我们在类中重载了自定义版本,那么这两个都会被禁用!!!我们new的时候只会调用我们自定义的operator new函数。
看代码:
class Foo {
public:
void* operator new(std::size_t size)
{
cout<<size<<endl;
std::cout << "operator new" << std::endl;
return std::malloc(size);
}
void* operator new[](std::size_t size)
{
cout<<size<<endl;
std::cout << "operator new []" << std::endl;
return std::malloc(size);
}
void operator delete (void * ptr){
cout<<"operator delete"<<endl;
free(ptr);
}
void operator delete[] (void * ptr){
cout<<"operator delete []"<<endl;
free(ptr);
}
long long a=1;
};
int main()
{
Foo *px = new Foo;
delete px;
Foo * ps = new Foo[3];
delete []ps;
}
运行结果:
8
operator new
operator delete
24
operator new []
operator delete []
我们同样可以重载operator delete 、operator new[] 、operator delete []
三、operator new的多版本重载
operator new可以重载多个版本,但是注意,所有版本的第一个参数都必须是无符号整数!我们通过new关键字进行使用的时候,不能传入第一个参数,从第二个参数开始传入。看代码:
class Foo {
public:
void* operator new(std::size_t size)
{
cout<<size<<endl;
std::cout << "operator new" << std::endl;
return std::malloc(size);
}
void* operator new(std::size_t size, int test)
{
cout<<"size = "<<size<<" test = "<<test<<endl;
std::cout << "operator new" << std::endl;
return std::malloc(size);
}
void* operator new(std::size_t size, string test)
{
cout<<"size = "<<size<<" test = "<<test<<endl;
std::cout << "operator new" << std::endl;
return std::malloc(size);
}
long long a=1;
};
int main()
{
Foo *p1 = new(123) Foo; //调用的是void* operator new(std::size_t size, int test)
delete p1;
Foo *p2 = new("name") Foo; //调用的是void* operator new(std::size_t size, string test)
delete p2;
}
运行结果:
size = 8 test = 123
operator new
size = 8 test = name
operator new
这个特性非常非常有用,可以很好地解释Placement new.
四、Placement new
Placement new,顾名思义,就是在指定位置new一个对象出来。placement new 是重载operator new 的一个标准、全局的版本。
请记住一句话:Placement new就是operator new函数!!!
函数定义如下:
_GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT
{ return __p; }
第一个参数size_t完全被忽略了,而且完全没有malloc的过程,上层传入了一个指针,原封不动地返回这个指针。后面的行为是这样的:编译器根据operator new返回的指针,在这个地址上执行构造函数,从而达到了定点new的目的。这个指针未必指向堆,也可以指向栈!!!
Placement new 的优势
(1)用placement new 避免频繁申请内存,一块内存可以多次利用。
(2)可以提高构建对象的效率。如果堆中空间不足,可以将对象构建在我们事先准备的缓冲区中。
(3)配合共享内存使用,可以在进程间传递对象!!!这个简直是IPC的大神器!!!
五、Placement new的使用
看代码:
class Foo {
public:
Foo(){
cout<<"start!!!"<<endl;
}
~Foo(){
cout<<"end!!!"<<endl;
}
long long a=123;
};
int main()
{
char * buff = new char [sizeof(Foo)]; //在堆上定点new
Foo *p1 = new(buff) Foo;
cout<<"a = "<<p1->a<<endl;
//delete p1; 有风险,这一块内存实际上不被这个对象独占
p1->~Foo();
delete [] buff;
cout<<endl;
char buf[sizeof(Foo)]; //在栈上定点new
Foo *p2 = new(buf) Foo;
cout<<"a = "<<p2->a<<endl;
p2->~Foo();
return 0;
}
使用步骤如下:
(1)申请内存,这个内存可能是在堆上也可能是在栈上。
(2)在给定的地址上定点new。
(3)使用这个对象。
(4)执行析构函数。
注意点如下:
(1)使用Placement new的时候,最好不要自己重载operator new函数,因为一旦有了自己重载的版本,标准库的版本就会被禁用。当然如果自己重载一个和标准库中的inline void* operator new(std::size_t, void* __p);函数一模一样的,也是可以的。
(2)用户不被允许自己调用构造函数,但是被允许自己调用析构函数。
(3)定点构造的对象使用完以后,不要直接delete,因为这块内存可能会被用来构造其他对象,只能手动调用析构函数。