C++11---智能指针

在使用C++编程时,要求使用malloc/new申请出来的空间必须使用free/delete进行释放,如果程序员没有对使用malloc/new申请的空间在使用free/delete进行释放,则可能会造成内存泄露问题。但是,在C++中有些情况下,即使成员进行了释放也可能存在一些安全隐患,例如下面的程序:

void MergeSort(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int)*n);
    _MergeSort(a, 0, n - 1, tmp);
    // 这里假设处理了一些其他逻辑
    vector<int> v(1000000000, 10);
    // ...
    free(tmp);
}
int main()
{
    int a[5] = { 4, 5, 2, 3, 1 };
    MergeSort(a, 5);
    return 0;
}
  • 在MergeSort中使用malloc申请了空间,虽然说在最后使用free进行了释放。
  • 但是,如果在malloc和free之间抛出了异常,该异常由main函数中进行捕获,那么free也是无法被执行的。

这时,就需要使用智能指针来解决了。

1、RAII

RAII是一种利用对象的生命周期对资源进行管理的简单方式。

在构造对象时获取资源,接着控制对资源的访问,使之在对象声明周期内都有效,最后在对象析构的时候释放资源。

2、智能指针原理

智能指针就是在RAII的方式上,对指针类加上*重载和->重载,使之具有指针的行为。

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
        : _ptr(ptr)
    {}
    ~SmartPtr()
    {
        if(_ptr)
            delete _ptr;
    }
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
    T* _ptr;
};
struct Date
{
    int _year;
    int _month;
    int _day;
};
int main()
{
    SmartPtr<int> sp1(new int);
    *sp1 = 10
    cout<<*sp1<<endl;
    SmartPtr<int> sparray(new Date);
    // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    sparray->_year = 2018;
    sparray->_month = 1;
    sparray->_day = 1;
}

简单来说,智能指针就是使用RAII的特性+operator*和operator->使其具备有指针一样的行为。 

3、智能指针的演进过程

在只能指针中存在一个问题就是,当使用智能指针拷贝对象时,会发生一些意想不到的事情。

  • 在拷贝只能指针对象时,只能是使用值拷贝(针对不同的对象指针有不同的拷贝方式,因此没办法做到深拷贝)。
  • 值拷贝存在一个问题,多个对象指向同一块内存。
  • 其中一个释放,其他的也就不能访问了。

其实C++中对智能指针的演进过程其实也就是对这个问题的解决过程。

1)auto_ptr

C++98的库中提供的智能指针就是auto_ptr:

// C++库中的智能指针都定义在memory这个头文件中
#include <memory>
class Date
{
public:
    Date() { cout << "Date()" << endl;}
    ~Date(){ cout << "~Date()" << endl;}
    int _year;
    int _month;
    int _day;
};
int main()
{
    auto_ptr<Date> ap(new Date);
    auto_ptr<Date> copy(ap);
    // auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
    // C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr
    ap->_year = 2018;
    return 0;
}

在C++98中auto_ptr解决这个问题的方法是,拷贝构造采用类似移动构造的方式,将原来对象的空间移动到新的对象上。但是,这也存一个问题,前边的对象就会被悬空,如果使用者不注意可能会出现一些意向不到的错误。 

// 模拟实现一份简答的AutoPtr,了解原理
template<class T>
class AutoPtr
{
public:
    AutoPtr(T* ptr = NULL)
        : _ptr(ptr)
    {}
    ~AutoPtr()
    {
        if(_ptr)
            delete _ptr;
    }
    // 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
    // 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
    AutoPtr(AutoPtr<T>& ap)
        : _ptr(ap._ptr)
    {
        ap._ptr = NULL;
    }
    AutoPtr<T>& operator=(AutoPtr<T>& ap)
    {
        // 检测是否为自己给自己赋值
        if(this != &ap)
        {
            // 释放当前对象中资源
            if(_ptr)
                delete _ptr;
            // 转移ap中资源到当前对象中
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }
    T& operator*() {return *_ptr;}
    T* operator->() { return _ptr;}
private:
    T* _ptr;
};
int main()
{
    AutoPtr<Date> ap(new Date);
    // 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
    // 通过ap对象访问资源时就会出现问题。
    AutoPtr<Date> copy(ap);
    ap->_year = 2018;
    return 0;
}

2)unique_ptr

