智能指针是C++11引入的新特性,因为C++使用内存的时候很容易出现内存泄漏、野指针、悬空指针的问题,所以C++11引入了智能指针来管理内存。
这篇文章会详细介绍了三种智能指针:std::unique_ptr、std::shared_ptr和std::weak_ptr的原理、使用方法和适用场景。
目录
std::shared_ptr
共享式指针,同一时刻可以有多个指针指向一个对象。
shared_ptr对象一旦被销毁,或者其值通过赋值操作或显式调用shared_ptr::reset而发生变化,就会立即释放它们共同拥有的对象的所有权。
shared_ptr指针的对象有获取指针所有权以及共享该指针所有权的能力,一旦它们获取了所有权,当最后一个所有者释放它时,指针的所有者组负责释放掉它。
我们先来看一下官方的构造函数
构造函数
能看超过三分钟都是神人了,我们还是来看几个实例吧!
#include <iostream>
#include <memory>
struct C {int* data;};
int main () {
std::shared_ptr<int> p1;
std::shared_ptr<int> p2 (nullptr);
std::shared_ptr<int> p3 (new int);
std::shared_ptr<int> p4 (new int, std::default_delete<int>());
std::shared_ptr<int> p5 (new int, [](int* p){delete p;}, std::allocator<int>());
std::shared_ptr<int> p6 (p5);
std::shared_ptr<int> p7 (std::move(p6));
std::shared_ptr<int> p8 (std::unique_ptr<int>(new int));
std::shared_ptr<C> obj (new C);
std::cout << "use_count:\n";
std::cout << "p1: " << p1.use_count() << '\n';
std::cout << "p2: " << p2.use_count() << '\n';
std::cout << "p3: " << p3.use_count() << '\n';
std::cout << "p4: " << p4.use_count() << '\n';
std::cout << "p5: " << p5.use_count() << '\n';
std::cout << "p6: " << p6.use_count() << '\n';
std::cout << "p7: " << p7.use_count() << '\n';
std::cout << "p8: " << p8.use_count() << '\n';
return 0;
}

p1 是一个默认构造的 std::shared_ptr,它没有管理任何对象。因此,其引用计数为 0。
p2 是通过一个指向 nullptr 的构造函数创建的 std::shared_ptr。虽然它被显式地初始化为空,但它仍然是一个没有管理任何实际对象的 shared_ptr,因此引用计数为 0。
p3 管理一个新分配的 int 对象。因为它是唯一一个管理该对象的 shared_ptr,所以引用计数为 1。
p4 管理一个新分配的 int 对象,并显式地指定了 std::default_delete<int>() 作为删除器。尽管提供了自定义删除器,但 p4 仍然是唯一一个管理该对象的 shared_ptr,因此引用计数为 1。
p5 管理一个新分配的 int 对象,并指定了一个 lambda 函数作为删除器,同时还提供了一个 std::allocator<int>()。在创建 p6 之前,p5 是唯一一个管理该对象的 shared_ptr,因此引用计数为 1。
p6 是通过复制 p5 创建的,因此它们共享同一个管理的 int 对象。复制操作增加了对象的引用计数,所以现在 p5 和 p6 都指向该对象,引用计数为 2。
但是上图打印的p6的use_count不是0吗?
那是因为p7的创建导致 p6 不再管理该对象。
p7 是通过移动 p6 创建的,移动操作将 p6 所管理的对象的所有权转移给 p7。在 p7 创建后,p6 不再拥有该对象的所有权,但引用计数在 p6 销毁之前不会减少。正确的理解应该是,在 p7 创建后且 p6 超出作用域或被重置之前,引用计数保持为 2,但 p6 不再参与该计数(因为它已被移动)。一旦 p6 超出作用域或被重置,引用计数将减少。

std::shared_ptr的底层原理
shared_ptr在内部维护一个引用计数,其只有两个指针成员,一个是指针所管理的数据的地址 ,还有一个指针是控制块(Control Block)的地址,包括引用计数,weak_ptr计数,删除器,分配器。
需要注意的是,因为不同的指针对象需要共享相同的内存,所以引用计数存储在堆上。
unique_ptr只有一个指针,指向所管理的数据的地址,因此它的大小也是shared_ptr的一半。
#include<iostream>
#include<memory>
int main()
{
std::cout<<sizeof(std::shared_ptr<int>)<<std::endl;
std::cout<<sizeof(std::unique_ptr<int>)<<std::endl;
}
下面是64位环境下的运行结果:

