C++智能指针

智能指针

​ C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。

为什么要使用智能指针?

​ 智能指针就是用来解决内存泄露问题。如何解决?智能指针的本质是一个类,当超出类的作用域时,类就会调用析构函数,自动释放资源,所以就解决了内存泄露问题。

C++ 中有四种智能指针:auto_pt、unique_ptr、shared_ptr、weak_ptr 其中后三个是 C++11 支持,第一个已经被 C++11 弃用且被 unique_prt 代替,不推荐使用。

1. auto_ptr

随着 C++11 标准的出现(最新标准是 C++20),std::auto_ptr 已经被彻底放弃,取而代之是 std::unique_ptr。

#include<iostream>
#include<memory>

int main() {
    //初始化方式1
    std::auto_ptr<int> ap1(new int(8));
    //初始化方式2
    std::auto_ptr<int> ap2 ;
    ap2.reset(new int(10));
    // std::cout << *ap1 << std::endl;
    // std::cout << *ap2 << std::endl;
    std::auto_ptr<int> ap3(ap1);

    std::cout << (ap1.get() == NULL ? "ap1 is empty." : "ap1 is not empty.") << std::endl;
    std::cout << (ap3.get() == NULL ? "ap3 is empty." : "ap3 is not empty.") << std::endl;
    
    //测试赋值构造
    std::auto_ptr<int> ap4;
    ap4 = ap2 ;
    std::cout << (ap2.get() == NULL ? "ap2 is empty." : "ap2 is not empty.") << std::endl;
    std::cout << (ap4.get() == NULL ? "ap4 is empty." : "ap4 is not empty.") << std::endl;
    
    return 0;
}

输出:
ap1 is empty.
ap3 is not empty.
ap2 is empty.
ap4 is not empty.

分析: 上述代码中分别利用拷贝构造(ap1 => ap3)和 赋值构造(ap2 => ap4)来创建新的 std::auto_ptr 对象,因此 ap1 持有的堆对象被转移给 ap3,ap2 持有的堆对象被转移给 ap4。而 ap1 和 ap2 已经指向 NULL,若现在再对 ap1 和 ap2 进行访问并操作,将会出现内存错误问题。

为什么auto_ptr会被摒弃?

​ 因为如果两个auto_ptr指针指向同一个对象时,当该对象的生存周期结束后,系统会调用析构函数,这样导致的结果是程序对同一个对象删除了2次,造成程序出错。例如上面例子中的ap1和ap3,ap1已经指向NULL了,但是析构还是会进行资源的回收,这时就会出现错误,就类似浅拷贝深拷贝问题。

2. unique_ptr

2.1 定义

作为对 std::auto_ptr 的改进,std::unique_ptr 对其持有的堆内存具有唯一拥有权,也就是 std::unique_ptr 不可以拷贝或赋值给其他对象,其拥有的堆内存仅自己独占,std::unique_ptr 对象销毁时会释放其持有的堆内存。

#include<iostream>
#include<memory>

int main() {
    //初始化方式1
    std::unique_ptr<int> up1(new int(8));
    //初始化方式2
    std::unique_ptr<int> up2;
    up2.reset(new int(10));
    //初始化方式3   在C++11中并没有引入make_unique对象,直到C++14才引入make_unique
    //应该尽量使用初始化方式3的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全
    std::unique_ptr<int> up3 = std::make_unique<int>(12);
   
    return 0;
}

鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,std::unique_ptr 类的拷贝构造函数和赋值运算符(operator =)被标记为 delete。

unique_ptr(const unique_ptr &) = delete; 

unique_ptr &operator=(const unique_ptr &) = delete;

另外unique_ptr还有更聪明的地方:当程序试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编泽器允许这么做;如果源unique_ptr将存在一段时间,编译器将禁上这么做,例如:

unique_ptr<int> up1(new int(10) ) ;
unique_ptr<int> up2;
up2 = up1;        //1、这样赋值是不允许的
unique_ptr<int> up3;
up3 = unique_ptr<int>(new int(8));    //2、而这样子赋值是允许的

