18.1 C++内存高级话题-new、delete的进一步认识
18.2 C++内存高级话题-new内存分配细节探秘与重载类内operator new、delete
18.3 C++内存高级话题-内存池概念、代码实现和详细分析
18.4 C++内存高级话题-嵌入式指针概念及范例、内存池改进版
18.5 C++内存高级话题-重载全局new/delete、定位new及重载
文章目录
5.重载全局new/delete、定位new及重载
5.1 重载全局operator new和operator delete操作符
之前学习了类中的operator new、operator delete以及operator new[]、operator delete[]的重载。忘记的读者请进行适当的复习。
其实,也可以重载全局的operator new、operator delete以及operator new[]、operator delete[],当然,在重载这些全局函数的时候,一定要放在全局空间里,不要放在自定义的命名空间里,否则编译器会报语法错。
在文件的前面位置,增加如下代码:
void* operator new(size_t size) //重载全局operator new
{
return malloc(size);
}
void* operator new[](size_t size)//重载全局operator new[]
{
return malloc(size);
}
void operator delete(void* phead)//重载全局operator delete
{
free(phead);
}
void operator delete[](void* phead)//重载全局operator delete[]
{
free(phead);
}
class A
{
public:
A() //构造函数
{
cout << "A::A()" << endl;
}
~A() //析构函数
{
cout << "A::~A()" << endl;
}
}
int main()
{
int* pint = new int(12); //调用重载的operator new
delete pint; //调用重载的operator delete
char* parr = new char[10]; //调用重载的operator new[]
delete[] parr; //调用重载的operator delete[]
A* p = new A(); //调用重载的operator new ,之后也执行了类A的构造函数
delete p; //执行了类A的析构函数,之后也调用了重载的operator delete
A* pa = new A[3](); //调用一次重载的operator new[],之后执行了三次类A的构造函数
delete[] pa; //执行了三次类A的析构函数,之后也调用了重载的operator delete[]
}
虽然可以重载全局的operator new、operator delete、operator new[]、operator delete[],但很少有人这样做,因为这种重载影响面太广。读者知道有这样一回事就行了。一般都是重载某个类中的operator new、operator delete,这样影响面比较小(只限制在某个类内),也更实用。
当然,如果类A中又重载了operator new、operator delete、operator new[]、operator delete[],那么类中的重载会覆盖掉全局的重载。在类A中重载这几个操作符(成员函数),完整的类A代码如下:
class A
{
public:
A() //构造函数
{
cout << "A::A()" << endl;
}
~A() //析构函数
{
cout << "A::~A()" << endl;
}
void* operator new(size_t size)
{
A* ppoint = (A*)malloc(size);
return ppoint;
}
void operator delete(void* phead)
{
free(phead);
}
void* operator new[](size_t size)
{
A* ppoint = (A*)malloc(size);
return ppoint;
}
void operator delete[](void* phead)
{
free(phead);
}
};
结果一目了然:类中的重载会覆盖掉全局的重载。
5.2 定位new(placementnew)
前面学习的new操作符都是传统的new,调用关系如下,这个前面已经讲解过了:
A * pa = new A();
operator new();
malloc();
A::A();
除了传统new之外,还有一种new叫作“定位new”,翻译成英文就是placement new,因为它的用法比较独特,所以并没有对应的placementdelete的说法。
那么,定位new和传统new有什么区别呢?当然有区别。在讲解区别之前,首先要注意定位new的层次关系。定位new也和传统new处于同一个层次,但定位new的功能却是:在已经分配的原始内存中初始化一个对象。请注意这句话的两个重要描述点:
· 已经分配:意味着定位new并不分配内存,也就是使用定位new之前内存必须先分配好。
· 初始化一个对象,也就是初始化这个对象的内存,可以理解成其实就是调用对象的构造函数。
总而言之,定位new就是能够在一个预先分配好的内存地址中构造一个对象。
存在一种可能性,在以后做的项目中,可能因为某些特殊的需要,读者可能会突然觉得定位new似乎比传统new灵活,更方便,这时就是定位new出场的时候。定位new的格式如下:
new (地址) 类类型(参数)
这里直接通过一个范例来演示定位new。为了防止与重载的operator new、operator delete产生冲突等,把上面所写的代码全部注释掉。
创建一个叫作PLA的类:
class PLA
{
public:
int m_a;
PLA() :m_a(0) //构造函数
{
cout << "PLA::PLA()构造函数执行" << endl;
}
PLA(int tempvalue) :m_a(tempvalue) //构造函数
{
cout << "PLA::PLA(int tempvalue)构造函数执行" << endl;
}
~PLA() //析构函数
{
cout << "PLA::~PLA()析构函数执行" << endl;
}
//定位new操作符的重载,注意参数是比传统new多一个参数的
void* operator new(size_t size, void* phead)
{
//这里增加一些自己的额外代码,用于统计之类的,但不要分配内存
return phead;//收到内存开始地址也只返回内存开始地址即可
}
};
int main()
{
void* mymemPoint = (void*)new char[sizeof(PLA)]; //内存必须事先分配出来,为了内存分配通用性,这里返回void *类型
//开始用这个返回的void*指针
PLA* pmyAobj1 = new(mymemPoint) PLA(); //定位new:调用无参构造函数,这里并不额外分配内存
void* mymemPoint2 = (void*)new char[sizeof(PLA)];
PLA* pmyAobj2 = new(mymemPoint2) PLA(12); //定位new:调用带一个参数的构造函数,这里并不额外分配内存
//释放
pmyAobj1->~PLA(); //根据需要,有析构函数就可以调用析构函数
pmyAobj2->~PLA();
delete[](void*)pmyAobj1; //分配时用char[],释放时用delete[],本行等价于delete[](void*)mymemPoint;
delete[](void*)pmyAobj2;//本行等价于delete[](void*)mymemPoint2;
}
可以看到,一般来说,写程序的时候,构造函数都是不会被直接调用的(直接调用编译器会报错),而上面这种定位new的写法就等同于可以直接调用构造函数。
而析构函数是能够直接调用的(上面的代码就直接调用了析构函数)。
上面这些结论,希望读者有相关的认识。
可以把断点设置在定位new这行代码上并跟踪调试,当程序执行流程停到断点行时切换到反汇编窗口观察定位new都调用了哪些代码,看到的内容如图所示。
从图中可以看到定位new的调用关系如下表示:
PLA * pa = new (分配好的内存的首地址) PLA();// 定位new操作符
operator new(); //函数 这里没有调用malloc
PLA::PLA(); //调用构造函数
图的operator new可以追踪进去,发现它并不像传统的new是要调用malloc来分配内存的,而这个operator new中看起来并没有分配内存。
前面学习了针对一个传统的new操作符,可以在一个类中重载它所调用的operator new和operator delete函数,并在其中来分配和释放内存。
其实,定位new所调用的operator new操作符也能重载,但是定位new没有对应的operator delete操作符。
定位new所调用的operator new操作符的重载代码如下。在类PLA中,增加用public修饰的如下operator new成员函数,注意其形参:
定位new操作符的重载,注意参数是比传统new多一个参数的
void* operator new(size_t size, void* phead)
{
//这里增加一些自己的额外代码,用于统计之类的,但不要分配内存
return phead;//收到内存开始地址也只返回内存开始地址即可
}
读者可以通过设置断点确认上面这段代码在执行定位new代码行时能够被调用。
5.3 多种版本的operator new重载
其实可以重载很多版本的operator new,只要每个版本参数不同就可以。第一个参数固定,类型都是size_t(类似于无符号整型),表示这个对象的sizeof值,其他参数通过调用new时指定进去即可。
在main主函数中,代码如下:
PLA* pla = new(1234, 56) PLA(); //这其实并没有实际分配内存,也没有调用类的构造函数
编译一下,出现警告:“void *PLA::operator new(size_t,int,int)”表示未找到匹配的删除运算符。如果初始化引发异常,则不会释放内存。
这个警告可以不理会,也可以在PLA类中增加对应的operator delete重载以避免这个警告(但这并不是必需的):
void* operator new(size_t size, int tvp1, int tvp2)
{
return NULL;
}
void operator delete(void* phead, int tvp1, int tvp2)
{
return;
}
可以设置断点并进行跟踪调试,上面重载的operator new的第二个参数和第三个参数分别传递进去了1234和56,而第一个参数,系统默认传递进去的是sizoef(PLA)的值。
另外注意,这种new的用法并不会去调用类PLA的构造函数。所以,这个重载的operator new里面要做什么事,完全由程序员来控制。