C++智能指针

1,031 阅读5分钟

智能指针

智能指针技术应对于内存泄漏问题。智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。

智能指针的作用是管理一个指针,因为存在以下这种情况:**申请的空间在函数结束时忘记释放,造成内存泄漏。**使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

在某种程度上,智能指针是对于内存拥有权问题的控制

unique_ptr

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。可以避免忘记调用delete造成的内存泄漏。

std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1,std::unique_ptr对象销毁时会释放其持有的堆内存。初始化一个 std::unique_ptr 对象:

//初始化方式1
std::unique_ptr<int> sp1(new int(123));
//初始化方式2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
std::unique_ptr<int> sp3 = std::make_unique<int>(123);

禁止复制语义

std::unique_ptr 禁止复制语义

unique_ptr<string> pu1(new string ("hello world")); 
unique_ptr<string> pu2; 
pu2 = pu1;                                      // #1 不允许
unique_ptr<string> pu3; 
pu3 = unique_ptr<string>(new string ("You"));   // #2 允许

编译不通过,因为unique_ptr要保证资源的唯一拥有权,允许#2是因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。

如果需要将一个unique_ptr赋给另一个需要使用标准库函数std::move()(相当于将一个 std::unique_ptr 对象持有的堆内存转移给另外一个

#include <memory>
int main()
{
 std::unique_ptr<int> sp1(std::make_unique<int>(123));
 std::unique_ptr<int> sp2(std::move(sp1));
 std::unique_ptr<int> sp3;
 sp3 = std::move(sp2);
 return 0;
}

利用 std::move 将 sp1 持有的堆内存(值为 123)转移给 sp2,再把 sp2 转移给 sp3。最后, sp1 和 sp2 不再持有堆内存的引用,变成一个空的智能指针对象。空的智能指针对象仍旧调用就崩溃,但是这个语法能强调你是在转移所有权,让你清晰的知道自己在做什么,从而不乱调用原有指针。

unique_ptr使用

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的作用域,內存釋放
}

shared_ptr

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。资源可以被多个指针共享,使用计数机制来表明资源被几个指针共享。

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。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::cout << *sh_ptr<< std::endl; //获得指针的值

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;

        if(!wp.expired()){
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;
        }
    }
    //delete memory
}
  • use_count 返回引用计数的个数
  • unique 返回是否是独占所有权( use_count 为 1)
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
  • reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的
int main()
{
	string *s1 = new string("s1");

	shared_ptr<string> ps1(s1);
	shared_ptr<string> ps2;
	ps2 = ps1;

	cout << ps1.use_count()<<endl;	//2
	cout<<ps2.use_count()<<endl;	//2
	cout << ps1.unique()<<endl;	//0

	string *s3 = new string("s3");
	shared_ptr<string> ps3(s3);

	cout << (ps1.get()) << endl;	//033AEB48
	cout << ps3.get() << endl;	//033B2C50
	swap(ps1, ps3);	//交换所拥有的对象
	cout << (ps1.get())<<endl;	//033B2C50
	cout << ps3.get() << endl;	//033AEB48

	cout << ps1.use_count()<<endl;	//1
	cout << ps2.use_count() << endl;	//2
	ps2 = ps1;
	cout << ps1.use_count()<<endl;	//2
	cout << ps2.use_count() << endl;	//2
	ps1.reset();	//放弃ps1的拥有权,引用计数的减少
	cout << ps1.use_count()<<endl;	//0
	cout << ps2.use_count()<<endl;	//1
}

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。

当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。weak_ptr可以用来解决shared_ptr的死锁问题

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象, 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。

#include <iostream>
#include <memory>
int main()
{
 //创建一个std::shared_ptr对象
 std::shared_ptr<int> sp1(new int(123));
 std::cout << "use count: " << sp1.use_count() << std::endl;
 //通过构造函数得到一个std::weak_ptr对象
 std::weak_ptr<int> sp2(sp1);
 std::cout << "use count: " << sp1.use_count() << std::endl;
 //通过赋值运算符得到一个std::weak_ptr对象
 std::weak_ptr<int> sp3 = sp1;
 std::cout << "use count: " << sp1.use_count() << std::endl;
 //通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
 std::weak_ptr<int> sp4 = sp2;
 std::cout << "use count: " << sp1.use_count() << std::endl;
 return 0;
}

无论通过何种方式创建 std::weak_ptr 都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。

std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptr 的 lock()方法得到一 个 std::shared_ptr 对象然后继续操作资源

//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
 return;
std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn)
{
 //对conn进行操作,省略...
}

std::weak_ptr 的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消 息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。

class Subscriber
{
};
class SubscribeManager
{
public:
 void publish()
 {
     for (const auto& iter : m_subscribers)
     {
         if (!iter.expired())
         {
             //TODO:给订阅者发送消息
         }
     }
 }
private:
 std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};