浅谈C++11智能指针

微信搜索“编程笔记本”,获取更多信息
------------- codingbook2020 -------------

C++ 需要程序员手动管理内存,而内存的使用又离不开指针。申请的空间在函数结束时忘记释放,就会造成内存泄漏。而智能指针的作用就是管理指针,使用指针指针可以很大程度上避免内存泄漏的问题。

智能指针的本质是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会释放资源。智能指针的作用原理就是在函数结束时自动释放内存空间,而不需要程序员手动释放。

1. auto_ptr

auto_ptr 是 STL 的智能指针之一,由 C++98 引入,定义在头文件 <memory>中。出于安全考虑,C++11 已经弃用 auto_ptr,用更安全、更灵活的 unique_ptr 代替。auto_ptr 采用所有权模式,auto_ptr 对象通过初始化指向由 new 创建的动态内存,它是这块内存的拥有者,一块内存不能同时被分给两个拥有者。当 auto_ptr 对象生命周期结束时,其析构函数会将 auto_ptr 对象拥有的动态内存自动释放。

我们下来看一个示例:

auto_ptr<string> p1 = new string("I am a string");  // p1 拥有该字符串的所有权
auto_ptr<string> p2;
p2 = p1;    // 语句合法,此时字符串的所有权被 p2 剥夺

接下来我们可以正常地通过 p2 去访问字符串,但 p1 并没有被销毁,它仍然指向该字符串,但当我们试图通过 p1 去访问字符串就会报错,导致程序崩溃。

正是由于潜在这种内存崩溃的问题,C++11 已经弃用 auto_ptr。

2. unique_ptr

发现问题就要解决问题。unique_ptr 就是为了解决 auto_ptr 转移所有权带来的弊端而提出来的。unique_ptr 采用独占式拥有严格拥有的概念,确保同一时间内只有一个指针指向给定内存。unique_ptr 不共享它管理的对象,也无法复制到其他的unique_ptr ,只能移动 unique_ptr ,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个 unique_ptr ,并且原始的 unique_ptr 不再拥有该资源。

我们再来看一下上面的例子:

unique_ptr<string> p1 = new string("I am a string");  // p1 严格拥有该字符串的所有权
unique_ptr<string> p2;
p2 = p1;    // 语句不合法

正是由于编译器不允许上面示例中的赋值行为,才有效避免了 p1 不再指向有效数据的问题,因此 unique_ptr 比 auto_ptr 更安全。

当然了,这只是说编译器不允许对 unique_ptr 对象之间直接进行赋值,但我们还有另外一种方式,看下面的示例:

unique_ptr<string> p1 = new string("I am a string");  // p1 严格拥有该字符串的所有权
unique_ptr<string> p2;
// p2 = p1;    // 语句不合法
p2 = unique_ptr<string>(new string("I am a string"));    // 语句合法

这又是为什么呢?仔细分析一下就不难得出结论。unique_ptr<string>(new string("I am a string"))虽然是一个 unique_ptr 对象,但这个对象在此之前并没有被任何变量拥有,也就是说,这是个临时右值,既然它没有被任何变量拥有,那么我们可以首次将其“安置”某一变量,“安置”完成后,临时对象就会自动调用析构函数进行销毁,就不会留下悬挂的 unique_ptr 对象了。

既然不允许赋值,那我们前面说的可以转移资源的管理权限又要怎么实现呢?

原来,C++ 设计了一个标准库函数 std::move(),通过该函数我们能够实现unique_ptr 之间的移动。

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main() {
    unique_ptr<string> p1(new string("I am a string"));
    unique_ptr<string> p2;
    p2 = move(p1);

    cout << *p2 << endl;    // I am a string

    // cout << *p1 << endl;    // 错误,因为 p1 已经不再拥有该字符串的管理权
    // 但是 p1 仍然是个 unique_ptr 对象,我们可以重新为其赋值
    p1 = unique_ptr<string> (new string("I am also a string"));
    cout << *p1 << endl;    // I am also a string

    return 0;
}

/*
运行结果:
I am a string
I am also a string
*/

3. shared_ptr
shared_ptr 采用共享式拥有概念。多个智能指针可以指向相同的对象,该对象及其相关资源会在最后一个引用被销毁时释放。顾名思义,shared_ptr 指向的资源会被多个指针共享,它使用计数机制来记录资源被多少指针共享,通过成员函 use_count()来查看资源所有者的数量。通过调用 reset()函数,当前指针会释放资源的所有权,资源所有者数量减一。除了可以通过 new 来构造,还可以通过传入 auto_ptr,unique_ptr,weak_ptr 来构造。