​ 其中 1 留下悬挂unique_ptr,这可能寻致危害。而 2 不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给up3后就会被销毁。这种随情况而异的行为表明,unique_ptr优于允许两种赋值的auto_ptr。

注: 如果确实想执行类似与 1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std:move(),让你能够将一个unique_ ptr赋给另一个。例如:

std::unique_ptr<int> up1(std::make_unique<int>(123));
    std::unique_ptr<int> up2(std::move(up1));
    std::cout << ((up1.get() == nullptr) ? "up1 is NULL" : "up1 is not NULL") << std::endl;
    
    std::unique_ptr<int> up3;
    up3 = std::move(up2);
    std::cout << ((up2.get() == nullptr) ? "up2 is NULL" : "up2 is not NULL") << std::endl;

输出:
up1 is NULL
up2 is NULL

​ 以上代码利用 std::move 将 up1 持有的堆内存(值为 123)转移给 up2,再把 up2 转移给 up3。最后,up1 和 up2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数或移动赋值运算符的类才行,而std::unique_ptr 正好实现了这二者。

2.2 常用函数

std::unique_ptr 有几个常用函数如下:

void reset(pointer p = pointer())

​ 释放当前由 unique_ptr(如果有)管理的指针并获得参数 p(参数 p 默认为 NULL)的所有权。如果 p 是空指针(例如默认初始化的指针),则 unique_ptr 变为空,调用后不管理任何对象。

pointer release()

​ 返回管理的指针并将其替换为空指针, 释放其管理指针的所有权。这个调用并不会销毁托管对象,但是将 unique_ptr 对象管理的指针解脱出来。如果要强制销毁所指向的对象,请调用 reset 函数或对其执行赋值操作。

element_type* get()

​ 返回存储的指针,不会使 unique_ptr 释放指针的所有权。因此,该函数返回的值不能于构造新的托管指针,如果为了获得存储的指针并释放其所有权,请调用 release。

void swap (unique_ptr& x)

​ 将 unique_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏二者。

3. shared_ptr

3.1 定义

​ std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个 std::shared_ptr 对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)。std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

示例代码:

int main()
{
    //初始化方式1
    std::shared_ptr<int> sp1(new int(123));

    //初始化方式2
    std::shared_ptr<int> sp2;
    sp2.reset(new int(123));

    //初始化方式3
    std::shared_ptr<int> sp3;
    sp3 = std::make_shared<int>(123);

    return 0;
}

和 std::unique_ptr 一样,优先使用 std::make_shared 去初始化一个 std::shared_ptr 对象。

示例代码2:

#include <iostream>
#include <memory>

class A {
public:
    A() {
        std::cout << "A constructor" << std::endl;
    }

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

void test() {
    std::shared_ptr<A> sp1(new A());
    std::cout << "use count: " << sp1.use_count() << std::endl;

    std::shared_ptr<A> sp2(sp1);
    std::cout << "use count: " << sp1.use_count() << std::endl;

    sp2.reset();
    std::cout << "use count: " << sp1.use_count() << std::endl;

    {
        std::shared_ptr<A> sp3 = sp1;
        std::cout << "use count: " << sp1.use_count() << std::endl;
    }
    std::cout << "use count: " << sp1.use_count() << std::endl;
}
int main() {
    test();
    return 0;
}

输出:
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor

3.2 常用函数

std::shared_ptr 有几个常用函数如下:

void swap (unique_ptr& x)

​ 将 shared_ptr 对象的内容与对象 x 进行交换,在它们两者之间转移管理指针的所有权而不破坏或改变二者的引用计数。

void reset()

void reset (ponit p)

​ 没有参数时,先将管理的计数器引用计数减一并将管理的指针和计数器置清零。有参数 p 时,先做面前没有参数的操作,再管理 p 的所有权和设置计数器。

element_type* get()

​ 得到其管理的指针。

long int use_count()

​ 返回与当前智能指针对象在同一指针上共享所有权的 shared_ptr 对象的数量,如果这是一个空的 shared_ptr,则该函数返回 0。如果要用来检查 use_count 是否为 1,可以改用成员函数 unique 会更快。

bool unique()

​ 返回当前 shared_ptr 对象是否不和其他智能指针对象共享指针的所有权,如果这是一个空的 shared_ptr,则该函数返回 false。

element_type& operator\*()

​ 重载指针的 * 运算符,返回管理的指针指向的地址的引用。

element_type* operator->()

​ 重载指针的 -> 运算符,返回管理的指针,可以访问其成员。

explicit operator bool()

​ 返回存储的指针是否已经是空指针,返回的结果与 get() != 0 相同。

3.3 线程安全问题

详细参考

