unique_ptr
unique_ptr 用来管理具备专属所有权的资源,它通过指针持有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象。下面两种情况下,所管理的资源被释放:
- unique_ptr 对象被销毁(离开作用域,调用析构函数)
- 通过 operator= 或 reset() 赋值另一指针给管理它的 unique_ptr 对象
- 对象的释放通过调用 get_deleter()(ptr) 实现,默认删除器使用 delete 运算符,也可以自定义删除器释放对象。
std::unique_ptr 有两个版本:
- 管理单个对象(new 分配)
template<
class T,
class Deleter = std::default_delete<T>
> class unique_ptr;
- 管理动态分配的对象数组(new[] 分配)
template <
class T,
class Deleter
> class unique_ptr<T[], Deleter>;
示例代码:
#include <cassert>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <locale>
#include <memory>
#include <stdexcept>
// 用于下面运行时多态演示的辅助类
struct B
{
virtual ~B() = default;
virtual void bar() { std::cout << "B::bar\n"; }
};
struct D : B
{
D() { std::cout << "D::D\n"; }
~D() { std::cout << "D::~D\n"; }
void bar() override { std::cout << "D::bar\n"; }
};
// 消费 unique_ptr 的函数能以值或以右值引用接收它
std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
{
p->bar();
return p;
}
// 用于下面自定义删除器演示的辅助函数
void close_file(std::FILE* fp)
{
std::fclose(fp);
}
// 基于 unique_ptr 的链表演示
struct List
{
struct Node
{
int data;
std::unique_ptr<Node> next;
};
std::unique_ptr<Node> head;
~List()
{
// 循环按顺序销毁各列表节点,默认析构函数将会递归调用其 `next` 指针的析构函数,
// 这在足够大的链表上可能造成栈溢出。
while (head)
{
auto next = std::move(head->next);
head = std::move(next);
}
}
void push(int data)
{
head = std::unique_ptr<Node>(new Node{data, std::move(head)});
}
};
int main()
{
std::cout << "1) 独占所有权语义演示\n";
{
// 创建一个(独占)资源
std::unique_ptr<D> p = std::make_unique<D>();
// 转移所有权给 `pass_through`,而它再通过返回值将所有权转移回来
std::unique_ptr<D> q = pass_through(std::move(p));
// p 现在是已被移动的“空”状态,等于 nullptr
assert(!p);
}
std::cout << "\n" "2) 运行时多态演示\n";
{
// 创建派生类资源并通过基类指向它
std::unique_ptr<B> p = std::make_unique<D>();
// 动态派发如期工作
p->bar();
}
std::cout << "\n" "3) 自定义删除器演示\n";
std::ofstream("demo.txt") << 'x'; // 准备要读取的文件
{
using unique_file_t = std::unique_ptr<std::FILE, decltype(&close_file)>;
unique_file_t fp(std::fopen("demo.txt", "r"), &close_file);
if (fp)
std::cout << char(std::fgetc(fp.get())) << '\n';
} // `close_file()` 于此调用(若 `fp` 为空)
std::cout << "\n" "4) 自定义 lambda 表达式删除器和异常安全性演示\n";
try
{
std::unique_ptr<D, void(*)(D*)> p(new D, [](D* ptr)
{
std::cout << "由自定义删除器销毁...\n";
delete ptr;
});
throw std::runtime_error(""); // `p` 若为普通指针则此处将泄漏
}
catch (const std::exception&)
{
std::cout << "捕获到异常\n";
}
std::cout << "\n" "5) 数组形式的 unique_ptr 演示\n";
{
std::unique_ptr<D[]> p(new D[3]);
} // `D::~D()` 被调用 3 次
std::cout << "\n" "6) 链表演示\n";
{
List wall;
const int enough{1'000'000};
for (int beer = 0; beer != enough; ++beer)
wall.push(beer);
std::cout.imbue(std::locale("en_US.UTF-8"));
std::cout << "墙上有 " << enough << " 瓶啤酒...\n";
} // 销毁所有啤酒
}
shared_ptr
shared_ptr 管理具备共享所有权的资源,多个 shared_ptr 对象可持有同一对象。下列情况之一出现时销毁对象并解分配其内存():
- 最后剩下的持有对象的 shared_ptr 被销毁;
- 最后剩下的持有对象的 shared_ptr 被通过 operator= 或 reset() 赋值为另一指针。
实现机制:引用计数
std::shared_ptr 中有两个指针:
- 所存储的指针(get()) 所返回的指针)
- 指向控制块 的指针
控制块是一个动态分配的对象,其中包含:
- 指向被管理对象的指针或被管理对象本身
- 删除器(类型擦除)
- 分配器(类型擦除)
- 持有被管理对象的 shared_ptr 的数量
- 涉及被管理对象的 weak_ptr 的数量
shared_ptr 的构造函数会将控制块中的引用计数加1 ,析构函数会将引用计数减1,如果该计数器减至零,控制块就会调用被管理对象的析构函数。但控制块本身直到 std::weak_ptr 计数器同样归零时会解分配其自身。
使用 make_shared 的优势:
- 调用 std::make_shared 创建 shared_ptr 时,以单次分配创建控制块和被管理对象。被管理对象在控制块的数据成员中原位构造。
- 使用 new 初始化 std::shared_ptr 时:
- 首先,使用 new 在堆上为对象实例分配内存并完成初始化。
- 然后,使用已分配的对象指针来构造 std::shared_ptr 实例。
示例代码:
#include <chrono>
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>
using namespace std::chrono_literals;
struct Base
{
Base() { std::cout << "Base::Base()\n"; }
// 注意:此处非虚析构函数 OK
~Base() { std::cout << "Base::~Base()\n"; }
};
struct Derived : public Base
{
Derived() { std::cout << "Derived::Derived()\n"; }
~Derived() { std::cout << "Derived::~Derived()\n"; }
};
void print(auto rem, std::shared_ptr<Base> const& sp)
{
std::cout << rem << "\n\tget() = " << sp.get()
<< ", use_count() = " << sp.use_count() << '\n';
}
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(987ms);
std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
print("线程中的局部指针:", lp);
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
print("创建共享的 Derived (为 Base 指针)", p);
std::thread t1{thr, p}, t2{thr, p}, t3{thr, p};
p.reset(); // 从 main 释放所有权
print("在 3 个线程间共享所有权并从 main 释放所有权:", p);
t1.join();
t2.join();
t3.join();
std::cout << "线程全部已完成,最后一个删除了 Derived。\n";
}
weak_ptr
-
std::weak_ptr 是一种智能指针,它持有对被 std::shared_ptr 管理的对象的非拥有性(“弱”)引用。在访问所引用的对象前必须先转换为 std::shared_ptr。
-
std::weak_ptr 用来表达临时所有权的概念:当某个对象只有存在时才需要被访问,而且随时可能被他人删除时,可以使用 std::weak_ptr 来跟踪该对象。需要获得临时所有权时,则将其转换为 std::shared_ptr,此时如果原来的 std::shared_ptr 被销毁,则该对象的生命期将被延长至这个临时的 std::shared_ptr 同样被销毁为止。
-
std::weak_ptr 的另一用法是打断 std::shared_ptr 所管理的对象组成的环状引用。若这种环被孤立(例如无指向环中的外部共享指针),则 shared_ptr 引用计数无法抵达零,而内存被泄露。可通过令环中的指针之一为弱指针来避免这种情况。
示例代码:
#include <iostream>
#include <memory>
std::weak_ptr<int> gw;
void observe()
{
std::cout << "gw.use_count() == " << gw.use_count() << "; ";
// 使用之前必须制作一个 shared_ptr 副本
if (std::shared_ptr<int> spt = gw.lock())
std::cout << "*spt == " << *spt << '\n';
else
std::cout << "gw 已过期\n";
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
observe();
}
observe();
}
一些关于智能指针的面试问题
- 什么是智能指针?解释其作用和重要性。
- 智能指针是C++中一种特殊的指针类型,它以类的形式封装了原始指针,用来自动管理动态分配的内存,从而避免内存泄漏、悬挂指针、重复释放等问题。智能指针通过自动执行析构函数来释放内存,确保即使在异常情况下也能正确清理资源,这极大地提高了代码的安全性和可靠性。
- 要点:智能指针是什么
- 为什么在C++中使用智能指针而不是原始指针?
- 列举并解释C++标准库中提供的几种智能指针类型
- 什么时候应该使用unique_ptr?它有何特点?
- 解释unique_ptr的转移语义(move semantics)。
- 如何正确地使用std::move与unique_ptr?
- shared_ptr的工作原理是什么?特别是它的引用计数机制。
- shared_ptr与unique_ptr的主要区别是什么?
- 解释循环引用问题,并说明如何使用weak_ptr来解决这个问题。
- 什么是weak_ptr?它存在的目的是什么?
- 如何从weak_ptr获得一个shared_ptr?为什么需要这样的转换?
- shared_ptr是不是线程安全?
- 为什么推荐用mak_eshared创建指针?
- weak_ptr如何检测指针是否被销毁?
- 如何将unique_ptr转换成shared_ptr类型?
- 捕获shared_ptr时如果不想延长对象生命周期怎么做?
- unique_ptr能否被另一个unique_ptr拷贝呢?