矿泉水网站模板,互联网营销师证书,网站上传文件存储方式,手机上有那种网站吗参考#xff1a;
恋恋风辰官方博客
动态内存管理 - cppreference.com
SRombauts/shared_ptr#xff1a; 一个最小的 shared/unique_ptr 实现#xff0c;用于处理 boost/std#xff1a;#xff1a;shared/unique_ptr 不可用的情况。
C智能指针_c 智能指针-CSDN博客
当…参考
恋恋风辰官方博客
动态内存管理 - cppreference.com
SRombauts/shared_ptr 一个最小的 shared/unique_ptr 实现用于处理 boost/stdshared/unique_ptr 不可用的情况。
C智能指针_c 智能指针-CSDN博客
当我们谈论shared_ptr的线程安全性时我们在谈论什么 - 知乎
C 智能指针 - 全部用法详解-CSDN博客 文章目录 [toc] 序言1. shared_ptr1.1 shared_ptr 内存模型1.2 shared_ptr的使用1.2.1 make_shared1.2.2 shared_ptr与new结合1.2.3 reset()1.2.4 通过智能指针共享数据1.2.5 shared_ptr是线程安全的吗 2. weak_ptr2.1 循环引用问题2.2 其他成员函数2.2.1 use_count()2.2.2 expired()2.2.3 lock() 3. enable_from_this_shared4. unqiue_ptr
序言
传统的手动管理内存方式如 new 和 delete虽然灵活但也容易引发内存泄漏new的对象在作用域结束后没有被及时释放、悬空指针指针的指向对象已被删除或释放但仍有其他指针保留了对该内存位置的引用和重复释放一个指针指向的内存被多次重复释放等问题。随着项目规模的扩大和代码复杂性的增加这些问题不仅让程序员疲于奔命也直接影响了软件的可靠性和可维护性。
智能指针就是为了实现类似于Java中的垃圾回收机制。Java的垃圾回收机制使程序员从繁杂的内存管理任务中彻底的解脱出来在申请使用一块内存区域之后无需去关注应该何时何地释放内存Java将会自动帮助回收。但是出于效率和其他原因可能C设计者不屑于这种傻瓜氏的编程方式C本身并没有这样的功能其繁杂且易出错的内存管理也一直为广大程序员所诟病。
更进一步地说智能指针的出现是为了满足管理类中指针成员的需要。包含指针成员的类需要特别注意复制控制和赋值操作原因是复制指针时只复制指针中的地址而不会复制指针指向的对象一块内存地址可能被多个对象所指向。当类的实例在析构的时候可能会导致垂悬指针问题即指针的指向对象已被删除或释放但仍有其他指针保留了对该内存位置的引用。
**管理类中指针成员的方法一般有两种方式**一种是采用值型类这种类是给指针成员提供值语义value semantics当复制该值型对象时会得到一个不同的新副本。这种方式典型的应用是string类。另外一种方式就是智能指针实现这种方式的指针所指向的对象是共享的。
智能指针不仅提供了内存管理的自动化还增强了代码的安全性和可读性是现代 C 中推荐的内存管理方式之一。本篇文章旨在系统地介绍 C 标准库中的三种常用智能指针std::unique_ptr、std::shared_ptr 和 std::weak_ptr。
智能指针并不是指针而是行为类似于指针的类对象这种对象具有指针不包含的其他功能。简单来说智能指针能帮助我们管理动态分配的内存它会帮助我们自动释放new出来的内存从而避免 new 和 delete引发的一系列问题比如内存泄漏、悬空指针和重复释放等。
C里面有四个智能指针auto_ptr、share_ptr、unique_ptr、weak_ptr。其中后三个是C11支持的并且第一个已经在C11弃用这里我们只学习后三个。
1. shared_ptr
这三类智能指针模板都定义了类似指针的对象可以将new获得直接或间接的地址赋给这种对象当智能指针过期时其析构函数将使用delete来释放内存。因此如果将new返回的地址赋给这些对象将无需记住稍后释放这些内存在智能指针过期时这些内存会自动释放RAII。 我们可以回顾一下我们在并发编程10这篇文章中提到的问题shared_ptr是线程安全的吗我只简单做了回答在这篇文章中我将在下面详细分析。 1.1 shared_ptr 内存模型
shared_ptr有以下两个作用
std::shared_ptr使用引用计数每一个shared_ptr的拷贝都指向相同的内存。只有在最后一个shared_ptr副本对象析构的时候内存才会被释放。sharedd_ptr共享被管理的对象同一时刻可以有多个shared_ptr拥有对象的所有权多线程中不同线程可以对指向同一内存的sharedd_ptr副本中的数据进行安全访问或修改但多个线程不能对同一个sharedd_ptr对象中的数据进行修改当最后一个shared_ptr对象销毁时被管理对象自动销毁。
简单来说shared_ptr实现包含了两个部分
一个指向堆上创建的对象的裸指针 raw_ptr。一个指向内部隐藏的、共享的管理对象 shared_count_object。其中use_count是当前这个堆上对象被多少对象引用了简单来说就是引用计数。 图片来源https://github.com/SRombauts/shared_ptr 如上图所示shared_ptr内部包含了两个指针一个Ptr to T指向目标管理对象T object另一个Ptr to Control Block指向控制块Control Block。控制块包含了一个引用计数(reference count)、一个弱计数(weak count)和其他数据(other data)比如删除器、分配器等。
引用计数会累加共享同一块资源内存的shared_ptr对象数量是shared_ptr的核心在任何情况下都是线程安全的因为引用计数的实现过程是原子操作。 为了满足线程安全的要求引用计数通常使用类似于 std::atomic::fetch_add 的操作并结合 std::memory_order_relaxed 进行递增递减操作则需要更强的内存排序以确保控制块能够安全销毁。 简单举一个例子
std::shared_ptrint p1(new int(1));
std::shared_ptrint p2p1;shared_ptr有很多构造函数这里使用的构造函数原型为
template class Y
explicit shared_ptr( Y* ptr );template class Y
shared_ptr operator( const shared_ptrY r ) noexcept;二者的内存模型如下所示 图片来源https://blog.csdn.net/LCZ411524/article/details/143648637 很明显p1和p2都指向同一内存空间T Object而且引用计数为2只有当p1和p2都被释放后引用计数减为0的同时智能指针管理的对象才会被释放。
1.2 shared_ptr的使用
上面多次说过通过new和delete创建的对象存在很多隐患但我还要在这里重复提醒
当一个函数返回局部变量的指针非new创建时外部使用该指针可能会造成崩溃或逻辑错误。因为局部变量随着函数的右}释放了。当在作用域内使用new创建的对象在作用域结束后仍未被delete那么该内存不存在任何对象指向内存泄漏。如果多个指针指向同一个堆空间其中一个释放了堆空间使用其他的指针时会造成崩溃悬空指针。对一个指针多次delete会造成double free问题重复释放。两个类对象A和B分别包含对方类型的指针成员互相引用时如何释放是个问题。
1.2.1 make_shared
我们可以通过构造函数来创建一个智能指针也可以通过make_shared来构造智能指针但更推荐后者因为
std::make_shared 减少了内存分配的次数 使用 new 创建 当直接使用 std::shared_ptr 时需要两次内存分配 为所管理的对象分配内存。为 std::shared_ptr 的控制块控制引用计数和资源信息分配内存。 使用 make_shared std::make_shared 会在一次内存分配中同时分配对象和控制块的内存避免了额外的内存分配。
#include memory// 使用 new 创建
std::shared_ptrint sp1(new int(10)); // 两次内存分配// 使用 make_shared 创建
std::shared_ptrint sp2 std::make_sharedint(10); // 一次内存分配
直接使用 new 创建 std::shared_ptr 可能引发异常时的资源泄漏问题。 如果在 std::shared_ptr 的构造过程中发生异常new 分配的资源可能无法正确释放导致内存泄漏。std::make_shared 是异常安全的因为其分配和构造过程是一体化的保证资源不会泄漏。
// 错误代码
void exception_test() {std::shared_ptrint sp(new int[100]); // 动态分配数组throw std::runtime_error(Error occurred); // 如果异常发生数组内存泄漏
}// 正确代码
void exception_test() {std::shared_ptrint sp std::make_sharedint[100](); // 异常安全资源会正确管理
}当直接使用 new 时需要确保动态分配的内存与 std::shared_ptr 的删除器匹配。 如果使用默认删除器管理动态分配的数组会导致未定义行为数组不会被正确释放。std::make_shared 自动匹配删除器避免了这种错误。
// 错误代码
void test() {std::shared_ptrint sp(new int[10]); // 错误默认删除器无法正确释放数组
}
// 正确代码
void test() {std::shared_ptrint[] sp std::make_sharedint[](10); // 正确删除器自动匹配
}但注意当存在以下情况时不应该使用make_shared来构造shared_ptr对象而应直接构造shared_ptr对象 需要自定义删除器 std::make_shared 自动使用 delete 来销毁对象但如果我们创建对象管理的资源不是通过new分配的内存那么需要我们自定义一个删除器来销毁该内存或者我们需要为 std::shared_ptr 提供自定义的删除逻辑例如释放资源时需要执行额外的操作那么 std::make_shared 就不适用了。在这种情况下我们需要通过shared_ptr的构造函数来创建对象并传递一个自定义的删除器。 创造对象的构造函数是保护或私有时 当我们想要创建的对象没有公有的构造函数时make_shared无法使用 对象的内存可能无法及时回收 make_shared 只分配一次内存这看起来很好减少了内存分配的开销。问题来了weak_ptr会保持控制块(强引用以及弱引用的信息)的生命周期而因此连带着保持了对象分配的内存只有最后一个weak_ptr离开作用域时内存才会被释放。原本强引用减为 0 时就可以释放的内存现在变为了强引用若引用都减为 0 时才能释放意外的延迟了内存释放的时间。这对于内存要求高的场景来说是一个需要注意的问题。 需要管理数组 std::make_shared 不能直接用于创建和管理动态数组。如果你希望管理动态数组应该使用 std::shared_ptrT[] 或者手动管理内存避免数组越界等问题。
int main() {// 使用 make_shared 管理单个对象auto sp1 std::make_sharedint(10);// 错误不能直接使用 make_shared 管理数组// auto sp2 std::make_sharedint[](10); // 不能这样// 正确的做法std::shared_ptrint[] sp2 std::make_sharedint[](10); // 适用于数组
}在上段代码中我们可以使用make_shared来创建shared_ptr管理单个对象但不能将make_shared用来创建一个数组并返回给auto类型的变量。
因为std::make_sharedint[](10) 会返回一个 std::shared_ptrint[] 类型的指针但这里的 auto 推导无法正确推断出数组类型因为 std::shared_ptrint[] 是一个特殊类型它不是一个普通的 std::shared_ptrint 类型。因此在这种情况下auto 无法推导出正确的类型。
而后者显式地指定了 std::shared_ptrint[] 类型这样编译器就知道我们正在创建一个指向 int[] 数组的智能指针并且 std::make_sharedint[](10) 会返回一个合适的 std::shared_ptrint[] 类型的对象。 简单使用
//定义一个指向整形5的指针
auto psint2 make_sharedint(5);
//判断智能指针是否为空
if (psint2 ! nullptr)
{cout psint2 is *psint2 endl;
}auto psstr2 make_sharedstring(hello zack);
if (psstr2 ! nullptr !psstr2-empty())
{cout psstr2 is *psstr2 endl;
}对于智能指针的使用和普通的内置指针没什么区别通过判断指针是否为nullptr可以判断是否为空指针。通过-可以取指针内部的成员方法或者成员变量。
当我们需要获取内置类型管理资源时可以通过智能指针的方法get()返回其底层管理的内置指针。 注意通过get()函数返回的内置指针时要注意以下问题 我们不能主动通过delete回收该指针要交给智能指针自己回收否则会造成double free或者使用智能指针产生崩溃等问题。也不能用get()返回的内置指针初始化另一个智能指针因为两个智能指针引用一个内置指针会出现问题比如一个释放了另一个不知道就会导致崩溃等问题。 因为get() 方法返回的原始指针即裸指针不增加智能指针对对象的引用计数或所有权管理
std::shared_ptrint sp1 std::make_sharedint(10);
int* raw_ptr sp1.get();// 错误使用裸指针初始化另一个 shared_ptr
std::shared_ptrint sp2(raw_ptr); // 错误sp2 和 sp1 都会管理同一个内存这里raw_ptr 是 sp1 管理的对象的裸指针但 raw_ptr 不会增加对象的引用计数也不会管理其生命周期。当我们通过 raw_ptr 初始化 sp2 时sp2 会成为一个新的智能指针指向相同的内存区域。由于 sp1 和 sp2 都管理同一个内存对象但它们并没有共享引用计数。裸指针的生命周期与智能指针不同它不被智能指针的生命周期管理这可能会导致以下错误
多次释放同一内存如果两个智能指针都拥有相同的裸指针而其中一个智能指针释放了这个指针所管理的资源另一个智能指针会在其析构时试图释放相同的资源。这会导致“双重释放”错误通常会导致程序崩溃。悬挂指针如果原始智能指针在另一个智能指针之前被销毁那么另一个智能指针会变成一个悬挂指针。虽然这个智能指针指向有效内存但该内存已被释放访问它会导致未定义行为通常会崩溃。 get()用来将指针的访问权限传递给代码只有在确定代码不会delete裸指针的情况下才能使用get。特别是永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。 1.2.2 shared_ptr与new结合
我们可以传给shared_ptr一个new构造的指针对象
auto psint shared_ptrint(new int(5));
auto psstr shared_ptrstring(new string(hello zack));原型在上面也说过是
template class Y
explicit shared_ptr( Y* ptr );因为该构造函数是explicit的。因此我们不能将一个内置指针隐式转换为一个智能指针必须使用直接初始化形式来初始化一个智能指针
//错误不能用内置指针隐式初始化shared_ptr
// shared_ptrint psint2 new int(5);
//正确显示初始化
shared_ptrstring psstr2(new string(good luck));除了智能指针之间的赋值赋值构造函数外还以通过一个智能指针构造另一个
shared_ptrstring psstr2(new string(good luck));
//可以通过一个shared_ptr 构造另一个shared_ptr
shared_ptrstring psstr3(psstr2);
shared_ptrstring psstr4;
psstr4 psstr2;以上方法构造的智能指针都共享同一个引用计数。
在构造智能指针的同时可以指定自定义的删除方法替代shared_ptr本身内置的delete操作
//可以设置新的删除函数替代delete
shared_ptrstring psstr4(new string(good luck for zack), delfunc);void delfunc(string *p)
{if (p ! nullptr){delete (p);p nullptr;}cout self delete endl;
}我们实现了自己的delfunc函数作为删除器回收了内置指针并且打印了删除信息。这样当psstr4执行析构时会打印”self delete”。
1.2.3 reset()
reset()不带参数时若智能指针s是唯一指向该对象的指针则释放并置空。若智能指针s不是唯一指向该对象的指针则引用计数减一同时将s置为空。reset()带参数时若智能指针s是唯一指向该对象的指针则释放并指向新的对象。若智能指针s不是唯一指向该对象的指针则引用计数减一并指向新的对象。
p.reset() ; //将p重置为空指针所管理对象引用计数 减1
p.reset(p1); //将p重置为p1的值,p管控的对象计数减1p接管对p1指针的管控
p.reset(p1,d); //将p重置为p1的值p 管控的对象计数减1并使用d作为删除器reset()的功能是为shared_ptr重新开辟一块新的内存让shared_ptr绑定这块内存
shared_ptrint p(new int(5));
// p重新绑定新的内置指针
p.reset(new int(6));上述代码为p重新绑定了新的内存空间。
reset常用的情况是判断智能指针是否独占内存如果引用计数为1也就是自己独占内存就去修改否则就为智能指针绑定一块新的内存进行修改防止多个智能指针共享一块内存一个智能指针修改内存导致其他智能指针受影响。
//如果引用计数为1unique返回true
if (!p.unique())
{//还有其他人引用所以我们为p指向新的内存p.reset(new int(6));
}
// p目前是唯一用户
*p 1024;使用智能指针的另一个好处就是当程序崩溃时智能指针也能保证内存空间被回收
void execption_shared()
{shared_ptrstring p(new string(hello zack));//此处导致异常int m 5 / 0;//即使崩溃也会保证p被回收
}即使运行到 m 5 / 0处程序崩溃智能指针p也会被回收。有时候我们传递个智能指针的指针不是new分配的那就需要我们自己给他传递一个删除器
void delfuncint(int *p)
{cout *p in del func endl;
}void delfunc_shared()
{int p 6;shared_ptrint psh(p, delfuncint);
}如果不传递delfuncint会造成p被智能指针delete因为p是栈空间的变量用delete会导致崩溃。
1.2.4 通过智能指针共享数据
我们定义一个 StrBlob 类该类通过 shared_ptr 实现智能指针管理用于共享一个 vectorstring 类型的容器。
class StrBlob
{
public://定义类型用于表示 StrBlob 中存储的元素的数量typedef std::vectorstring::size_type size_type;StrBlob();//通过初始化列表构造StrBlob(const initializer_liststring li);//返回vector大小size_type size() const { return data-size(); }//判断vector是否为空bool empty(){return data-empty();}//向vector写入元素void push_back(const string s){data-push_back(s);}//从vector弹出元素void pop_back();//访问头元素std::string front();//访问尾元素std::string back();private:shared_ptrvectorstring data;//检测i是否越界void check(size_type i, const string msg) const;// 判断容器是否无元素的标志string badvalue;
};该类只实现了默认构造函数和一个带有初始化列表参数的构造函数后者允许我们通过初始化列表例如{one, two, three}来初始化 StrBlob 对象。但因为StrBlob未重载赋值运算符也没有实现拷贝构造函数所以StrBlob对象之间的赋值是浅拷贝浅拷贝只赋值对象本身的值或引用并不会复制对象所引用的内存或对象也就是浅拷贝只创建了一个新的对象并将原对象的指针或引用直接复制到新对象中而没有复制指针所指向的数据因而内部成员data会随着StrBlob对象的赋值修改引用计数浅拷贝。
当然我们也可以实现拷贝构造函数和赋值运算符但只能用浅拷贝的方式实现不能实现深拷贝即拷贝指针指向的对象而不是拷贝指针本身这样即使其他指针消失但仍然不影响该指针指向这块内存
// 默认构造函数
StrBlob::StrBlob()
{data make_sharedvectorstring();
}
// 复制构造函数// 列表初始化
StrBlob::StrBlob(const StrBlob other)
{data sb.data;
}
// 列表初始化
StrBlob::StrBlob(const initializer_liststring li)
{data make_sharedvectorstring(li);
}
// 赋值运算符
StrBlob operator(const StrBlob other)
{if (this ! other) {data other.data;}return *this;
}注意将data other.data修改为data(std::make_sharedstd::vectorstd::string(*other.data))或data std::make_sharedstd::vectorstd::string(*other.data)即可更正为深拷贝。 浅拷贝只复制对象的值或引用多个对象共享同一个动态分配的内存空间修改其中一个对象的数据会影响到另一个对象。
深拷贝复制对象本身的值并且对对象引用的内存进行递归复制确保每个对象拥有独立的内存修改其中一个对象的数据不会影响另一个对象。 图片来源C primer plus深拷贝 实现检查越界的函数
//检测i是否越界
void StrBlob::check(size_type i, const string msg) const
{if (i data-size()){throw out_of_range(msg);}
}实现front访问首元素
string StrBlob::front()
{//不要返回局部变量的引用if (data-size() 0){return badvalue;}return data-front();
}如果队列为空返回一个空的字符串。但是如果我们直接构造一个空字符串返回这样就返回了局部变量的引用局部变量会随着函数结束而释放造成安全隐患。所以我们可以返回类的成员变量badvalue作为队列为空的标记。当然如果不能容忍队列为空的情况可以通过抛出异常来处理那我们用这种方式改写front
string StrBlob::front()
{check(0, front on empty StrBlob);return data-front();
}同样实现back()和pop_back()
string StrBlob::back()
{check(0, back on empty StrBlog);return data-back();
}
void StrBlob::pop_back()
{check(0, back on pop_back StrBlog);data-pop_back();
}这样我们通过定义StrBlob类达到共享vector的方式。多个StrBlob操作的是一个vector向量。
我们可以通过use_count函数获得当前托管指针的引用计数
void StrBlob::printCount()
{cout shared_ptr use count is data.use_count() endl;
}1.2.5 shared_ptr是线程安全的吗
现在我们可以很简单的回答这个问题并不是。 引用计数是线程安全的 shared_ptr仅有引用计数是线程安全的因为在shared_ptr的控制块中引用计数变量使用类似于 std::atomic::fetch_add 的操作并结合 std::memory_order_relaxed 进行递增递减操作则需要更强的内存排序以确保控制块能够安全销毁。其关键在于使用了原子操作对引用计数进行增加或减少所以是线程安全的。 而且因为引用计数是线程安全的多个线程可以安全地操作引用计数和访问管理对象即使这些 shared_ptr 实例是同一对象的副本且共享所有权也是如此所以管理共享资源的生命周期是线程安全的不用担心因为多线程操作导致资源提早释放或延迟释放。 shared_ptr本身并不是线程安全的 但是**shared_ptr本身并不是线程安全的**shared_ptr 对象实例包含一个指向控制块的指针和一个指向底层元素的指针。这两个指针的操作在多个线程中并没有同步机制。因此如果多个线程同时访问同一个 shared_ptr 对象实例并调用非 const 成员函数如 reset 或 operator这些操作会导致对这些指针的并发修改进而引发数据竞争就像我们在并发编程10中说的一样独立的原子操作当然是线程安全的但是如果原子操作依赖于非原子操作那么这个过程可能就是非线程安全的。举例
情况一当多线程操作同一个shared_ptr对象时
// 按指针传入
void fn(shared_ptrA* sp) {...
}
// 按引用传入
void fn(shared_ptrA sp) {...if (..) {sp other_sp;} else if (...) {sp other_sp2;}
}std::thread t1(fn, std::ref(sp1));
std::thread t2(fn, std::ref(sp1));如果我们将shared_ptr对象的指针或引用传入给可调用对象当创建不同线程对shared_ptr进行修改时比如修改其指向如 reset 或 operator。sp原先指向的引用计数的值要减去1other_sp指向的引用计数值要加1。然而这几步操作加起来并不是一个原子操作并发编程10)在原子操作中说过并不是所有原子操作都是线程安全的如果原子操作依赖于非原子操作那么这个过程可能就是非线程安全的这里的条件判断并不是原子操作如果多少线程都在修改sp的指向的时候那么可能会出问题。比如在导致计数在操作减一的时候其内部的指向已经被其他线程修改过了。引用计数的异常会导致某个管理的对象被提前析构后续在使用到该数据的时候触发core dump。
如果不调用shared_ptr的非const成员函数修改shared_ptr那么就是线程安全的。
情况二当多线程操作不同shared_ptr对象时
如果不是同一 shared_ptr 对象管理的数据是同一份引用计数共享但shared_ptr不是同一个对象每个线程读写的指针也不是同一个引用计数又是线程安全的那么自然不存在数据竞争可以安全的调用所有成员函数。
// 按值传递
void fn(shared_ptrA sp) {...if (..) {sp other_sp;} else if (...) {sp other_sp2;}
}
std::thread t1(fn, std::ref(sp1));
std::thread t2(fn, std::ref(sp1));这时候每个线程内看到的sp他们所管理的是同一份数据用的是同一个引用计数。但是各自是不同的对象当发生多线程中修改sp指向的操作的时候是不会出现非预期的异常行为的也就是线程安全的。 shared_ptr所管理的对象不是线程安全的 尽管前面我们提到了如果是按值捕获或传参的shared_ptr对象那么是该对象是线程安全的。然而话虽如此但却可能让人误入歧途。因为我们使用shared_ptr更多的是操作其中的数据对齐管理的数据进行读写而不是修改shared_ptr的指向。尽管在按值捕获的时候shared_ptr本身是线程安全的我们不需要对此施加额外的同步操作比如加解锁、条件变量、call_once 和once_flag 但是这并不意味着shared_ptr所管理的对象是线程安全的
shared_ptr本身和shared_ptr管理的对象是两个东西并不是同一个如果我们要多线程处理shared_ptr所管理的资源我们需要主动的对其施加额外的同步操作比如加解锁、条件变量、call_once 和once_flag。
如果shared_ptr管理的数据是STL容器那么多线程如果存在同时修改的情况是极有可能触发core dump的。比如多个线程中对同一个vector进行push_back或者对同一个map进行了insert。甚至是对STL容器中并发的做clear操作都有可能出发core dump当然这里的线程不安全性其实是其所指向数据的类型的线程不安全导致的并非是shared_ptr本身的线程安全性导致的。尽管如此由于shared_ptr使用上的特殊性所以我们有时也要将其纳入到shared_ptr相关的线程安全问题的讨论范围内。
除了STL容器的并发修改操作这里指的是修改容器的结构并不是修改容器中某个元素的值后者是线程安全的前者不是protobuf的Message对象也是不能并发操作的比如一个线程中修改Message对象set、add、clear另外一个线程也在修改或者在将其序列化成字符串都会触发core dump。
STL容器如何解决线程安全可以参考这篇文章C STL容器如何解决线程安全的问题 - 知乎
最后有很多人可能认为引用计数是通过智能指针的静态成员变量所管理的但这很明显是错的
shared_ptrA sp1 make_sharedA(x);
shared_ptrA sp2 make_sharedA(y);两个完全不相干的sp1和sp2只要模板参数T是同一个类型即使管理的资源不是同一个但如果使用静态成员变量管理引用计数那么二者就会共享同一个计数。
2. weak_ptr
我们在shared_ptr说到了new和delete可能会引发内存泄漏问题作用域内new的对象在作用域结束后仍未delete此时没有任何对象指向这片内存但是shared_ptr本身也可能会引发内存泄漏问题即循环引用问题。
2.1 循环引用问题
shared_ptr 循环引用问题是指**两个或多个对象之间通过shared_ptr相互引用导致对象无法被正确释放从而造成内存泄漏。**常见的情况是两个对象A和B它们的成员变量互相持有了对方的shared_ptr。当A和B都不再被使用时它们的引用计数不会降为0无法被自动释放。比如
class Girl;class Boy {
public:Boy() {cout Boy 构造函数 endl;}~Boy() {cout ~Boy 析构函数 endl;}void setGirlFriend(shared_ptrGirl _girlFriend) {this-girlFriend _girlFriend;}
private:shared_ptrGirl girlFriend;
};class Girl {
public:Girl() {cout Girl 构造函数 endl;}~Girl() {cout ~Girl 析构函数 endl;}void setBoyFriend(shared_ptrBoy _boyFriend) {this-boyFriend _boyFriend;}
private:shared_ptrBoy boyFriend;
};void useTrap() {shared_ptrBoy spBoy(new Boy());shared_ptrGirl spGirl(new Girl());// 陷阱用法spBoy-setGirlFriend(spGirl);spGirl-setBoyFriend(spBoy);// 此时boy和girl的引用计数都是2cout r_count of spBoy is : spBoy.use_count() endl;cout r_count of spGirl is : spGirl.use_count() endl;
}int main(void) {useTrap();system(pause);return 0;
}我们通过useTrap()函数创建了两个shared_ptr对象其中spBoy用于存储一个Boy类spGirl用于存储一个Girl类但是Boy类中有一个智能指针变量用于存储Girl类而Girl类中有一个智能指针变量用于存储Boy类。如果给智能指针spBoy和spGirl管理对象的成员变量赋值那么会造成循环引用问题。此时创建的智能指针无法被销毁因为引用计数总不为0。
代码输出为
Boy 构造函数
Girl 构造函数
r_count of spBoy is : 2
r_count of spGirl is : 2确实因为二者的引用计数总为2或1这两个类不能被正确析构。
有没有方法解决这个问题呢这时候我们就用到了智能指针**weak_ptr**。 weak_ptr是一种弱引用不会增加对象的引用计数在对象释放时会自动设置为nullptr。它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造 它的构造和析构不会引起引用记数的增加或减少当然weak_ptr其实不需要析构函数因为它不需要管理和释放资源即没有RAII。 同时weak_ptr 没有重载operator*和operator-不支持访问资源但可以使用 weak_ptr.lock() 获得一个可用的 shared_ptr 对象当对象已经释放时会返回一个空shared_ptr。
那么上面的代码可以修改为
class Girl;class Boy {
public:Boy() {cout Boy 构造函数 endl;}~Boy() {cout ~Boy 析构函数 endl;}void setGirlFriend(shared_ptrGirl _girlFriend) {this-girlFriend _girlFriend;// 在必要的使用可以转换成共享指针shared_ptrGirl sp_girl;sp_girl this-girlFriend.lock();cout r_count of spGirl is : sp_girl.use_count() endl;// 使用完之后再将共享指针置NULL即可sp_girl NULL;}
private:weak_ptrGirl girlFriend;
};class Girl {
public:Girl() {cout Girl 构造函数 endl;}~Girl() {cout ~Girl 析构函数 endl;}void setBoyFriend(shared_ptrBoy _boyFriend) {this-boyFriend _boyFriend;}private:shared_ptrBoy boyFriend;
};void useTrap() {shared_ptrBoy spBoy(new Boy());shared_ptrGirl spGirl(new Girl());spBoy-setGirlFriend(spGirl);cout r_count of spGirl is : spGirl.use_count() endl;spGirl-setBoyFriend(spBoy);cout r_count of spBoy is : spBoy.use_count() endl;cout r_count of spGirl is : spGirl.use_count() endl;
}
int main(void) {useTrap();system(pause);return 0;
}我们将Boy类的私有成员变量类型由shared_ptr更换为了weak_ptr此时对该变量赋值不会造成引用计数的增加自然就解决了循环引用问题。
代码输出为
Boy 构造函数
Girl 构造函数
r_count of spGirl is : 3
r_count of spGirl is : 1
r_count of spBoy is : 2
r_count of spGirl is : 1
~Girl 析构函数
~Boy 析构函数spGirl的引用计数之所以为3是因为在调用setGirlFriend函数时按值传入一个spGirl对象在函数内部拥有一个spGirl副本此时引用计数为2当创建了一个shared_ptrGirl类型的对象sp_girl并调用weak_ptrGirl的lock函数获取shared_ptrGirl赋予给sp_girl时引用计数变为了3。但它们都是局部变量所以当函数作用域结束后都会被自动释放所以最后引用计数变味了1。
spBoy内部的成员变量类型是shared_ptr而不是weak_ptr所以引用计数为2但是当spGirl被释放后spBoy的引用计数为1此时spBoy也可以正常释放。
2.2 其他成员函数
2.2.1 use_count()
weak_ptr同样可以通过调用use_count()方法获取当前观察资源的引用计数
shared_ptrint sp(new int(10));
weak_ptrint wp1(sp);
// 或者
weak_ptrint wp2;
wp2 sp;cout wp1.use_count() endl; //结果为输出1
cout wp2.use_count() endl; //结果为输出12.2.2 expired()
通过expired()成员函数去检查指向的资源是否过期判断当前weak_ptr智能指针是否还有托管的对象有则返回false无则返回true。
如果返回true等价于 use_count() 0即已经没有托管的对象了当然可能还有析构函数进行释放内存但此对象的析构已经临近或可能已发生。
std::weak_ptrint gw;void f() {// expired判断当前智能指针是否还有托管的对象有则返回false无则返回trueif (!gw.expired()) {std::cout gw is valid\n; // 有效的还有托管的指针} else {std::cout gw is expired\n; // 过期的没有托管的指针}
}int main() {{auto sp std::make_sharedint(42);gw sp;f();}// 当{ }体中的指针生命周期结束后再来判断其是否还有托管的指针f();return 0;
}代码输出
gw is valid
gw is expired在 { } 中sp的生命周期还在gw还在托管着make_shared赋值的指针(sp)所以调用f()函数时打印gw is valid\n; 当执行完 { } 后sp的生命周期已经结束已经调用析构函数释放make_shared指针内存(sp)gw已经没有在托管任何指针了调用expired()函数返回true所以打印gw is expired\n;
2.2.3 lock()
可以通过调用lock()成员函数获取监视的shared_ptr
使用lock将资源锁住lock会将weap_ptr转为shared_ptr即使weap_ptr指向的shared_ptr资源被释放也不影响使用当对象已经释放时会返回一个空shared_ptr如果要访问weap_ptr指向的数据必须使用lock将weap_ptr转为shared_ptr才能访问到。在多线程中要防止一个线程在使用智能指针而另一个线程删除指针指针问题可以使用weak_ptr的lock()方法。
auto sp std::make_sharedint(42); // 创建一个共享指针;
std::weak_ptrint wp(sp);shared_ptrint p;
if (!wp.expired())
{p wp.lock(); // 如果要取到weak_ptr中的需要先使用lock将weak_ptr转为shard_ptr才能取值sp.reset();cout *p endl; // 42cout p.use_count() endl; // 1cout sp.use_count() endl; // 0
}3. enable_from_this_shared
在一个类的成员函数中我们不能直接将this指针作为shared_ptr返回而需要通过派生std::enable_shared_from_this类通过其方法shared_from_this来返回指针。原因是std::enable_shared_from_this类中有一个weak_ptr这个weak_ptr用来观察this智能指针调用shared_from_this()方法其实是调用内部这个weak_ptr的lock()方法将所观察的shared_ptr返回。
class MyClass : public std::enable_shared_from_thisMyClass
{
public:shared_ptrMyClass GetSelf() {//return shared_ptrMyClass(this); 直接返回this的共享智能指针如果直接返回MyClass会被析构两次return shared_from_this();}MyClass() {cout MyClass() endl;};~MyClass() {cout ~MyClass() endl;};
};int main()
{shared_ptrMyClass sp1(new MyClass);cout sp1.use_count()endl;shared_ptrMyClass sp2 sp1-GetSelf();cout sp1.use_count()endl;cout sp2.use_count()endl;
}在外面创建MyClass对象的智能指针和通过对象返回this的智能指针都是安全的因为shared_from_this()是std::enable_shared_from_thisMyClass内部的weak_ptr调用lock()方法之后返回的智能指针。在离开作用域之后sp1和sp2会自动析构其引用计数减为0MyClass对象会被析构不会出现MyClass对象被析构两次的问题。需要注意的是获取自身智能指针的函数仅在shared_ptr的构造函数被调用之后才能使用因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。
上述代码的输出为
MyClass()
1
2
2
~MyClass()很明显sp1和sp2共用同一个引用计数共享资源但确实两个shared_ptr对象可以修改指向而不影响其他shared_ptr对象。 但注意你不能直接将this指针作为shared_ptr返回回来 我在前面说过不能将智能指针通过get()函数返回的裸指针用于初始化或reset另一个指针。通过这种方法初始化的智能指针其实和原本在类内部构造的智能指针是两个独立的对象它们不共享引用计数仅仅只是管理的资源相同。如果多次析构会造成同一个资源被重复析构两次的问题。
所以不要将this指针作为shared_ptr返回回来因为this指针本质上是一个裸指针因此可能会导致重复析构
class MyClass
{
public:shared_ptrMyClass GetSelf() {return shared_ptrMyClass(this);//不要这样做}MyClass() {cout MyClass() endl;};~MyClass() {cout ~MyClass() endl;};
};int main()
{// sp1与sp2都会调用new MyClass的析构函数一个对象析构两次shared_ptrMyClass sp1(new MyClass);shared_ptrMyClass sp2 sp1-GetSelf();return 0;
}在这个例子中由于用同一个指针this)构造了两个智能指针sp1和sp2而他们之间是没有任何关系的在离开作用域之后this将会被构造的两个智能指针各自析构导致重复析构的错误。
4. unqiue_ptr
unique_ptr和shared_ptr不同unique_ptr不允许所指向的内容被其他指针共享所以unique_ptr不允许拷贝构造和赋值。
void use_uniqueptr()
{//指向double类型的unique指针unique_ptrdouble udptr;//一个指向int类型的unique指针unique_ptrint uiptr(new int(42));// unique不支持copy// unique_ptrint uiptr2(uiptr);// unique不支持赋值// unique_ptrint uiptr3 uiptr;
}尽管unqiue_ptr不能拷贝或赋值但可以通过调用release()、reset()或移动语义将指针的所有权从一个非constunique_ptr转移给另一个unique
a. release()
release() 会释放 unique_ptr 的所有权返回原始指针同时将当前 unique_ptr 置为空。释放后需要手动管理返回的裸指针可以将其转移到另一个 unique_ptr 中。使用 release() 后原来的 unique_ptr 不再管理资源必须确保资源由新的管理者接管否则会导致内存泄漏。
b. reset()
reset() 会释放当前 unique_ptr 所管理的资源并接管一个新的指针。可以通过 reset() 将一个裸指针直接交给新的 unique_ptr。调用 reset() 后原来的 unique_ptr 被释放接管新资源。
c. std::move
std::unique_ptr 支持移动构造和移动赋值可以安全地将所有权从一个 unique_ptr 转移到另一个。转移后原来的 unique_ptr 不再拥有资源变为 nullptr。
void use_uniqueptr()
{//定义一个upstrunique_ptrstring upstr(new string(hello zack));std::cout upstr: *upstr \n;// upstr.release()返回其内置指针并将upstr置空// 用upstr返回的内置指针初始化了upstr2unique_ptrstring upstr2(upstr.release());std::cout upstr2: *upstr2 \n;unique_ptrstring upstr3(new string(hello world));std::cout upstr3: *upstr3 \n;//将upstr3的内置指针转移给upstr2// upstr2放弃原来的内置指针指向upstr3返回的内置指针。upstr2.reset(upstr3.release());std::cout upstr2: *upstr2 \n;// 通过移动语义将upstr2的所有权转移给upstrupstr std::move(upstr2);std::cout upstr: *upstr \n;// 通过移动语义将upstr的所有权转移给upstr4unique_ptrstring upstr4(std::move(upstr));std::cout upstr4: *upstr4 \n;
}输出为
upstr: hello zack
upstr2: hello zack
upstr3: hello world
upstr2: hello world
upstr: hello world
upstr4: hello world不能拷贝unique_ptr的规则有一个例外我们可以“拷贝“或”赋值”一个将要被销毁的非constunique_ptr。最常见的例子是从函数返回一个unique_ptr
std::unique_ptrint createUniquePtr() {auto ptr std::make_uniqueint(42); // 创建局部 unique_ptrreturn ptr; // 返回时自动调用移动构造
}int main() {std::unique_ptrint myPtr createUniquePtr(); // 接收函数返回值std::cout Value: *myPtr std::endl;return 0;
}虽然这个过程好像确实是在调用拷贝构造函数创建了一个副本但std::unique_ptr 的拷贝构造和拷贝赋值是被明确删除的 delete无法直接复制。
因为std::unique_ptr支持移动构造和移动赋值所以当函数返回一个局部的 std::unique_ptr 时C 会隐式应用移动构造并不会真的尝试拷贝它。
我在文章中简单说过编译器会根据传入参数自动选择构造函数
如果对象的拷贝构造函数可用但移动构造函数也存在push_back 会选择合适的构造函数 如果传递的是一个左值会调用拷贝构造函数。如果传递的是一个右值会调用移动构造函数。 如果对象的拷贝构造函数不可用但移动构造函数存在push_back 的选择 如果传递的是一个左值会直接报错编译器无法将左值隐式转换为右值从而调用移动。如果传递的是一个右值会调用移动构造函数。
在这里函数返回一个临时的unique_ptr变量是右值。而unique_ptr禁止拷贝或复制但允许移动拷贝或移动赋值所以编译器会自动调用unique_ptr的移动构造函数。因此从函数返回时并不会违反 std::unique_ptr 的独占所有权规则。
其他内容可参考我的个人博客 爱吃土豆的个人博客