目录
原始指针的问题
使用 C++的指针可以动态开辟存储空间,但若在使用完毕后忘记释放(或在释放之前,程序 throw 出错误,导致没有释放),导致该内存单元一直被占据直到程序结束,即发生了所谓的内存泄漏。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
智能指针正好能够弥补这些问题,因为它本质是存放在栈的模板对象,只是在栈内部包了一层指针。而栈在其生命周期结束时,其中的指针指向的堆内存也自然被释放了。因而实现了智能管理的效果,不需要考虑内存问题了,其实有点类似某种单例写法,程序运行结束,也不用考虑单例对象内存问题。
【注】内存泄漏是指堆内存的泄漏。堆,就是那些由 new 分配的内存块。
情况1: int * p = new int(0); 忘记用delete释放,
情况2:int * p = new int(0);
delete p; p为置nullptr,会变成野指针
情况3:int * p = new int(0);
delete p;
p=nullptr; 如果内存申请不成功,new会抛出异常,而我们却什么都没有做!
情况4:int *ptr = new(nothrow) int(0);
if(!ptr)
{
cout << "new fails."
return 0;
}
if (hasException()) //若程序突然发生异常,就转到异常处理函数去了,最终导致程序终止,下面就没有释放
throw exception();
delete ptr;
ptr = nullptr;
当然,我们可以在“hasException()”为真时释放内存:但,我们并不总会想到这么做。而且,这样子做也显得麻烦,不够人性化。
智能指针
因此智能指针的作用就是为了保证使用堆上对象的时候,对象一定会被释放,但只能释放一次,并且释放后指向该对象的指针应该马上归 0。
为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。
我们使用智能指针的原因至少有以下三点:
1)智能指针能够帮助我们处理资源泄露问题;
2)它也能够帮我们处理空悬指针(野指针)的问题;
3)它还能够帮我们处理比较隐晦的由异常造成的资源泄露。
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、weak_ptr。
- unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。(不用手动去delete)
- shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
几乎每一个有分量的程序都需要“在相同时间的多处地点处理或使用对象”的能力。为此,我们必须在程序的多个地点指向(refer to)同一对象。虽然C++语言提供引用(reference)和指针(pointer),还是不够,因为我们往往必须确保当“指向对象”的最末一个引用被删除时该对象本身也被删除,毕竟对象被删除时析构函数可以要求某些操作,例如释放内存或归还资源等等。
所以我们需要“当对象再也不被使用时就被清理”的语义。Class shared_ptr提供了这样的共享式拥有语义。也就是说,多个shared_ptr可以共享(或说拥有)同一对象。对象的最末一个拥有者有责任销毁对象,并清理与该对象相关的所有资源。
shared_ptr的目标就是,在其所指向的对象不再被使用之后(而非之前),自动释放与对象相关的资源。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。指针a指向类q,指针b指向类w,类q和类w中都有一个智能指针q.i和w.o,此时令a.i=b, b.o=a; 就形成了相互引用的死锁,可以看到fun函数中a,b之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针a,b析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(q,w的析构函数没有被调用)如果把其中一个改为weak_ptr就可以了。
auto_ptr是用于C++11之前的智能指针。由于 auto_ptr 基于排他所有权模式:两个指针不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr 主要有两大问题:
o 复制和赋值会改变资源的所有权,不符合人的直觉。
o 在 STL 容器中无法使用auto_ptr ,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
unique_ptr特性
o 拥有它所指向的对象
o 无法进行复制构造,也无法进行复制赋值操作(拷贝构造和赋值构造被=delete)
o 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
unique_ptr可以:
o 为动态申请的内存提供异常安全
o 将动态申请内存的所有权传递给某个函数
o 从某个函数返回动态申请内存的所有权
o 在容器中保存指针
unique_ptr十分依赖于右值引用和移动语义。
例子1:unique_ptr
class test {
public:
test() {
}
test(int id,const char*name) {
this->id = id;
this->name = new char[strlen(name) + 1]; //开辟空间
strcpy(this->name, name);
}
void func() {
cout << "success!"<< endl;
}
private:
int id;
char* name;
};
int main() {
unique_ptr<test> uni(new test);
//unique_ptr<test> b = uni; //不能进行控制权转换,因为unique_ptr中的拷贝构造和赋值操作符delete了,所以也就意味着,他和auto_ptr有区别,控制权唯一
unique_ptr<test>base2 = move(uni);//base1变成empty 但是使用move函数可以实现,把右值的对象(right)移动给左值(_myt&),并且右值清空。
/*
int main(){
std::unique_ptr<int> uptr(new int(10)); //绑定动态对象
//std::unique_ptr<int> uptr2 = uptr; //不能賦值
//std::unique_ptr<int> uptr2(uptr); //不能拷貝
std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
uptr2.release(); //释放所有权
}
//超過uptr的作用域,內存釋放
*/
}
例子2:share_ptr
shared_ptr<test> s1(new test(1,"xiaoli"));
shared_ptr<test> s2 = s1;
shared_ptr<test> s3 = s2;
cout << s1.use_count() << endl; //输出此类被几个share指针使用了
s3.reset(); //计数减1
cout << s1.use_count() << endl;
/*
int a = 10;
std::shared_ptr<int> ptra = std::make_shared<int>(a);
*/
例子3:weak_ptr
shared_ptr<int> sh_ptr = make_shared<int>(10);
cout << sh_ptr.use_count() << endl;
weak_ptr<int> wp(sh_ptr); //提供类似观测器的功能,不能操作资源
cout << wp.use_count() << endl;
if (!wp.expired()) { //判断此weak指针是否允许lock
shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr lock后可产生一个由weak变成正常share的指针,从而操作资源
*sh_ptr = 100;
cout << wp.use_count() << endl; //这时候use_count()就会计数了
}
循环引用问题:
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
//A();
~A() {
cout << "kill A\n";
}
shared_ptr<B> pb;
private:
};
class B
{
public:
//B();
~B() {
cout << "kill B\n";
}
shared_ptr<A> pa;
private:
};
int main() {
shared_ptr<A> sa(new A()); //new出来的A对象引用计数为1
shared_ptr<B> sb(new B());
if (sa && sb)
{
sa->pb = sb;
sb->pa = sa; //A对象引用计数变为2
}
cout << "sa use count:" << sa.use_count() << endl;
cout << "sb use count:" << sb.use_count() << endl;
//函数结束,sa析构,A的引用计数变为1,但永远不会为0。造成内存泄漏
//同理B的引用计数也不会为0
return 0;
}
如此一来,A和B都互相指着对方吼,“放开我的引用!“,“你先放我的我就放你的!”,于是悲剧发生了。
问题:share_ptr是线程安全的吗?其底层实现是怎么样的?
作者:陈硕
原文:https://blog.csdn.net/solstice/article/details/8547547
先说结论:因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁。
shared_ptr 的数据结构
shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。具体来说,shared_ptr<Foo> 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。
图 1:shared_ptr 的数据结构。
为了简化并突出重点,后文只画出 use_count 的值:
以上是 shared_ptr<Foo> x(new Foo); 对应的内存数据结构。
如果再执行 shared_ptr<Foo> y = x; 那么对应的数据结构如下。
但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。
中间步骤 1,复制 ptr 指针:
中间步骤 2,复制 ref_count 指针,导致引用计数加 1:
步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),我见过的都是先1后2。
既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。
多线程无保护读写 shared_ptr 可能出现的 race condition
考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:
shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
一开始,各安其事。
线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。
同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。
先是步骤 1:
再是步骤 2:
这时 Foo1 对象已经销毁,x.ptr 成了空悬指针!
最后回到线程 A,完成步骤 2:
多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。
当然,race condition 远不止这一种,其他线程交织(interweaving)有可能会造成其他错误。
思考,假如 shared_ptr 的 operator= 实现是先复制 ref_count(步骤 2)再复制 ptr(步骤 1),会有哪些 race condition?
从源码角度看:
shared_ptr继承了下面的模板类,用它来管理引用计数。其中有两个变量一个表示shared_ptr的引用数,另外一个表示weak_ptr的引用数,我们知道weak_ptr不会增加只能指针的引用数也就是说不持有对象,他的使用必须通过lock方法获取它指向的shared_ptr才能使用。
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
public:
_Sp_counted_base() noexcept
: _M_use_count(1), _M_weak_count(1) { }
virtual
~_Sp_counted_base() noexcept
{ }
//当_M_use_count为0时调用,是个纯虚函数(必须实现),这个函数的作用是释放指针指向的对象所持有的资源,即*this
virtual void
_M_dispose() noexcept = 0;
// 当_M_weak_count为0时调用,释放自己本身的资源,即this
// _M_weak_count = _M_weak_count + (_M_use_count!= 0),当_M_weak_count和_M_use_count都为0时释放this
virtual void
_M_destroy() noexcept
{ delete this; }
virtual void*
_M_get_deleter(const std::type_info&) noexcept = 0;
//增加一个引用
void
_M_add_ref_copy()
{ __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
void
_M_add_ref_lock();
bool
_M_add_ref_lock_nothrow();
void
_M_release() noexcept
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
//首先use_count减去1,并对比减操作之前的值,如果减之前是1,说明减后是0,a1没有任何shared_ptr指针指向它了将销毁对象
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
//如果destory和dispose存在内存屏障,保证dispose函数的效果在destory函数的调用该线程的可见性
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
//同时对a1的weak_count减去1,也对比减操作之前的值,如果减之前是1,说明减后是0,a1没有weak_ptr指向它了,
//应该将管理对象销毁,于是调用_M_destroy()销毁了管理对象
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
-1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
}
void
_M_weak_add_ref() noexcept
{ __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); }
void
_M_weak_release() noexcept
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
_M_destroy();
}
}
//获取引用计数
long
_M_get_use_count() const noexcept
{
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}
private:
_Sp_counted_base(_Sp_counted_base const&) = delete;
_Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
_Atomic_word _M_use_count;
_Atomic_word _M_weak_count;
};
这里的智能指针的引用计数在手段上使用了atomic原子操作,只要在shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。
虽然通过原子操作解决了引用计数的计数的线程安全问题, 但是智能指针指向的对象的线程安全问题,智能指针没有做任何的保证。 首先智能指针有两个变量,一个是指向的对象的指针,还有一个就是我们上面看到的引用计数管理对象, 当智能指针发生拷贝的时候,标准库的实现是先拷贝智能指针,再拷贝引用计数对象(拷贝引用计数对象的时候,会使use_count加一),这两个操作并不是原子的,隐患就出现在这里。
手撸一个简单的智能指针
原文链接:https://blog.csdn.net/u013611405/article/details/88047741
写的很棒,做了一点点修改
一句话介绍shared_ptr智能指针:多个shared_ptr中的T *ptr能指向同一个内存区域(同一个对象),并共同维护同一个引用计数器。
一般来说,智能指针的实现需要以下步骤:
1.一个模板指针T* ptr,指向实际的对象。
2.一个引用次数(必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete)。
3.重载operator*和operator->,使得能像指针一样使用shared_ptr。
4.重载copy constructor,使其引用次数加一。
5.重载operator=,如果原来的shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
然后将新的对象引用次数加一。
6.重载析构函数,使引用次数减一并判断引用是否为零(是否调用delete)。
#include<iostream>
#include <string>
using namespace std;
template <typename T>
class Shared_ptr {
public:
// 空参构造 空指针
Shared_ptr() :count(0), _ptr((T*)0) {};
// 构造函数 count必须new出来
Shared_ptr(T* p) : count(new int(1)), _ptr(p) {};
// 拷贝构造函数 使其引用次数加一
Shared_ptr(Shared_ptr<T>& another) :count(&(++ *another.count)), _ptr(another._ptr) {};
// 重载 operator*和operator-> 实现指针功能
T* operator->() { return _ptr; };
T& operator*() { return *_ptr; };
// 重载operator=
// 如果原来的Shared_ptr已经有对象,则让其引用次数减一并判断引用是否为零(是否调用delete)。
// 然后将新的对象引用次数加一。
Shared_ptr<T>& operator=(Shared_ptr<T>& another) {
if (this == &another)
return *this;
++ *another.count;
if (this->_ptr && 0 == --*this->count) { //本指针原有对象计数为1,赋值之后应该析构
delete count;
delete _ptr;
cout << "delete ptr in =" << endl;
}
this->_ptr = another._ptr;
this->count = another.count;
return *this;
}
// 析构函数 使引用次数减一并判断引用是否为零(是否调用delete)。
~Shared_ptr()
{
if (_ptr && 0 == --*count) {
delete count;
delete _ptr;
cout << "delete ptr in ~" << endl;
}
}
int getRef() { return *count; }
private:
T* _ptr;
int* count; // should be int*, rather than int
};
添加测试:
int main() {
Shared_ptr<string> p1(new string("abc"));
cout << "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;
Shared_ptr<string> p2(p1);
cout << "after p2 share with p1: " << endl;
cout<< "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;
cout << "p2's ref: " << p2.getRef() << " ,p2's obj: " << *p2 << endl;
cout << "creat p3 = hello" << endl;
Shared_ptr<string> p3(new string("hello"));
cout << "p3's ref: " << p3.getRef() << " ,p3's obj: " << *p3 << endl;
cout << "after p3 = p2:" << endl;
p3 = p2;
cout << "p1's ref: " << p1.getRef() << " ,p1's obj: " << *p1 << endl;
cout << "p2's ref: " << p2.getRef() << " ,p2's obj: " << *p2 << endl;
cout << "p3's ref: " << p3.getRef() << " ,p3's obj: " << *p3 << endl;
system("pause");
return 0;
}
结果如下: