一文讲透 C++ New 表达式

温故而知新

一,引子

C++ 里调用C的函数,这个C函数返回的malloc内存。如何避免内存泄漏? 仅靠手搓free,肯定不靠谱。万一在free前return那会造成泄漏...
如何管理malloc的内存,防止内存泄漏,是个值得思考的问题。
方法应该有几种,比如function object,智能指针... 选择了std::unique_ptr 来管理malloc的内存对象。

template<class T, class Deleter = [std::default_delete]<T>>
class unique_ptr;
template <class T, class Deleter>
class unique_ptr<T[], Deleter>;

//通常情况可以创建如下unique_ptr对象:
std::unique_ptr<int[]> uq(new int[8]);

问题先留一会,结尾再回答这个问题。这里就引出了new 关键词。

二,new 表达式

2.1 创建对象

new 是用来创建并初始化动态时长的对象,生命周期不跟随所在函数的生命周期。new 是基于堆上创建,堆上对象并不跟随函数本身结束而释放,所以要注意销毁,以防内存泄露.
new 创建对象的方法:

new (type) new-initializer(optional)
new type new-initializer(optional)
new (placement-args)(type)new-initializer(optional)
new (placement-args) type new-initializer(optional)

Type :类型除了常规的基本数据类型,还可以是数组,可以是类型推导占位符(auto decltype),可以是模板名。 比如模板类型:

template <class T>
class MyClass {T m;public: MyClass(T p):m(p){};};
MyClass<int>* ptr = new MyClass(10);

后面两种是比较少见的placement new。第三节再举例。
initializer :new 初始化,对象的初始化可以用小括号()或{}大括号。 小括号初始化的劣势,对象的初始化和函数申明容易混乱。比如:
TestClass obj();
可能被解析成函数申明。 如果类存在std::initializer_list 类型的构造函数。大括号{}形式的构造对象,将优先使用初始化列表的构造函数。

class TestClass {
    public:
    TestClass(int a, int b) {
        std::cout << "Regular constructor called with a = " << a << ", b = " << b << std::endl;
    }
    TestClass(std::initializer_list<int> initList) {
        std::cout << "Initializer list constructor called with elements: ";
        for (auto elem : initList) { std::cout << elem << " "; }
        std::cout << std::endl; 
    }
};

TestClass obj{10, 20}; // 调用初始化列表构造函数,因为{10, 20}是一个初始化列表

2.2 分配内存

new 表达式会使用适当的内存分配函数来创建内存, 这种函数学名叫operator new,如果创建的是数组则称之为:operator new[]. new 表达式会把对应size传递给operator new,一般是sizeof(T). 对于这节来说,需要注意的只有一点:局部和c++全局的operator new. 两者在使用方式上注意要点如下:
1, 操作符"::"
如果在创建对象的时候,new前加"::"修饰则表示:你指定的是全局的那个operator new,换言之,你想使用的是c++库提供的new。编译的时候也会忽略局部或者类里的operator new。
2, 没有操作符"::"
如果没有局部的operator new,默认会使用全局,当存在局部的,则会优先使用局部的operator new.

#include <iostream>
#include <memory>
class TestClass {
    public:
    TestClass(int a=0, int b=0) {
        std::cout << "Regular constructor called: "<<this<<", a="<< a << ", b = " << b << std::endl;
    }
    ~TestClass() {
        std::cout << "~ Destructor called: "<<this << std::endl;
    }
    TestClass(std::initializer_list<int> initList) {
        std::cout << "Initializer list constructor called with elements: "<<this<<", ";
        for (auto elem : initList) { std::cout << elem << " "; }
        std::cout << std::endl; 
    }
    void* operator new(size_t count) {
        std::cout<<"private new:"<<count<<std::endl;
        return ::operator new(count);
    }
    
    void operator delete(void *p) {
        std::cout<<"private delete:"<<p<<std::endl;
        ::operator delete(p);
    }
    int m;
};

int main()
{
    TestClass* ptr1 = new TestClass;
    TestClass* ptr2 = ::new TestClass{2,2};
    delete ptr2;
    ::delete ptr1;
    return 0;
}
private new:4
Regular constructor called: 0x57fb0b7356c0, a=0, b = 0  //局部operator new ,ptr1
Initializer list constructor called with elements: 0x57fb0b7356e0, 2 2  //全局operator new,ptr2
~ Destructor called: 0x57fb0b7356e0
private delete:0x57fb0b7356e0//局部 delete ,ptr2
~ Destructor called: 0x57fb0b7356c0 //全局delete ,ptr1

可以得到以下几点:
1, 先分配内存,后调用构造函数
2, 先析构,后调用operator delete
3, operator new 和 operator delete 不必一一对应.甚至,可以不定义局部delete 也是可以的。

