当前位置:   article > 正文

C++11:智能指针_智能指针c++11

智能指针c++11

1. 介绍

到目前为止,我们编写的程序中所使用的对象都有着严格定义的生存期:

  • 全局对象:程序启动时分配,在程序结束时销毁。
  • 局部对象:当我们进入其定义所在的程序块时被创建,在离开块时销毁。
  • 局部static对象:在第一次使用前分配,在程序结束时销毁。

我们的程序到目前为止只使用过静态内存或栈内存:

  • 静态内存:保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
  • 栈内存:保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

1.1 动态内存与智能指针

除了局部和static对象外,C++还支持动态分配对象。动态分配的对象的生存期与它们在哪里创建是无关的,只有当显式地被释放时,这些对象才会销毁。动态对象的正确释放是编程中极其容易出错的地方。为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。

在C++中,动态内存的管理是通过一对运算符来完成的:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;
  • delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题;

  • 内存泄漏:有时我们会忘记释放内存,在这种情况下就会产生内存泄漏
  • 非法指针:有时在尚有指针引用内存的情况下我们就释放了它,在这种情况下就会产生引用非法内存的指针

C++11为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。==智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。==新标准库提供的这两种智能指针的区别在于管理底层指针的方式:

  • shared_ptr允许多个指针指向同一个对象;
  • unique_ptr则“独占”所指向的对象。

标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在<memory>头文件中。

智能指针是一个类,它能够管理(真正的)指针引用的内存,同时它还能像指针一样被使用,就像仿函数(函数对象)能像函数一样被使用一样。

2. 使用

下面以shared_ptr为例。

2.1 创建

类似vector等容器,智能指针也是模板。因此当我们创建一个智能指针时,必须提供额外的信息:指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字。

shared_ptr<int> p1; // 指向int的智能指针
shared_ptr<string> p2; // 指向string的智能指针
  • 1
  • 2

2.2 使用

智能指针的使用方式与普通指针类似。解引用一个智能指针返回它指向的对象。

