C++进阶(七) : 探讨C++11的三种智能指针【详解】

C++ 标准模板库 STL(Standard Template Library) 一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,其中 auto_ptr 是 C++98 提出的,C++11 已将其摒弃,并提出了 unique_ptr 替代 auto_ptr。

目录

一、unique_ptr

1、std::unique_ptr初始化

2、std::unique_ptr常用操作

二、std::shared_ptr

1、std::shared_ptr初始化

2、std::shared_ptr常用操作

3、shared_ptr缺点

4、shared_ptr相互引用会后果

三、weak_ptr

1、常用操作

2、解决std::shared_ptr相互引用的问题


一、unique_ptr

std::unique_ptr是一个独占型的智能指针,大多数场景下,std::unique_ptr和裸指针有着相同的尺寸,不允许赋值和拷贝操作,只能够移动。。

std::unique_ptr以两个形式提供,一种是单个对象(std::unique_ptr<T>),一种是数组(std::unique_ptr<T[]>),std::unique_ptr对两种形式提供的成员函数有所不同,例如,对单个对象形式不提供索引运算符(operator[]),对数组形式不提供提领运算符(operator*和operatpr->)。

1、std::unique_ptr初始化

std::unique_ptr可以使用裸指针直接初始化,也可以使用std::make_unique方法进行初始化( C++14 引入)。

std::unique_ptr<int> ptr1(new int(0));
std::unique_ptr<int> ptr2(std::make_unique<int>());
std::unique_ptr<int> ptr3 = ptr1;            // 错误,不能复制
std::unique_ptr<int> ptr4 = std::move(ptr1); // 可以移动

2、std::unique_ptr常用操作

std::unique_ptr特有的操作如下:

unique_ptr<T> u1 // 空的unique_ptr,指向类型为T的对象,u1使用默认删除函数delete来释放它管理的指针
unique_ptr<T, D> u2 // u2是指向类型为T的unique_ptr对象,会使用类型为D的对象来释放它管理的指针
unique_ptr<T, D> u(d) // u是指向类型为T的unique_ptr对象,用类型为D的对象d来释放它管理的指针
u.release() // u切断它和管理的指针间的联系,并返回指针,此时指针并不会被释放
u.reset()   // 释放u指向的对象
u.reset(q) // 释放u当前管理的指针,并令u管理新指针q
u.reset(nullptr)    // 释放u当前管理的指针,并令u管理nullptr

调用 release 会切断std::unique_ptr和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:

p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但我们必须记得 delete(p)
delete(p);

如果默认的析构函数不满足要求,我们可以自定义析构函数,代码实现及运行结果如下:

class MyTest {
public:
    //自定义类构造函数
    MyTest() {
        cout << "MyTest" << endl;
    };
 
    //自定义类析构函数
    ~MyTest() {
        cout << "~MyTest" << endl;
    };
};
 
int main()
{
    {
        //std::unique_ptr的自定义析构函数,t可以为MyTest类型及其子类
        auto MyDelete = [](MyTest* t) {
            cout << "MyDelete" << endl;
            delete t;
        };
        //智能指针初始化
        std::unique_ptr<MyTest,decltype(MyDelete)> p(new MyTest, MyDelete);
    }
    return 0;
}

最后一点,std::unique_ptr可以很方便的转换为std::shared_ptr型别,但反之则不行。例如:

//可以将std::unique_ptrz转换为std::shared_ptr
std::unique_ptr<int> u(new int);
std::shared_ptr<int> p = std::move(u);
 
 
//不可以std::shared_ptr转换为std::unique_ptr
std::shared_ptr<int> u(new int);
std::unique_ptr<int> p = std::move(u);

二、std::shared_ptr

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

  • 最后剩余的shared_ptr拥有该对象的对象被销毁;
  • shared_ptr通过operator=reset()为最后一个拥有该对象的人分配另一个指针。

