这是 C++ 网课学习笔记

建议先阅读上一节 右值引用 & 移动语义 或对内容有一定了解后再来阅读本篇笔记

 

1 引入: 为什么我们需要智能指针

1.1 问题一

观察以下代码

void science(double* data, int N) {
double* temp = new double[N*2];
do_setup(data, temp, N);
if (!needed(data, temp, N))
return;
calculate(data, temp, N);
delete[] temp;
}

不难理解, 函数 science() 接受两个参数, 一个表示数组 data 的首元素地址, 一个表示数组的长度

这说明了一个数组类型包含了三个信息: 数组元素类型, 首元素地址和长度; 然而当我们把数组作为指针传入时, 我们却丧失了长度这个信息, 只能用一个额外的 N 储存这个信息

回到函数体本身, 这个函数实现的功能很简单, 假如 needed() 为真, 则对 data 调用 calculate(). 但是这个函数却有一个很大很大的缺陷, 假如 needed() 为假, 我们 new 出来的 temp 指针所指向的地址的内容就无法被释放, 成为垃圾了

这是传统指针遇到的第一个问题, 我们无法通过指针类型判断一个指针应该在什么时候被释放

 

1.2 问题二

float* science(float* x, float* y, int N) {
float* z = new float[N];
saxpy(2.5, x, y, z, N);
delete[] x;
delete[] y;
return z;
}

这个函数也不难理解, 我们通过 x 和 y 计算出了 z, 同时 x 和 y 在函数内部就被释放了, 而 z 被保留, 这些东西都需要用户自己清楚, 很可能导致 UB

这是传统指针遇到的第二个问题, 传统指针是用户不友好的, 很多信息你只能在代码层面解读, 而无法从接口层告知

 

1.3 指针包含了太多信息

  1. 对于单个对象或数组

    • 单个对象: 我们使用 new 和 delete, 不支持 ++, – 或 []
    • 数组: 我们使用 new[] 和 delete[], 支持 ++, – 或 []

    然而不管指向什么东西的指针, 他们都长一个样: T*, 但是我们却需要根据其指向对象的类型的不同使用不同的操作, 这显然是不友好的

  2. 所有权

    • 所有者必须在使用完成后及时释放内存
    • 非所有者无权释放内存

    防止没有释放导致浪费或重复释放导致出错

  3. 是否可以为空

正是因为指针包括了很多很多无法简单被类型描述的特征, 这些特征又是刚需的, 所以我们需求一种 智能 的指针

  • 他知道使用 delete / delete[]
  • 生命周期结束后自动销毁
  • 是否可以为空指针

其实, STL 容器就包含了这种 智能 , 我们只负责调用某个容器, 如 string, 而并不关心它什么时候销毁, 或者是否可以为空和销毁方式

 

2 垃圾回收

回收不可达的对象

2.1 对象的可达性

首先第一个问题就是, 如何判断一个对象的可达性?

  • 维护一个 reference counting, 每有一个指针指向这个对象, 就 ++, 否则就 –
  • 周期运行 mark-and-sweep (from root set), 每次运行的时候, 通过递归的方式标记有哪些内存是可访问的, 回收其他所有不可达的内存

 

2.2 回收多少内存

第二个问题是, 回收多少内存空间

C++ 并不是一个类型安全的语言

  • 例如一个 void* 类型指针指向某个地址, 当我们回收它时, 我们并不知道具体要回收多少内存

 

3 std::unique_ptr

#include<memory>

unique_ptr 假设自己是对象唯一的所有者(由用户保证), 这样就能保证 unique_ptr 在析构的时候可以直接释放, 不用考虑其他指针

正因如此, unique_ptr 无法被拷贝, 只能移动

std::unique_ptr<T> p(new T(/* some parameters */)), q;
p->member();
q = std::move(p); // legal behaviour
q = p; // illegal behaviour

举个栗子

WidgetBase* create_widget(InputType);

class MyClass {
std::unique_ptr<WidgetBase> owner;
public:
MyClass(InputType inputs)
: owner(create_widget(inputs)) { }
~MyClass() = default;
// ... member functions that use owner-> ...
};