当我们拷贝一个shared_ptr时,引用计数加1,
当我们销毁一个shared_ptr时,引用计数减一,当引用计数减到0时,会释放shared_ptr的两个指针(即指向管理数据的指针和指向数据块的指针)。
而当我们赋值一个shared_ptr指针时,我们先递减左侧运算对象的引用计数,如果引用计数变为0,我们就释放左侧运算对象的内存以及引用计数的内存,然后拷贝右侧计算对象的拷贝指针和引用计数指针,最后递增引用计数。
下面来看我模拟实现的一个shared_ptr类:
模拟实现简单shared_ptr类
#include <iostream>
class RefCount {
public:
RefCount() : count(0) {}
void increment() { ++count; }
void decrement() {
if (--count == 0) {
delete this;
}
}
int& get() { return count; }
private:
int count;
};
template<typename T>
class shared_ptr {
public:
// 构造函数
shared_ptr() : _ptr(nullptr), _count(nullptr) {}
explicit shared_ptr(T* ptr) : _ptr(ptr), _count(new RefCount()) {
_count->increment();
}
// 拷贝构造函数和拷贝赋值运算符,增加引用计数
shared_ptr(const shared_ptr& other) : _ptr(other._ptr), _count(other._count) {
if (_count) {
_count->increment();
}
}
shared_ptr& operator=(const shared_ptr& other) {
if (this != &other) {
// 减少当前对象的引用计数
if (_count && --_count->get() == 0) {
delete _ptr;
delete _count;
}
// 复制其他对象的指针和引用计数
_ptr = other._ptr;
_count = other._count;
if (_count) {
_count->increment();
}
}
return *this;
}
// 移动构造函数和移动赋值运算符,转移所有权
shared_ptr(shared_ptr&& other) noexcept : _ptr(other._ptr), _count(other._count) {
other._ptr = nullptr;
other._count = nullptr;
}
shared_ptr& operator=(shared_ptr&& other) noexcept {
if (this != &other) {
// 减少当前对象的引用计数
if (_count &&-- _count->get() == 0) {
delete _ptr;
delete _count;
}
// 转移所有权
_ptr = other._ptr;
_count = other._count;
other._ptr = nullptr;
other._count = nullptr;
}
return *this;
}
// 解引用和成员访问操作符
T& operator*() const {
if (!_ptr) {
throw std::runtime_error("shared_ptr is null");
}
return *_ptr;
}
T* operator->() const {
if (!_ptr) {
throw std::runtime_error("shared_ptr is null");
}
return _ptr;
}
// 获取原始指针
T* get() const {
return _ptr;
}
int use_count() const
{
if(_count) return _count->get();
return 0;
}
// 检查是否为空
bool operator!() const {
return _ptr == nullptr;
}
// 析构函数,减少引用计数并释放对象
~shared_ptr() {
if (_count && --_count->get() == 0) {
delete _ptr;
delete _count;
}
}
private:
T* _ptr;
RefCount* _count;
};
int main() {
shared_ptr<int> ptr1(new int(42));
std::cout << "Value: " << *ptr1 << std::endl;
std::cout << "Initial use_count: " << ptr1.use_count() << std::endl;
// 共享所有权
shared_ptr<int> ptr2 = ptr1;
std::cout << "Value after copy: " << *ptr2 << std::endl;
std::cout << "Use_count after copy: " << ptr1.use_count() << std::endl;
std::cout << "Use_count of ptr2: " << ptr2.use_count() << std::endl;
return 0;
}

