C++动态内存:(二)重载new和delete

别等时光非礼了梦想. 提交于 2019-12-13 14:36:57

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

一、重载的原因

    用new创建动态对象时会发生两件事:(1)使用operatoe new()为对象分配内存(经常是调用malloc)(2)调用构造函数来初始化内存。相对应的调用delete运算符会(1)首先调用析构函数(2)调用operator delete()释放内存(经常是调用free)。我们无法控制构造函数和析构函数的调用,是由编译器调用的。但可以改变内存分配函数operator new()和operator delete()。链接:C++中的new/new[],delete/delete[]

    使用了new和delete的内存分配系统是为了通用目的而设计的,但是在特殊的情形下并不能满足需要。最常见的改变分配系统的原因常常是出于效率考虑:

(1)增加分配和归还的速度。创建和销毁一个特定的类的非常多的对象,以至于这个运算成了速度的瓶颈。

(2)堆碎片。分配不同大小的内存会在堆上产生很多碎片,以至于虽然内存可能还有,但由于都是碎片,找不到足够大的内存块来满足需要。通过为特定的类创建自己的内存分配器,可以确保这种情况不会发生。例如在嵌入式和实时系统里,程序可能必须在有限资源情况下运行很长时间,这样的系统就要求内存花费相同的时间且不允许出现堆内存耗尽或者出现很多碎片。

(3)检测运用上的错误。例如:new所得的内存,delete时却失败了,导致内存泄漏;在new所得内存上多次delete,导致不确定行为;数据"overruns”(写入点在分配内存区块尾端之后)或“underruns”(写入点在分配区块之前)。可以超额分配,然后在额外空间放置特定的byte patterns(签名)来进行检测。

(4)统计使用动态内存的信息。

(5)为了降低缺省内存管理器带来的空间额外开销。

(6)为了弥补缺省分配器中的非最佳齐位。

(7)为了将相关对象成簇集中。例如,为了将特定的某个数据结构在仪器使用,并且使用时缺页中断频率降至最低。new和delete的palcement版本有可能完成这样的集簇行为。

(8)获得非传统的行为。例如:分配和归还共享内存。

 二、重载全局的new和delete

 1、重载了全局的new和delete后将使默认的new和delete不能载被访问,甚至在这个重新定义里也不能调用它们。

 2、重载的new必须有一个size_t参数。这个参数由编译器产生并传递给我们,它是要分配的对象的长度。函数返回值为一个大于等于这个对象长度的指针。

 3、operator new()的返回值是一个void*,而不是指向任何特定类型的指针。所做的是分配内存,而不是完成一个对象建立——直到构造函数调用了才完成了对象的创建,它是编译器确保的动作,不在我们的控制范围之内。

 4、operator delete()的参数是一个指向由operator new()分配的内存的void*,而不是指向任何特定类型的指针。参数是一个void*是因为它是在调用析构函数后得到的指针。析构函数从存储单元里移去对象。operator delete()的返回类型是void。

5、功能示例:


#include <stdio.h>
#include<stdlib.h>
void *operator new(size_t sz)
{
    printf("operator new:%d Bytes\n",sz);
    void *m=malloc(sz);
    if(!m)
        puts("out of memory");
    return m;
}
void operator delete(void *m)
{
    puts("operator delete");
    free(m);
}
class S
{
    int i[100];
public:
    S(){puts("S:S()");}
    ~S(){puts("~S::S()");}
};
int main( )
{
    puts("(1)Creating & Destroying an int");
    int *p=new int(47);
    delete p;
    puts("(2)Creating & Destroying an S");
    S *s=new S;
    delete s;
    puts("(3)Creating & Destroying S[3]");
    S *sa=new S[3];
    delete []sa;
    return 0;
}
输出:


 函数说明:

(1)使用printf()和puts()而不是iostream,是因为创建一个iostream对象时(像全局的cin/cout/cerr),它们调用new去分配内存。用printf()不会进入死锁状态,因为它不会调用new来初始化自身。

(2)数组的输出为1204,而不是1200,说明额外的内存被分配用于存放它所包含对象的数量信息。

(3)输出情况说明都使用了由全局重载版本的new和delete。