auto_ptr中的方式,显然存在很大的缺陷,因此C++11中又提供了unique_ptr

unique_ptr的解决方式简单粗暴,既然在智能指针中拷贝构造会出现问题,unique_ptr干脆直接防拷贝。

// 模拟实现一份简答的UniquePtr,了解原理
template<class T>
class UniquePtr
{
public:
    UniquePtr(T * ptr = nullptr)
        : _ptr(ptr)
    {}
    ~UniquePtr()
    {
        if(_ptr)
            delete _ptr;
    }
    T& operator*() {return *_ptr;}
    T* operator->() {return _ptr;}
private:
    // C++98防拷贝的方式:只声明不实现+声明成私有
    UniquePtr(UniquePtr<T> const &);
    UniquePtr & operator=(UniquePtr<T> const &);

    // C++11防拷贝的方式:delete
    UniquePtr(UniquePtr<T> const &) = delete;
    UniquePtr & operator=(UniquePtr<T> const &) = delete;
private:
    T * _ptr;
};

3)shared_ptr

虽然说前边的两种方式都一定程度上解决了拷贝的问题,但是还是不够完美,因此C++11中又提供了shared_ptr。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
template<class T>
class sharedPtr
{
private:
	T* _ptr; //
	int* pcount;//引用计数
	mutex* pmutex; //互斥锁
public:
	sharedPtr(T* ptr = nullptr)
		:_ptr(ptr),
		pcount(new int(1))
		, pmutex(new mutex)
	{}

	//拷贝构造和赋值运算符重载
	sharedPtr(const sharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, pcount(sp.pcount)
		, pmutex(sp.pmutex)
	{
		pmutex->lock();
		(*pcount)++;
		pmutex->unlock();
	}

	sharedPtr<T>& operator=(const sharedPtr<T>& sp)
	{
		//不能自己给自己赋值
		if (_ptr != sp._ptr)
		{
			//释放管理的旧资源
			Release();

			//拷贝新资源
			_ptr = sp._ptr;
			pcount = sp.pcount;
			pmutex = sp.pmutex;

			pmutex->lock();
			(*pcount)++;
			pmutex->unlock();
		}

		return *this;
	}

	//指针一样的行为
	T& operator*()
	{
		return *_ptr;
	}

	sharedPtr<T>* operator->()
	{
		return _ptr;
	}

	~sharedPtr()
	{
		Release();
	}
private:
	void Release()
	{
		//锁要最后释放,因此必须有个标志
		bool flag = false;
		//释放资源
		pmutex->lock();
		if (--(*pcount) == 0)
		{
			delete _ptr;
			delete pcount;

			flag = true;
		}
		pmutex->unlock();

		if (flag == true)
			delete pmutex;
	}
};

1)shred_ptr线程安全问题

shared_ptr中_ptr和pcount都是new出来的,多个对象同时操作就有可能出现线程安全问题,但是对pcount的操作都是加锁的,因此pcount不会产生线程安全问题。但是,_ptr每办法加锁,因此多个线程对_ptr操作可能会产生想爱你城安全问题。

void SharePtrFunc(SharedPtr<Date>& sp, size_t n)
{
cout << sp.Get() << endl;
for (size_t i = 0; i < n; ++i)
{
    // 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
    SharedPtr<Date> copy(sp);
    // 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
    copy->_year++;
    copy->_month++;
    copy->_day++;
}
}
int main()
{
    SharedPtr<Date> p(new Date);
    cout << p.Get() << endl;
    const size_t n = 100;
    thread t1(SharePtrFunc, p, n);
    thread t2(SharePtrFunc, p, n);
    t1.join();
    t2.join();
    cout << p->_year << endl;
    cout << p->_month << endl;
    cout << p->_day << endl;
    return 0;
}

2)循环引用

  • node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete
  • node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  • node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  • 也就是说_next析构了,node2就释放了。
  • 也就是说_prev析构了,node1就释放了。
  • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

