一、 引入智能指针的目的【内存泄漏】
- 说起智能指针,首先想到的是为什么要引入智能指针?裸指针有什么问题?来看下面一段代码
for(int i=0; i<100; i++)
char* p = new char[100];
- 在这段代码中,我们通过
new
分配了10000个字节的堆内存空间。但由于作用域的限制,这10000个字节无法被其它地方使用,也没有被释放,就造成了内存泄漏。- 所谓内存泄漏,本质上就是指针变量与其指向的资源不一定会同时被释放。那么如何才能保证指针变量与其指向的资源被同时释放呢?有聪明的程序员想到,如果某块资源没有任何指针指向它,就应该被释放。这也就引入了用引用计数的方式来实现智能指针。[1]
- 引用计数实际比较容易理解,就是指向资源的指针数量,如果有新增指针指向这块资源引用计数就加1,如果有指向这块资源的指针被释放引用计数就减1。当引用计数为0时就意味着没有任何指针指向它,就被释放掉。这样的指针也就被称为智能指针。
- 从
C++11
开始引入了三种常见的智能指针,分别是std::shared_ptr
,std::weak_ptr
,std::unique_ptr
,接下来将分别介绍一下这三种智能指针。
二、std::shared_ptr
- 先来看一段代码,来自cppreference [2]
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
struct Base
{
Base() { std::cout << " Base::Base()\n"; }
// Note: non-virtual destructor is OK here
~Base() { std::cout << " Base::~Base()\n"; }
};
struct Derived: public Base
{
Derived() { std::cout << " Derived::Derived()\n"; }
~Derived() { std::cout << " Derived::~Derived()\n"; }
};
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Base> lp = p; // thread-safe, even though the
// shared use_count is incremented
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get()
<< ", lp.use_count() = " << lp.use_count() << '\n';
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
std::cout << "Created a shared Derived (as a pointer to Base)\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // release ownership from main
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";
}
- 我们来简单分析一下这段代码,
main
函数中先创建了一个指向子类对象的基类指针p
【std::make_shared
可以代替new
,通过get
方法可以拿到裸指针,use_count()
返回该对象的引用计数】所以第一段输出为:
Base::Base()
Derived::Derived()
Created a shared Derived (as a pointer to Base)
p.get() = 0x100200018, p.use_count() = 1
- 接下来创建了三个线程对象, 分别是
t1
,t2
,t3
。这个例子实际上是为了解释std::shared_ptr
是线程安全的,原因是std::shared_ptr
的引用计数是原子操作的。【reset()
可用来减少一个引用计数】所以第二段的输出如下:
Shared ownership between 3 threads and released
ownership from main:
p.get() = 0x0, p.use_count() = 0
local pointer in a thread:
lp.get() = 0x100200018, lp.use_count() = 6
local pointer in a thread:
lp.get() = 0x100200018, lp.use_count() = 4
local pointer in a thread:
lp.get() = 0x100200018, lp.use_count() = 2
- 这里稍微多解释下,为什么
lp.use_count()
的结果是6,4,2【其实还可能会有其它的结果】?因为构建三个线程使得p
的引用计数为3,三个线程的lp
又指向p
,所以lp
最大值为6,每次离开作用域后引用计数减2,所以这里的结果是6,4,2。- 最后是析构,当最后一个线程被
delete
时引用计数为0因此发生析构,输出如下:
Derived::~Derived()
Base::~Base()
All threads completed, the last one deleted Derived
三、std::weak_ptr
- 同样先来看一段代码,来自现代C++教程:高速上手C++11/14/17/20 [3]
#include <iostream>
#include <memory>
struct A;
struct B;
struct A {
std::shared_ptr<B> pointer;
~A() {
std::cout << "A has been destroyed" << std::endl;
}
};
struct B {
std::shared_ptr<A> pointer;
~B() {
std::cout << "B has been destroyed" << std::endl;
}
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->pointer = b;
b->pointer = a;
}
- 在这段代码中,由于A,B的内部又有
pointer
智能指针相互引用,导致最终a,b被销毁后,智能指针的引用计数不为0,所以依然无法被析构。如下图所示:
- 解决这个问题的办法就是将
shared_ptr
替换成weak_ptr
,因为shared_ptr
是强引用,而weak_ptr
是弱引用,弱引用不会引起引用计数的增加。如下图所示:
四、std::unique_ptr
- 相比于前两种智能指针而言,
std::unique_ptr
的特性是独占资源,不允许和其它指针共享。但是std::unique_ptr
支持移动语义,即std::move
。来看以下一段代码,来自learncpp [4]
#include <iostream>
#include <memory>
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
std::unique_ptr<Resource> res1(std::make_unique<Resource>());
// std::unique_ptr<Resource> res2 = res1; // 非法,因为拷贝构造被delete了
std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
std::unique_ptr<Resource> res2(std::move(res1));
std::cout << "res1 is " << (static_cast<bool>(res1) ? "not null\n" : "null\n");
std::cout << "res2 is " << (static_cast<bool>(res2) ? "not null\n" : "null\n");
return 0;
}
- 首先要注意的是和
std::make_shared
不同,std::make_unique
是在C++14
实现的,据说是因为C++标准委员会忘了...[3]- 其次简单分析下代码可知,
res1
指向了一块资源,此时的res1
不为空。但是std::move
将其移动给了res2
,此时res1
为空,res2
不为空。输出结果如下:
Resource acquired
res1 is not null
res1 is null
res2 is not null
Resource destroyed
下一篇:C++技能点之智能指针(二)
参考
[1] C++ 智能指针:原理与实现【这篇写的很好】
[2] cppreference
[3] 现代C++教程:高速上手C++11/14/17/20【C++的新特性通俗易懂】
[4] learncpp