  • 共享引用计数器的不同share_ptr被多个线程写,因为share_ptr的引用次数加减操作内部自动加锁减锁,所以是线程安全的。

  • 同一个share_ptr被多个线程读,是线程安全的

  • 同一个share_ptr被多个线程写,不是线程安全的。所以当我们要用多个线程对同一个share_ptr进行写操作时,应该自己加锁。

4. weak_ptr

4.1 定义

​ weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,进行该对象的内存管理的是那个强引用的shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为配合stared_ptr而引入的一种智能指针来协助stared_ptr工作,它只可以从一个stared_ptr或 另一个weak_ptr对象构造,它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决stared_ptr相互引用时的死锁问题。如果脱两个stared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转换,shared_ptr可以直接赋值给它,它可以通过lock函数来获得shared_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;
}

输出:
use count: 1
use count: 1
use count: 1
use count: 1

示例代码2:

#include <iostream>
#include <memory>

class B;
class A {
public:
    //std::shared_ptr<B> pb_;  如果此处也使用shared_ptr则A和B都无法释放,计数会一直为2,造成死锁问题
    std::weak_ptr<B> pb_;
    ~A() {
        std::cout << "A delete" << std::endl;
    }
};
class B {
public:
    std::shared_ptr<A> pa_;
    ~B() {
        std::cout << "B delete" << std::endl;
    }
};
void test() {
    std::shared_ptr<B> pb(new B());
    std::shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    std::cout << pb.use_count() << std::endl;
    std::cout << pa.use_count() << std::endl;
}
int main() {
    test();
    return 0;
}

输出:
1
2
B delete
A delete

分析: 可以看到test函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb_; 这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

注意: 的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();

4.2 常用函数

std::weak_ptr 有几个常用函数 如下:

void swap (weak_ptr& x)

​ 将当前 weak_ptr 对象的内容与 x 的内容交换。

void reset()

​ 将当前 weak_ptr 对象管理的指针和计数器变成空的,就像默认构造的一样。

long int use_count()

​ 返回与当前 weak_ptr 对象在同一指针上共享所有权的 shared_ptr 对象的数量。

bool expired()

​ 检查是否过期,返回 weak_ptr 对象管理的指针为空,或者和他所属共享的没有更多 shared_ptr。lock 函数一般需要先调用 expired 判断,如果已经过期,就不能通过 weak_ptr 恢复拥有的 shared_ptr。此函数应返回与(use_count() == 0)相同的值,但是它可能以更有效的方式执行此操作。

void swap (weak_ptr& x)

​ 如果它没有过期,则返回一个 shared_ptr,其中包含由 weak_ptr 对象保留的信息。如果 weak_ptr 对象已经过期,则该函数返回一个空的 shared_ptr(默认构造一样)。因为返回的 shared_ptr 对象也算作一个所有者,所以这个函数锁定了拥有的指针,防止它被释放(至少在返回的对象没有释放它的情况下)。 此操作以原子方式执行。

4.3 获取weak_ptr指向内存的的数据

