浅析内存分配

2021年01月14日 阅读数:11
这篇文章主要向大家介绍浅析内存分配,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
源码面前,了无秘密 ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ-- 侯捷

今天打算来总结一下C++中的内存分配的一些事情,几乎咱们写的每一程序都离不开内存分配这个话题,而不一样的程序对内存分配的需求又有不一样,尤为在一些嵌入式开发当中,经常须要程序员自定义内存分配的细节,因此今天的话题就从C++中的newdelete开始讲起。程序员

1. new 和 delete

你可能会据说过newnew operatordelete以及delete operator,其实当你听到这些概念的时候,说的就是newdelete,他们表示的都是C++中的操做符,C++中一般使用new表达式去为对象分配内存,他们不容许被重载cookie

  • 当咱们使用new在堆上为对象分配一块空间时,以下
struct Complex {
  Complex() = default; // C++11用法,让编译器帮咱们生成默认构造函数(ctor)
  Complex(double real, double imag) : real_{ real }, imag_{ imag } {}

private:
  double real_;
  double imag_;
};

Complex* complex = new Complex(1.0, 2.0);
Complex* array = new Complex[10];  // 若是没有默认ctor,这里编译器会出错

实际上C++默默执行了下面三步操做函数

  • 首先调用全局命名空间的operator new(或 operator new[])函数来分配一块原始内存,注意这块内存并未初始化,关于operator new的细节咱们下一小节再来讨论;
  • 转型,将原始指针转化为对象类型;
  • 调用构造函数,分配空间,返回指向该对象的指针。

上面的new表达式就被编译转化为相似下面的形式:性能

// new 先分配内存,在调用构造
void* mem = operator new(sizeof(Complex));
complex = static_cast<Complex*>(mem);
complex->Complex::Complex(1.0, 2.0);   // 这里是不可以直接调用构造函数,这里只是演示,可是能够借用其它的手法调用,后面第3节咱们会说到

当咱们使用delete来释放堆上分配的空间时,实际上执行了下面两步操做操作系统

  • 先调用析构函数;
  • 调用全局命名空间的operator delete(或 operator new[])函数来释放内存。
delete complex;
delete[] array;

实际上他们被编译器转化为:设计

Complex::~Complex(complex);  //  调用析构函数(dtor)
operator delete(complex);

2. operator new 和 operator delete

newdelete不一样,这两个是C++标准定义的两个全局函数,能够被重载(C++11标准中分别给了6个重载的版本),用来定制特定的内存分配机制。指针

  • 破冰

前面咱们说到了newdelete表达式调用了operator newoperator delete来申请内存和释放内存,其实这两个函数底层调用的就是咱们熟知的mallocfree两个函数。code

  • 伊始

再开始重载咱们本身的operator newoperator delete函数以前,先带你们看一下他们在标准库中的接口形式对象

void* operator new (std::size_t size);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
void* operator new (std::size_t size, void* ptr) noexcept; // placemen
void* operator new[] (std::size_t size);
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) noexcept;
void* operator new[] (std::size_t size, void* ptr) noexcept; // placement

void operator delete (void* ptr) noexcept;
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
void operator delete (void* ptr, void* voidptr2) noexcept; // placement
void operator delete (void* ptr) noexcept;
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) noexcept;
void operator delete (void* ptr, void* voidptr2) noexcept; // placement

// C++14 之后 operator delete 多引入了下面四种形式的重载
void operator delete (void* ptr, std::size_t size) noexcept; // with size
void operator delete (void* ptr, std::size_t size, const std::nothrow_t& nothrow_constant) noexcept; // nothrow with size
void operator delete[] (void* ptr, std::size_t size) noexcept;
void operator delete[] (void* ptr, std::size_t size, const std::nothrow_t& nothrow_constant) noexcept;

当咱们重载了本身的成员operator newoperator delete以后,咱们就能够定制本身的内存分配行为了,咱们通常都是重载成员operator newoperator delete,千万要特别当心重载全局命名空间的和operator newoperator delete函数,这将影响到全部的newdelete行为,通常不建议这么作。接口

  • 一个简单的内存池实现

咱们经过一个简单的内存池实现来看看如何重载这些函数。

  • 接口
// allocator.h
struct Allocator {
  void* allocate(size_t size);
  void deallocate(void* head);

private:
  struct obj {
    obj* next;    // embedded pointer
  };

  obj* free_head_;
  const size_t chunk_ {20};
};
  • 实现
void* Allocator::allocate(size_t size) {
    obj* temp;

    if (free_head_ == nullptr) {
        free_head_ = static_cast<obj*>(malloc(size * chunk_));

        temp = free_head_;
        for (int i = 0; i < chunk_ -1; ++i) {
            temp->next = (obj*)((char*)temp + size);
            temp = temp->next;
        }
        temp->next = nullptr;
    }

    temp = free_head_;
    free_head_ = temp->next;
    return temp;
}

