• C++内存管理:其三、new和delete的行为拆分


    new和delete都是C++的关键字,不可重载。其底层的行为可以看作多个函数的组合。

    一、自己实现new与delete的功能

    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    第一行:
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    运行结果:

    8
    operator new
    operator delete
    24
    operator new []
    operator delete []
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们同样可以重载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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    运行结果:

    size = 8 test =  123
    operator new
    size = 8 test =  name
    operator new
    
    • 1
    • 2
    • 3
    • 4

    这个特性非常非常有用,可以很好地解释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; }
    
    • 1
    • 2

    第一个参数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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    使用步骤如下:
    (1)申请内存,这个内存可能是在堆上也可能是在栈上。
    (2)在给定的地址上定点new。
    (3)使用这个对象。
    (4)执行析构函数。

    注意点如下:
    (1)使用Placement new的时候,最好不要自己重载operator new函数,因为一旦有了自己重载的版本,标准库的版本就会被禁用。当然如果自己重载一个和标准库中的inline void* operator new(std::size_t, void* __p);函数一模一样的,也是可以的。
    (2)用户不被允许自己调用构造函数,但是被允许自己调用析构函数。
    (3)定点构造的对象使用完以后,不要直接delete,因为这块内存可能会被用来构造其他对象,只能手动调用析构函数。

  • 相关阅读:
    【深度学习】YOLOv5替换自有VOC数据集
    SpringBoot中使用PageHelper插件实现Mybatis分页
    Spring拓展知识:后置处理器与事件监听器
    计算机专业毕设课设选题攻略
    算法之数组篇
    EasySwipeMenuLayout - 独立的侧滑删除
    SpringBoot热部署
    JAVA安装教程 (windows)
    Java 设计模式——抽象工厂模式
    测开 - 自动化测试 selenium - 自动化概念 && 测试环境配置 - 细节狂魔
  • 原文地址:https://blog.csdn.net/jiexianxiaogege/article/details/133685228