建设培训学校网站,贵阳企业自助建站系统,二级子域名查询入口,wordpress所有文章页面提示#xff1a;文章写完后#xff0c;目录可以自动生成#xff0c;如何生成可参考右边的帮助文档 文章目录 为什么会出现智能指针#xff1f;对于独占资源使用std::unique_ptr对于共享资源使用std::shared_ptr当std::shared_ptr可能悬空时使用std::weak_ptr优先考虑使用st… 提示文章写完后目录可以自动生成如何生成可参考右边的帮助文档 文章目录 为什么会出现智能指针对于独占资源使用std::unique_ptr对于共享资源使用std::shared_ptr当std::shared_ptr可能悬空时使用std::weak_ptr优先考虑使用std::make_unique和std::make_shared而非new 为什么会出现智能指针
原始指针在使用时的缺点
它的声明不能指示所指到底是单个对象还是数组。它的声明没有告诉你用完后是否应该销毁它即指针是否拥有所指之物。如果你决定你应该销毁指针所指对象没人告诉你该用delete还是其他析构机制比如将指针传给专门的销毁函数。如果你发现该用delete。 可能不知道该用单个对象形式“delete”还是数组形式“delete[]”。如果用错了结果是未定义的。假设你确定了指针所指知道销毁机制也很难确定你在所有执行路径上都执行了恰为一次销毁操作包括异常产生后的路径。少一条路径就会产生资源泄漏销毁多次还会导致未定义行为。一般来说没有办法告诉你指针是否变成了悬空指针dangling pointers即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。
智能指针smart pointers是解决这些问题的一种办法。 智能指针包裹原始指针它们的行为看起来像被包裹的原始指针但避免了原始指针的很多陷阱。所以我们应该更倾向于智能指针而不是原始指针。几乎原始指针能做的所有事情智能指针都能做而且出错的机会更少。
在C11中存在四种智能指针std::auto_ptrstd::unique_ptrstd::shared_ptr std::weak_ptr。都是被设计用来帮助管理动态对象的生命周期在适当的时间通过适当的方式来销毁对象以避免出现资源泄露或者异常行为。
对于独占资源使用std::unique_ptr
默认情况下std::unique_ptr大小等同于原始指针而且对于大多数操作包括取消引用他们执行的指令完全相同。 std::unique_ptr体现了专有所有权语义。一个非空的std::unique_ptr始终拥有其指向的内容。 std::unique_ptr只允许移动不允许拷贝
移动std::unique_ptr会将所有权从源指针转移到目的指针。如果允许拷贝则会有两个std::unique_ptr指向相同内容导致重复销毁。析构时非空std::unique_ptr会销毁指向的资源默认通过对原始指针调用delete实现。
templatetypename T, typename Deleter std::default_deleteT
class unique_ptr {
private:T* ptr; // 管理的原始指针Deleter deleter; // 删除器默认使用 std::default_deleteTpublic:// 默认构造constexpr unique_ptr() noexcept : ptr(nullptr), deleter(Deleter()) {}// 构造接收裸指针和删除器explicit unique_ptr(T* p) noexcept : ptr(p), deleter(Deleter()) {}unique_ptr(T* p, Deleter d) noexcept : ptr(p), deleter(d) {}// 禁止拷贝构造和拷贝赋值unique_ptr(const unique_ptr) delete;unique_ptr operator(const unique_ptr) delete;// 移动构造unique_ptr(unique_ptr other) noexcept: ptr(other.ptr), deleter(std::move(other.deleter)) {other.ptr nullptr;}// 移动赋值unique_ptr operator(unique_ptr other) noexcept {if (this ! other) {reset(other.release());deleter std::move(other.deleter);}return *this;}// 析构释放资源~unique_ptr() {if (ptr) deleter(ptr);}// 释放资源所有权返回裸指针T* release() noexcept {T* tmp ptr;ptr nullptr;return tmp;}// 重置释放当前资源并管理新资源void reset(T* p nullptr) noexcept {if (ptr ! p) {if (ptr) deleter(ptr);ptr p;}}// 访问底层裸指针T* get() const noexcept { return ptr; }// 提供 operator* 和 operator- 方便使用T operator*() const noexcept { return *ptr; }T* operator-() const noexcept { return ptr; }// 检查是否为空explicit operator bool() const noexcept { return ptr ! nullptr; }
};默认情况下销毁将通过delete进行但是在构造过程中std::unique_ptr对象可以被设置为使用自定义删除器当资源需要销毁时可调用的任意函数或者函数对象包括lambda表达式。
#include iostream
#include memory// 自定义删除器使用 delete
struct myDeleter {void operator()(int* ptr) const {log Using custom deleter with delete! ;delete ptr; }
};int main() {std::unique_ptrint, myDeleter ptr(new int(42));std::cout Value: *ptr std::endl;return 0;
}对于共享资源使用std::shared_ptr
std::shared_ptr通过引用计数reference count来确保它是否是最后一个指向某种资源的指针引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。 std::shared_ptr构造函数通常递增引用计数值析构函数递减值。 如果std::shared_ptr在计数值递减后发现引用计数值为零没有其他std::shared_ptr指向该资源它就会销毁资源。
#include iostream
#include atomictemplate typename T, typename Deleter std::default_deleteT
struct ControlBlock {std::atomicint use_count; // shared_ptr 引用计数std::atomicint weak_count; // weak_ptr 引用计数T* ptr; // 指向被管理的资源Deleter deleter; // 用户指定的删除器ControlBlock(T* p, Deleter d Deleter()): use_count(1), weak_count(1), ptr(p), deleter(d) {}void release_shared() {if (--use_count 0) {deleter(ptr); // 调用自定义删除器release_weak(); // 尝试销毁控制块}}void release_weak() {if (--weak_count 0) {delete this; // 控制块自身使用默认 delete}}
};// shared_ptr 类
template typename T
class SharedPtr {
private:T* ptr; // 原始资源指针ControlBlockT* control_block; // 控制块指针public:// 构造函数explicit SharedPtr(T* p nullptr) : ptr(p) {if (ptr) {control_block new ControlBlockT(ptr);} else {control_block nullptr;}std::cout SharedPtr Constructor: ptr std::endl;}// 拷贝构造函数SharedPtr(const SharedPtr other) : ptr(other.ptr), control_block(other.control_block) {if (control_block) {control_block-use_count; // 增加引用计数}std::cout SharedPtr Copy Constructor: ptr std::endl;}//std::shared_ptr构造函数通常递增引用计数值。// 移动构造函数SharedPtr(SharedPtr other) noexcept : ptr(other.ptr), control_block(other.control_block) {other.ptr nullptr;other.control_block nullptr;std::cout SharedPtr Move Constructor std::endl;}// 析构函数~SharedPtr() {if (control_block --control_block-use_count 0) {std::cout SharedPtr Destructor: ptr std::endl;delete control_block; // 删除控制块}}// 重载赋值运算符SharedPtr operator(const SharedPtr other) {if (this ! other) {if (control_block --control_block-use_count 0) {delete control_block;}ptr other.ptr;control_block other.control_block;if (control_block) {control_block-use_count; // 增加引用计数}}return *this;}// 访问资源T* get() const { return ptr; }// 解引用操作符T operator*() const { return *ptr; }// 指针访问操作符T* operator-() const { return ptr; }// 获取引用计数int use_count() const { return control_block ? control_block-use_count : 0; }
};引用计数暗示着性能问题
std::shared_ptr大小是原始指针的两倍因为它内部包含一个指向资源的原始指针还包含一个指向资源控制块原始指针。控制块的内存必须动态分配。shared_ptr 的引用计数控制块必须单独用 new 分配内存因为它不能放在被管理的对象里。被管理的对象根本不知道自己被 shared_ptr 所管理了更不知道引用计数在哪儿。所以shared_ptr 只能自己额外分配一块内存来记录引用次数。递增递减引用计数必须是原子性的。在多线程程序中同一个资源可能被多个 shared_ptr 管理而这些 shared_ptr 可能同时被不同线程读写。比如一个线程销毁了 shared_ptr它会把引用计数减 1另一个线程可能刚好拷贝了这个 shared_ptr它会把引用计数加 1。如果这些加减操作不是原子的引用计数就可能错乱。
#include iostream
#include memory
#include chrono
#include vectorconstexpr size_t N 1000000; // 可调大小防止内存爆炸struct MyObject {int value;MyObject(int v) : value(v) {}
};void test_raw_pointer() {std::vectorMyObject* pointers;auto start std::chrono::high_resolution_clock::now();for (size_t i 0; i N; i) {pointers.push_back(new MyObject(i));}auto mid std::chrono::high_resolution_clock::now();long long sum 0;for (auto ptr : pointers) {sum ptr-value;}for (auto ptr : pointers) {delete ptr;}auto end std::chrono::high_resolution_clock::now();size_t total_mem N * sizeof(MyObject);std::cout [原始指针] \n分配时间: std::chrono::durationdouble(mid - start).count() 秒, ;std::cout 访问与释放时间: std::chrono::durationdouble(end - mid).count() 秒, ;std::cout 总内存估算: total_mem / (1024.0 * 1024) MB, ;std::cout 求和值: sum std::endl;std::cout std::endl;
}void test_shared_ptr() {std::vectorstd::shared_ptrMyObject pointers;auto start std::chrono::high_resolution_clock::now();for (size_t i 0; i N; i) {pointers.push_back(std::make_sharedMyObject(i));}auto mid std::chrono::high_resolution_clock::now();long long sum 0;for (const auto ptr : pointers) {sum ptr-value;}pointers.clear(); // 引用计数归零auto end std::chrono::high_resolution_clock::now();size_t control_block_size 24; // 控制块估算size_t total_mem N * (sizeof(MyObject) control_block_size);std::cout [shared_ptr] \n分配时间: std::chrono::durationdouble(mid - start).count() 秒, ;std::cout 访问与释放时间: std::chrono::durationdouble(end - mid).count() 秒, ;std::cout 总内存估算: total_mem / (1024.0 * 1024) MB, ;std::cout 求和值: sum std::endl;
}int main() {test_raw_pointer();test_shared_ptr();system(pause); return 0;
} 类似std::unique_ptrstd::shared_ptr使用delete作为资源的默认销毁机制但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是
在 std::unique_ptr 中删除器是智能指针类型的一部分
std::unique_ptrT, Deleter ptr;在 shared_ptr中删除器是控制块ControlBlock的一部分而不是直接作为智能指针类型的一部分。
std::shared_ptrT ptr(p, Deleter{});auto loggingDel [](Widget *pw) //自定义删除器{ makeLogEntry(pw);delete pw;};std::unique_ptr //删除器类型是Widget, decltype(loggingDel) //指针类型的一部分 upw(new Widget, loggingDel);std::shared_ptrWidget //删除器类型不是spw(new Widget, loggingDel); //指针类型的一部分这种设置使得std::shared_ptr的设计更为灵活 你可以创建多个 std::shared_ptr它们指向相同类型 T 的对象。但使用不同的删除器不会导致智能指针类型不同。 比如考虑有两个std::shared_ptr每个自带不同的删除器。
控制块创建需要注意的点 std::make_shared总是创建一个控制块。 当从独占指针即std::unique_ptr上构造出std::shared_ptr时会创建控制块。 std::unique_ptrMyObject uptr std::make_uniqueMyObject();
std::shared_ptrMyObject sptr std::move(uptr); //此时创建了一个新的控制块当从原始指针上构造出std::shared_ptr时会创建控制块。 auto pw new Widget; //pw是原始指针
std::shared_ptrWidget spw1(pw, loggingDel); //为*pw创建控制块以下情况会出现未定义行为 auto pw new Widget; //pw是原始指针
std::shared_ptrWidget spw1(pw, loggingDel); //为*pw创建控制块
//将同样的原始指针传递给spw2的构造函数会再次为*pw创建一个控制块
std::shared_ptrWidget spw2(pw, loggingDel); //为*pw创建第二个控制块因此*pw有两个引用计数值每一个最后都会变成零然后最终导致*pw销毁两次。第二个销毁会产生未定义行为。
注意点1 避免把原始指针传给 std::shared_ptr 构造函数推荐使用std::make_shared。但是std::make_shared不接受删除器参数。
auto spw1 std::make_sharedWidget(); 注意点2 如果必须传给std::shared_ptr构造函数原始指针直接传new出来的结果不要传指针变量。
std::shared_ptrWidget spw1(new Widget, loggingDel); //直接使用new的结果
std::shared_ptrWidget spw2(spw1); //spw2使用spw1一样的控制块当std::shared_ptr可能悬空时使用std::weak_ptr
std::weak_ptr通常从std::shared_ptr上创建。当从std::shared_ptr上创建std::weak_ptr时两者指向相同的对象但是std::weak_ptr不会影响所指对象的引用计数
auto spw std::make_sharedWidget();//spw创建之后指向的Widget的引用计数为1。
std::weak_ptrWidget wpw(spw); //wpw指向与spw所指相同的Widget。RC仍为1
spw nullptr; //RC变为0Widget被销毁。//wpw现在悬空悬空的std::weak_ptr被称作已经expired过期。 我们通常期望的是检查std::weak_ptr是否已经过期如果没有过期则访问其指向的对象,但是因为std::weak_ptr缺少解引用操作没有办法写这样的代码。
if (!wp.expired()) {// ❌ 想这么写但无法解引用 weak_ptr//std::weak_ptr 是一个非拥有型的观察者指针它不控制资源的生命周期。wp-doSomething();
}需要一个原子操作避免其他线程对指向这对象的std::shared_ptr重新赋值或者析构检查std::weak_ptr是否已经过期如果没有过期就访问所指对象。
因为std::weak_ptr缺少解引用操作, 那么怎么实现访问所指对象呢 这可以通过从std::weak_ptr创建std::shared_ptr来实现
第一种形式 std::weak_ptr::lock它返回一个std::shared_ptr如果std::weak_ptr过期这个std::shared_ptr为空
std::shared_ptrWidget spw1 wpw.lock(); //如果wpw过期spw1就为空struct ControlBlock {T* ptr; // 原始指针size_t shared_count; // shared_ptr 的引用计数size_t weak_count; // weak_ptr 的引用计数含控制块自身
};std::shared_ptrT lock() const {if (control_block-shared_count 0)return std::shared_ptrT(control_block);elsereturn std::shared_ptrT(); // 空
}std::shared_ptrWidget spw3(wpw); //如果wpw过期抛出std::bad_weak_ptr异常另外一个使用std::weak_ptr的例子考虑一个持有三个对象A、B、C的数据结构A和C共享B的所有权因此持有std::shared_ptr
假定从B指向A的指针也很有用。应该使用哪种指针
有三种选择
原始指针。使用这种方法如果A被销毁但是C继续指向BB就会有一个指向A的悬空指针。而且B不知道指针已经悬空所以B可能会继续访问就会导致未定义行为。std::shared_ptr。这种设计A和B都互相持有对方的std::shared_ptr导致的std::shared_ptr环状结构A指向BB指向A阻止A和B的销毁。A和B都被泄漏程序无法访问它们但是资源并没有被回收。std::weak_ptr。这避免了上述两个问题。如果A被销毁B指向它的指针悬空但是B可以检测到这件事。尤其是尽管A和B互相指向对方B的指针不会影响A的引用计数因此在没有std::shared_ptr指向A时不会导致A无法被销毁。
优先考虑使用std::make_unique和std::make_shared而非new
std::make_shared是C11标准的一部分但std::make_unique不是。它从C14开始加入标准库。如果在使用C11一个基础版本的std::make_unique是很容易自己写出如下
templatetypename T, typename... Ts
std::unique_ptrT make_unique(Ts... params)
{return std::unique_ptrT(new T(std::forwardTs(params)...));
}make_unique只是将它的参数完美转发到所要创建的对象的构造函数从new产生的原始指针里面构造出std::unique_ptr并返回这个std::unique_ptr。
auto upw1(std::make_uniqueWidget()); //使用make函数
std::unique_ptrWidget upw2(new Widget); //不使用make函数auto spw1(std::make_sharedWidget()); //使用make函数
std::shared_ptrWidget spw2(new Widget); //不使用make函数使用make函数的原因和异常安全有关。假设我们有个函数按照某种优先级处理Widget
void processWidget(std::shared_ptrWidget spw, int priority);假设我们有一个函数来计算相关的优先级
int computePriority();我们在调用processWidget时使用了new而不是std::make_shared
processWidget(std::shared_ptrWidget(new Widget), //潜在的资源泄漏computePriority());这段代码可能在new一个Widget时发生泄漏。 调用的代码和被调用的函数都用std::shared_ptr且std::shared_ptr就是设计出来防止泄漏的。它们会在最后一个std::shared_ptr销毁时自动释放所指向的内存这段代码怎么会泄漏呢
答案和编译器将源码转换为目标代码有关。在运行时一个函数的实参必须先被计算这个函数再被调用所以在调用processWidget之前必须执行以下操作processWidget才开始执行
表达式“new Widget”必须计算例如一个Widget对象必须在堆上被创建负责管理new出来指针的std::shared_ptr构造函数必须被执行computePriority必须运行
编译器不需要按照执行顺序生成代码。“new Widget”必须在std::shared_ptr的构造函数被调用前执行因为new出来的结果作为构造函数的实参但computePriority可能在这之前之后或者之间执行。也就是说编译器可能按照这个执行顺序生成代码
执行“new Widget”执行computePriority运行std::shared_ptr构造函数
如果按照这样生成代码并且在运行时computePriority产生了异常那么第一步动态分配的Widget就会泄漏。因为它永远都不会被第三步的std::shared_ptr所管理了。
使用std::make_shared可以防止这种问题。
processWidget(std::make_sharedWidget(), //没有潜在的资源泄漏computePriority());在运行时std::make_shared和computePriority其中一个会先被调用。
如果是std::make_shared先被调用在computePriority调用前动态分配Widget的原始指针会安全的保存在作为返回值的std::shared_ptr中。如果computePriority产生一个异常那么std::shared_ptr析构函数将确保管理的Widget被销毁。如果首先调用computePriority并产生一个异常那么std::make_shared将不会被调用因此也就不需要担心动态分配Widget。
std::make_shared的另一个特性与直接使用new相比是效率提升。使用std::make_shared允许编译器生成更小更快的代码并使用更简洁的数据结构。考虑以下对new的直接使用
std::shared_ptrWidget spw(new Widget);这段代码需要进行内存分配但它实际上执行了两次直接使用new需要为Widget进行一次内存分配为控制块再进行一次内存分配。
如果使用std::make_shared代替
auto spw std::make_sharedWidget();一次分配足矣。这是因为std::make_shared分配一块内存同时容纳了Widget对象和控制块。这种优化减少了程序的静态大小因为代码只包含一个内存分配调用并且它提高了可执行代码的速度因为内存只分配一次。