在这个代码段里面, 我们定义了一个 MyClass 类, 拥有一个私有成员 owner, 类型是 unique_ptr

MyClass 的构造函数利用了一个返回值为 WidgetBase* 类型的辅助函数 create_widget(), 为 owner 提供具体的构造实现

值得注意的是, 在析构函数里面, 我们只用 ~MyClass() = default;, 而无需自己做内存释放, 这是因为一旦自己的生命周期结束, 作为 unique_ptr 的成员变量 owner 就会自己消亡, 递归的 delete / delete[] 属于 owner 的内存

因为 unique_ptr 的不可拷贝的特点, MyClass 将无法拥有拷贝构造函数

 

3.1 实现思路

成员类型

template <typename T>
struct unique_ptr {
// ...
using element_type = T; // 指向对象的类型
using pointer = T*; // 本身的指针类型
// ...
};

构造与析构

template <typename T>
class unique_ptr {
T* ptr;
public:
unique_ptr() noexcept : ptr(nullptr) { } // 说明 unique_ptr 是可为空的
explicit unique_ptr(T* p) noexcept : ptr(p) { } // 假如接受了一个 T*, 则利用 T* 去构造 ptr
~unique_ptr() noexcept { delete ptr; } // 析构函数
// ...
};

移动构造与移动赋值

template <typename T> struct unique_ptr {
// ...
unique_ptr(unique_ptr const&) = delete;
unique_ptr& operator=(unique_ptr const&) = delete;
// 拷贝构造和拷贝赋值均被删除了

unique_ptr(unique_ptr&& o) noexcept
: ptr(std::exchange(o.ptr, nullptr)) { }
// 移动构造就是交换接收到的 unique_ptr&& 和目标的 ptr
unique_ptr& operator=(unique_ptr&& o) noexcept {
delete ptr; // 回收被赋值指针的 ptr
ptr = o.ptr; // 做交换
o.ptr = nullptr;
return *this; // 返回本身的引用
}
// ...
};

重载运算符

template <typename T>
struct unique_ptr {
// ...
T& operator*() const noexcept {
return *ptr;
}
T* operator->() const noexcept {
return ptr;
}
// ...
};

其他成员函数

template <typename T> struct unique_ptr {
// 释放 ptr, 并返回一个指向 ptr 指向的对象的指针 old
T* release() noexcept {
T* old = ptr;
ptr = nullptr;
return old;
}
// 重置 ptr 为 p
void reset(T* p = nullptr) noexcept {
delete ptr;
ptr = p;
}
// 获得指针 ptr
T* get() const noexcept {
return ptr;
}
// 判断是否为空指针
explicit operator bool() const noexcept {
return ptr != nullptr;
}
};

实际上, unique_ptr 就是它的成员 ptr 的一个包装, 通过特定的基本函数和运算符重载使他具有一定的特性–智能

 

3.2 std::make_unique()

回顾之前的一段实例代码

std::unique_ptr<T> p(new T(/* some parameters */)), q;
p->member();
q = std::move(p); // legal behaviour
q = p; // illegal behaviour

我们发现一个很别扭的事情: 有 new, 却没有 delete

这种不美观的事情 C++ 是不愿意看到的, 因此 C++11 引入了 make_unique() 函数把 std::unique_ptr<T> p(new T(/* some parameters */)), q; 改写为:

auto p = std::make_unique<T>(/* some parameters */);

NO MORE RAW NEW!!!

make_unique 内部实现如下:

template <typename T, typename... Args>
unique_ptr<T> make_unique(Args&&... args);

类似于完美转发

 

3.3 数组类型

事实上, std::unique_ptr 也为数组类型做出了偏特化 std::unique_otr<T[]>

  • 在析构函数中, 默认使用 delete[], 而不是 delete
  • 提供了 operator[]

同时, std::make_unique 也为数组类型做了偏特化

  • 接受的参数是数组大小而非构造函数参数

有了这么多工具, 我们就可以对

void science(double* data, int N) {
double* temp = new double[N*2];
do_setup(data, temp, N);
if (!needed(data, temp, N))
return;
calculate(data, temp, N);
delete[] temp;
}

