智能指针解决的问题
1.忘记 delete 内存:会导致内存泄漏问题,且除非是内存耗尽否则很难检测到这种错误。
2.使用已经释放掉的对象:如果能够记得在释放掉内存后将指针置空并在下次使用前判空,尚可避免这种错误。
3.同一块内存释放两次:如果有两个指针指向相同的动态分配对象,则很容易发生这种错误。
4.发生异常时的内存泄漏:若在 new 和 delete 之间发生异常,则会导致内存泄漏。
适用范围
C++11 中提供了三种智能指针,分别是 shared_ptr , unique_ptr 和 weak_ptr 。shared_ptr 允许多个指针指向同一个对象,unique_ptr 则“独占”所指向的对象,weak_ptr 则是和share_ptr 相辅相成的伴随类。
这三种类型都定义在头文件memory中。类似vector,智能指针也是模板,需要在尖括号内给出类型信息。shared_ptr 和 unique_ptr 的使用方式和普通指针类似,都可以使用*和->等运算符。
shared_ptr的用法。智能指针shared_ptr的用法
weak_ptr可以解决shared_ptr的循环引用问题。
unique_ptr最像裸指针,但更为安全,保证资源的释放,不能复制只能移动。
基本语法
#include <iostream>
#include <memory>
class Person
{
public:
Person(int v)
{
value = v;
std::cout << "Person " << value << " is constructed."<< std::endl;
}
~Person()
{
std::cout << "Person " << value << " is destructed." << std::endl;
}
int value;
};
int main()
{
## //shared_ptr的使用
// 初始化方式 1,默认初始化的智能指针保存着一个空指针
std::shared_ptr<Person> p1;
// 初始化方式 2,使用标准库函数 make_shared,make_shared 传递的构造参数必须与 Person 的某个构造函数相匹配
// Person 2 的引用计数为 1,p2 指向它
auto p2 = std::make_shared<Person>(2);
// 初始化方式 3,使用 new 返回的指针来初始化智能指针,此种方式必须使用直接初始化
// Person 3 的引用计数为 1,p3 指向它
std::shared_ptr<Person> p3(new Person(3));
// 将 shared_ptr 作为一个条件判断,若指向一个对象,则为 true。 p1 没有指向一个对象,故 !p1 为 true
if (!p1)
{
// 赋值操作会递增右侧操作数的引用计数,递减左侧操作数的引用计数
// Person 2的引用计数为 2,p1、p2 指向它
p1 = p2;
// Person 2的引用计数为 1,p1 指向它
// Person 3的引用计数为 2,p2、p3 指向它
p2 = p3;
// 解引用智能指针,获得它指向的对象
// 输出结果为 Person 2
std::cout << "This is Person " << (*p1).value << std::endl;
// 输出结果为 Person 3
std::cout << "This is Person " << p2->value << std::endl;
// 交换 p1 和 p2 的指针
// Person 2的引用计数为 1,p2 指向它
// Person 3的引用计数为 2,p1、p3 指向它
p1.swap(p2);
// 当智能指针中有值的时候,调用 reset() 会使引用计数减 1
// Person 3的引用计数为 1,p3 指向它
p1.reset();
// Person 3的引用计数为 0,被销毁
p3.reset();
}
## //unique_ptr的使用
// unique_ptr 的初始化方式同 shared_ptr,不一一列出
// make_unique 函数是 C++14 中才加入的
// Person 4的引用计数为 1,p4 指向它
auto p4 = std::make_unique<Person>(4);
// Person 5的引用计数为 1,p5 指向它
std::unique_ptr<Person> p5(new Person(5));
// 某个时刻只能有一个 unique_ptr 指向一个对象
// 所以,unique_ptr 不支持拷贝,也不支持赋值
// std::unique_ptr<Person> p5(p4); // 错误,不支持拷贝
// p5 = p4; // 错误,不支持赋值
// release函数使得 p4 放弃对指针的控制权,返回指针并置 p4 为空
// Person 4 的引用计数为 1,p6 指向它
std::unique_ptr<Person> p6(p4.release());
## //weak_ptr的使用
// weak_ptr 指向一个由 shared_ptr 管理的对象,但不会改变 shared_ptr 的引用计数。 weak_ptr 不控制所指对象的生存期,所以,即使有weak_ptr指向对象,对象也还是会被释放
std::weak_ptr<Person> p7(p2);
// 由于 weak_ptr 所指对象可能不存在,所以我们不能用 weak_ptr,直接访问对象,而必须调用 lock(),若不存在,则返回一个空 shared_ptr; 若存在,则返回weak_ptr所指对象的 shared_ptr
// Person 2的引用计数为 2,p2、p8 指向它
if (auto p8 = p7.lock())
{
// use_count() 函数返回共享 weak_ptr 所指对象的 shared_ptr 数量
// 这里的输出结果为 2
std::cout << p7.use_count() << std::endl;
}
}
以上代码的输出结果为:
Person 2 is constructed.
Person 3 is constructed.
This is Person 2
This is Person 3
Person 3 is destructed.
Person 4 is constructed.
Person 5 is constructed.
2
Person 4 is destructed.
Person 5 is destructed.
Person 2 is destructed.
shared_ptr原理
当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete。所有管理同一个裸指针的shared_ptr,都共享一个引用计数器,每当一个shared_ptr被赋值(或拷贝构造)给其它shared_ptr时,这个共享的引用计数器就加1,当一个shared_ptr析构或者被用于管理其它裸指针时,这个引用计数器就减1,如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源。
下面是一个基于引用计数的智能指针的实现,需要实现构造,析构,拷贝构造,=操作符重载,重载*和->操作符。
#include <iostream>
#include <memory>
template<typename T>
class SmartPointer
{
private:
T * _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) :
_ptr(ptr)
{
if (_ptr)
{
_count = new size_t(1);
}
else
{
_count = new size_t(0);
}
}
SmartPointer(const SmartPointer &ptr)
{
if (this != &ptr)
{
this->_ptr = ptr._ptr;
this->_count = ptr._count;
++(*this->_count);
}
}
SmartPointer& operator=(const SmartPointer &ptr)
{
if (this->_ptr == ptr._ptr)
{
return *this;
}
if (this->_ptr)
{
--(*this->_count);
if (this->_count == 0)
{
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
++(ptr._count);
this->_count = ptr._count;
return *this;
}
T& operator*()
{
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
T* operator->()
{
assert(this->_ptr == nullptr);
return this->_ptr;
}
~SmartPointer()
{
--(*this->_count);
if (0 == *this->_count )
{
delete this->_ptr;
delete this->_count;
}
}
size_t use_count()
{
return *this->_count;
}
};
int main()
{
SmartPointer<int> sp(new int(10));
SmartPointer<int> sp2(sp);
SmartPointer<int> sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
这个智能指针的简单实现模仿的是 share_ptr 的行为,不难发现,引用计数的存在会带来一些性能影响:
- shared_ptr 的尺寸是裸指针的两倍:因为内部既包含一个指向该资源的裸指针,也包含一个指向该资源的引用计数的裸指针。
- 引用计数的内存必须动态分配
- 引用计数的递增和递减必须是原子操作:原子操作一般比非原子操作慢。我们的实现版本里为了简单起见没有实现原子操作。
shared_ptr 的循环引用问题
B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个 shared_ptr,此时A和B的生命周期互相由对方决定,都无法从内存中销毁。
举个循环引用的简单例子。
#include <iostream>
#include <memory>
using namespace std;
class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
shared_ptr<BB> m_bb_ptr; //!
};
class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};
int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
// BB先出作用域(后进先出),B的引用计数减少为1,不为0,所以堆上的BB空间没有被释放,且BB持有的AA也没有机会被析构,AA的引用计数也完全没减少
// AA后出作用域,同理AA的引用计数减少为1,不为0,所以堆上AA的空间也没有被释放
AA::AA() called
BB::BB() called
ptr_a use_count: 1
ptr_b use_count: 1
ptr_a use_count: 2
ptr_b use_count: 2
如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用弱智能指针weak_ptr即可。
#include <iostream>
#include <memory>
using namespace std;
class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
weak_ptr<BB> m_bb_ptr; //!
};
class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};
int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
// BB先出作用域(后进先出),B的引用计数减少为1,为0,所以堆上的BB空间被释放,且BB持有的AA被析构,AA的引用计数也减1
// AA后出作用域,同理AA的引用计数减1,为0,所以堆上AA的空间也被释放
AA::AA() called
BB::BB() called
ptr_a use_count: 1
ptr_b use_count: 1
ptr_a use_count: 2
ptr_b use_count: 1
BB::~BB() called
AA::~AA() called
weak_ptr的作用就是:在需要时变出一个shared_ptr,在其它时候不干扰shared_ptr的引用计数。
1.weak_ptr用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。
2.weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。
3.weak_ptr提供了expired()与lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空“shared_ptr)。
std::shared_ptr<int> sh = std::make_shared<int>();
// 用一个shared_ptr初始化
std::weak_ptr<int> w(sh);
// 变出shared_ptr
std::shared_ptr<int> another = w.lock();
// 判断weak_ptr所观察的shared_ptr的资源是否已经释放
bool isDeleted = w.expired();
shared_ptr 关联与独立
多个共享指针指向同一个空间,它们的关系可能是关联(我们所期望的正常关系)或是独立的(一种错误状态)。
只有用一个shared_ptr为另一个shared_ptr赋值或拷贝时,才将这连个共享指针关联起来,直接使用地址值会导致各个shared_ptr独立。
shared_ptr<int> sp1(new int(10));
shared_ptr<int> sp2(sp1), sp3;
sp3 = sp1;
//一个典型的错误用法
shared_ptr<int> sp4(sp1.get());
cout << sp1.use_count() << " " << sp2.use_count() << " "
<< sp3.use_count() << " " << sp4.use_count() << endl;
//输出 3 3 3 1
sp1,sp2,sp3是相互关联的共享指针,共同控制所指内存的生存期,sp4虽然指向同样的内存,却是与sp1,sp2,sp3独立的,sp4按自己的引用计数来关联内存的释放。
谨慎使用p.get()的返回值
p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
- 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的 。保存为裸指针不知什么时候就会变成空悬指针 ,保存为shared_ptr则产生了独立指针。
- 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误
shared_ptr注意事项
指定删除器
std::shared_ptr可以指定删除器的一个原因是其默认删除器不支持数组对象。
不要用一个原始指针初始化多个shared_ptr,原因在于会造成二次销毁
int *p5 = new int;
std::shared_ptr<int> p6(p5);
std::shared_ptr<int> p7(p5);// logic error
直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏
// 声明
void f(A *p1, B *p2);
// 使用
f(new A, new B);
上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏
如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏
//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);
//使用
f(shared_ptr<A> (new A),shared_ptr<B>(new B));
因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:
1)new A发生在shared_ptr< A >构造发生之前
2)new B发生在shared_ptr< B >构造发生之前
3)两个shared_ptr的构造发生在函数f的调用之前
在满足上面三条性质的前提下,各操作的顺序可以任意执行
若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏
//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);
//使用
f(make_shared<A>(),make_shared<B>());
依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行。如果make_shared< A > 构造完成了,make_shared< B >中抛出异常,那么A的资源能被正确释放。与上面用new来初始化的情形对比,make_shared保证了第二new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,故在异常发生时,能正确释放资源。
总结:请总是使用make_shared来生成shared_ptr
禁止通过shared_from_this()返回this指针
这样做可能也会造成二次析构
避免循环引用
内存无法释放
new的普通指针与shared_ptr转换
如图所示,这会发生什么情况?答案是输出的会是随机数,因为经过func函数后,我们用p初始化的临时智能指针已经被析构了,引用计数先+1,后-1。所以经过func函数后,p指向的对象被释放,再解引用自然无法得到我们想要的结果。
#include<iostream>
#include <memory>
using namespace std;
void func(shared_ptr<int>)
{
;
}
int main()
{
int a = 5;
auto p = new int(5);
func(shared_ptr<int>(p));
cout << *p << endl;
return 0;
}
这种情况下,正确的做法如图所示:一开始就使用智能指针。
#include<iostream>
#include <memory>
using namespace std;
void func(shared_ptr<int>)
{
;
}
int main()
{
//int a = 5;
auto p = make_shared<int>(5);
func(shared_ptr<int>(p));
cout << *p << endl;
return 0;
}
make_shared用法
除了以下情况尽量使用make_shared:
1、 make_shared只能针对new出来的,对于使用工厂创建出来的对象无能为力。
2、 需要定制删除器时,make_shared无能为力。