#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main() {
    shared_ptr<string> p1(new string("I am a string"));
    shared_ptr<string> p2;
    p2 = p1;

    cout << *p1 << endl;    // I am a string
    cout << *p2 << endl;    // I am a string

    cout << p1.use_count() << endl;    // 2
    p1.reset();
    cout << p2.use_count() << endl;    // 1

    return 0;
}

/*
运行结果:
I am a string
I am a string
2
1
*/

下面介绍几个 shared_ptr 常用的成员函数。

use_count    // 返回当前资源的引用计数
unique       // 返回当前资源是否被独占(use_count=1)
swap         // 交换两个 shared_ptr 对象
reset        // 放弃当前资源的所有权,引起引用计数 -1
get          // 返回当前资源的指针(由于重载了()方法,因此 p 与 p.get() 等价)

4. weak_ptr

weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象。weak_ptr 设计的目的是为了配合 shared_ptr 工作,提供了对 shared_ptr 管理对象的一个访问手段。weak_ptr 可以从一个 shared_ptr 或另一个 weak_ptr 对象来构造,它的构造函数和析构函数不会引起引用计数的增加或减少。weak_ptr 和 shared_ptr 之间可以相互转换,shared_ptr 可以 直接赋值给 weak_ptr ,weak_ptr 通过成员函数 lock()来获得 shared_ptr 。

关于 weak_ptr 是怎么配合 share_ptr 工作的,我们通过后面的具体示例来说明。


既然智能指针这么牛皮,那使用智能指针应该不会引起内存泄漏了吧?
答案是:你想得美!

下面我们就来看一下使用智能指针仍然存在内存泄漏的示例:

#include <iostream>
#include <memory>

using namespace std;

class B;

class A {
public:
    void setp(shared_ptr<B> p) {
        ppb = p;
    }
    ~A() {
        cout << "A is destroyed" << endl;
    }
private:
    shared_ptr<B> ppb;
};

class B {
public:
    void setp(shared_ptr<A> p) {
        ppa = p;
    }
    ~B() {
        cout << "B is destroyed" << endl;
    }
private:
    shared_ptr<A> ppa;
};


void test() {
    shared_ptr<A> pa(new A);
    shared_ptr<B> pb(new B);

    pa->setp(pb);
    pb->setp(pa);

    cout << pa.use_count() << endl;    // 2
    cout << pb.use_count() << endl;    // 2
}

int main() {
    test();
    return 0;
}

上面的示例,在 test 函数中定义了两个智能指针对象,他们互相引用,从而导致这两个智能指针的引用计数都是 2 。当跳出 test 函数时,两个智能指针的引用计数各自减一,此时二者的引用计数都为 1 。

那这意味着什么呢?

智能指针的引用计数不为零,那么智能指针的析构函数就不会被调用,也就是说,在 test 函数中请求的资源就不会被释放。而 test 函数已经退出了,资源却没有被释放,这无疑是一种内存泄漏。

那这个问题该怎么解决呢?还记得上面我们讲过的 weak_ptr 吗?对了,该它登场了!

前面说到 weak_ptr 不会引起引用计数的增减,那我们将其中的一个 shared_ptr 改为 weak_ptr 来看看会怎么样。

#include <iostream>
#include <memory>

using namespace std;

class B;

class A {
public:
    void setp(shared_ptr<B> p) {
        ppb = p;
    }
    ~A() {
        cout << "A is destroyed" << endl;
    }
private:
    shared_ptr<B> ppb;
};

class B {
public:
    void setp(weak_ptr<A> p) {
        ppa = p;
    }
    ~B() {
        cout << "B is destroyed" << endl;
    }
private:
    weak_ptr<A> ppa;
};


void test() {
    shared_ptr<A> pa(new A);
    shared_ptr<B> pb(new B);

    pa->setp(pb);
    pb->setp(pa);

    cout << pa.use_count() << endl;    // 1
    cout << pb.use_count() << endl;    // 2
}

int main() {
    test();
    return 0;
}

/*
运行结果:
1
2
A is destroyed
B is destroyed
*/

上面的示例中,我们将类 B 中的私有智能指针改为 weak_ptr 。从结果中可以看到,pa 的引用计数为 1 ,pb 的引用计数为 2 。这是因为,将 shared_ptr 传递给 weak_ptr 并不会使得 shared_ptr 的引用计数增加。还可以看到,当 test 函数退出时,pa 的作用域结束,pa 的引用计数从 1 减为 0 ,所以 A 的析构函数被调用,当 A 的析构函数被调用后,pb 的引用计数会从 2 减为 1 ,而随着 test 函数的退出,pb 的作用域结束,pb 的引用计数又从 1 减为 0, 所以 B 的析构函数被调用。至此,资源被清理完成,未产生内存泄漏。

点击下方图片关注我,或微信搜索**“编程笔记本”**,获取更多信息。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值