std::shared_ptr的尺寸是裸指针的两倍,因为其内部包含两个指针,一个指向对象,另一个指向控制块(control block),由于这个控制块需要在多个shared_ptr之间共享,所以它在堆中分配。

shared_ptr引用计数达到零,则控制块调用托管对象的析构函数。但在std::weak_ptr计数器也达到零之前,控制块不会释放自身。

该控制块是一个动态分配的对象,它包含:

  • 指向托管对象或托管对象本身的指针;
  • 删除器(类型擦除);
  • 分配器(类型擦除);
  • shared_ptr拥有托管对象的数量;
  • weak_ptr引用托管对象的数量。

shared_ptr对象本身是线程安全的,也就是说shared_ptr的引用计数增加和减少的操作都是原子的。

和std::unique_ptr不同的是其不能处理数组,只能处理单个对象。

1、std::shared_ptr初始化

同std::unique_ptr一样,std::shared_ptr也有一个make_shared 的标准初始化函数(c++11)。 此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。

// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);

// p4 指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');

// p5指向一个未初始化的int
shared_ptr<int> p5 = make_shared<int>();

shared_ptr<int> pi = new int (1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));	// 正确:使用了直接初始化形式

2、std::shared_ptr常用操作

make_shared<T>(args) // 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化此对象
shared_ptr<T> p(q) // p是shared_ptr q的拷贝;此操作会递增q中的引用计数。q中的指针必须能转换成T*
p = q // p和q都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p中的引用计数,递增q中的引用计数。若p中的引用计数变为0,则将其管理的原内存释放
p.unique() // 若p.use_count()为1,返回true;否则返回false
p.use_count() // 返回与p共享对象的智能指针数量;可能很慢,主要用于调试
p.reset () //若p是唯一指向其对象的shared_ptr,reset会释放此对象。
p.reset(q) //若传递了可选的参数内置指针q,会令P指向q,否则会将P置为空。
p.reset(q, d) //若还传递了参数d,将会调用d而不是delete 来释放q

实际上,我们在对std::shared_ptr的引用计数进行操作时,其成本是很高的,因为这个是原子操作。

与std::unique_ptr类似,std::shared_ptr也可以自定义析构器,且析构器的大小不会影响std::shared_ptr的大小,因为函数是分配在堆上的,方式上与std::unique_ptr差别也比较大的,代码示例如下:

//自定义析构器myDelete1 
auto myDelete1 = [](Widget* w){
    cout<<"myDelete1"<<endl;
};
 
//自定义析构器myDelete2
auto myDelete2 = [](Widget* w){
    cout<<"myDelete2"<<endl;
};
 
//pw1的自定义析构器为myDelete1
std::shared_ptr<Widget> pw1(new Widget,myDelete1);
 
//pw2的自定义析构器为myDelete2
std::shared_ptr<Widget> pw2(new Widget,myDelete2);
 
//虽然pw1与pw2的自定义析构函数不一样看,但都是std::shared_ptr<Widget>类型,可以相互转换
//而如果是std::unique_ptr则不行,因为其类型不同
pw1 = pw2;

3、shared_ptr缺点

使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr举例:

  • shared_ptr的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。
  • 引用计数的内存必须动态分配。虽然一点可以使用make_shared()来避免,但也存在一些情况下不能够使用make_shared()
  • 增加和减小引用计数必须是原子操作,性能会下降很多

4、shared_ptr相互引用会后果

shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。看下面的例子:

#include <iostream>
#include <memory>

class Parent;  // Parent类的前置声明

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    std::shared_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::shared_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());  // 1  parent.use_count() == 1
    std::shared_ptr<Child> child(new Child());  // 2     child.use_count() == 1
    parent->son = child;     // 3   child.use_count() == 2 and parent.use_count() == 1 
    child->father = parent;  // 4   child.use_count() == 2 and parent.use_count() == 2

    return 0;
}

/*
输出:
hello Parent
hello child
*/

