文章首发
【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针
前言
大家好,明天是【重学C++】系列的第二讲,咱们来聊聊C++的智能指针。
为什么须要智能指针
在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,须要咱们手动调配和开释。治理这些资源是个技术活,一不小心,就会导致内存透露。
咱们再给两段代码,切身体验下原生指针治理内存的噩梦。
void foo(int n) { int* ptr = new int(42); ... if (n > 5) { return; } ... delete ptr;}void other_fn(int* ptr) { ...};void bar() { int* ptr = new int(42); other_fn(ptr); // ptr == ?}
在foo
函数中,如果入参n
> 5, 则会导致指针ptr
的内存未被正确开释,从而导致内存透露。
在bar
函数中,咱们将指针ptr
传递给了另外一个函数other_fn
,咱们无奈确定other_fn
有没有开释ptr
内存,如果被开释了,那ptr
将成为一个悬空指针,bar
在后续还持续拜访它,会引发未定义行为,可能导致程序解体。
下面因为原生指针使用不当导致的内存透露、悬空指针问题都能够通过智能指针来轻松防止。
C++智能指针是一种用于治理动静分配内存的指针类。基于RAII设计理念,通过封装原生指针实现的。能够在资源(原生指针对应的对象)生命周期完结时主动开释内存。
C++规范库中,提供了两种最常见的智能指针类型,别离是std::unique_ptr
和 std::shared_ptr
。
接下来咱们别离具体开展介绍。
吃独食的unique_ptr
std::unique_ptr
是 C++11 引入的智能指针,用于治理动态分配的内存。每个 std::unique_ptr
实例都领有对其所蕴含对象的惟一所有权,并在其生命周期完结时主动开释对象。
创立unique_ptr
对象
咱们能够std::unique_ptr
的构造函数或std::make_unique
函数(C++14反对)来创立一个unique_ptr
对象,在超出作用域时,会主动开释所治理的对象内存。示例代码如下:
#include <memory>#include <iostream>class MyClass {public: MyClass() { std::cout << "MyClass constructed" << std::endl; } ~MyClass() { std::cout << "MyClass destroyed" << std::endl; }};int main() { std::unique_ptr<MyClass> ptr1(new MyClass); // C++14开始反对std::make_unique std::unique_ptr<int> ptr2 = std::make_unique<int>(10); return 0;}
代码输入:
MyClass constructedMyClass destroyed
拜访所治理的对象
咱们能够像应用原生指针的形式一样,拜访unique_ptr
所指向的对象。也能够通过get
函数获取到原生指针。
MyClass* naked_ptr = ptr1.get();std::cout << *ptr2 << std::endl; // 输入 10
开释/重置所治理的对象
应用reset函数能够开释
unique_ptr所治理的对象,并将其指针重置为
nullptr或指定的新指针。
reset`大略实现原理如下
template<class T> void unique_ptr<T>::reset(pointer ptr = pointer()) noexcept { // 开释指针指向的对象 delete ptr_; // 重置指针 ptr_ = ptr;}
该函数次要实现两件事:
- 开释
std::unique_ptr
所治理的对象,以防止内存透露。 - 将
std::unique_ptr
重置为nullptr
或治理另一个对象。
code show time:
#include <iostream>#include <memory>class MyClass {public: MyClass() { std::cout << "MyClass constructed" << std::endl; } ~MyClass() { std::cout << "MyClass destroyed" << std::endl; }};int main() { // 创立一个 std::unique_ptr 对象,指向一个 MyClass 对象 std::unique_ptr<MyClass> ptr(new MyClass); // 调用 reset,将 std::unique_ptr 重置为治理另一个 MyClass 对象 ptr.reset(new MyClass); return;}
挪动所有权
一个对象资源只能同时被一个unique_ptr
治理。当尝试把一个unique_ptr
间接赋值给另外一个unique_ptr
会编译报错。
#include <memory>int main() { std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = p1; // 编译报错 return 0;}
为了把一个 std::unique_ptr
对象的所有权挪动到另一个对象中,咱们必须配合std::move
挪动函数。
#include <memory>#include <iostream>int main() { std::unique_ptr<int> p1 = std::make_unique<int>(42); std::unique_ptr<int> p2 = std::move(p1); // ok std::cout << *p2 << std::endl; // 42 std::cout << (p1.get() == nullptr) << std::endl; // true return 0;}
这个例子中, 咱们把p1
通过std::move
将其治理对象的所有权转移给了p2
, 此时p2
接管了对象,而p1
不再领有治理对象的所有权,即无奈再操作到该对象了。
乐于分享的shared_ptr
shared_ptr
是C++11提供的另外一种常见的智能指针,与unique_ptr
独占对象形式不同,shared_ptr
是一种共享式智能指针,容许多个shared_ptr
指针独特领有同一个对象,采纳援用计数的形式来治理对象的生命周期。当所有的 shared_ptr
对象都销毁时,才会主动开释所治理的对象。
创立shared_ptr
对象
同样的,C++也提供了std::shared_ptr
构造函数和std::make_shared
函数来创立std::shared_ptr
对象。
#include <memory>int main() { std::shared_ptr<int> p1(new int(10)); std::shared_ptr<int> p2 = std::make_shared<int>(20); return;}
多个shared_ptr
共享一个对象
能够通过赋值操作实现多个shared_ptr
共享一个资源对象,例如
std::shared_ptr<int>p3 = p2;
shared_ptr
采纳援用计数的形式治理资源对象的生命周期,通过调配一个额定内存当计数器。
当一个新的shared_ptr被创立时,它对应的计数器被初始化为1。每当赋值给另外一个shared_ptr
共享同一个对象时,计数器值会加1。当某个shared_ptr
被销毁时,计数值会减1,当计数值变为0时,阐明没有任何shared_ptr
援用这个对象,会将对象进行回收。
C++提供了use_count
函数来获取std::shared_ptr
所治理对象的援用计数,例如
std::cout << "p1 use count: " << p1.use_count() << std::endl;
开释/重置所治理的对象
能够应用reset
函数来开释/重置shared_ptr
所治理的对象。大略实现原理如下(不思考并发场景)
void reset(T* ptr = nullptr) { if (ref_count != nullptr) { (*ref_count)--; if (*ref_count == 0) { delete data; delete ref_count; } } data = ptr; ref_count = (data == nullptr) ? nullptr : new size_t(1); }
data
指针来存储管理的资源,指针ref_count
来存储计数器的值。
在 reset 办法中,须要缩小计数器的值,如果计数器缩小后为 0,则须要开释治理的资源,如果缩小后不为0,则不会开释之前的资源对象。
如果reset指定了新的资源指针,则须要从新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为nullptr
shared_ptr应用注意事项
防止循环援用
因为 shared_ptr
具备共享同一个资源对象的能力,因而容易呈现循环援用的状况。例如:
struct Node { std::shared_ptr<Node> next; };int main() { std::shared_ptr<Node> node1(new Node); std::shared_ptr<Node> node2(new Node); node1->next = node2; node2->next = node1;}
在上述代码中,node1
和 node2
相互援用,在析构时会发现计数器的值不为0,不会开释所治理的对象,产生内存透露。
为了防止循环援用,能够将其中一个指针改为 weak_ptr
类型。weak_ptr
也是一种智能指针,通常配合shared_ptr
一起应用。
weak_ptr是一种弱援用,不对所指向的对象进行计数援用,也就是说,不减少所指对象的援用计数。当所有的shared_ptr
都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有weak_ptr
都会变成nullptr
,这时咱们就能够利用expired()
办法来判断这个weak_ptr
是否曾经生效。
咱们能够通过weak_ptr
的lock()
办法来取得一个指向共享对象的shared_ptr
。如果weak_ptr
曾经生效,lock()
办法将返回一个空的shared_ptr
。
上面是weak_ptr
的根本应用示例:
#include <iostream>#include <memory>int main() { std::shared_ptr<int> sp = std::make_shared<int>(42); // 创立shared_ptr对应的weak_ptr指针 std::weak_ptr<int> wp(sp); // 通过lock创立一个对应的shared_ptr if (auto p = wp.lock()) { std::cout << "shared_ptr value: " << *p << std::endl; std::cout << "shared_ptr use_count: " << p.use_count() << std::endl; } else { std::cout << "wp is expired" << std::endl; } // 开释shared_ptr指向的资源,此时weak_ptr生效 sp.reset(); std::cout << "wp is expired: " << wp.expired() << std::endl; return 0;}
代码输入如下
shared_ptr value: 42shared_ptr use_count: 2wp is expired: 1
回到shared_ptr
的循环援用问题,利用weak_ptr不会减少shared_ptr的援用计数的特点,咱们将Node.next的类型改为weak_ptr
, 防止node1和node2相互循环援用。批改后代码如下
struct Node {
std::weak_ptr<Node> next;
};
int main() {
std::shared_ptr<Node> node1(new Node);std::shared_ptr<Node> node2(new Node); node1->next = std::weak_ptr<Node>(node2); node2->next = std::weak_ptr<Node>(node1); ;
}
#### 防止裸指针与`shared_ptr`混用先看看以下代码
int* q = new int(9);
{
std::shared_ptr<int> p(new int(10));...q = p.get();
}
std::cout << *q << std::endl;
`get`函数返回 `std::shared_ptr` 所持有的指针,然而不会减少援用计数。所以在shared_ptr析构时,将该指针指向的对象给开释掉了,导致指针`q`变成一个悬空指针。#### 防止一个原始指针初始化多个`shared_ptr`
int* p = new int(10);
std::shared_ptr<int> ptr1(p);
// error: 两个shared_ptr指向同一个资源,会导致反复开释
std::shared_ptr<int> ptr2(p);
## 总结防止手动治理内存带来的繁琐和容易出错的问题。咱们明天介绍了三种智能指针:`unique_ptr`、`shared_ptr`和`weak_ptr`。每种智能指针都有各自的应用场景。`unique_ptr`用于治理独占式所有权的对象,它不能拷贝但能够挪动,是最轻量级和最快的智能指针。`shared_ptr`用于治理多个对象共享所有权的状况,它能够拷贝和挪动。`weak_ptr`则是用来解决`shared_ptr`循环援用的问题。下一节,咱们将本人入手,从零实现一个C++智能指针。敬请期待<center>- END -</center>【往期举荐】[01 C++如何进行内存资源管理](https://mp.weixin.qq.com/s?__biz=MzI4MTc0NDg2OQ==&mid=2247484850&idx=1&sn=56524ae728e1968ce38a7a210430ddf5&chksm=eba5c138dcd2482e4cc50983ed93a9dcbfc03f495603d11f0efb7c4ae349e843d3230db4bccc&token=2022782435&lang=zh_CN#rd)