int main()
{
    shared_ptr<int> p1(new int(1));

    cout << p1 << endl;
    cout << *p1 << endl;
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

输出:

0x6000031d0030
1
  • 1
  • 2

虽然智能指针是一个类,但是其中重载了操作指针的运算符,用起来就像指针一样。还可以像指针一样访问类中的成员:

class Person
{
public:
    Person(const string& name)
    	: _name(name)
    {}
    const string& GetName()
    {
        return _name;
    }
private:
    string _name;
};
int main()
{
    shared_ptr<Person> p(new Person("小明"));

    cout << p->GetName() << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

输出:

小明
  • 1

使用上非常简单,就像真正的指针一样,下面将讨论智能指针管理资源的原理。

3. 原理

智能指针的实现,必须解决下面三个问题:

  • RAII,将资源交给对象的生命周期管理,即构造对象时开始获取(管理)资源,析构对象时释放资源;
  • 像真正的指针一样使用;
  • 支持智能指针对象的拷贝。

其中最容易实现的是像指针一样使用,只要重载使用指针的运算符即可。其次是实现RAII,最后是智能指针对象的拷贝。

3.1 RAII

RAII(Resource Acquisition Is Initialization),资源获取即初始化,由Bjarne Stroustrup(C++之父)提出。是一种将资源的生命周期绑定到对象的生命周期的 C++ 编程技术,它有助于避免资源泄漏并简化错误处理。这是设计智能指针核心思想。

RAII在智能指针上的体现:智能指针在构造时获取一个原始指针,并在析构时释放它:

int main()
{
    shared_ptr<int> p(new int(1)); // 构造时获取内存
    cout << *p << endl; // 访问内存
    // 出了作用域,自动释放内存
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

RAII是一种利用对象生命周期来管理资源的技术,它的意义在于:

  • 避免资源泄漏,因为对象在析构时会自动释放所占用的资源。
  • 简化代码,因为不需要手动管理资源的分配和释放。
  • 增强异常安全性,因为即使发生异常,对象也会被正常析构。

RAII是C++中一种非常重要和实用的编程范式,它体现了C++的设计哲学:让编译器帮助我们做更多的事情。

3.2 像指针一样使用

下面将用一个自定义智能指针SmartPtr为例:

template<class T>
class SmartPtr
{
public:
    SmartPtr(T* ptr = nullptr) // 构造时接管内存
        : _ptr(ptr)
    {}
    ~SmartPtr()      // 析构时释放内存
    {
        cout << "delete:" << _ptr << endl; // 提示语句
        delete _ptr;
    }

    // 重载操作符
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
private:
    T* _ptr;
};
int main()
{
    SmartPtr<int> p(new int(1));
    cout << *p << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

其中重载了*->运算符,使得使用这个类就像使用指针一样。智能指针是一个模板类,以能够管理任何类型的指针引用的内存,如果模板参数是一个有公有成员的类,那么还能使用->访问其成员。

当智能指针未初始化时,赋予nullptr缺省值。

3.3 支持智能指针对象拷贝

上面实现的智能指针SmartPtr是极不完善的,如果想实现拷贝构造和拷贝赋值:

int main()
{
    SmartPtr<int> p1(new int(1));
    SmartPtr<int> p2(p1); // 拷贝构造

    SmartPtr<int> p3(new int(2));
    SmartPtr<int> p4(new int(2));
    p4 = p3; // 拷贝赋值

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

输出:

delete:0x600003c84030
delete:0x600003c84030
  • 1
  • 2

错误(Clion):

malloc: *** error for object 0x600003c84030: pointer being freed was not allocated
  • 1

造成程序崩溃的原因是在这个类中没有实现拷贝构造函数和拷贝赋值函数,而编译器默认生成的全都是对内置类型的浅拷贝(值拷贝):相当于p1和p2、p3和p4共同管理同一块空间。当出了p1的作用域后,调用析构函数,释放空间,p2再次调用析构函数时导致这块已经被释放的空间再次被释放。p3和p4同理。

要解决浅拷贝造成的二次析构问题,就要实现深拷贝的拷贝构造函数和拷贝赋值函数吗?

答案是否定的,智能指针的功能需求是模拟指针的使用,本质是帮指针托管资源,那么指针的拷贝或赋值操作就相当于两个指针指向同一块内存空间。资源管理权转移,通过不负责任的拷贝,会导致被拷贝对象悬空。虽然资源能得到释放,但是会造成垂悬指针。智能指针将内存资源的管理和对象的生命周期绑定在一起,如果只是像上面一样简单地满足RAII,那么一定会发生二次析构的问题,因为创建的智能指针对象一定会调用析构函数,且不论程序是否正常结束。

  • 程序正常结束:对象出了作用域调用析构函数;

  • 程序不正常结束:例如抛异常,跳转到catch块相当于跳转到另一个函数的栈帧中,也相当于出了作用域,依然调用析构函数。

下面以标准库中(C++98)智能指针auto_ptr为例。

auto_ptr

出现多次析构问题的本质是同一块内存空间被多个对象通过管理,如果将资源的管理权只交给一个对象,就不会出现多次析构问题。

int main()
{
    auto_ptr<int> p1(new int(1));
    auto_ptr<int> p2(p1);

    auto_ptr<int> p3(new int(2));
    auto_ptr<int> p4(new int(2));
    p3 = p4;

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

然而,将一个对象对资源的管理权转移后,就意味着这个对象再对资源访问是一个非法操作,程序会因此崩溃。如果让不熟悉auto_ptr原理的人使用,因为拷贝操作而造成非法指针或内存泄漏是有可能的,而这也是致命的错误,因此许多公司明文规定禁止auto_ptr的使用,进而用C++11的unique_ptr和shared_ptr取代。

通过模拟实现auto_ptr理解其原理,以更好地理解unique_ptr和shared_ptr。同样地,实现auto_ptr也需要满足三点,其中像指针一样使用仍然不变。

RAII
  • 析构函数:需要对它管理的指针判空,只有指针非空时才能对其进行释放资源操作,释放资源以后对其置空。

  • 拷贝构造函数:用传入对象管理的内存资源来构造当前对象,并将传入对象管理资源的指针置空。

  • 拷贝赋值函数:先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。

namespace xy
{
    template<class T>
    class auto_ptr
    {
    public:
        auto_ptr(T* ptr = nullptr)
            :_ptr(ptr)
        {}
        ~auto_ptr()
        {
            if (_ptr != nullptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                _ptr = nullptr;
            }
        }
        auto_ptr(auto_ptr<T>& ap)
            :_ptr(ap._ptr)
        {
            ap._ptr = nullptr; // 管理权转移后置空ap
        }
        auto_ptr& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                delete _ptr;       // 释放自己管理的资源
                _ptr = ap._ptr;    // 接管ap对象的资源
                ap._ptr = nullptr; // 管理权转移后置空ap
            }
            return *this;
        }

        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

测试:

class A
{
public:
    A(const int a = 0)
            : _a(a)
    {}
    ~A()
    {}

    int _a;
};
int main()
{
    xy::auto_ptr<A> ap1(new A);
    ap1->_a++;
    cout << ap1->_a << endl;

    xy::auto_ptr<A> ap2(ap1); // ap2接管ap1的资源
    ap2->_a++;
    //cout << ap1->_a << endl; // error
    cout << ap2->_a << endl;

    xy::auto_ptr<A> ap3(new A);
    ap3 = ap2; // ap3接管ap2的资源
    // cout << ap2->_a << endl; // error

    ap3->_a++;
    cout << ap3->_a << endl;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

输出:

1
2
3
delete: 0x6000014a0030
  • 1
  • 2
  • 3
  • 4

对于auto_ptr,当一个智能指针接管另一个智能指针管理的资源后,原来的指针已经解除对资源的引用,是一个悬垂指针。如果对其进行任何访问操作,会造成程序崩溃,例如main()中被注释的语句。

下面介绍标准库中的两种智能指针,并通过模拟实现理解它们的原理。

4. 标准库中的智能指针

库中的智能指针并不是凭空出世的,在此之前,boost社区对auto_ptr进行改良,推出了几种智能指针。

Boost 社区是一个提供 C++ 库的开源项目,Boost 大概是最重要的第三方C++ 库。其作者有很多是C++ 标准委员会的成员。Boost 的很多子库后来都成为C++ 的标准库。它包括了很多高质量和实用的库,其中就有智能指针库。

Boost 的智能指针库提供了六种智能指针模板,分别是 scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr 和 intrusive_ptr。它们和 C++11 的智能指针有一些相似之处,也有一些不同之处。比如,Boost 的 shared_ptr 和 weak_ptr 可以用于数组,而 C++11 的不可以;而 C++11 的 unique_ptr 可以使用移动语义,而 Boost 的 scoped_ptr 不可以。Boost 社区对 C++ 标准的发展也有一定的影响,比如 C++11 的 shared_ptr 和 weak_ptr 就是基于 Boost 的实现。

4.1 unique_ptr

C++98的auto_ptr因为拷贝和赋值操作而造成内存泄漏和悬垂指针的问题而饱受诟病,C++11引入的unique_ptr则粗暴地砍掉了它的拷贝和赋值功能。这通过C++11引入的关键字delete的新功能实现。

在C++11之前,可以通过将构造函数和拷贝赋值函数私有声明实现。

模拟实现

模拟实现的过程:去除auto_ptr中拷贝和赋值的函数。

namespace xy
{
    template<class T>
    class unique_ptr
    {
    public:
        unique_ptr(T* ptr = nullptr)
                :_ptr(ptr)
        {}
        ~unique_ptr()
        {
            if (_ptr != nullptr)
            {
                cout << "delete:" << _ptr << endl;
                delete _ptr;
                _ptr = nullptr;
            }
        }
        unique_ptr(auto_ptr<T>& ap) = delete;
        unique_ptr& operator=(auto_ptr<T>& ap) = delete;

        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}
int main()
{
    xy::unique_ptr<int> uqp1(new int);
    xy::unique_ptr<int> uqp2(new int);

    uqp2 = uqp1; // error

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

对于以上赋值语句,输出(Clion):

malloc: *** error for object 0x6000019a4030: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debug
delete:0x6000019a4030
delete:0x6000019a4030
  • 1
  • 2
  • 3
  • 4

C++98中,delete的意思是不让编译器自动生成默认函数,而C++11为了实现这个智能指针,赋予delete一个新功能:不允许调用。

4.2 shared_ptr

shared_ptr就是支持正常拷贝的auto_ptr。shared_ptr和其他智能指针的主要区别是它支持共享所有权,也就是说,多个shared_ptr对象可以拥有同一个资源,而不会造成内存泄漏或悬空指针。其他智能指针,如unique_ptr或weak_ptr,只能有一个所有者或不能控制资源的生命周期。

shared_ptr通过一个指针保持对一个对象的共享所有权。多个shared_ptr对象可以拥有同一个对象。当以下情况之一发生时,对象被销毁并释放其内存:

  • 拥有该对象的最后一个shared_ptr被销毁;
  • 通过reset()函数将shared_ptr赋值为另一个指针。

引用计数

auto_ptr转移资源后造成内存泄漏和悬垂指针的主要原因就是每个auto_ptr智能指针对象管理的资源是各自独立的,非此即彼。shared_ptr共享同一个资源,内存资源只在最后一个智能指针解除引用时释放,这样就不会造成资源被单方面地接管造成的问题。

image-20230306145422175

引用计数使得一个空间可以被多个对象管理,当引用计数为0时,说明已经没有智能指针管理这块内存空间了,此时才能释放资源,弥补了auto_ptr的缺陷。要知道引用计数的值,只需要调用shared_ptr的成员函数use_count()即可。

int main()
{
    shared_ptr<int> sp1(new int(1));
    shared_ptr<int> sp2(sp1);
    *sp1 = 10; // 拷贝后旧指针管理的资源依然能访问
    cout << sp1.use_count() << endl;

    shared_ptr<int> sp3(new int(0));
    shared_ptr<int> sp4(new int(2));
    sp3 = sp4;
    cout << sp3.use_count() << endl; // 赋值后旧指针管理的资源依然能访问
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

输出:

2
2
  • 1
  • 2

模拟实现

  • 增加count成员变量,表示引用计数;

  • 构造函数:当获取到资源则设置count=1,表示当前只有一个智能指针对象管理此资源;

  • 拷贝构造函数:将传入的智能指针对象中的count++,表示新增了一个管理者;

  • 拷贝赋值函数:将本智能指针的count--,表示解除对当前资源的引用,然后再将传入的智能指针对象中的count++,表示管理新的资源;

  • 析构函数:count--,表示解除对当前管理资源的引用,如果count=0则释放资源;

  • 重载*->运算符,使shared_ptr对象具有指针一样的行为。

其中,operator=的重载需要注意两个问题:

  • 内存泄漏:赋值时要把自己的引用计数给对方,赋值代表对方要共同接管自己管理的资源,所以对方的引用计数也要-1;
  • 自我赋值:本质也会造成内存泄漏,自我赋值后资源的管理权并未发生变化,但是引用计数却+1了,到真正最后一个对象时,引用计数仍不为0(如果自我赋值1次,那就是1),造成资源不能释放,内存泄漏。
namespace xy
{
    template<class T>
    class shared_ptr
    {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pcount(new int(1))
        {}
        ~shared_ptr()
        {
            if (--(*_pcount) == 0)
            {
                if (_ptr != nullptr)
                {
                    cout << "delete:" << _ptr << endl;
                    delete _ptr;
                    _ptr = nullptr;
                }
                delete _pcount;
                _pcount = nullptr;
            }
        }
        shared_ptr(shared_ptr<T>& sp)
                :_ptr(sp._ptr)
                , _pcount(sp._pcount)
        {
            (*_pcount)++;
        }
        shared_ptr<T>& operator=(shared_ptr<T>& sp)
        {
            if (_ptr != sp._ptr)       // 管理同一资源的智能指针赋值无意义
            {
                if (--(*_pcount) == 0) // 将管理的资源的引用计数-1
                {
                    cout << "delete:" << _ptr << endl;
                    delete _ptr;
                    delete _pcount;
                }
                _ptr = sp._ptr;       // 与传入的智能指针共享资源
                _pcount = sp._pcount; // 将自己的引用计数和传入的智能指针同步
                (*_pcount)++;         // 引用计数+1,表示自己是新增的管理者
            }
            return *this;
        }
        // 获取引用计数
        int use_count()
        {
            return *_pcount;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pcount; // 引用计数
    };
}
int main()
{
    xy::shared_ptr<int> sp1(new int(1));
    cout << sp1.use_count() << endl;
    xy::shared_ptr<int> sp2(sp1);
    cout << sp1.use_count() << endl;
    cout << sp2.use_count() << endl;

    xy::shared_ptr<int> sp3(new int(0));
    cout << sp3.use_count() << endl;

    xy::shared_ptr<int> sp4(new int(2));
    sp3 = sp4;
    cout << sp3.use_count() << endl;
    cout << sp4.use_count() << endl;

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82

输出:

1
2
2
1
delete:0x6000007f4050
2
2
delete:0x6000007f4070
delete:0x6000007f4030
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意:

shared_ptr中的引用计数是存放在堆区的,因为这样可以让所有指向同一个对象的shared_p。如果引用计数在栈区,那么当一个shared_ptr改变指向或者离开作用域时,就无法通知其他shared_ptr更新引用计数了。因此,引用计数也不能是静态成员,每个类型实例化的智能指针对象时共用静态成员,这会导致管理相同资源的对象和管理不同资源的对象共用同一个引用计数。

由于在堆区的引用计数和同一类型的智能指针是绑定在一起的,当智能指针释放资源时,也需要释放引用计数占用的内存。

定制删除器

实际上,不是所有的对象都是new出来的,也可能是new[],因此释放对象的资源也可能是delete[]。例如:

  • 当你想在释放对象时执行一些额外的操作,例如关闭文件、释放资源、记录日志等。
  • 当你想使用一个不同于delete的函数来销毁对象,例如free、fclose、Release等。
  • 当你想管理一个不是通过new分配的对象,例如一个栈上的对象或一个全局变量。
  • 当你想管理一个不是单个对象而是一个数组或容器的对象。

定制删除器可以让你更灵活地控制shared_ptr如何管理和释放它所指向的对象。

假设你想管理一个打开的文件,但是你不能使用delete来关闭它,而是使用fclose:

#include <iostream>
#include <memory>
#include <cstdio>

int main()
{
    // 创建一个shared_ptr,管理一个打开的文件
    // 使用fclose作为定制删除器
    std::shared_ptr<FILE> file(fopen("test.txt", "w"), fclose);

    // 写入一些内容到文件
    fputs("Hello world", file.get());

    // 当file离开作用域时,会调用fclose来关闭文件
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

这样就可以避免使用delete来释放一个不是通过new分配的对象,从而导致危险行为。shared_ptr在实例化对象时,有两个参数:

template <class U, class D>
shared_ptr (U* p, D del);
  • 1
  • 2

其中:

  • p:需要让智能指针管理的资源。
  • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

实际上,删除器就是一个被工具封装的动作,这个动作就是用特定的方式释放资源。

总的来说,当智能指针管理的资源不是通过new出来的时候,就需要用对象类型和定制删除器构造智能指针。

例如可以传入一个lambda表达式作为定制删除器:

int main()
{
    shared_ptr<int> sp1(new int[5], [](int* ptr)
    {
        cout << "delete[]:" << ptr << endl;
        delete[] ptr;
    });


    shared_ptr<FILE> sp2(fopen("test.cpp", "r"), [](FILE* ptr)
    {
        cout << "fclose: " << ptr << endl;
        fclose(ptr);
    });

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

lambda表达式在定制删除器中的应用,体现了lambda表达式的内联属性。

unique_ptr没办法用lambda表达式,因为它是一个对象,unique_ptr必须传模板参数(类型)。

库中的实现比较复杂,因为要实现模板以满足各种定制器的类型。但总而言之就是根据实际情况定制一个类按照特定方式释放资源,在创建对象的时候将这个特定的动作作为参数。

4.3 weak_ptr

shared_ptr造成的循环引用问题

shared_ptr解决了auto_ptr可能造成内存泄漏和悬垂指针的问题,但是在少数情况下,shared_ptr也会造成内存泄漏,即循环引用问题。

循环引用是指当一个对象或者一个单元格内的公式直接或间接地引用了自己或者另一个对象。这样会导致内存泄漏或者计算错误。也就是说,如果两个shared_ptr指针互相引用,那么它们的引用计数永远不会为零,也就无法释放内存。

例如:

struct Node
{
    std::shared_ptr<Node> _next;
    std::shared_ptr<Node> _prev;
    
    ~Node()
    {
        cout << "~Node" << endl;
    }
};
int main()
{
    std::shared_ptr<Node> n1(new Node);
    std::shared_ptr<Node> n2(new Node);
    // 循环引用
    n1->_next = n2;
    n2->_prev = n1;
    
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

为了share_ptr可以使用Node,所以Node的两个指针都是shared_ptr<Node>类型的。

其中,两个赋值语句造成了两个结点对象n1和n2循环引用:

image-20230306230632036

n1n2 两个 Node 对象通过 _next_prev 成员变量相互引用,导致它们的引用计数永远不为零,从而无法被销毁。

死循环:资源只有在引用计数为1时才能被销毁。左边资源只有当右边的_prev释放以后引用计数才为0,而右边资源只有当左边的_next释放以后引用计数才为0。

解决这个问题的一种方法是使用 std::weak_ptr 来代替其中任意一个方向上的 std::shared_ptr

std::weak_ptr 是一种智能指针,它用来解决 std::shared_ptr 循环引用的问题。它不会增加所指向对象的引用计数,因此不会影响对象的销毁。

对于上面的例子,造成问题的本质是引用计数永不为0,那么只要将其中一个智能指针改为weak_ptr即可:

struct Node
{
    std::shared_ptr<Node> _next;
    std::weak_ptr<Node> _prev; // 将_prev用weak_ptr管理
    
    ~Node()
    {
        cout << "~Node" << endl;
    }
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

与shared_ptr的关系

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期。也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,它的构造和析构不会引起引用记数的增加或减少。弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

也就是说,weak_ptr是为了弥补shared_ptr循环引用而生的,它没有RAII的特性,不直接管理资源,只是shared_ptr的跟班,这也是weak_ptr支持使用shared_ptr构造的原因。

在使用上,weak_ptr支持指针所有的操作。它不是一个功能型的智能指针,而是辅助型,它的使命是解决shared_ptr造成的循环引用问题。

shared_ptr 不同,weak_ptr 不能直接访问所指向的对象。要访问对象,需要先调用 lock() 方法将其转换为 shared_ptr。如果所指向的对象已经被销毁,则 lock() 方法返回空指针。

模拟实现

  • 构造函数:无参构造
  • 拷贝构造:支持参数是shared_ptr类型和本身类型构造,同时接管shared_ptr管理的资源,但不增加引用计数
  • 拷贝赋值:同上。智能指针一般有get()接口,所以返回指针时可以调用
  • 像指针一样

它是一个辅助型指针,在shared_ptr内部实现。

namespace xy
{
    template<class T>
    class weak_ptr
    {
    public:
        weak_ptr()
            :_ptr(nullptr)
        {}
        weak_ptr(const shared_ptr<T>& sp)
            :_ptr(sp.get())
        {}
        weak_ptr& operator=(const shared_ptr<T>& sp)
        {
            _ptr = sp.get();
            return *this;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

5. 常见问题

为什么需要智能指针?

智能指针是一种用来管理在堆上分配的内存的工具。它将普通的指针封装为一个栈对象,当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。智能指针还可以防止忘记调用delete释放内存和程序异常进入catch块忘记释放内存。

C++11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个对象共享。

什么是引用计数?

是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。

RAII是什么?

RAII是Resource Acquisition Is Initialization的简称,中文翻译为“资源获取即初始化”。它是C++语言中的一种管理资源、避免泄漏的良好方法。它的原理是在构造函数中申请分配资源,在析构函数中释放资源。

智能指针的发展历史?

C++98中产生了第一个智能指针auto_ptr,但它存在较多问题。C++boost给出了更加实用的scoped_ptr和shared_ptr和weak_ptr。C++11标准引入了unique_ptr和shared_ptr和weak_ptr,其中unique_ptr对应的是boost中的scoped_ptr。

智能指针技术经过20多年的发展,特别是C++11标准引入shared_ptr和unique_ptr之后,趋于成熟。

C++有哪些智能指针?它们之前的区别和使用场景?

C++11中推出了三种智能指针,unique_ptr、shared_ptr和weak_ptr,同时也将auto_ptr置为废弃。

  • unique_ptr是独占资源所有权的指针,当我们独占资源的所有权的时候,可以使用unique_ptr对资源进行管理——离开unique_ptr对象的作用域时,会自动释放资源。这是很基本的RAII思想。

  • shared_ptr是共享资源所有权的指针。它使用引用计数来跟踪共享对象的引用数量。当引用计数变为0时,对象被销毁。

  • weak_ptr是共享资源的观察者,需要和shared_ptr一起使用,不影响资源的生命周期。它可以解决循环引用问题。

智能指针有一些自己的问题,比如循环引用、性能开销、处理数组、线程安全等,在使用智能指针前都需要详细了解。

模拟实现简易的智能指针,例如shared_ptr。

见4.2。

什么是循环引用,如何解决?原理?

循环引用是指两个或多个对象相互引用,导致它们之间形成一个循环。这会导致一些问题,比如在使用引用计数的垃圾回收机制中,循环引用的对象永远无法被释放。

解决循环引用的一种方法是使用弱引用,即不增加对象的引用计数,因此不会影响垃圾回收。例如,在C++中,可以使用weak_ptr来解决shared_ptr之间的循环引用问题。当判断是否为无用对象时仅考虑强引用计数是否为0,不关心弱引用计数的数量。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/神奇cpp/article/detail/781547
推荐阅读
相关标签
  

闽ICP备14008679号