tags:
- Cpp
Smart Pointers in C++
Do this at first: RAII and Scope in C++
在C++中,我们使用 new
和 delete
/delete[]
在堆上实例化和删除对象。当你使用裸指针时,同一个作用域中的 new
和 delete
的数量总是需要匹配的,不然就可能引起内存泄漏。
在使用裸指针删除对象时,你还需要注意类型的匹配(single-object form using delete
, array-object using delete[]
),不然就可能引起未定义行为。
此外,即使裸指针的使用天衣无缝,异常的发生也可能导致一些内存问题。(RAII)
尽管裸指针的使用强大且高效,但是人为地管理这些资源的释放是容不得一点粗心的。C++的标准库(C++11/C++14标准)中,我们有四种类型的智能指针,分别是:
auto_ptr
unique_ptr
auto_ptr
的替代品,独占所有权的智能指针。make_unique
工厂函数。shared_ptr
make_shared<T>
工厂函数。make_shared<T[]>
的支持。weak_ptr
这些智能指针通过对裸指针的包装,使得我们可以放心地申请资源而不担心发生任何的资源泄漏。这些智能指针会管理动态申请对象的生命周期,并在合适的适合销毁对象(包括异常事件)。
为了避免裸指针给我们带来的可能的内存泄漏。在编写 C++ 程序时,我们应当避免使用裸指针。
std::unique_ptr
: A Scoped Exclusive-Ownership Pointer当我们有使用智能指针的需求时,std::unique_ptr
该是我们第一个想到的。作为一种scoped pointer,如我们在 RAII and Scope in C++ 中展示的一样,std::unique_ptr
的大小和裸指针的大小是一样的。当你使用std::unique_ptr
时,所使用的指令和使用裸指针的指令是一样的。
std::unique_ptr
提供独占所有权的特性,也就是说在任何时候,std::unique_ptr
都能保证只能有一个指针拥有资源。因此我们不能进行复制/拷贝操作,只能实现移动操作。试想,如果我们对 std::unique_ptr
进行了复制,就会有两个拥有对象所有权的指针,这与其的设计初衷相悖。当 std::unique_ptr
移动时,所有权转移,原来的指针会丢失对资源的所有权(nullptr
),从而保持独占所有权的特性。
std::unique_ptr
移动的操作示例:
#include <memory>
// Create a unique pointer using make_unique factory function.
std::unique_ptr<Entity> uptr1 = std::make_unique<Entity>();
// or `std::unique_ptr<Entity> uptr1 = std::unique_ptr<Entity>(new Entity);`
// Change ownership to uptr2.
std::unique_ptr<Entity> uptr2 = std::move(upre1);
std::unique_ptr<T[]>
当你需要一个指向数组的 unique pointer 时,我们就要用到 std::unique_ptr<T[]>
。这种智能指针在析构时会调用 delete[]
,以正确释放数组内存。
std::unique_ptr<T, Deleter>
除了模板参数T
之外,std::unique_ptr
也一直有第二个模板参数,我们称之为Deleter
,即删除器。删除器的作用是什么呢?智能指针难道不应该接管一切么?智能指针让我们免于资源泄漏,但是我们需要删除器来释放这些资源。通过智能指针管理非常见的资源(非堆上资源)时,我们就需要定义一个自己的删除器。
如果不定义任何的Deleter,unique_ptr
就会使用默认的删除器std::default_delete
。默认的删除器负责对堆上动态资源进行释放。
template<class T, class Deleter = std::default_delete<T>>
class unique_ptr {
T *p_ = nullptr;
Deleter d_;
public:
explicit unique_ptr(T* p = nullptr, Deleter d = Deleter()) : p_(p), d_(d) {}
~unique_ptr() {
if (p_) d_(p_);
}
// Copy is not allowed.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// Object is movable.
unique_ptr(unique_ptr&& other) noexcept : p_(other.p_), d_(std::move(other.d_)) {
other.p_ = nullptr;
}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
if (p_) d_(p_);
p_ = other.p_;
d_ = std::move(other.d_);
other.p_ = nullptr;
}
return *this;
}
T* get() const { return p_; }
T* release() { T* tmp = p_; p_ = nullptr; return tmp; }
void reset(T* p = nullptr) {
if (p_ != p) {
if (p_) d_(p_);
p_ = p;
}
}
};
template<class T>
struct default_delete {
void operator()(T *p) const { delete p; }
};
如果我们想在作用域内释放内存,我们可以使用reset()
方法提前释放内存。
文件句柄、网络连接、设备句柄等这些资源可能并不是在堆上分配的,我们并不能用delete
删除这些资源。Deleter的作用就显现出来了,对那些不能delete的资源进行管理。智能指针可以通过自定义删除器来正确释放它们。假设我们有一个数据库连接:
struct DBConnection{
// All kinds of resources.
// All kinds of methods.
};
struct DBDeleter{
void operator()(DBConnection* db){
std::cout << "Closing Database Connection.\n";
dbClosing(db);
}
};
class DBManager{
std::unique_ptr<DBConnection, DBDeleter> dbConnect_;
public:
explicit DBManager(DBConnection* dbConnect) : dbConnect_(dbConnect){}
void query(const std::string& q){
if(dbConnect_){
db_query(dbConnection_.get(), q.c_str());
}
}
void close(){
dbConnection_.reset();
}
};
int main(){
DBConnection* db = dbOpen("example.db");
DBManager dbManager(db);
dbManager.query("SELECT * FROM Student");
// dbManager.close(); // <-Close the database now.
return 0;
} // <- Close the database by using DBDeleter.
std::shared_ptr
: A Reference Counted Pointerstd::shared_ptr
是最接近裸指针的智能指针。不同于上一节中的 std::unique_ptr
,使用 std::shared_ptr
创建的对象的所有权是由所有指向该对象的 std::shared_ptr
共享的。只有当最后一个 std::shared_ptr
停止指向对象时,对象的资源才会被释放。
std::shared_ptr
的这种共享特性是通过引用计数来实现的。通过引用计数,shared_ptr
就能知道当前有多少个指针在共享资源。这种对对象资源的控制使得 std::shared_ptr
需要额外的控制块来管理对象资源的释放。那么控制块放到哪里?
控制块通常会放在堆上,与对象一起分配。这样做的好处是确保控制块的生命周期与对象的生命周期一致。当所有指向对象的 shared_ptr
被销毁时(并且这时如果weak ref count = 0),控制块也会被自动释放。如果控制块放在栈上,当函数返回或栈帧被销毁时,控制块就会被销毁,而不管是否还有 shared_ptr
指向对象。这将导致引用计数失效,可能在某些情况下导致对象被提前销毁,或者造成资源泄漏。
因而,std::make_shared
工厂函数总会创建一个控制块,并且 std::shared_ptr
的大小是裸指针大小的两倍大(一个指针指向对象,一个指针指向控制块)。而且控制块的内存必须在堆上动态分配。当std::unique_ptr
转换为std::shared_ptr
时,构造函数也是会生成相应的控制块。
要实现引用计数,我们在构造函数中加入reference_count++;
,并在拷贝操作时做同样的事情来增加引用计数。并且要在引用计数为0时释放掉对象资源。在对引用计数(弱引用计数同)进行操作时,由于++
、--
是read-modify-write操作指令,所以对引用计数的操作必须是原子性的。
下面我们对std::shared_ptr
进行拷贝时,我们拷贝了ptr to T和ptr to control block,并使得引用计数增加1。但当我们进行移动操作时,我们不需要对引用计数进行操作,这是因为所有权转移了。
我们已经有一个指向对象的指针了,为什么控制块中还有指向执行对象的指针?假设我有这几个类
struct Fruit { int juice; };
struct Vegetable { int fiber; };
struct Tomato : Fruit, Vegetable { int sauce; };
当我创建了:
std::shared_ptr<Tomato> tomato = make_shared<Tomato>;
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> fruit = tomato;
当我们创建一个指向 Tomato
实例的 std::shared_ptr<Fruit>
或 std::shared_ptr<Vegetable>
,这些指针会指向 Tomato
对象内的 Fruit
或 Vegetable
部分。由于控制块仍然是共享的,它们最终管理的是同一个 Tomato
实例,所以控制块会指向这个完整的对象。
std::shared_ptr
和 std::unique_ptr
一样,std::shared_ptr
也使用 delete
作为其默认的资源销毁机制(Resource-destruction mechanism)。但是不同的是,每个 std::unique_ptr
独立持有对所指对象的控制权,而 std::shared_ptr
的控制权是共享的。对象资源的控制由控制块来完成。
这种由于控制权的不同,就使得std::unique_ptr
本身有权力来管理对象。所以std::unique_ptr
中,deleter是智能指针的一部分。
struct CustomDeleter {
void operator()(int* p) const {
delete p;
}
};
// Deleter type is part of the smart pointer.
std::unique_ptr<int, CustomDeleter> p1(new int);
而std::shared_ptr
不同,没有指针真正拥有对象的ownership,ownership由控制块管理管理。因此,在std::shared_ptr
中,deleter并不作为智能指针的一部分。而控制块又只有一个,所以即使我们有两个std::shared_ptr
,只要指向资源相同,Deleter就是相同的:
struct CustomDeleter {
void operator()(int* p) const {
delete p;
}
};
// Deleter is not part of the smart pointer.
std::shared_ptr<int> p(new int, CustomDeleter());
std::shared_ptr<int> p2 = p;
std::shared_ptr
Could Be Optimized当我们用工厂函数创建好std::shared_ptr
了之后,我们在堆上会存在两块空间,我们可以将这两块空间合并起来,以避免外部碎片的产生。大多数的库都支持下面这种实现方式。
如果你使用 make_shared
工厂函数来创建共享指针,那么就会优化成这样:
由于我们的控制块和对象资源在同一块内存块中,我们甚至能够将控制块中的指针优化掉。
std::weak_ptr
: A Non-Owning shared_ptr
在shared_ptr中,我们用ref count来控制对象的生命周期。然而,当两或多个shared_ptr
相互持有对方的shared_ptr
时,就可能发生循环引用,导致引用计数永远不会降到0,从而导致内存泄漏。为了避免循环引用的情况,我们可以使用weak_ptr
,它不会增加引用计数,允许对象正常销毁。
prevent dangling pointer
循环XX
shared_ptr
共享controlled object,因此其指向对象的生命周期由shared_ptr
所控制。而weak_ptr
之间共享控制块。当对象已经被销毁,如果这时weak ref count为0,控制块就会被销毁。
weak_ptr
的内存结构和 shared_ptr
是相同的。在学习shared_ptr
时,我们实际上并没有用到weak reference,而这个结构的存在和控制块的生命周期是息息相关的。weak_ptr
并不和shared_ptr
一样共享其指向的对象,也就是说,你并不能解引用一个weak_ptr
,你必须用lock()
方法先将其转换成一个shared_ptr
,这个方法是线程安全的。
#include <iostream>
#include <memory>
struct test {
int a = 10;
};
int main() {
std::shared_ptr<test> t1 = std::make_shared<test>();
std::weak_ptr<test> t2 = t1;
if (auto t2_shared = t2.lock()) {
std::cout << t1->a << " " << t2_shared->a << std::endl;
} else {
std::cout << "t2 is expired" << std::endl;
}
return 0;
}
std::enable_shared_from_this
CRTP