说明:程序只是示范最简单的使用方法,具体的重载还有很多细节需要考虑,具体参考第四节“注意事项”


6、全局new实现伪码

void* operator new(std::size_t size)throw(std::bad_alloc)
{
    using namespce std;
    if(size==0) 
    {
        size=1;
    };
    while(true)
        尝试分配 size bytes;
    if(分配成功)
        return (一个指针指向分配得来的内存);
    //分配失败:找出new_handling函数
    new_handler globalHandler=set_new_handler(0);
    set_new_handler(globalHandler);
 
    if(globalHandler)(*globalHandler)();
    else
        throw std::bad_alloc();
}
7、全局delete实现伪码
void operator delete(void *mem)throw()
{
    if(mem==0) return;
    归还mem所指内存;
}
三、对类重载new和delete

1、为一个重载new和delete,尽管不必显式地使用static,但实际上仍在创建static成员函数。

2、当编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。但如果要创建这个类的一个对象数组时,全局的operator new()就会被立即调用,用来为这个数组分配内存。当然可以通过为这个类重载运算符的数组版本,即operator new[]和operator delete[]来控制对象数组的内存分配。

3、使用继承时,重载了的类的new和delete不能自动继承使用。

4、功能示例():


#include<iostream>
using namespace std;
class Widget
{
    int i[10];
public:
    Widget(){cout<<"*";}
    ~Widget(){cout<<"~";}
    void* operator new(size_t sz)
    {
        cout<<"Widget::new: "<<sz<<"bytes"<<endl;
        return ::new char[sz];
    }
    void operator delete(void* p)
    {
        cout<<"Widget::delete "<<endl;
        ::delete []p;
    }
    void *operator new[](size_t sz)
    {
        cout<<"Widget::new[]: "<<sz<<"bytes"<<endl;
        return ::new char[sz];
    }
    void operator delete[](void *p)
    {
        cout<<"Widget::delete[] "<<endl;
        ::delete []p;
    }
};
int main( )
{
    cout<<"(1-1) new Widget"<<endl;
    Widget *w=new Widget;
    cout<<"\n(1-2) delete Widget"<<endl;
    delete w;
    cout<<"\n(2-1)new Widget[25]"<<endl;
    Widget *const wa=new Widget[25];
    cout<<"\n(2-2)delete []Widget"<<endl;
    delete []wa;
    return 0;
}
结果:

程序分析:

(1)因为没有重载全局版本的operator new()和operator delete(),因此可以使用cout

(2)语法上除了多一对括号外数组版本的new与delete与单个对象版本的是一样的。不管是哪种情况,我们都要决定要分配的内存的大小。数组版本的大小指的是整个数组的大小。

(3)从结果可以看出:都是调用的重载版本的,new先分配内存再调用构造函数,delete先调用析构函数然后释放内存。

说明:程序只是示范最简单的使用方法,具体的重载还有很多细节需要考虑,具体参考第四节“注意事项”。完整实现的伪码为:


6、member版本operator new和operator delete实现伪码

class Base
{
public:
    static void *operator new(std::size_t size)throw(std::bad_alloc);
    static void operator delete(void *rawMemory,std::size_t size)throw();
    ...
};
void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
    if(size!=sizeof(Base))//sizeof(空类)为1,因此不需要判断size==0的情况
        return ::operator new(size);
    ...
}
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
    if(rawMemory==0)return;
    if(size!=sizeof(Base))
    {
        ::operator delete(rawMemory);
        return;
    }
    归还rawMemory所指的内存;
    return;
}
动态创建时,只需写Base *pBase=new Base;size的大小编译器会计算并传递给new。


四、注意事项

1、当重载operator new()和operator delete()时,我们只是改变了原有内存分配方法(重载operator new()唯一需要做的就是返回一个足够大的内存块的指针)。编译器将用重载的new代替默认版本去分配内存并调用构造函数。

2、在重载的operator news内应该包含一个循环,反复调用某个new_handler函数。链接:C++ new_handler和set_new_handler

3、注意数据位对齐。C++要求所有的operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作的,因此令operator new返回一个得自malloc的指针是安全的(调用malloc得到分配的内存块指针后,不要再对指针进行偏移)。

