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,就是相当于一个函数:

image-20230218164104604

我们可以使用 operator new(512)来分配一段内存,得到一个void指针。

而new expression就是调用了new operator,它不仅仅分配了内存,还在得到的raw 内存上,调用了构造函数:

image-20230218164309308

相当于两个过程, 调用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的方法。

image-20230218164943600

new expression有四种格式,其实3, 4实际上就是调用 placement new:

image-20230218165053124

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在指定的内存处进行初始化:

image-20230218170800064

Output:

image-20230218170811041

这里我们首先用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是一对大括号括起来的参数,则使用聚合初始化。

    例如:

    image-20230218173126850

Output:

image-20230218173257560

输出就是直接聚合初始化的结果,也就是{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 allocatedeallocate

使用例子,动态分配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;
}

结果,可以看到分配的内存的确是对齐的:

image-20230218190736228

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;
}

image-20230218193459134

posted @ 2023-02-18 19:37  kalice  阅读(547)  评论(0编辑  收藏  举报