什么时候用shared_ptr
当多个对象或组件需要共享对同一动态分配资源的所有权,并且希望自动管理该资源的生命周期以避免内存泄漏时,应使用 shared_ptr。大型应用程序或系统中,不同的组件可能需要共享某些资源。使用 shared_ptr 可以方便地实现这些组件之间的资源共享,同时确保资源的正确释放。
std::unique_ptr类
和shared_ptr不同,unique_ptr独享指向的对象。
它不能拷贝构造和赋值。
// 禁止拷贝构造函数和拷贝赋值运算符,以确保唯一所有权
unique_ptr(const unique_ptr&other) = delete;
unique_ptr&operator=(const unique_ptr&other) = delete;
但是允许移动构造和移动赋值,也就是转移所有权
std::unique_ptr<int> p1 = std::make_unique<int>(0);
std::unique_ptr<int> p2 = std::move(p1);
std::move函数可以将一个unique_ptr转移给另一个unique_ptr或者shared_ptr,转移后,原来的unique_ptr失去对内存的控制权限,变成空指针。
// 移动构造函数和移动赋值运算符,允许所有权转移
unique_ptr(unique_ptr&&other) : _ptr(other._ptr)
{
other._ptr=nullptr;
}
unique_ptr&operator=(unique_ptr&&other)
{
if(this!=&other)
{
delete _ptr;
_ptr=other._ptr;
other._ptr=nullptr;
}
return *this;
}
下面是我模拟实现的一个unique_ptr类。
模拟实现unique_ptr类
#include<iostream>
template<typename T>
class unique_ptr
{
public:
unique_ptr() : _ptr(nullptr) {}
unique_ptr(T*ptr) : _ptr(ptr) {}
// 禁止拷贝构造函数和拷贝赋值运算符,以确保唯一所有权
unique_ptr(const unique_ptr&other) = delete;
unique_ptr&operator=(const unique_ptr&other) = delete;
// 移动构造函数和移动赋值运算符,允许所有权转移
unique_ptr(unique_ptr&&other) : _ptr(other._ptr)
{
other._ptr=nullptr;
}
unique_ptr&operator=(unique_ptr&&other)
{
if(this!=&other)
{
delete _ptr;
_ptr=other._ptr;
other._ptr=nullptr;
}
return *this;
}
T&operator*() const
{
if(!_ptr)
{
throw std::runtime_error("unique_ptr is null");
}
return *_ptr;
}
T*operator->() const
{
if(!_ptr)
{
throw std::runtime_error("unique_ptr is null");
}
return _ptr;
}
// 获取原始指针
T*get()
{
return _ptr;
}
bool operator!() const {
return _ptr == nullptr;
}
~unique_ptr()
{
delete _ptr;
}
private:
T*_ptr;
};
int main() {
unique_ptr<int> ptr1(new int(42));
std::cout << "Value: " << *ptr1 << std::endl;
// 转移所有权
unique_ptr<int> ptr2 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is now null" << std::endl;
}
std::cout << "Value after move: " << *ptr2 << std::endl;
return 0;
}
运行结果如下。

什么时候用unique_ptr
当只有一个所有者需要管理动态分配资源的整个生命周期,并且希望确保该资源在所有者被销毁时自动释放时,使用unique_ptr。
std::weak_ptr类
std::weak_ptr是一种智能指针,它指向一个std::shared_ptr管理的对象。但是,它不会增加对象的引用计数,因此,它不会影响对象的生命周期。这种指针的主要作用是协助std::shared_ptr工作,它可以访问std::shared_ptr管理的对象,但是它不拥有该对象。std::weak_ptr可从std::shared_ptr或另一个std::weak_ptr对象构造,它的构造和析构不会引起引用计数的增加或减少。
#include <iostream>
#include <memory>
class B;
class A {
public:
std::shared_ptr<B> ptrB;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptrA;
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b;
b->ptrA = a;
return 0;
}
在上面的代码中,a和b的引用计数都是2,因为它们相互引用。当局部作用域结束时,我们期望a和b被销毁,但由于循环引用,它们实际上并没有被销毁,导致内存泄漏。
使用weak_ptr解决循环引用
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::weak_ptr<B> ptrB; // 使用weak_ptr来打破循环引用
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptrA;
~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->ptrB = b; // a现在持有一个weak_ptr到b
b->ptrA = a; // b仍然持有一个shared_ptr到a
}
std::cout << "Shared pointers have been destroyed correctly." << std::endl;
return 0;
}
在这个修改后的代码中,A类中的ptrB是一个weak_ptr,它不会增加B对象的引用计数。因此,当a和b都离开作用域时,它们的引用计数都会降为零,并且它们所管理的资源会被正确释放。
2587

被折叠的 条评论
为什么被折叠?



