This post has been posted on The Introduction of C++ Smart Pointers, using github.io can get a better experience.
NOTE: There are some typos and errors may not be fixed in the post below, please read the post on github.io to get the latest version of this post.
引言
为了充分利用RAII
思想,C++ 11
开始引入了智能指针,本文介绍RAII
以及三种智能指针:
std::unique_ptr
std::shared_ptr
std::weak_ptr
除此之外,本文还会介绍智能指针的常用创建方法:
std::make_unique
std::make_shared
RAII
RAII
指的是Resources Acquisition Is Initialization
,其是一种C++
编程思想,指的是在初始化的时候就完成资源的分配,而在析构的时候自动释放资源。
在C++
中有很多RAII
思想的体现:
- 多线程中自动获取与释放锁对象
std::unique_lock
等; - 内存的自动申请与释放,智能指针;
- 自动执行
join
的std::jthread
等。
RAII
的目的是为了更好的组织代码,减少程序员犯错的可能。例如程序员可能忘记释放已经申请的内存或者锁,而利用RAII
在对象析构的时候会自动进行资源的释放。
智能指针
智能指针是用于实现内存资源RAII
的相关类型。
在我们需要在堆上进行内存申请的时候,我们往往会通过new
关键字来进行内存的申请:
class Base {
public:
int num{10};
Base() {}
~Base() { std::cout << "~Base()" << std::endl; }
};
Base *b = new Base;
delete b;
而对于申请的内存我们需要通过delete
进行释放以防止内存泄漏。
但是在实际开发过程中,我们很有可能忘记释放掉申请的内存,例如:
// memory leakage.
int test() {
Base *b = new Base;
if (!check()) { // do some check, but failed.
// delete b; // this may be forgotten easily.
return -1;
}
delete b;
return 0;
}
我们对于正常的情况记住了释放内存,但在出错的时候,可能就忘记释放内存了。
而智能指针能够有效的防止上面的情况发生。
std::unique_ptr
对于上面的代码我们完全可以使用std::unique_ptr
替换原始指针:
int test()
{
std::unique_ptr<Base> b(new Base);
if (!check()) { // do some check, but failed.
return -1;
}
return 0;
}
这样上面的代码不论如何只要在b
析构的时候便会释放掉申请的内存,执行上面的代码可以看到成功输出~Base()
。
智能指针的使用方式与普通的指针一样,通过*
和->
可以对指针进行解引用和获取成员变量的值:
std::unique_ptr<Base> b(new Base);
std::unique_ptr<int> p(new int);
*p = 10;
std::cout << *p << std::endl;
std::cout << b->num << std::endl;
通过智能指针也可以创建数组:
std::unique_ptr<int[]> p(new int[10]);
for (int i = 0; i < 10; i++) {
p[i] = i;
}
for (int i = 0; i < 10; i++) {
std::cout << p[i] << std::endl;
}
智能指针同样可以直接绑定到一个原始指针上面,不过需要注意的如果智能指针声明周期结束,那么原始的指针则变成了野指针:
int *p = new int{0};
{
std::unique_ptr<int> up(p);
std::cout << *up << std::endl;
}
// we cannot use p here, it's a dangling pointer.
// the dereference of a dangling pointer is a UB.
与指针相同,智能指针也能用来表现多态:
class Base {
public:
Base() { std::cout << "Base()" << std::endl; }
virtual ~Base() { std::cout << "~Base()" << std::endl; }
virtual void test() { std::cout << "Base test()" << std::endl; }
};
class Derived : public Base {
public:
void test() { std::cout << "Derived test()" << std::endl; }
};
std::unique_ptr<Base> base(new Derived);
base->test(); // "Derived test()"
return 0;
需要注意的是,不能有两个unique_ptr
绑定了同一块地址上的内存(所以std::unique_ptr
只支持移动语义,而不支持拷贝语义),同时注意不要让unique_ptr
去绑定栈上的内存(当然如果非要这样做的话,也可以把默认的deleter
给替换掉即可,但是这样并没有什么意义)。
第二个模板参数
智能指针也能接收第二个模板参数,其类型是一个可执行对象,同时接收一个指针作为参数,用于释放指针的资源。
默认的deleter
:如果第一个模板参数类型不是数组类型(即第一个模板参数不含有[]
),那么默认的deleter
通过delete
关键字进行释放内存;如果第第一个模板参数是数组类型(即第一个模板参数含有[]
)那么默认的deleter
通过调用delete []
进行内存释放。
由于默认deleter
的行为,这也是为什么不能传入指向栈上内存的指针的原因(而且栈上的内存会自动释放,也不需要使用只能指针来管理)。
高级用法 自定义RAII
智能指针除了能够实现对于指针的智能管理之外,其同样可以对于任意的需要申请以及释放的资源进行智能管理。
这里给出官网的打开文件的例子:
void close_file(std::FILE* fp) {
std::fclose(fp);
std::cout << "File closed" << std::endl;
}
{
using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;
// make sure there is demo.txt in current directory.
// otherwise the fp is nullptr
unique_file_t fp(std::fopen("demo.txt", "r"), &close_file);
} // here fp is finalized, so the close_file() will be called.
上面通过自定义deleter
通过对打开文件的自动关闭。
std::shared_ptr
std::shared_ptr
也是智能指针,其与std::unique_ptr
的一个不同是:可以有多个std::shared_ptr
与同一个地址进行绑定。其常常用于多线程。
在std::shared_ptr
中保存着一个引用计数,用来表示当前的地址绑定到了多少个std::shared_ptr
对象上,每有一个指向相同地址的std::shared_ptr
对象被创建(需要保证从一个std::shared_ptr
拷贝过来),其引用计数便会增加1
,当被析构时,其引用计数会减少1
,而引用计数减少到0
的时候,指向的资源便会被deleter
释放(默认deleter
与std::unique_ptr
中相同)。
下面给出一个例子:
int *p = new int;
std::shared_ptr<int> sp1(p);
{
// this is wrong, when we bind p with a shared_ptr, its ref_count is 1.
// so this will cause double free.
// only copy from a shared_ptr can make the ref_count increase correctly.
// sdt::shared_ptr<int> sp2(p);
std::shared_ptr<int> sp2(sp1);
std::cout << sp2.use_count() << std::endl; // 2
std::cout << sp1.use_count() << std::endl; // 2
}
std::cout << sp1.use_count() << std::endl; // 1
由于std::shared_ptr
本来是为多线程设计的,因此其保证了其内部的函数均为线程安全的,也就是use_count
等函数不会出现不一致的问题。但是对于指针指向的数据的操作在多线程中往往需要额外的手段实现同步。
std::weak_ptr
std::weak_ptr
严格意义上来讲并不是一个指针,其更像是一种弱引用,其可以延长数据的生命周期。
std::weak_ptr
通常通过一个std::shared_ptr
对象创建而来或者通过一个std::weak_ptr
对象拷贝而来。
当通过一个std::sahred_ptr
对象创建而来的时候,其并不会增加引用计数,例如下面的例子:
std::shared_ptr<Base> sp(new Base);
std::weak_ptr<Base> wp = sp;
std::cout << sp.use_count() << std::endl; // 1
std::cout << wp.use_count() << std::endl; // 1
可以通过std::weak_ptr
延长声明周期指的是std::weak_ptr::lock
方法能够创建一个新的std::shared_ptr
对象(如果内存还没被释放)此时引用计数均会增加1
:
std::shared_ptr<Base> sp(new Base);
std::weak_ptr<Base> wp = sp;
std::shared_ptr<Base> newSp = wp.lock();
std::cout << newSp.use_count() << std::endl; // 2
std::cout << wp.use_count() << std::endl; // 2
std::cout << sp.use_count() << std::endl; // 2
由于此时已经有新的std::shared_ptr
产生,那么将原来而std::shared_ptr
释放后,并不会释放资源:
sp.reset();
std::cout << newSp.use_count() << std::endl; // 1
std::cout << wp.use_count() << std::endl; // 1
而对于lock
方法如果在lock
的时候,资源已经被释放了,那么此时创建的std::shared_ptr
对象与nullptr
绑定,因此使用lock
之后我们往往需要先判断资源是否已经在lock
之前被释放:
std::shared_ptr<Base> sp(new Base);
std::weak_ptr<Base> wp = sp;
sp.reset(); // "~Base()"
std::shared_ptr<Base> newSp = wp.lock(); // newSp is bind with nullptr;
if (newSp != nullptr) {
// do something...
}
std::cout << sp.use_count() << std::endl; // 0
正如lock
函数的名字一样,其同样是线程安全的,其线程安全指的是:如果lock
返回的std::shared_ptr
对象并不是与nullptr
绑定,那么保证此时资源没有被释放,如果返回的std::shared_ptr
与nullptr
绑定,那么保证此时资源已经被释放。同样地,对于数据的访问依然需要通过锁或者其他手段实现同步。
更为方便的创建方式
在之前介绍的智能指针中还是需要使用到new
关键字,但是却没有使用delete
关键字,这很不符合RAII
,于是有了这两个函数的实现,能够完全脱离new
关键字进行智能指针的创建。
std::make_unique
C++ 14
开始支持。
使用方法非常简单,只需要要通过模板参数传入类型和构造器参数:
class Base {
public:
int num1;
int num2;
Base() = default;
Base(int i, int j) : num1(i), num2(j) {}
};
std::unique_ptr<Base> up = std::make_unique<Base>(1, 2); // new Base(1, 2);
// create an array.
// only one parameter is OK, the parameter is the size of the array.
// make sure that the Base() constructor exists.
std::unique_ptr<Base[]> upArray = std::make_unique<Base[]>(3); // new Base[3];
std::make_shared
C++ 11
开始支持。
该方法使用于std::make_unique
一样,只是返回的是std::shared_ptr
,故此处不在赘述。
One Funny Thing
std::make_unique
在C++ 14
才开始支持,而std::make_shared
在C++ 11
就已经支持了。据说是因为当时作者给搞忘了。
参考
std::unique_ptr cppreference
std::shared_ptr cppreference
std::weak_ptr cppreference
std::make_unique cppreference
std::make_shared cppreference