进行升级了:

void science(double* data, int N) {
auto temp = std::make_unique<double[]>(N * 2); // duoble[] 说明这是一个 double 类型的数组指针

// 利用 temp.get() 代替原来的 temp, 不改变 do_setup 的接口
do_setup(data, temp.get(), N);
if (!needed(data, temp.get(), N))
return;
calculate(data, temp.get(), N);
// 不论是如何退出函数, temp 都会被正常析构
}

 

3.4 所有权转移

由于 unique_ptr 的所有权是 unique 的, 所以, 我们可以通过上一节中学习的移动构造或移动赋值函数完成所有权的转移

auto a = std::make_unique<T>();
// ...
std::unique_ptr<T> b{ std::move(a) }; // 利用移动语义
// ...
a = std::move(b);

回到之前被诟病的代码:

float* science(float* x, float* y, int N) {
float* z = new float[N];
saxpy(2.5, x, y, z, N);
delete[] x;
delete[] y;
return z;
}

我们之前认为其无法清晰的向用户表达信息的转移和变化, 但是智能指针可以, 升级如下:

std::unique_ptr<float[]> science(
std::unique_ptr<float[]> x,
std::unique_ptr<float[]> y, int N) {
auto z = std::make_unique<float[]>(N);
saxpy(2.5, x.get(), y.get(), z.get(), N);
return z;
}

int main(){
// ...
auto result = science(std::move(upx), std::move(upy), N);
// ...
}

这样, 在用户调用这个函数的时候, 就能清晰的知道, upxupy 作为参数, 只能用右值引用的方式赋予, 用户一下就明白这个所有权实际上交接给了函数内部

需要给一个函数传递所有权时或需要从一个函数返回所有权时,按值传递 unique_ptr

unique_ptr<widget> factory();               // 返回所有权
void sink( unique_ptr<widget> ); // 交付所有权
void reseat( unique_ptr<widget>& ); // 可能要 reset ptr
void thinko( const unique_ptr<widget>& ); // 一般不使用

不是所有地方都要用智能指针的, 在不涉及所有权转移的地方, 可以继续使用指针和引用

 

4 std::shared_ptr

顾名思义, 这是可以共享的 unique_ptr

#include<memory>

shared_ptr 是对象的所有者(但不唯一), 当且仅当指向该对象的最后一个 shared_ptr 不再指向它时, 对象被释放

可以被拷贝!

 

4.1 接口

具体实现比较麻烦, 略去不讲

template <typename T>
struct shared_ptr {
// ...
shared_ptr() noexcept; // Creates empty shared_ptr
explicit shared_ptr(T*); // Starts managing an object
~shared_ptr() noexcept; // Decrements count, and ...
// Cleanup if count == 0
// ...
};

拷贝构造和移动构造

template <typename T>
struct shared_ptr {
// ...
shared_ptr(shared_ptr const&) noexcept; // copy ptrs, count++
shared_ptr(shared_ptr&&) noexcept; // transfer ownership
shared_ptr(unique_ptr<T>&&); // transfer ownership

// origin count will decrease, possibly cleanup:
shared_ptr& operator=(shared_ptr const&) noexcept;
shared_ptr& operator=(shared_ptr&&) noexcept;
shared_ptr& operator=(unique_ptr<T>&&);
// ...
};

其他成员函数

template <typename T>
struct shared_ptr {
// ...
T& operator*() const noexcept;
T* operator->() const noexcept;

void reset(T*);
T* get() const noexcept;
long use_count() const noexcept; // 获取管理的对象被指向数
explicit operator bool() const noexcept;
// ...
};

 

4.2 std::make_shared()

template <typename T, typename... Args>
shared_ptr<T> make_shared(Args&&... args);

减少一次内存申请, 一次性申请 Tcount 的内存空间

 

4.3 所有权转移

shared_ptr<widget> factory();               // source + shared ownership 
void share( shared_ptr<widget> ); // share: "will" retain refcount
void reseat( shared_ptr<widget>& ); // "will" or "might" reseat ptr
void may_share( const shared_ptr<widget>& );// "might" retain refcount