void Allocator::deallocate(void *head) {
    obj* temp = static_cast<obj*>(head);
    temp->next = free_head_;
    free_head_ = temp;
}

上面咱们定义了一个Allocator的类,将分配的内存块经过链表级联在一块儿,默认一次申请20个对象的大小的块,这个值根据不一样状况你能够修改,或者在构造的时候传入都行,根据实际状况。
实现了对一块大的内存的自我管理,申请的时候将free_head_ 指向的内存给用户,释放的时候将内存插入到链表头部。当内存不足的时候又会从新申请20个对象大小的内存块。

  • 优势

    1. 获得的每一大块的内存都是连续的,减小了malloc函数内存的浪费,malloc函数在申请内存的时候会在返回给用户的指针前面和后面插入一些额外的cookie信息,为了free的时候能够知道释放多大的内存;想要更深刻的了解能够参考effective C++第三版的条款50和51;
    2. 减小了malloc的调用次数,不过带来的性能不会特别大,malloc的效率其实很是高。
  • 缺点

上面的实现一个很大的不足就是,咱们将从操做系统申请的内存一直握在本身的手里,虽然没有发生内存泄漏,可是没能将内存再次还给操做系统。

  • 使用实例
// word.h
struct Word {
  Word () = default;
  Word (int size, int data) : size_(size), data_(data) {}

  static Allocator allocator;
  static void* operator new(size_t size) { return allocator.allocate(size); }
  static void* operator new[](size_t size) { return allocator.allocate(size); }
  static void operator delete(void* pointer) { return allocator.deallocate(pointer); }
  static void operator delete[](void* pointer) { return allocator.deallocate(pointer); }
  static void* operator new(size_t size, void* start) { return start; }  // 这是一个placement new

private:
  int size_;
  int data_;
};
Allocator Word::allocator;

咱们能够重载不少个class member operator new(),前提是每个重载的版本第一参数必须为size_t类型。

3. placement new 和 placement delete

  • 一个简单的例子

第1节咱们留下了一个问题,咱们说下面的代码是不可以直接调用构造函数的

complex->Complex::Complex(1.0, 2.0);

咱们将它稍微改写一下,变成下面的形式就能够调用构造函数了,其实下面的形式,就是咱们这一节要说的placement new,又叫定点new或者定位new

new(complex)Complex(1.0, 2.0);
  • 做用

它用于在给定的内存中初始化对象(不会分配内存),对于 operator new 分配的内存空间来讲咱们没法使用构造函数来构造对象。这个时候咱们可使用placement new形式来构造对象。
另外placement new容许咱们在一个特定的、预先分配的内存地址上构造对象,这个地址不只仅是堆上的内存(如上所示),也能够是栈上分配的空间,以下

std::string str[3];
for (int i = 0; i < 3; ++i) {
  new(str+i)std::string("num is " + std::to_string(i));
  std::cout << *(str+i) << std::endl;
}

// output:
num is 0
num is 1
num is 2
  • 参考

关于placement new的详细部分能够参考effective C++ 第三版的条款52以及C++ Primer 第五版的19.1.2小节。

4. new_handler 和 set_new_handler

operator new分配内存失败的时候,会抛出一个std::bad_alloc异常。在一些老的编译器可能不会抛出异常,而是返回零,不过你能够显示让编译器不抛出异常

new(std::nothrow)int[10];
// 称为nothrow形式
  • 形式

C++平台在抛出异常以前,会先调用一个函数,并且不止一次,这个函数能够由client指定的handler,下面咱们看看new_handler的形式和设定方法

typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw(); // C++98
new_handler set_new_handler(new_handler p) noexcept(); // C++11

说明一下,set_new_handler尾端声明的throw(),表示该函数不抛出异常,不过在C++11的时候被标记为废弃,改成noexcept,到C++17的时候throw()这种用法已经完全被删除了。
C++平台这样的设计是为了给用户一个机会,在内存不足的时候调用用户本身设定的handler,也就是由你来决定这个时候该如何抉择。

  • 设计选择

好的new_handler设计,通常有两个选择。

1. 想法设法让更多的内存可用,释放系统当前能够释放的空闲资源;
2. 调用`abort()`或`exit()`来终止程序。

5 . 补充

C++2.0以后引入两个新特性,一个是= delete,另外一个是get_new_handler,分别简单介绍一下。

  • = delete

咱们能够在operator newoperator delete函数尾部加上= delete,用来表示删除这个函数,不容许使用者调用。

// word.h
struct Word {
  Word () = default;
  static void* operator new(size_t size) = delete;
  static void* operator new[](size_t size) = delete;
  static void operator delete(void* pointer) = delete;
  static void operator delete[](void* pointer) = delete;
};

// 下面四条语句都会compile error
Word* word = new Word();
Word* words = new Word[3];
delete word;
delete[] words;
  • get_new_handler

用来获取new-handler函数,若是用户没有设定的话或者被重置,将返回一个nullptr

new_handler get_new_handler() noexcept;
上一篇: 可调用对象