那么该如何解决呢?

在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
 

struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_ptr<ListNode> _next;
    ~ListNode()
    { 
        cout << "~ListNode()" << endl; 
    }
};
int main()
{
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    node1->_next = node2;
    node2->_prev = node1;
    cout << node1.use_count() << endl;
    cout << node2.use_count() << endl;
    return 0;
}

 注意:RAII除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。例如unipue_lock和lock_guard

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: C++ Boost库是一个广泛使用的开源库集合,为C++提供了许多功能强大的工具和算法。在Boost库中,有一个名为C++11 Boost库的子模块,它专门为支持C++11标准而设计。 C++11标准引入了许多新的语言特性和库组件,以提高编程效率和代码可靠性。而C++11 Boost库则扩展了标准库,为开发者提供了更多的功能和工具。 C++11 Boost库的中文手册是为了方便中国程序员使用该库而编写的文档。该手册详细介绍了C++11 Boost库中各个模块的功能、用法和示例代码。通过阅读中文手册,程序员可以更清晰地了解每个模块的用途,以及如何正确地使用它们。 在C++11 Boost库中,有许多常用的模块,比如smart_ptr模块,提供了智能指针的功能,用于管理动态内存;thread模块,用于创建和管理线程;filesystem模块,提供了对文件和目录的操作等。在中文手册中,为每个模块都提供了详细的说明和示例,帮助开发者快速上手和运用。 总之,C++11 Boost库中的中文手册为中国的程序员提供了方便和便捷的学习和使用Boost库的途径。通过阅读手册,开发者可以更加深入地了解每个模块的使用方法,从而更高效地开发和调试他们的C++程序。 ### 回答2: Boost库是一个C++的开源库集合,提供了丰富的功能和工具,可以帮助开发者更加高效地进行C++编程。Boost库中文手册是对Boost库的相关文档进行中文翻译的版本。 Boost库包含了众多的模块和组件,如智能指针、函数对象、线程、正则表达式、文件系统等,覆盖了C++开发中常用的领域。通过使用Boost库,开发者可以在项目中集成各种功能强大、稳定可靠的组件,提高代码的复用性和可维护性。 Boost库中文手册对于理解和学习Boost库的使用非常有帮助。手册中提供了详细的说明和示例代码,对于各个组件的功能、用法和特性进行了清晰的介绍,使得开发者可以快速上手和了解各个组件的使用细节。 通过学习Boost库中文手册,开发者可以深入了解各个模块和组件的详细功能和使用方法,使得他们能够更加灵活地利用Boost库来解决实际的开发问题。同时,手册中也提供了相应的参考资料和链接,方便开发者进一步扩展他们的学习和实践。 总之,Boost库中文手册是一个非常宝贵的资源,可以帮助开发者更好地利用Boost库进行C++编程。通过掌握Boost库提供的丰富功能和工具,开发者可以在项目中提高代码的效率和质量,同时也能够更好地满足对于C++编程的需求。 ### 回答3: Boost是一个开源的C++库,提供了很多功能丰富的组件,可以帮助开发者更轻松地进行C++程序开发。Boost库中包含了很多不同的模块,涵盖了各种不同的领域和功能。 11-boost库中文手册是指Boost库的第11章,该章节主要介绍了一些与字符串处理相关的功能组件。在这个章节中,我们可以了解到Boost库提供了一些强大的字符串处理工具,可以用于字符串的分割、合并、格式化、替换等操作。 例如,Boost库中的split函数可以帮助我们将一个字符串按照指定的分隔符进行分割,并将分割后的子字符串存入一个容器中。另外,Boost库还提供了format函数,用于格式化字符串,能够方便地将变量插入到字符串中。 此外,Boost库还提供了一些用于字符串匹配和替换的函数,例如regex_match和regex_replace等。这些函数可以帮助开发者高效地进行字符串模式匹配和替换。 总之,11-boost库中文手册是关于Boost库中字符串处理的部分。通过使用Boost库中提供的强大函数和组件,开发者可以更加方便地进行字符串处理和操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂嘚程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值