1.RAII
(Resource Acquisition Is Initialization资源获取即初始化 )
既然类是C++中的主要抽象工具,那么就将资源抽象为类,用局部对象来表示资源,把管理资源的任务转化为管理局部对象的任务。把资源放进对象内,用资源来管理对象,便是 C++ 编程中最重要的编程技法之一,即 RAII ,它是 "Resource Acquisition Is Initialization" 的首字母缩写。智能指针便是利用 RAII 的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
C++11 中提供了三种智能指针,分别是 shared_ptr , unique_ptr 和 weak_ptr 。shared_ptr 允许多个指针指向同一个对象,unique_ptr 则“独占”所指向的对象,weak_ptr 则是和share_ptr 相辅相成的伴随类
智能指针(shared_ptr)能够自动释放所指向的对象,其实现原理却并不复杂。简单一说:
(1)每次创建类的新对象时,初始化指针并将引用计数置为1;
(2)当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数。
(3)对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;这是因为左侧的指针指向了右侧指针所指向的对象,因此右指针所指向的对象的引用计数加1。调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
// 智能指针的简单实现
#ifndef smart_ptr_h
#define smart_ptr_h
#include <stdio.h>
#include <iostream>
#include <memory>
#include <atomic>
using namespace std;
template<typename T>
class smart_ptr
{
private:
atomic<size_t *> _count; // 这里是一个指针
T * _ptr;
public:
smart_ptr(T* ptr = nullptr)
{
if(!ptr)
{
this->_count = new size_t(0);
}else{
this->_count = new size_t(1);
}
}
// 复制构造函数
smart_ptr(const smart_ptr& ptr)
{
if(this != ptr)
{
this->_ptr = ptr;
this->_count = ptr->_count;
++(*this->_count); // 引用次数+1
}
}
// 赋值构造函数
smart_ptr& operator=(const smart_ptr& ptr)
{
if(this == ptr)
return *this;
else
{
--(*this->_count);
if(0 == this->_count) {
this->~smart_ptr();
}
++(*ptr->_count);
return *ptr;
}
}
~smart_ptr()
{
--(*this->_count);
if(*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
};
#endif /* smart_ptr_h */
2.使用智能指针需要主要的几点
2.1 shared_ptr 的循环引用问题
shared_ptr 意味着你的引用和原对象是一个强联系。你的引用不解开,原对象就不能销毁。滥用强联系,这在一个运行时间长、规模比较大,或者是资源较为紧缺的系统中,极易造成隐性的内存泄漏,这会成为一个灾难性的问题。更糟的是,滥用强联系可能造成循环引用的灾难。即:B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个 shared_ptr,此时A和B的生命周期互相由对方决定,事实上都无法从内存中销毁。 更进一步,循环引用不只是两方的情况,只要引用链成环都会出现问题。
class A
{
private:
std::shared_ptr<B> m_b;
};
class B
{
private:
std::shared_ptr<A> m_a;
}
int main()
{
while(true)
{
std::shared_ptr<A> a(new A);
std::shared_ptr<B> b(new B);
a->m_b = b;
b->m_a = a;
}
}
如此一来,A和B都互相指着对方吼,“放开我的引用!“,“你先发我的我就放你的!”,于是悲剧发生了,内存泄漏了。当然循环引用本身就说明设计上可能存在一些问题,如果特殊原因不得不使用循环引用,那可以让引用链上的一方持用普通指针(或弱智能指针weak_ptr)即可。
对象A想释放自身的资源的时候,发现B的对象还存在,没办法释放掉成员B的资源,因此A等待B先释放,同样,B在释放的时候也需要等待A的资源先释放掉,这样相互等待导致内存泄漏。
2.2 weak_ptr使用
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
std::weak_ptr<int> wp(sh_ptr);
cout<<*wp<<endl; // 这里是不被允许的
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include <iostream>
#include <memory>
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;
if(!wp.expired()){ // 判断被观测的资源是否还存在
// 从被观测的shared_ptr获取一个可用的shared_ptr,这样同样会增加引用
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr2 = 100;
std::cout << wp.use_count() << std::endl; // 输出为2
}
}
//delete memory
}
2.3 unique_ptr的使用
unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
#include <iostream>
#include <memory>
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.4解决循环引用问题
参考网站:https://www.cnblogs.com/wxquare/p/4759020.html
·
#include <iostream>
#include <memory>
class Child;
class Parent;
class Parent {
private:
//std::shared_ptr<Child> ChildPtr;
std::weak_ptr<Child> ChildPtr;
public:
void setChild(std::shared_ptr<Child> child) {
this->ChildPtr = child;
}
void doSomething() {
if (this->ChildPtr.use_count()) {
}
}
~Parent() {
}
};
class Child {
private:
std::shared_ptr<Parent> ParentPtr;
public:
void setPartent(std::shared_ptr<Parent> parent) {
this->ParentPtr = parent;
}
void doSomething() {
if (this->ParentPtr.use_count()) {
}
}
~Child() {
}
};
int main() {
std::weak_ptr<Parent> wpp;
std::weak_ptr<Child> wpc;
{
std::shared_ptr<Parent> p(new Parent);
std::shared_ptr<Child> c(new Child);
p->setChild(c);
c->setPartent(p);
wpp = p;
wpc = c;
std::cout << p.use_count() << std::endl; // 2
std::cout << c.use_count() << std::endl; // 1
}
std::cout << wpp.use_count() << std::endl; // 2
std::cout << wpc.use_count() << std::endl; // 1
return 0;
}
3. auto_ptr与shared_ptr、unique_ptr
在C++11中已经放弃auto_ptr转而推荐使用unique_ptr和shared_ptr。unique跟auto_ptr类似同样只能有一个智能指针对象指向某块内存。但它还有些其他特性。unique_ptr对auto_ptr的改进如下:
1、auto_ptr支持拷贝构造与赋值操作,但unique_ptr不直接支持
auto_ptr通过拷贝构造或者operator=赋值后,对象所有权转移到新的auto_ptr中去了,原来的auto_ptr对象就不再有效,这点不符合人的直觉。unique_ptr则直接禁止了拷贝构造与赋值操作。
auto_ptr<int> ap(new int(10));
auto_ptr<int> one (ap) ; // ok
auto_ptr<int> two = one; //ok
unique_ptr<int> ap(new int(10));
unique_ptr<int> one (ap) ; // 会出错
unique_ptr<int> two = one; //会出错
2、unique_ptr可以用在函数返回值中
unique_ptr像上面这样一般意义上的复制构造和赋值或出错,但在函数中作为返回值却可以用.
unique_ptr<int> GetVal()
{
unique_ptr<int> up(new int(10 );
return up;
}
unique_ptr<int> uPtr = GetVal(); //ok
实际上上面的的操作有点类似于如下操作
unique_ptr<int> up(new int(10);
unique_ptr<int> uPtr2 = std:move(up) ;
// 这里是显式的所有权转移. 把up所指的内存转给uPtr2了
// 而up不再拥有该内存
3、unique_ptr可做为容器元素
我们知道auto_ptr不可做为容器元素,会导致编译错误。虽然unique_ptr同样不能直接做为容器元素,但可以通过move语意实现。
unique_ptr<int> sp(new int(88) );
vector<unique_ptr<int> > vec;
vec.push_back(std::move(sp));
// vec.push_back( sp ); error:
// cout << *sp << endl;
std::move让调用者明确知道拷贝构造、赋值后会导致之前的unique_ptr失效。
3.1 关于auto_ptr的几种注意事项:
1、auto_ptr不能共享所有权。
2、auto_ptr不能指向数组
3、auto_ptr不能作为容器的成员。
4、不能通过赋值操作来初始化auto_ptr
std::auto_ptr<int> p(new int(42)); // OK
std::auto_ptr<int> p = new int(42); // ERROR
// 本想通过new int(42)来产生临时对象temp(问题出在这里),再由temp拷贝构造产生p。
// 这是因为auto_ptr 的构造函数被定义为了explicit
std::auto_ptr<int> p = auto_ptr<int>(new int(42)); // Success
5、不要把auto_ptr放入容器
3.2 关于shared_ptr的几种注意事项:
1、shared_ptr是Boost库所提供的一个智能指针的实现,shared_ptr就是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针.
2、shared_ptr比auto_ptr更安全;
3、shared_ptr是可以拷贝和赋值的,拷贝行为也是等价的,并且可以被比较,这意味这它可被放入标准库的一般容器(vector,list)和关联容器中(map)。