很惊讶的发现,用了shared_ptr管理资源,没有调用 Parent 和 Child 的析构函数,表示资源最后还是没有释放!内存泄漏还是发生了。

原因分析:

  • 执行语句1,构造了一个共享指针parent
  • 执行语句2,构造了一个共享指针child
  • 执行语句3,child被赋值给parent的成员变量,其引用计数+1
  • 执行语句3,parent被赋值给child的成员变量,其引用计数+1
  • 此时parent及child的引用计数都为2,所以,如果想要parent的引用计数变为0,则child必须释放(从而析构掉内部管理资源father的共享指针),但是如果想要child引用计数变为0,必须要parent释放(从而析构掉内部管理资源son的共享指针
  • 这种情况就是个死循环

这时候,就引入了另一个共享指针 weak_ptr。

三、weak_ptr

weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

1、常用操作

weak_ptr<T> w;	// 空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(shared_ptr p);// 与p指向相同对象的weak_ptr, T必须能转换为p指向的类型
w = p;	// p可以是shared_ptr或者weak_ptr,赋值后w和p共享对象
w.reset();	// weak_ptr置为空
w.use_count();	// 与w共享对象的shared_ptr的计数
w.expired();	// w.use_count()为0则返回true,否则返回false
w.lock();	// w.expired()为true,返回空的shared_ptr;否则返回指向w的shared_ptr

这里再提供两种安全获取std::shared_ptr状态的方法:

auto spw = std::make_shared<Widget>();
 
//spw的引用计数并不会增大
std::weak_ptr wpw(spw);
 
//第一种
//返回std::weak_ptr指向的std::shared_ptr,若wpw为空,则wpw失效悬空
//若wpw不为空,将引用计数+1,保证访问期间不会析构
std::shared_ptr<Widget>spw1 = wpw.lock(); 
 
//第二种
//若wpw失效,将抛出异常,否则将引用计数+1,保证访问期间不会析构
std::shared_ptr<Widget> spw3(wpw);

注意:对于std::shared_ptr的控制块,只有当其std::weak_ptr的引用次数为0时才会被释放。

2、解决std::shared_ptr相互引用的问题

#include <iostream>
#include <memory>

class Parent;  // Parent类的前置声明

class Child {
public:
    Child() { std::cout << "hello child" << std::endl; }
    ~Child() { std::cout << "bye child" << std::endl; }

    // 测试函数
    void testWork()
    {
        std::cout << "testWork()" << std::endl;
    }

    std::weak_ptr<Parent> father;
};

class Parent {
public:
    Parent() { std::cout << "hello Parent" << std::endl; }
    ~Parent() { std::cout << "bye parent" << std::endl; }

    std::weak_ptr<Child> son;
};

void testParentAndChild() {

}

int main() {
    std::shared_ptr<Parent> parent(new Parent());
    std::shared_ptr<Child> child(new Child());
    parent->son = child;
    child->father = parent;
    std::cout << "parent_ref:" << parent.use_count() << std::endl;
    std::cout << "child_ref:" << child.use_count() << std::endl;

    // 把std::weak_ptr类型转换成std::shared_ptr类型,以调用内部成员函数
    std::shared_ptr<Child> tmp = parent.get()->son.lock();
    tmp->testWork();
    std::cout << "tmp_ref:" << tmp.use_count() << std::endl;

    return 0;
}

/*
输出:
hello Parent
hello child
parent_ref:1
child_ref:1
testWork()
tmp_ref:2
bye child
bye parent
*/

由以上代码运行结果我们可以看到:

  • 所有的对象最后都能正常释放,不会存在上一个例子中的内存没有释放的问题;
  • parent 和 child 在 main 函数中退出前,引用计数均为 1,也就是说,对std::weak_ptr的相互引用,不会导致计数的增加。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Chiang木

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

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

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

打赏作者

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

抵扣说明:

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

余额充值