15.智能指针

目录

1. 为什么需要智能指针

1.1 C++ 需要智能指针的原因

1.2 Java 不需要智能指针的原因

2. 智能指针的使用及原理

2.1 RAII

2.1 auto_ptr

2.2 unique_ptr

2.3 shared_ptr

2.4 weak_ptr

2.5 shared_ptr的循环引用

2.6 删除器

3.C++11 和 boost中智能指针的关系


1. 为什么需要智能指针

1.1 C++ 需要智能指针的原因

  1. 手动内存管理

    • C++ 要求开发者手动管理内存分配和释放,使用 newdelete 操作符。

    • 手动管理容易导致内存泄漏、悬空指针等问题。

  2. 作用域和生命周期

    • C++ 中对象的生命周期由作用域决定,离开作用域后对象会被销毁。

    • 对于动态分配的对象,需要确保在适当的时候释放内存。

1.2 Java 不需要智能指针的原因

  1. 自动垃圾回收

    • Java 使用垃圾回收机制(GC)自动管理内存,开发者无需手动释放内存。

    • GC 会定期清理不再使用的对象,防止内存泄漏。

  2. 对象生命周期

    • Java 中对象的生命周期由垃圾回收器管理,开发者只需创建对象,GC 会在对象不再被引用时回收内存。

  3. 引用类型

    • Java 提供了强引用、软引用、弱引用和虚引用,帮助开发者更灵活地管理对象生命周期,而不需要智能指针。


2. 智能指针的使用及原理

C++11中提供的智能指针都只能管理单个对象的资源,没有提供管理一段空间资源的智能指针

2.1 RAII

RAII是一种利用对象生命周期来控制程序资源的简单技术。

        在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两个好处:

        1.不需要显式的释放资源

        2.采用这种方式,对象所需的资源在其生命周期内始终保持有效

智能指针的原理:

        1.具有RAII的特性

        2.重载operator* 和 operator->,具有像指针一样的行为。

其中重载这两个操作符是很重要的:

1.operator* 的作用

operator* 是解引用运算符,用于获取智能指针所管理的对象的引用。它的实现通常如下:

T& operator*() const 
{
    return *_ptr;
}

解释:

  • _ptr 是智能指针内部管理的裸指针。

  • *_ptr 解引用裸指针,返回它所指向的对象的引用。

  • 返回类型是 T&,即对象的引用,允许用户直接修改所管理的对象。

auto_ptr<int> ptr(new int(42));
int value = *ptr;  // 解引用,获取 ptr 管理的 int 值
*ptr = 100;        // 修改 ptr 管理的 int 值

2. operator-> 的作用

operator-> 是成员访问运算符,用于访问智能指针所管理的对象的成员。它的实现通常如下:

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

解释:

  • operator-> 返回裸指针 _ptr,使得用户可以通过智能指针直接访问所管理对象的成员。

  • 返回类型是 T*,即裸指针。

struct MyStruct 
{
    int value;
    void print() { std::cout << value << std::endl; }
};

auto_ptr<MyStruct> ptr(new MyStruct{42});
ptr->value = 100;  // 访问 MyStruct 的成员 value
ptr->print();      // 调用 MyStruct 的成员函数 print

operator-> 的返回值必须是裸指针(T*),因为 C++ 语言规定 -> 运算符必须作用在指针上。

  1. ptr->print() 被解析为 ptr.operator->()->print()

  2. ptr.operator->() 返回 _ptr(一个裸指针)。

  3. 通过 _ptr 调用 print()

2.1 auto_ptr

        C++98版本的库中就提供了auto_ptr的智能指针。下面演示了auto_ptr的使用及其问题。

auto_ptr的实现原理:管理权转移的思想。

template <typename T>
class auto_ptr {
public:
    // 构造函数,接受一个裸指针并接管其所有权
    explicit auto_ptr(T* ptr = nullptr) : ptr_(ptr) {}

    // 析构函数,释放所管理的内存
    ~auto_ptr() {
        delete ptr_;
    }

    // 拷贝构造函数,转移所有权
    auto_ptr(auto_ptr& other) : ptr_(other.release()) {}

    // 拷贝赋值运算符,转移所有权
    auto_ptr& operator=(auto_ptr& other) {
        if (this != &other) {
            delete ptr_;
            ptr_ = other.release();
        }
        return *this;
    }

    // 解引用运算符,获取所管理对象的引用
    T& operator*() const {
        return *ptr_;
    }

    // 箭头运算符,访问所管理对象的成员
    T* operator->() const {
        return ptr_;
    }

