CPP内存分配的详细指南——new和allocator以及智能指针
Motivation
cpp里面的内存管理一直让我头疼万分,最近重新翻了翻cpp prime plus这本书,被里面各种new搞得头皮发麻,于是就有了这篇博文。主要记录我自己对cpp里面内存管理的问题。
New
New Expression 和 Operator New
new
这个操作符一直以来,自从我学cpp的那天起就一直让我头疼万分。
首先明确,平时在使用的new的时候,我们使用的是new expression,而不是new operator。new expression和new operator有很大区别。
new operator,就是相当于一个函数:
我们可以使用 operator new(512)
来分配一段内存,得到一个void指针。
而new expression就是调用了new operator,它不仅仅分配了内存,还在得到的raw 内存上,调用了构造函数:
相当于两个过程, 调用new operator再调用构造函数。如果这样使用new expression:
auto ptr = new A(2); // 直接初始化
auto ptr = new A{1, 2, 3} // 列表初始化
而operator new中的参数则由编译器来自动赋值。
operator new(sizeof(A));
A(2);
Placement New
new operator定义了很多函数, 而placement new就是调用这些new operator的方法。
new expression有四种格式,其实3, 4实际上就是调用 placement new:
If
placement-params
are provided, they are passed to the allocation function as additional arguments.如果placement-params 给出,那么它们将作为额外的参数传递给new operator。
例如:new (std::align_val_t(32)) A()
实际上就是调用了 operator ne w(sizeof(A), std::align_val_t(32))
同样除了使用align我还可以使用new在指定的内存处进行初始化:
Output:
这里我们首先用operator new申请一段内存对齐的地址,接着在这一段内存上构建了一个std::vector 类,我们可以使用这个类的实例指针进行push_back,最后我们使用 std::destroy_at
对该处地址上的类实例进行析构。
New expression的构造
之前提到,new expression分为两个步骤,一个是使用new operator进行内存分配,一个是在分配得到的内存上进行构造。
这一部分我也是直接看CPPreference。new expression的四种形式,后面总是跟着一个initializer,而这个initializer就是决定了如何进行构造:
-
对于非数组类型,即
new int(2)
或者new A;
,也就是我们只是new一个单独的object。- 如果initializer缺失,那么使用默认初始化
- 如果initializer是一对括号括起来的参数例如
new A(1,2)
,那么加快直接初始化,相当于调用A的构造函数。 - 如果initializer是一对大括号括起来的参数,
new A{1,2}
,那么使用列表初始化
-
如果new 的type是数组:
- 如果initializer缺失,数组所有element使用默认初始化。
- 如果initializer是一对空括号
()
,那么所有元素使用值初始化。 - 如果initializer是一对大括号括起来的参数,则使用聚合初始化。
例如:
Output:
输出就是直接聚合初始化的结果,也就是{1,0}, {3, 1}。这里我开始有个疑惑,为什么得到的结果不是vector<int>(1,0) vector<int>(3, 1)。我猜测可能在执行时候,如果类自己实现了 std::initializer_list 的构造函数,那么聚合初始化优先匹配这个构造函数,如果没有实现initializer_list 的构造函数,那么会去匹配其他构造函数。(update! 读了effective modern cpp之后,我才明白initializer构造函数的匹配是最强烈的)
例如:
class A {
public:
A(int a, int b) {
std::cout << "from A(int a, int b) constructor" << std::endl;
}
A(std::initializer_list<int> a) { // 定义了initializer_list
std::cout << "from initializer_list constructor" << std::endl;
}
};
auto a_ptr = new A[2]{{1, 0}, {1, 1}};
OUTPUT:
from initializer_list constructor
from initializer_list constructor
class A {
public:
A(int a, int b) {
std::cout << "from A(int a, int b) constructor" << std::endl;
}
// A(std::initializer_list<int> a) { // 定义了initializer_list
// std::cout << "from initializer_list constructor" << std::endl;
// }
};
auto a_ptr = new A[2]{{1, 0}, {1, 1}};
OUTPUT:
from A(int a, int b) constructor
from A(int a, int b) constructor
可以其匹配规则是优先匹配initializer_list constructor,然后是其他constructor。
delete
如果是new expression得到的pointer,那么用delete expression进行析构。
如果是new operator得到的内存,那么用delete operator进行内存释放。
allocator
allocator类用于自定义底层内存的分配:
template<typename T>
class MyAllocator {
public:
using value_type = T;
using pointer = T*;
MyAllocator() = default;
template<typename U>
MyAllocator(const MyAllocator<U>&) {}
pointer allocate(std::size_t n) {
return static_cast<pointer>(operator new(n * sizeof(T)));
}
void deallocate(pointer p, std::size_t n) {
operator delete(p);
}
// Construct object, no need
template <typename... Args>
void construct(T* p, Args&&... args) {
::new(static_cast<void*>(p)) T(std::forward<Args>(args)...);
}
// Destroy object, no need
void destroy(T* p) noexcept {
std::destroy(p);
}
};
int main() {
std::vector<int, MyAllocator<int>> vec;
vec.push_back(42);
return 0;
}
在使用的时候,只需要自己定义好value_type
pointer
allocate
和 deallocate
使用例子,动态分配vector内存,但是是32对齐:
#include <new>
#include <iostream>
#include <vector>
template<typename T>
class MyAllocator{
public:
using value_type = T;
MyAllocator() = default;
T* allocate(std::size_t n ){
return static_cast<T*>(operator new(n, std::align_val_t(32)));
}
void deallocate(T* p, size_t n ){
operator delete(p);
}
};
int main (int argc, char *argv[])
{
std::vector<int, MyAllocator<int>> a;
a.push_back(10);
std::cout << "a.data() addr mod 32 is "<< reinterpret_cast<uint64_t>(a.data()) % 32 << std::endl;
std::cout << std::hex << reinterpret_cast<uint64_t>(a.data()) << std::endl;
return 0;
}
结果,可以看到分配的内存的确是对齐的:
unique_ptr和shared_ptr
把unique_ptr和shared_ptr 与new进行组合,就可以很好地进行内存管理啦啦啦
#include <memory>
#include <iostream>
#include <new>
#include <vector>
class A {
public:
A(int a, int b) {
std::cout << "from A(int a, int b) constructor" << std::endl;
}
A() { std::cout << "from A() constructor" << std::endl; }
~A() { std::cout << "from destructor" << std::endl; }
};
template <typename T> struct deletor {
deletor() = default;
void operator()(T *ptr) { delete[] ptr; }
};
int main(int argc, char *argv[]) {
std::unique_ptr<A, deletor<A>> uptr(new (std::align_val_t(32)) A[2],
deletor<A>{});
std::shared_ptr<A> sptr(new A[2]{{1, 2}, {2, 3}}, deletor<A>{});
std::cout << "uptr addr " << std::hex
<< reinterpret_cast<uint64_t>(uptr.get()) << " mod 32 is "
<< reinterpret_cast<uint64_t>(uptr.get()) % 32 << std::endl;
std::cout << "sptr addr " << std::hex
<< reinterpret_cast<uint64_t>(sptr.get()) << " mod 32 is "
<< reinterpret_cast<uint64_t>(sptr.get()) % 32 << std::endl;
return 0;
}