三, placement new

new 表达式的第4种:

::(optional) new (placement-args)  type new-initializer (optional)

相对常用的创建方式多了一个参数placement-args placement new 是一种特殊的operator new,如何理解operator new和placement new的关系呢? 就好比,正方形是特殊的矩形。
placement new 方式的特殊地方在于: 在已分配的内存地址上(placement-args),存放新的数据。
有了这层理解就很简单了。

class TestClass {
    public:
    TestClass(int a=0, int b=0) {
        std::cout << "Regular constructor called: "<<this<<", a="<< a << ", b = " << b << std::endl;
    }
    ~TestClass() {
        std::cout << "~ Destructor called: "<<this << std::endl;
    }
    TestClass(std::initializer_list<int> initList) {
        std::cout << "Initializer list constructor called with elements: "<<this<<", ";
        for (auto elem : initList) { std::cout << elem << " "; }
        std::cout << std::endl; 
    }
    void* operator new(size_t count) {
        std::cout<<"private new:"<<count<<std::endl;
        return ::operator new(count);
    }
    void* operator new(size_t count,void *p) {
        std::cout<<"Local-Placement new:"<<count<<std::endl;
        return p;
    }
    
    void operator delete(void *p) {
        std::cout<<"private delete:"<<p<<std::endl;
        ::operator delete(p);
    }
    int m;
};

int main()
{
    TestClass* ptr1 = new TestClass;
    TestClass* ptr2 = ::new TestClass{2,2};
    std::cout<<">>>>>>test placement case:\n";
    TestClass* ptr3 = new (ptr1) TestClass {30,30};
    TestClass* ptr4 = ::new (ptr2) TestClass {40,40}; //to call global
    std::cout<<"p3: "<<ptr3<<" ,p4: "<<ptr4<<std::endl;
    std::cout<<">>>Destructor<<<\n";
    delete ptr2;
    ::delete ptr1;
    return 0;
}

ptr3用的是局部的placement new, ptr4使用的是全局的::placement new
placement-args 参数分别是ptr1, ptr2。

如结果: ptr3==ptr1 , ptr4 == ptr2

private new:4
Regular constructor called: 0x5e60cb37e6c0, a=0, b = 0
Initializer list constructor called with elements: 0x5e60cb37e6e0, 2 2 
>>>>>>test placement case:
Local-Placement new:4
Initializer list constructor called with elements: 0x5e60cb37e6c0, 30 30 
Initializer list constructor called with elements: 0x5e60cb37e6e0, 40 40 
p3: 0x5e60cb37e6c0 ,p4: 0x5e60cb37e6e0
>>>Destructor<<<
~ Destructor called: 0x5e60cb37e6e0
private delete:0x5e60cb37e6e0
~ Destructor called: 0x5e60cb37e6c0

placement new 唯一要注意的点:placement-args必须是有效的堆上地址,否则都是未定义行为。注意调用delete,析构也会调用。
placement new,不好用,用的不好, 潜在风险点是什么?欢迎留言讨论...

四, 如何使用unique_ptr 管理malloc 堆上的内存

new 匹配 delete, malloc 匹配 free. 所以原则上malloc的内存需要用free来释放。

根据unique_ptr的定义,可以自定义deleter,即:

std::unique_ptr<int[],void(*)(void*)> uq(ptr,free);
#include <iostream>
#include <memory>
#include <cstring>
class HeapBuffer {
    int *mptr;
    public:
    HeapBuffer(int*p=nullptr):mptr(p){};
    ~HeapBuffer(){
        std::cout<<"destructor,mptr:"<<mptr<<std::endl;
        if (mptr!=nullptr) {
            std::cout<<"to free mptr:"<<mptr<<std::endl;
            free(mptr);
            mptr=nullptr;
        }
    }
};
void fun(int* ptr){
    //std::unique_ptr<int[],void(*)(void*)> up(ptr,free);//方法1
    HeapBuffer buff(ptr);  //方法2
}
int main() {
    // Write C++ code here
    int *ptr = (int*)malloc(sizeof(int)*10);
    std::cout << "main ptr:"<<ptr<<std::endl;
    fun(ptr);
    std::cout << "main exit"<<std::endl;
    
    return 0;
}

需要牢记:new 或者 malloc 出来的内存, 需要去释放!

最后一问

网上经常说重载new,说的是什么,重载的又是什么?人们口中的new又是什么。
一次性解决模糊不清的概念。 重载new 本质是指的是重载operator new,或者叫new 操作符,或者操作符new.
经常提到的new 一个对象,指的是表达式new.
表达式new 和操作符 new的关系是包含关系,operator new是 new expression的工作子集。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值