# 内存管理 ![C++应用程序](./img/Memory_1.png) ## Primitives 基础写法 ### new / delete | 分配 | 释放 | 类型 | 可否重载 | | --- | --- | --- | --- | | malloc() | free() | C函数 | 不可 | | new | delete | C++表达式 | 不可 | | ::operator new() | ::operator delete() | C++函数 | 可 | | allocator::allocate() | allocator::deallocate() | C++标准库 | 可自由设计并与之搭配任何容器 | ```cpp #include #include using namespace std; int main() { void* p1 = malloc(521); free(p1); complex* p2 = new complex; delete p2; void* p3 = ::operator new(512); ::operator delete(p3); #ifdef _MSC_VER // 以下两个函数都是non-static,一定要通过object调用,以下分配3个int空间 int* p4 = allocator().allocate(3, (int*)0); // 上面的 (int*)0 暂时没有用 // 通过 allocator() 创建要给临时对象,执行临死对象的allocate方法 allocator().deallocate(p4, 3); // 通过 allocator() 创建要给临时对象,执行临死对象的deallocate方法 #endif // _MSC_VER #ifdef __BORLANDC__ // 以下两个函数都是non-static,一定要通过object调用,以下分配5个int空间 int* p4 = allocator().allocator(5); allocator().deallocate(p4, 5); #endif // __BORLANDC__ #ifdef __GNUC__ // 以下两个函数都是static,可通过全名调用,一下分配512bytes void* p4 = alloc::allocate(512); alloc::deallocate(p4, 512); #endif // __GNUC__ return 0; } ``` > 上述代码为四种内存分配的基本用法 ----- ```cpp class Complex { public: Complex(int x, int y) { m_x = x; m_y = y; std::cout << "constructor Complex" << std::endl; } Complex() { m_x = 0; m_y = 0; std::cout << "constructor Complex" << std::endl; } private: int m_x; int m_y; }; void func_new() { Complex* p1 = new Complex(1, 2); delete p1; // 等价于 Complex* p2; try { void* mem = operator new(sizeof(Complex)); p2 = static_cast(mem); p2->Complex::Complex(1, 2); operator delete(p2); // 这里直接调用构造函数的动作 只有编译器才可以这么写 自己写不同平台可能报错 } catch (std::bad_alloc) { } } ``` > tip: 上述代码中说明了不可直接`pc->Complex::Complex(1,2)`方法,如果想直接调用构造函数可以运用**placement new**,写法是`new(p)Complex(1,2)` ----- ```cpp // gcc 中 operator new的实现方式 void *operator new(std::size_t size) _THROW_BAD_ALLOC { if (size == 0) size = 1; void* p; while ((p = ::malloc(size)) == nullptr) { // If malloc fails and there is a new_handler, // call it to try free up memory. std::new_handler nh = std::get_new_handler(); if (nh) nh(); else #ifndef _LIBCPP_NO_EXCEPTIONS throw std::bad_alloc(); #else break; #endif } return p; } ``` 从上面的代码可以看到,`new`的实现方式就是调用了`operator new`方法,在强转之后执行了对应的构造函数 而`operator new`方法中,真正分配内存的是`malloc`方法 > [网上看到的资料](https://witgao.com/2021/07/07/c++/libc++%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B9%8Bnew%E5%92%8Cdelete/) ----- ```cpp Complex* pc = new Complex(1, 2); // ... some operation delete pc; // 编译器将delete转换成下面操作 pc->~Complex(); // 先析构 operator delete(pc); // 然后释放内存 ``` > 上述为delete的操作,这里直接调用析构函数不会报错 ```cpp void operator delete(void* ptr) noexcept { ::free(ptr); } ``` > `operator delete`底层通过`free`释放内存 ----- 上述代码中`pc->Complex::Complex(1, 2)`直接执行构造函数没有报错,并不能表示我们可以都这么直接调用构造,下面代码就是一个反例 ```cpp string* pstr = new string; pstr->string::string(); pstr->~string(); delete pstr; ``` 这里的代码会明显报错`class std::basic_string has no member named string` 这里报错的原因是`string`本来的名字其实叫`basic_string`,是被`typedef`成`string`,所以`string`的构造函数应该是`basic_string()`才对 除了`typedef`导致构造函数名字不对的问题之外,**编译器的严格程度**不同也会出现报错,即存在平台差异性,所以不推荐手动调用构造函数 ### array new / array delete ```cpp Complex* pca = new Complex[3]; // 唤起三次构造函数 // ... some operation delete[] pca; // 唤起三次析构 ``` > 如果写的是`delete`而不是`delete[]`,那么编译器只会执行一次析构函数,而不是三次,也就是说有两个对象的析构函数没有执行,如果对象中存在`new`操作,这样会导致无法执行`delete`进而导致内存泄漏 ![C++应用程序](./img/Memory_2.png) 从上图中可以看到,`new`出的内存块除了包含三个`Complex`对象外,还包含一些`cookie`信息,这些信息帮助操作系统释放整块内存,而`delete`与`delete[]`区别仅仅在于数组中所有对象是否都执行析构 ```cpp class A{ public: int id; A(): id(0) { std::cout << "default " << this << " " << this->id << std::endl; }; A(int i): id(i) { std::cout << this << " " << "int cons" << i << std::endl; } ~A() { std::cout << " ~ " << this << " " << this->id << std::endl; } }; void test_placement_new() { A* buf = new A[3]; // 调用A的默认构造函数,创建对象 A* tmp = buf; for (int i = 0; i < 3; i++) { new(tmp++)A(i); // placement new的写法,在tmp地址的构建A对象,调用A的构造函数 } delete[] buf; } ``` ```bash default 00A64CF4 0 default 00A64CF8 0 default 00A64CFC 0 00A64CF4 int cons0 00A64CF8 int cons1 00A64CFC int cons2 ~ 00A64CFC 2 ~ 00A64CF8 1 ~ 00A64CF4 0 ``` 通过**查看输出**,可以发现这上述代码的执行顺序 ### placement new - `placement new` 允许我们将object建构于`allocated memory`(已经分配的内存)中 - 没有`placement delete`,因为`placement new`根本没有分配内存 > 因为是构建于已经分配的内存中,所以需要一个现成的被分配的内存空间的指针 ```cpp #include void func(){ char* buf = new char[sizeof(Complex) * 3]; Complex* pc = new(buf)Complex(1, 2); // .... delete[] buf; } void func_1(){ Complex* pc = new(buf)Complex(1, 2); // 上面一行代码 等价下面的代码 Complex* pc; try{ void* mem = operator new(sizeof(Complex), buf); pc = static_cast(mem); pc->Complex::Complex(1, 2); } catch(std::bad_alloc){ } } // 下面为当 operator new的两个参数为size_t和void*的源码 void* operator new(size_t, void* loc){ return loc; } ``` 上面的代码使用了 `placement new`,它可以让你在已经分配好的内存上构造对象。`placement new`的语法是:`new (address) (type) initializer`。其中,`address` 是你指定的内存地址,`type` 是你要构造的对象的类型,`initializer` 是你要传递给对象构造函数的参数 定位 new 操作符的优点是可以提高性能和异常安全性,因为它不需要再次分配内存,而且可以在程序员控制的内存上创建对象。定位 new 操作符的缺点是需要手动调用对象的析构函数来释放资源,而且不能使用 delete 操作符来删除对象,只能删除分配给它们的内存。 分析上述代码可以发现 1. `operator new(size_t, void* loc)`什么都没做,因为此时`loc`是已经分配好了的内存地址,所以不需要再分配内存 2. `placement new`其实就是执行了一次构造函数 ### C++分配内存的途径 ![C++应用程序](./img/Memory_3.png) 一般来说,我们会重载类的`Foo::operator new`和`Foo::operator delete`,将对象的创建和内存分配自己来控制(通过自己控制可以减少内存块上的cookie的使用) ```cpp Foo*p = (Foo*)malloc(sizeof(Foo)); new(p)Foo(x); // ... p->~Foo(); free(p); ``` 当然也可以重载全局`::operator new / ::operator delete` ```cpp // 测试重写代码 void* myAlloc(size_t size){ return malloc(size); } void myFree(void* ptr){ return free(ptr); } inline void* operator new(size_t size){ cout << "my new" << std::endl; return myAlloc(size); } inline void* operator new[](size_t size){ cout << "my new[]" << std::endl; return myAlloc(size); } inline void operator delete(void* ptr){ cout << "my delete" << std::endl; myFree(ptr); } inline void operator delete[](void* ptr){ cout << "my delete[]" << std::endl; myFree(ptr); } ``` 更常见的写法是**类中重载** ```cpp class Foo { public: int _id; long _data; std::string _str; public: Foo() : _id(0) { std::cout << "default ctor " << this << " " << this->_id << std::endl; } Foo(int i): _id(i) { std::cout << "ctor " << this << " " << this->_id << std::endl; } virtual ~Foo() { std::cout << "dtor " << this << std::endl; } static void* operator new(size_t size); static void operator delete(void* pdead, size_t size); static void* operator new[](size_t size); static void operator delete[](void* pdead, size_t size); }; void* Foo::operator new(size_t size) { Foo* p = (Foo*)malloc(size); std::cout << "my alloc Foo " << p << std::endl; return p; } void Foo::operator delete(void* pdead, size_t size) { std::cout << "my delete " << pdead << std::endl; free(pdead); } void* Foo::operator new[](size_t size) { Foo* p = (Foo*)malloc(size); std::cout << "my alloc Foo " << p << std::endl; return p; } void Foo::operator delete[](void* pdead, size_t size) { std::cout << "my delete " << pdead << std::endl; free(pdead); } int main() { std::cout << sizeof(Foo) << std::endl; Foo* p = new Foo(7); delete p; Foo* pArray = new Foo[5]; delete[] pArray; return 0; } ``` ```bash 40 my alloc Foo 00C2DA78 ctor 00C2DA78 7 dtor 00C2DA78 my delete 00C2DA78 my alloc Foo 00C30110 default ctor 00C30114 0 default ctor 00C3013C 0 default ctor 00C30164 0 default ctor 00C3018C 0 default ctor 00C301B4 0 dtor 00C301B4 dtor 00C3018C dtor 00C30164 dtor 00C3013C dtor 00C30114 my delete 00C30110 ``` 但是,如果使用`::new`和`::delete`则会绕过类定义的`operator new / operator delete`方法,转而执行**全局的**`new / delete` ```cpp int main() { std::cout << sizeof(Foo) << std::endl; Foo* p = ::new Foo(7); ::delete p; Foo* pArray = ::new Foo[5]; ::delete[] pArray; return 0; } ```