​ weak_ptr是一种弱指针,它具有类似share_ptr的行为,但是不会影响它所指向的资源引用计数。weak_ptr不能被解引用,也不能被测试是否为null,所以我们不能直接用它来访问它所指向的资源,即便weak_ptr能够被解引用,这么做也是不安全的,因为判断是否过期和解引用是两个独立的动作,在多线程的环境下,会出现线程竞争,所以这不是一种线程安全的做法。所以当我们要访问weak_ptr所指向的资源时,应当将weak_ptr转化为一个share_ptr来访问资源,把weak_ptr转化为share_ptr后会引起weak_ptr所指向的share_ptr的资源计数器的增长。

​ 通常我们在使用weak_ptr时,会考虑先用expired()判读是否过期,然后再进行转换。

实例:

int main() {
    shared_ptr<int> sp(new int(123));
    weak_ptr<int> wp(sp);
//  sp.reset();
    if (!wp.expired()) {
        // 第一种方法,调用lock
        shared_ptr<int> pa = wp.lock();
        cout << *pa << endl; 
        // 第二种方法 直接及那个weak_ptr作为share_ptr构造函数的参数 
        shared_ptr<int> pa2(wp);
        cout << *pa2 << endl; 
    }
    else {
        cout << "wp指向对象为空" << endl;
    }
}

5. share_ptr的简单实现

(并未详细测试,仅供参考)

#include<iostream>

template<typename T>
class SharePtr {
public:
    SharePtr() : m_ptr((T*)0), m_count(0) {}
    //构造函数
    SharePtr(T* p) : m_ptr(p), m_count(new int(1)) {
        std::cout << "构造函数" << std::endl;
    }
    //拷贝构造函数  一个新对象拷贝另一个对象,则被拷贝对象的count计数器+1
    SharePtr(SharePtr<T> &s) {
        ++*s.m_count;
        this->m_ptr = s.m_ptr;
        this->m_count = s.m_count;
        std::cout << "拷贝构造函数   "  << std::endl;
    }
    //重载operator= 如果原来的shareptr已经有对象,则释放掉 , 然后指向新对象
    SharePtr<T>& operator=(SharePtr<T> &s) {
        if(this != nullptr) this->release();
        ++*s.m_count;
        this->m_ptr = s.m_ptr;
        this->m_count = s.m_count;
        std::cout << "赋值构造函数" << std::endl;
        return *this;
    }
    //析构函数 析构时指向的对象计数器减一
    ~SharePtr() {
        std::cout << "析构函数" << std::endl;
        release();
    }
    //重载operator*
    T& operator*() {
        return *m_ptr;
    }
    //重载operator->
    T* operator->() {
        return m_ptr;
    }
    int getCount() {
        return *m_count;
    }
protected:
    void release() {
        if(--*this->m_count == 0) {
            delete m_ptr;
            m_ptr = nullptr;
            delete m_count;
            m_count = nullptr;
        }
    }

private:
    T *m_ptr;
    int *m_count;
};
int main() {
    SharePtr<int> sp1(new int(10));
    std::cout << sp1.getCount() << std::endl;
    SharePtr<int> sp2(sp1);
    std::cout << sp1.getCount() << std::endl;
    {
        //SharePtr<int> sp3 = sp1; 如果使用这种写法的话并不会用到重载=,而是直接调用拷贝构造,详见https://blog.csdn.net/quietbxj/article/details/113738713
        SharePtr<int> sp3(new int(11));
        sp3 = sp1; //重载=进行赋值
        std::cout << sp1.getCount() << std::endl;
    }
    std::cout << sp1.getCount() << std::endl;
    return 0;
}

6. weak_ptr的简单实现

参考资料

7. 其他

7.1 智能指针的大小

在 32 位机器上,unique_ptr 占 4 字节,shared_ptr 和 weak_ptr 占 8 字节。

在 64 位机器上,unique_ptr 占 8 字节,shared_ptr 和 weak_ptr 占 16 字节。

unique_ptr 的大小总是和原始指针大小一样,shared_ptr 和 weak_ptr 大小是原始指针的一倍。

7.2 注意事项

参考资料

参考资料

参考资料1

参考资料2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值