C++ 动态内存分配 new / delete、operator new、placement new


C++动态内存分配是一个较为复杂的环节,特别是容易发生内存泄露等问题,今天将不会讨论内存泄露的问题,今天仅是讨论关于c++ 中的new 表达式的相关内容。


new / delete expression

所谓的 new 表达式,就是我们做常用的用于分配动态对象的表达式,如下操作就是运用new表达式进行动态对象的分配。

int* p = new int(1024);
string* p1 = new string("一条舔狗!");
string *p2 = new string[10];//将会待用3次,string的默认构造函数

采用new 表达式分配的对象将会分配在堆上,记得对它进行delete。

delete p;
delete p1;
delete [] p2;

如果分配的是数组则应该用 delete [ ] 。


operator new / delete

operator new / delete 负责向系统申请内存,一般由operator new / delete 申请的内存为原始内存,在该内存上并没有特定的对象,因此,对于用该运算符申请得到的内存我们需要手动为其构造对象,并手动的调用对象的析构函数。
operator new / delete 可以重载,通过重载,可以改变向内存申请内存的方式。在c++编译器的内部,实际上operator new / delete是通过调用C语言函数malloc / free 实现堆内存的申请和释放的。

 //向系统申请一块原始内存,大小为10 个string对象的大小
 void* rawMem = operator new(10 * sizeof(string));

通过operator new / delete分配的内存还需要通过强化类型转换,最后可以调用placement new(下文讲述)在其上构造对象。


placement new

placement new 用于在一块已经分配了内存上构造某个对象,这就是为什么它会被称为placement new, palcement new 实际上并没有向内存申请内存空间,他仅是负责在一块已经分配的内存上构造对象。上面的代码我们已经获得一块原始内存,因此我们需要在上述的代码中构造相应的对象,因此我们需要先将原始内存强制转换相应的类型,然后通过placement new在上面构造对象。

//向系统盛情一块原始内存,大小为10 个string对象的大小
void* rawMem = operator new(10 * sizeof(string));
string * str = static_cast<string*>(rawMem);//将指针转换为string类型,将会转换为10个
//调用palacement new 构造对象
for (int i = 0; i < 10; ++i) {
    new (str + i)string(to_string(i));
}

new (str + i)string(to_string(i));表示在 str+i 所指向内存处构造一个string对象,传入的构造参数为to_string(i)。

对于通过上面的方式获得的内存我们需要将其进行释放,但是我们不能直接调用

delete[] str;//错误,因为指针str指向的内存并非通过new直接得到。

因为placement new 没有申请空间, 因此我们不能直接delete转型后得到的指针,我们应该先将在该内存中的对象析构,然后delete 原始内存。

//先析构对象在释放内存
for (int i = 9; i >= 0;i--) {
    str[i].~string();//调用析构函数
}
//释放原始内存
delete[](rawMem);

这样便完成了依次动态对象的申请和释放,那么我们的new expression 和new operator 以及placement new之间到底有什么关系?


new / delete expression 的实现原理

实际上,我们常用的new expression 是通过调用new opreator获得一块内存,然后在该内存上构造一个对象,左后返回构造好对象的指针。而delete expression 则是先析构对象然后再将对象的内存释放。

new expression 的过程:如 *pc = new Complex(1,2);

上述语句相当于相当于以下的几步操作:

1.void * mem = opreator new(sizeof(Complex));// 获得一块大小为Complex类大小的原始内存
2.pc = static_cast<Complex *>(mem)// 将指针转型为Complex类的指针
3. pc->Complex::Complex(1,2); //调用构造函数(或许是调用placement new)

简单的allocator

我们说过operator new 是通过调用malloc实现内存的封装的,那么我们就说说malloc分配的内存的缺点,malloc最大的缺点就是每次申请内存都会附加一些字节用于记录本次分配和格式的信息。如下图,假如一次申请对象,那么就会多出两个cookie的内存,假如每个cookie 4个字节,那么,如果我们调用malloc一百万次,我们将会浪费八百万个字节,这对于系统来说将是一种极大的浪费,然而我们的常用的对象一般都偏小,所以我们一般会采用其他的方式进行内存分配。
在这里插入图片描述

c++标准库中的allocator就是采用一次分配很大的空间,然后当需要得到时候在对空间中的进行切分,这样,可以减少因cookie而产生的浪费。对每一次分配的内存,一般用链表将其连接起来,每次申请内存时取下一块内存,当释放内存时便将内存放回链表中,以便下次使用。

下面我们通过一个简单的alloctor类,只要每个类重载operator new 便可以更改申请内存的方式。

class Allocator {

private:
    //定义一个节点指针,指向下一个未分配的内存空间,被称为嵌入式指针
    struct node{
        struct node *  next;
    };
    //表示某个类对应的预先分配空间的大小
    int chunk = 5;
    //指向分配的内存的起始位置
    node* ptr = nullptr;

public:
    void* allocate(size_t size);
    void delocate(void *,size_t);

};

//分配空间,大小为 size * chunck,每次调用allocate将会得到一块内存
void* Allocator::allocate(size_t size) {
	node* p;
	  if (ptr == nullptr) {//如果链表为空,则先申请一整块的内存
	      p = ptr = (node*)malloc((size ) * chunk);
	      for (int i = 0; i < chunk - 1; i++) {
	          p->next = (node*)((char*)p + size);//一个字符的大小为一个字节,相当于将p后移动size个字节
	          p = p->next;
	      }
	      //处理最后一个节点的指针,以防野指针
	      p->next = nullptr;
	  }
	  p = ptr;
	  ptr = ptr->next;
	  return p;
}

void Allocator::delocate(void * p,size_t size) {
    //将p放回链表的前端
    ((node*)p)->next = ptr;
    ptr = (node * )p;
}


//a如果一个类重载了operator new 那么它申请原始内存的方式将会被修改
class A {
public:

    A(int s_) :s(s_) {}

    void print() {
        cout << "舔狗 id: " <<s<< endl;
    }

 private:
    int s;
public:
    static Allocator alloc;
    //重载operator new
    static void* operator new(size_t size){
        cout << "舔狗归来" << endl;
        return alloc.allocate(size);
    }

    //重载operator delete
    static void operator delete(void * p, size_t size) {
        cout << "舔狗离开" << endl;
        alloc.delocate(p, size);
    }
};
//静态变量初始化
Allocator A::alloc;

int main()
{
    vector<A*>vec;
    for (int i = 0; i < 10; i++ ) {
        vec.push_back(new A(i));
    }
    for (auto i : vec) {
        i->print();
    }
    for (int i = 0; i < 10; i++) {
        delete vec[i];
    }
	return 0;
}

当某个类重载new operator ,那么当使用new expression 动态分配该类的对象时,将会通过重载的new、delete operator获取内存,而一次性获取大块的内存然后在进行切割,这样便可以减少cookie所占用的内存。

本文内容主要来自参考侯捷老师的课程:课程百度网盘连接如下

链接:https://pan.baidu.com/s/1cZeAwWvmQLXjk7S93kfN-Q
提取码:pukq

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值