智能指针

智能指针特点

智能指针封装了裸指针, 内部还是裸指针的调用

智能指针使用RAII特点, 将对象生命周期使用栈来管理

智能指针区分了所有权, 因此使用责任更为清晰

智能指针大量使用操作符重载和函数内联特点, 调用成本和裸指针无差别

unique_ptr

unique_ptr被设计称为一个零额外开销的智能指针, 使用它, 应该相比于手写new和delete没有额外开销, 不管是时间还是空间上.

独占拥有权, 不支持移动拷贝, 只支持移动(所有权转移)

shared_ptr

  • 共享所有权
  • 存储成本较裸指针多一个引用计数指针
  • 接口慎用(会导致蔓延问题)
  • 线程安全, 引用技术增减会减慢多核性能
  • 支持拷贝构造, 支持移动

unique_ptr使用起来很困难的原因, 在于unique_ptr解决重复释放的方式是禁止拷贝, 这样虽然有效率搞的优势, 但倒是使用困难, 容易犯错等.

相比之下, 牺牲效率换来自由度的shared_ptr则允许拷贝, 它解决重复释放的方式是通过引用计数

  • 当一个shared_ptr初始化时, 将计数器设为1
  • 当一个shared_ptr被拷贝时, 计数器加1
  • 当一个shared_ptr被解构时, 计数器减1. 减到0时, 则自动销毁它指向的对象.
  • 从而可以保证, 只要还有存在哪怕一个指针指向该对象, 就不会被解构.

weak_ptr

有时候我们希望维护shared_ptr的弱引用weak_ptr, 即: 弱引用的拷贝与解构不影响其引用计数器

之后有需要时, 可以通过lock()随机产生一个新的shared_ptr作为强引用. 但不locak的时候不影响计数

如果失效(计数器归零)则expired()会返回false, 且lock()也会返回nullptr

缺点:

  • shared_ptr需要维护一个atomic的引用计数器, 效率低, 需要额外的一块管理内存, 访问实际对象需要二级指针, 而且deleter使用了类型擦除技术.
  • 全部使用shared_ptr, 可能出现循环引用之类的问题, 导致内存泄漏, 依然需要使用不影响计数的原始指针或者weak_ptr来避免 例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>

struct{
std::shared_ptr<C> m_child;
std::shared_ptr<C> m_parent;
};

int main(){
auto parent = std::make_shared<C>();
auto child = std::make_shared<C>();

// 建立相互引用
parent->m_child = child;
child->m_parent = parent;

parent = nullptr; // parent 不会被释放, 因为child还指向它
child = nullptr; // child 也不会被释放, 因为parent还指向它

return 0;

}

循环引用

多个指针彼此指向对方

解决方案1:

只需要把其中逻辑上”不具有所属权”的哪一个改成weak_ptr即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>

struct{
std::shared_ptr<C> m_child;
C* m_parent;
};

int main(){
auto parent = std::make_shared<C>();
auto child = std::make_shared<C>();

// 建立相互引用
parent->m_child = child;
child->m_parent = parent.get();

parent = nullptr; // parent 会被释放, 因为child指向它的是原始指针
child = nullptr; // child 会被释放, 因为child的parent已经释放了

return 0;

}

解决方案2(续):

甚至还可以更好! 刚才提到unique_ptr的应用场景是”当该对象仅仅属于我时”. 既然都用了原始指针(假定它释放前我必然被释放). 因此我们完全可以把m_child变成一个标志这”完全所有权”的unique_ptr

这样也不需要shared_ptr维护一个原子计数器的开销了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>

struct{
std::unique_ptr<C> m_child;
C* m_parent;
};

int main(){
auto parent = std::make_unique<C>();
auto child = std::make_unique<C>();

// 建立相互引用
parent->m_child = std::move(child);
child->m_parent = parent.get();

parent = nullptr; // parent 会被释放, 因为child还指向它的是原始指针
// 此时child 也已经被释放了, 因为child完全属于parent

return 0;

}