源码面前,了无秘密 ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ-- 侯捷
今天打算来总结一下C++
中的内存分配的一些事情,几乎咱们写的每一程序都离不开内存分配这个话题,而不一样的程序对内存分配的需求又有不一样,尤为在一些嵌入式开发当中,经常须要程序员自定义内存分配的细节,因此今天的话题就从C++
中的new
和delete
开始讲起。程序员
你可能会据说过new
、new operator
、delete
以及delete operator
,其实当你听到这些概念的时候,说的就是new
和delete
,他们表示的都是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
的细节咱们下一小节再来讨论;上面的new
表达式就被编译转化为相似下面的形式:性能
// new 先分配内存,在调用构造 void* mem = operator new(sizeof(Complex)); complex = static_cast<Complex*>(mem); complex->Complex::Complex(1.0, 2.0); // 这里是不可以直接调用构造函数,这里只是演示,可是能够借用其它的手法调用,后面第3节咱们会说到
当咱们使用delete
来释放堆上分配的空间时,实际上执行了下面两步操做操作系统
delete complex; delete[] array;
实际上他们被编译器转化为:设计
Complex::~Complex(complex); // 调用析构函数(dtor) operator delete(complex);
与new
和delete
不一样,这两个是C++标准定义的两个全局函数,能够被重载(C++11标准中分别给了6个重载的版本),用来定制特定的内存分配机制。指针
前面咱们说到了new
和delete
表达式调用了operator new
和operator delete
来申请内存和释放内存,其实这两个函数底层调用的就是咱们熟知的malloc
和free
两个函数。code
再开始重载咱们本身的operator new
和operator 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 new
和operator delete
以后,咱们就能够定制本身的内存分配行为了,咱们通常都是重载成员operator new
和operator delete
,千万要特别当心重载全局命名空间的和operator new
和operator delete
函数,这将影响到全部的new
和delete
行为,通常不建议这么作。接口
咱们经过一个简单的内存池实现来看看如何重载这些函数。
// 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个对象大小的内存块。
优势
上面的实现一个很大的不足就是,咱们将从操做系统申请的内存一直握在本身的手里,虽然没有发生内存泄漏,可是没能将内存再次还给操做系统。
// 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
类型。
第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小节。
当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()`来终止程序。
C++2.0
以后引入两个新特性,一个是= delete
,另外一个是get_new_handler
,分别简单介绍一下。
咱们能够在operator new
和operator 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;
用来获取new-handler函数,若是用户没有设定的话或者被重置,将返回一个nullptr
。
new_handler get_new_handler() noexcept;