在C++中,动态内存的管理是通过一对运算符来完成的:
new,在动态内存中为对象分配空间并返回一个指向该对象的指针,可以选择对对象进行初始化;
delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时会忘记释放内存,在这种情况下会产生 内存泄露;有时在尚有指针引用内存的情况下就释放了它,在这种情况下就会产生引用非法内存的指针。
为了更容易(同时也更安全)地使用动态内存,C++11标准库提供智能指针(smart pointer)类型来管理动态对象。
智能指针的行为类似常规指针,重要的区别是它负责 自动释放所指的对象。
智能指针是模板类而不是指针。类似vector,当创建一个智能指针时,必须提供额外的信息即指针可以指向的类型。默认初始化的智能指针中保存着一个空指针。智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空。
1. auto_ptr (已被C++11舍弃)
最早的智能指针 auto_ptr 出现在 C++ 98 里面,目前已经被 C++11 标准舍弃
当一个指针拷贝构造另一个指针时,当前指针就将对空间的管理权交给拷贝的那个指针,当前指针就指向空);
2. Boost 的 scoped_ptr、shared_ptr、weak_ptr
因为 auto_ptr 有缺陷,但是 C++ 标准里面从 C++98 到 C++11 之间没有出现新的智能指针能解决这个缺陷,所以在这段时间内,boost 这个官方组织 就增加了智能指针(scoped_ptr,shared_ptr,weak_ptr等)
scoped_ptr 采用防拷贝的方式(防拷贝就是不允许拷贝,拷贝就会出错;防拷贝的实现:将拷贝构造和的赋值运算符重载只声明不实现,并且声明为私有)。
shared_ptr 为共享指针,里面采用引用计数,当有其他的 shared_ptr 指向同一块空间的时候就增加引用计数,当引用计数减为 0 的时候才释放该智能指针管理的那块空间。
weak_ptr 是位解决 shared_ptr 循环引用问题而出现的。
3. C++11 的 Unique_ptr、shared_ptr、Weak_ptr
C++11 在 Boost 库的基础上设计出三种新的智能指针。
C++11 里面的 unique_ptr 就是 boost 库里面的 scoped_ptr(防拷贝,独占);
C++11 的 shared_ptr 被广泛使用,在下面详细介绍。
shared_ptr 是一个引用计数智能指针,用于共享对象的所有权,也就是说它允许多个指针指向同一个对象。这一点与原始指针一致。
对于shared_ptr在拷贝和赋值时的行为,《C++Primer第五版》中有详细的描述:
每个shared_ptr都有一个关联的计数值,通常称为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。
例如,当用一个 shared_ptr初始化另一个 shred_ptr,或将它当做参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当我们给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为0,它就会自动释放自己所管理的对象。
最安全和高效的方法是调用make_shared库函数, 该函数会在堆中分配一个对象并初始化,最后返回指向此对象的share_ptr实例。如果你不想使用make_ptr,也可以先明确new出一个对象,然后把其原始指针传递给share_ptr的构造函数。
int main()
{
// 传递给make_shared函数的参数必须和shared_ptr所指向类型的某个构造函数相匹配
shared_ptr<string> pStr = make_shared<string>(10, 'a');
cout << *pStr << endl; // aaaaaaaaaa
int *p = new int(5);
shared_ptr<int> pInt(p);
cout << *pInt << endl; // 5
}
shared_ptr 的使用方式与普通指针的使用方式类似, 既可以使用解引用操作符 * 获得原始对象进而访问其各个成员,也可以使用指针访问符 -> 来访问原始对象的各个成员。
我们可以用一个shared_ptr对象来初始化另一个share_ptr实例,该操作会增加其引用计数值
int main()
{
shared_ptr<string> pStr = make_shared<string>(10, 'a');
cout << pStr.use_count() << endl; // 1
shared_ptr<string> pStr2(pStr);
cout << pStr.use_count() << endl; // 2
cout << pStr2.use_count() << endl; // 2
}
如果 shared_ptr 实例 p 和另一个 shared_ptr 实例 q 所指向的 类型相同或者可以相互转换,我们还可以进行诸如 p = q 这样赋值操作。
该操作会递减 p 的引用计数值,递增 q 的引用计数值。
p不再指向原来的对象,赋值操作后 p 指向 q 指向的对象。
class Example;
shared_ptr<Example> pStr = make_shared<Example>("a object");
shared_ptr<Example> pStr2 = make_shared<Example>("b object");
pStr = pStr2; // 此后pStr和pStr指向相同对象
shared_ptr提供了两个函数来检查其共享的引用计数值,分别是 unique() 和 use_count()。
在前面,我们已经多次使用过use_count()函数,该函数返回当前指针的引用计数值。值得注意的是 **use_count() 函数可能效率很低 **,应该只把它用于测试或调试。
unique() 函数用来测试该 shared_ptr 是否是原始指针 唯一拥有者 ,也就是 use_count() 的返回值为 1 时返回 true,否则返回 false。
int main()
{
shared_ptr<string> pStr = make_shared<string>(10, 'a');
cout << pStr.unique() << endl; // true
shared_ptr<string> pStr2(pStr);
cout << pStr2.unique() << endl; // false;
}
reset 的作用和赋值操作相似,调用者会指向新的对象
class Example;
shared_ptr<Example> pStr = make_shared<Example>("a object");
shared_ptr<Example> pStr2 = make_shared<Example>("b object");
pStr.reset(pStr2); // 此后pStr和pStr指向相同对象
参考 https://blog.csdn.net/Xiejingfa/article/details/50750037
shared_ptr 相对于 auto_ptr 来说是近乎完美的,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了 引用成环 的问题,这种问题靠它自己是没办法解决的,所以在 C++11 的时候将 shared_ptr 和 weak_ptr 一起引入了标准库,用来解决循环引用的问题。
weak_ptr 本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能指向shared_ptr对象,同时也不能将weak_ptr对象直接赋值给shared_ptr类型的变量,并且这样并不会改变引用计数的值。
查看 weak_ptr 的代码时发现,它主要有 lock、swap、reset、expired、operator=、use_count 几个函数,与 shared_ptr 相比多了 lock、expired 函数,但是却少了 get 函数,甚至连 operator* 和 operator-> 都没有,可用的函数数量少的可怜,下面通过一些例子来了解一下weak_ptr的具体用法。
#include
#include
using namespace std;
class CB;
class CA
{
public:
CA(){}
~CA() { cout << "~CA() called! " << endl; }
void set_ptr(shared_ptr<CB>& ptr) { m_ptr_b = ptr; }
private:
shared_ptr<CB> m_ptr_b;
};
class CB
{
public:
CB(){}
~CB() { cout << "~CB() called! " << endl; }
void set_ptr(shared_ptr<CA>& ptr) { m_ptr_a = ptr; }
private:
shared_ptr<CA> m_ptr_a;
};
int main()
{
shared_ptr<CA> ptr_a(new CA());
shared_ptr<CB> ptr_b(new CB());
cout << "a use count : " << ptr_a.use_count() << endl;
cout << "b use count : " << ptr_b.use_count() << endl;
ptr_a->set_ptr(ptr_b);
ptr_b->set_ptr(ptr_a);
cout << "a use count : " << ptr_a.use_count() << endl;
cout << "b use count : " << ptr_b.use_count() << endl;
return 0;
}
通过结果可以看到,最后 CA 和 CB 的对象并没有被析构,其中的引用效果如下图所示:
起初定义完 ptr_a 和 ptr_b 时,只有①③两条引用,然后调用函数 set_ptr 后又增加了②④两条引用,当主函数结束时, ptr_a 和 ptr_b被销毁,也就是①③两条引用会被断开,但是②④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。
解决这种状况的办法就是将两个类中的一个成员变量改为weak_ptr对象,因为weak_ptr不会增加引用计数,使得引用形不成环,最后就可以正常的释放内部的对象,不会造成内存泄漏,比如将CB中的成员变量改为weak_ptr对象。
将class CB 中 shared_ptr m_ptr_a; 修改为
weak_ptr m_ptr_a;
weak_ptr 中只有 函数 lock 和 expired 两个函数比较重要,因为它本身不会增加引用计数,所以它指向的对象可能在它用的时候已经被释放了,所以在用之前需要使用expired 函数来检测是否过期,lock 是为了保证在多线程环境下能安全使用,
class tester ;
void fun(shared_ptr<tester> sp)
{
// !!!在这大量使用sp指针.
shared_ptr<tester> tmp = sp;
}
int main()
{
shared_ptr<tester> sp1(new tester);
// 开启两个线程,并将智能指针传入使用。
thread t1(bind(&fun, sp1));
thread t2(bind(&fun, sp1));
t1.join();
t2.join();
return 0;
}
这个代码带来的问题很显然,由于多线程同时访问智能指针,并将其赋值到其它同类智能指针时,很可能发生两个线程同时在操作引用计数(但并不一定绝对发生),而导致计数失败或无效等情况,从而导致程序崩溃,如若不知根源,就无法查找这个bug,那就只能向上帝祈祷程序能正常运行。
引入weak_ptr可以解决这个问题,将 fun 函数修改如下:
void fun(weak_ptr<tester> wp)
{
if (!wk_ptr_a.expired())
{……}
}
此时这个方案只 **解决了多线程对引用计数同时访问的读写问题,**并没有解决对 share_ptr 指向的数据的多线程安全问题,因此 weak_ptr 只是安全的获得 share_ptr 的一种方式,因为可以确保在获得share_ptr的时候的多线程安全。
为了保证数据安全,我们还需要 lock 加锁。
void fun(weak_ptr<tester> wp)
{
shared_ptr<tester> sp = wp.lock;
if (sp)
{
// 在这里可以安全的使用sp指针.
}
else
{
std::cout << “指针已被释放!” << std::endl;
}
}
参考 https://blog.csdn.net/albertsh/article/details/82286999
参考 https://blog.csdn.net/man_sion/article/details/77196766
unique_ptr "独占"所指向的对象,基本用法和 shared_ptr 相同,两者的区别在于:
(0) 可以认为每个 shared_ptr 都有一个关联的计数器,通常称其为引用计数 (reference count)。无论何时拷贝一个 shared_ptr ,计数器都会递增。
例如,当用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。
当给 shared_ptr 赋予一个新值或是 shared_ptr 被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个 shared_ptr 的计数器变为 0,它就会自动释放自己所管理的对象。
(1) shared_ptr 的类型转换不能使用一般的 static_cast,这种方式进行的转换会导致转换后的指针无法再被 shared_ptr 对象正确的管理。应该使用专门用于 shared_ptr 类型转换的
static_pointer_cast() ,
const_pointer_cast()
dynamic_pointer_cast()。
(2) 可以通过构造函数、赋值函数或者 make_shared 函数初始化智能指针。
最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。
(3) 不要把一个 **原生指针 **给多个 shared_ptr 管理;
A* p = new A(10);
shared_ptr sp1§, sp2§;
sp1 和 sp2 并不会共享同一个对 p 的托管计数,而是各自将对 p 的托管计数都记为 1(sp2 无法知道 p 已经被 sp1 托管过)。这样,当 sp1 消亡时要析构 p,sp2 消亡时要再次析构 p,这会导致 程序崩溃。
只有通过拷贝或复制时,才会增加 shared_ptr 的引用计数。
当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其它 shared_ptr 指向相同的对象。
class A;
shared_ptr<A> sp1(new A(2));
shared_ptr<A> sp2(sp1); //拷贝操作
shared_ptr<A> sp3;
sp3 = sp2; //赋值操作
(4) 只有指向动态分配的对象的指针才能交给 shared_ptr 对象托管。将指向普通局部变量、全局变量的指针交给 shared_ptr 托管,编译时不会有问题,但程序运行时会出错,因为不能析构一个并没有指向动态分配的内存空间的指针。
(5) 在多线程环境中使用共享指针的代价非常大,这是因为你需要避免关于引用计数的数据竞争;shared_ptr并不是万能的,而且使用它们的话也是需要一定的开销的。
(6) 如果你使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器。
shared_ptr 的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。
所谓删除器就是:你需要写一个函数来释放不是通过 new 分配的内存 (那应该就是 malloc),这个函数就是删除器,你需要在定义智能指针的时候将这个函数传递给智能指针对象。
这样智能指针就知道该如何释放内存。
class A;
void deleter(A* p)
{
free(p);
}
int main ()
{
A*p=(A*)malloc(sizeof(A));
shared_ptr<A>sp(p,deleter);
参考 https://blog.csdn.net/fengbingchun/article/details/52202007
参考 https://blog.csdn.net/man_sion/article/details/77196766