4、除非必须,否则不要自己重载new和delete。因为可能会漏掉可移植性、齐位、线程安全等细节。必须时,可以借鉴使用一些开放源码的标准库(例如Boost程序库的Pool)。

5、客户要求0 bytes时operator new也得返回一个合法的指针。通常的处理方法是当申请0字节是,将它视为申请1-bytes。即


if(size==0)
{
   size=1;
}
6、重写delete时,要保证“删除null指针永远安全”。

7、对于某个特定类class X设计的operator new和operator delete通常是为大小刚好为sizeof(X)大小的对象而设计的。因此不要在继承类中使用该重载的new和delete。如果基类专属的operator new并非被设计用来对付派生的情况,可以将“内存申请量错误”的调用行为改为标准的operator new。像下例

void *Base::operator new(std::size_t size)throw(std::bad_alloc)
{
    if(size!=sizeof(Base))
        return ::operator new(size);
    ...
}
同样的,若类将 大小有误的分配行为转交::operator new执行,则必须将大小有误的删除行为转交::operator delete执行。如果要删除的对象派生自某个base class而后者欠缺virtual析构函数,C++传给operator delete的size_t数值可能不正确。
void Base::operator delete(void *rawMemory, std::size_t size)throw()
{
    if(rawMemory==0)return;
    if(size!=sizeof(Base))
    {
        ::operator delete(rawMemory);
        return;
    }
    归还rawMemory所指的内存;
    return;
}
五、placement new和placement delete

     以上所示为普通的,只带size_t参数的重载,但是如果 想要在指定内存位置上放置对象;或者想要在new时志记信息则需要带额外参数。

1、示例:

#include<iostream>
using namespace std;
class X
{
    
public:
    X(int ii=0):i(ii)
    {
        cout<<"this="<<this<<endl;
    }
    ~X()
    {
        cout<<"X::~X():"<<this<<endl;
    }
    void *operator new(size_t,void *loc)
    {
        return loc;
    }
    int i;
 
};
int main( )
{    
    int arr[10];
    cout<<"arr="<<arr<<endl;
    X *xp=new(arr)X(47);//X at location arr
    cout<<xp->i<<endl;
    xp->~X();//显示调用析构函数
    return 0;
}
输出:


函数说明:

(1)调用时,关键字new后时参数表(没有size_t参数,它由编译器处理),参数表后面是正在创建的对象的类名。

(2)operator new()仅返回了传递给它的指针。因此调用者可以决定将对象放置在哪里,这是在该指针所指的那块内存上,作为new表达式一部分的构造函数被调用。

(3)因为不是在堆上分配的内存,因此不能用动态内存机制释放内存。可以显示的调用析构函数(在其它情况下不要显示的调用析构函数)。

2、对于new,如果第一个函数(operator new)调用成功,第二个函数(构造函数)却抛出异常,则系统会调用第一个函数对应的operator delete版本,若没有则系统什么都不会做,会发生内存泄露。因此若重载一个带额外参数的operator new,那么也要定义一个带相同额外参数的operator delete。如果希望这些函数有着平常行为,只要令专属版本调用global版本即可。
     但是,如果没有在构造函数里抛出异常,则placement delete不会被调用。delete一个指针,调用的是正常形式(没有额外参数)的delete。


class Widget
{
public:
    ...
    static void* operator new(std::size_t size,std::ostream &logStream)
        throw(std::bad_alloc);
    static void operator delete(void*pMem,std::ostream&logStream) throw();
    static void operator delete(void* pMem)throw();
    ...
    
};
Widget *pw=new(std::cerr)Widget;
delete pw;
3、对于上述示例,需要注意避免让类专属的news掩盖其它的news。例如,对于上式:Widget *pw=new Widget;将被掩盖。同样道理,派生类中的operator news会掩盖global版本和继承而得的operator new版本。可以定义一个基类,包含所有正常形式的new和delete。然后自己定义的类继承这个基类,并且在类中中using声明取得标准形式。

参考资料:

1、《C++编程思想》

2、《Effective C++》


--------------------- 
作者:Z-H-I 
来源:CSDN 
原文:https://blog.csdn.net/zxx910509/article/details/64905107 
版权声明:本文为博主原创文章,转载请附上博文链接!

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!