    // 获取所管理的裸指针
    T* get() const {
        return ptr_;
    }

    // 释放所有权,返回裸指针并将内部指针置为空
    T* release() {
        T* ptr = ptr_;
        ptr_ = nullptr;
        return ptr;
    }

    // 重置所管理的指针,释放原有内存
    void reset(T* ptr = nullptr) {
        if (ptr_ != ptr) {
            delete ptr_;
            ptr_ = ptr;
        }
    }

private:
    T* ptr_;
};
int main()
{
    auto_ptr<int> p1(new int(10));
    auto_ptr<int> p2(p1);//管理权转移

    // cout << *p1 << endl;
    cout << *p2 << endl;
    return 0;
}

auto_ptr 的拷贝构造函数和拷贝赋值运算符会转移所有权。这意味着当一个 auto_ptr 对象被拷贝或赋值给另一个 auto_ptr 对象时,原对象会失去对所管理内存的所有权,新对象会接管所有权。

2.2 unique_ptr

C++11开始提供更合理的unique_ptr。

既然拷贝构造有问题,那干脆别拷贝了,这也是unique_ptr的实现原理,简单粗暴。

namespace zzy
{
    template <class T>
    class unique_ptr
    {
    public:
        explicit unique_ptr(T* ptr)
            : _ptr(ptr)
        {}

        ~unique_ptr()
        {
            if(_ptr)
            {
                cout << "~unique_ptr()" << endl;
                delete _ptr;
            }
        }

        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        
        unique_ptr(const unique_ptr&) = delete;
        unique_ptr& operator=(const unique_ptr&) = delete;
    private:
        T* _ptr;
    };
}

禁用拷贝时顺便把赋值也禁了。

2.3 shared_ptr

C++11开始提供更靠谱且支持拷贝的shared_ptr,很多时候让手撕的都是这个。

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

  1. shared_ptr在内部给每个资源都维护了一份计数,同来记录该份资源被几个对象共享
  2. 在对象被销毁时(析构时),说明该对象不使用该资源了,对象的引用计数减1
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放资源
  4. 如果不是0,说明除了自己还有其他对象在使用资源,不能释放该资源,否则其他对象就成野指针了
namespace zzy
{
    template <class T>
    class shared_ptr<T>
    {
    public:
        explicit shared_ptr(T* ptr = nullptr)
            : _ptr(ptr)
            ,_count(new int(1))
        {}

        shared_ptr(const shared_ptr<T>& sp)
            : _ptr(sp._ptr)
            ,_count(sp._count)
        {
            ++(*_count);
        }

        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            if(_ptr == sp._ptr)
                return *this;
            if(--(*_count)==0)
            {
                delete _ptr;
                delete _count;
            }
            _ptr = sp._ptr;
            _count = sp._count;
            ++(*_count);

            return *this;
        }

        ~shared_ptr()
        {
            if(--(*_count)==0)
            {
                cout << "delete:" << _ptr <<endl;
                delete _ptr;
                delete _count;
            }
        }

        int use_count()
        {
            return *_count;
        }

        T* operator->() const
        {
            return _ptr;
        }
        T& operator*() const
        {
            return *_ptr;
        }
        T* get() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _count;
    };
}

我们分析几个要点:

1. private中定义了两个变量,一个指向资源,一个指向计数,为什么要使用指针指向计数呢?

        我们期望一个资源有一个count,它可以有多个ptr去管理它。如果每个指针都有自己的一个,那肯定是不行的,此时,如果我们使用静态变量,当存在多份资源需要管理时,每个资源本应有一个,现在不管多少资源都只有一个count,这肯定也是不行的。所以还是使用指针指向某空间,多个指针管理同一块空间时,只要他们的_count一样就好了。

2.构造函数中,为什么使用new int(1);

        有一个资源要交给指针管理时,肯定调用构造,_ptr不用多说,而使用new是因为这个引用计数必须是一个动态分配的对象,而不是栈上的局部变量。如果引用计数是栈上的局部变量,那么当 shared_ptr 对象被复制或销毁时,引用计数会被错误地修改或销毁。

3.在赋值重载中,比如 sp1 = sp5

        if(--(*_count)==0);这时--的是sp1的 *_count ,因为sp1不再管理它原来的空间,原来空间的引用计数一定会--,如果减到0则释放。而后 _ptr = sp._ptr;  _count = sp._count;  ++(*_count);  此时++的是将sp5的_count赋给sp1的_count后的值,也就是sp1管理了sp5的空间,所以要++。

