内存分配

介绍 C++ 中的内存分配、管理以及智能指针的原理及使用

C++ 中内存的管理

程序地址空间

C++ 中,内存分布分为五块区域,分别是:栈;堆;全局变量和静态变量(存放于 data 段和 bss 段);常量;代码;

Image

上图是内核和用户的虚拟地址空间分布情况,其中,局部变量参数等都存放在中,这部分空间由系统进行管理;而中空间主要是用于用户调用 newmalloc 时分配的空间。这部分区域由用户管理,因此容易造成内存泄漏。

malloc 底层实现原理

  1. step1:从内存池中分配。若所需要内存 < 128KB,则从内存池中尝试分配。若无,则进行 brk 系统调用,从堆上申请内存。
  2. step2:若 > 128KB,不看内存池,直接使用 mmap 系统调用,从文件映射区(同时还存放动态库)中获得内存。

new

new 是一个操作符,能够触发析构函数的调用,并且在申请空间的时候也可以完成初始化。搭配 delete 使用

这里我觉得这种虚拟地址空间划分方式粒度太粗,不足以说明具体情况

智能指针

智能指针分为三类:shared_ptrunique_ptrweak_ptr(c98还引入了 auto_ptr,但已在 c++11中被废弃)

unique_ptr

unique_ptr 表示专属所有权,用 unique_ptr 管理的内存,只能被一个对象持有。故不支持复制赋值

1
2
auto w = std::make_unique<Widget>();  // 在 c++14 中,可以用 make_unique 方法来构造。
auto w2 = w; // 编译错误

因此只能通过移动来更改专属所有权:

1
2
auto w = std::make_unique<Widget>();
auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr

用法:需要引入头文件 <memory>,可以使用右值拷贝构造或 make 方法来构造指针。

1
2
unique_ptr<int> p1 = make_unique<int>(100);
unique_ptr<string> ps1(new string("good luck"));

适用场景

  1. 忘记 delete
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Box {
public:
    Box() : w(new Widget()) {}

    ~Box() {
      // 析构函数中忘记 delete w
    }
private:
    Widget* w;
};
  1. 异常安全
1
2
3
4
5
void process() {
    Widget* w = new Widget();
    w->do_something(); // 如果发生异常,那么 delete w 将不会执行,此时就会发生内存泄露
    delete w;  // 也可以用 try...catch 块捕捉异常,并在 catch 语句中 delete,但是不太美观 + 容易漏写
}

shared_ptr

shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。当引用计数为 0 的时候,则 delete 内存。

1
2
3
auto w = std::make_shared<Widget>();
auto w2 = w;
cout << w.use_count() << endl;  // g++ -std=c++11 main main.cc  output->2

同时,shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。如下:

1
2
auto w = std::make_shared<Widget>();
auto w2 = std::move(w); // 此时 w 等于 nullptr,w2.use_count() 等于 1

注意

  • shared_ptr 性能开销更大,几乎是 unique_ptr 的两倍(因为还要维护一个计数)
  • 考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢
  • 使用移动优化性能,尽量使用 std::move 来将 shared_ptr 转移给新对象。因为移动不用增加引用计数,性能更好

使用场景:通常用于指定,有可能多个对象同时管理一个内存的时候。

weak_ptr

weak_ptr 是为了解决 shared_ptr 双向引用的问题。即:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class B;
struct A {
    shared_ptr<B> b;
};
struct B {
    shared_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;

papb 存在着循环引用,根据 shared_ptr 引用计数的原理,papb 都无法被正常的释放。 对于这种情况, 我们可以使用 weak_ptr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class B;
struct A {
    shared_ptr<B> b;
};
struct B {
    weak_ptr<A> a;
};
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;

weak_ptr 不会增加引用计数,因此可以打破 shared_ptr 的循环引用。 通常做法是 parent 类持有 child 的 shared_ptr, child 持有指向 parent 的 weak_ptr。这样也更符合语义。

实现过程

鉴于看到面经中有同学被问到过智能指针的底层实现,因此这里给出两种智能指针(weak_ptr略)的简单实现方式。

Tips
  1. this 本身是一个指针,指向该类实例化后的对象本身;*this表示解引用,C++中对一个指针进行解引用,得到的是当前对象的引用,也就是对象本身
  2. 注意这里 (*this->_count) ++ 的用法
  3. 注意这里 = delete 的语法,用于显示地禁用特定的函数

shared_ptr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
template<typename T>
class shared_ptr {
private:
    T* _ptr;
    int* _count;  // 引用计数

public:
    // 构造函数
    shared_ptr(T* ptr = nullptr) : _ptr(ptr) {
        if (_ptr) _count = new int(1);
        else _count = new int(10);
    }

    // 拷贝构造
    shared_ptr(const shared_ptr& ptr) {
        if (this != ptr) {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count) ++ ;
        }
    }

    // 重载operator=
    shared_ptr& operator=(const shared_ptr & ptr) {
        if (this->_ptr == ptr._ptr) {
            return *this;
        }

        if (this->_ptr) {
            (*this->_count) -- ;
            if (*this->_count == 0) {
                delete this->_ptr;
                delete this->_count;
            }
        }
        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count) ++ ;
        return *this;
    }

    // operator*重载
    T& operator*() {
        if (this->_ptr) {
            return *(this->_ptr);
        }
    }

    // operator->重载
    T* operator->() {
        if (this->_ptr) {
            return this->_ptr;
        }
    }

    // 析构函数
    ~shared_ptr() {
        (*this->_count) -- ;
        if (*this->_count == 0) {
            delete this->_ptr;
            delete this->_count;
        }
    }

    // 返回引用计数
    int use_count() {
        return *this->_count;
    }
};

unique_ptr

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
template<typename T>
class unique_ptr {
private:
    T* _ptr;

public:
    // 构造函数
    unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}
    // 析构函数
    ~unique_ptr() { del() };

    // 先释放资源(如果持有),再持有资源
    void reset(T* ptr) {
        del();
        _ptr = ptr;
    }

    // 返回资源,资源的释放由调用方处理
    T* release() {
        T* ptr = _ptr;
        _ptr = nullptr;
        return ptr;
    }

    // 获取资源,调用方应该只使用不释放,否则会两次delete资源
    T* get() {
        return _ptr;
    }

private:
    // 释放
    void del() {
        if (_ptr == nullptr) return;
        delete _ptr;
        _ptr = nullptr;
    }

    // 禁用拷贝构造
    unique_ptr(const unique_ptr &) = delete;

    // 禁用拷贝赋值
    unique_ptr& operator = (const unique_ptr &) = delete;
};
yitao 支付宝支付宝
yitao 微信微信
0%