2.4 weak_ptr

        weak_ptr没有RAII特性,它不管理资源,它是对shared_ptr 的补充,用于解决 shared_ptr 可能导致的循环引用问题(循环引用问题我们下面马上会讲到),它的主要作用是观察资源,而不会影响资源的生命周期。

    template <class T>
    class weak_ptr
    {
    public:
        weak_ptr()
            : _ptr(nullptr)
        {}
        //支持用shared_ptr来构造
        explicit weak_ptr(const shared_ptr<T>& sp)
            : _ptr(sp.get())
        {}
        //赋值也一样
        weak_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();
            return *this;
        }
        T* operator->() const
        {
            return _ptr;
        }
        T& operator*() const
        {
            return *_ptr;
        }
        
    private:
        T* _ptr;
    };

2.5 shared_ptr的循环引用

struct ListNode
{
    int _data;
    shared_ptr<ListNode> _prev;
    shared_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; //1
    cout << node2.use_count() << endl; //1

    node1->_next = node2;
    node2->_prev = node1;

    cout << node1.use_count() << endl; //2
    cout << node2.use_count() << endl; //2
    return 0;
}

刚开始都是1个好理解,node1->_next = node2 后,指向node2的多了一个_next,所以_count变成2;node2->_prev = node1 后,指向node1的多了一个_prev,所以_count变成2。

当程序结束时,node1和node2析构了,他们各自的引用计数--变成了1,因为node1上还有一个_prev,node2上还有一个_next。也就是说只有这个_next 和 _prev 析构了,node1和node2的引用计数才会变0,node1和node2才会析构,但是_next是属于node的成员,node1释放了,_next才会析构,而node1又由_prev管理,这个_prev属于node2,直接循环引用,谁都不释放。

node2的_prev什么时候析构?-------        
node2析构时它就析构             |
        |                      |
        |                      |
node2什么时候析构?             |
node1的_next析构,node2就析构   |
        |                      |
        |                      |
node1的_next什么时候析构?      |
node1析构他就析构               |
        |                      |
        |                      |  
node1什么时候析构?             |
node2的_prev析构,node1就析构----

解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。

weak_ptr 下的next和prev不会增加node的引用计数。

2.6 删除器

        我们shared_ptr的底层是写死的,用的是delete,如果是new[] 出来的数据怎么办呢?用delete会崩溃,库里引入了定制删除器。

  • 不同资源可能需要不同的释放方式(例如 deletedelete[]free()、文件句柄关闭等)。

  • 直接硬编码 delete 会导致无法处理 new[]malloc 或其他自定义分配的资源。

在自定义 shared_ptr 中:

template <class T>
class shared_ptr 
{
public:
...
    // 允许用户传递自定义删除器
    template<class D>
    shared_ptr(T* ptr, D del)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _del(del)
	{}

    ~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			//delete _ptr;
			_del(_ptr);

			delete _pcount;
		}
	}
...
private:
	T* _ptr;
	int* _pcount;
	function<void(T*)> _del = [](T* ptr) {delete ptr; }; // 存储删除器
};

template<class T>
struct DeleteArray
{
    void operator()(T* ptr)
    {
        delete[] ptr;
    }
};
int main()
{
	zzy::shared_ptr<int> sp1(new int[10], DeleteArray<int>());
	zzy::shared_ptr<int> sp2((int*)malloc(sizeof(int)), [](int* ptr) {free(ptr); });
	zzy::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr)
	{
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
	});
	return 0;
}
  1. 默认删除器:适用于普通的 new 分配的资源。

  2. 自定义删除器:适用于 new[]、文件句柄、自定义资源等。

  3. 灵活性shared_ptr 的删除器机制使得它可以管理各种类型的资源,同时确保资源能够被正确释放。

3.C++11 和 boost中智能指针的关系

1.C++98中产生了第一个智能指针auto_ptr

2.Boost 提供了以下智能指针:

  • boost::shared_ptr:基于引用计数的智能指针,允许多个指针共享同一个对象。

  • boost::weak_ptr:与 boost::shared_ptr 配合使用,解决循环引用问题。

  • boost::scoped_ptr:独占所有权的智能指针,不能复制或移动。

Boost 的智能指针为 C++ 程序员提供了强大的工具,但需要额外安装和依赖 Boost 库。

3.C++11引入了unique_ptr 和 shared_ptr 和 weak_ptr,unique_ptr 对应 scoped_ptr。并且是参考实现的。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值