C++ 中的智能指针和 RAII 机制


一、RAII 机制

RAII(Resource Acquisition is Initialization)是由 C++ 之父 Bjarne Stroustrup 提出的,中文翻译为资源获取即初始化

资源的使用一般需要经历获取、使用和销毁三个步骤,其中资源的销毁往往是程序员容易遗忘的一个环节,所以我们就需要一种让资源自动销毁的方法。C++ 之父给出的解决方案就是 RAII 机制,它充分利用了 C++ 语言局部对象自动销毁的特性来控制资源的生命周期。

在下面的例子中,对于资源类 Resource 的使用,我们只需要显示地获取和使用即可,在 ResourceRAII 生命周期结束后会自动将 Resource 释放,而不需要我们显示调用 delete 方法来析构 Resource。

#include <iostream>

using namespace std;

class Resource {
public:
    Resource() {
        cout << "Resource()" << endl;
    };

    void useResource() {
        cout << "UseResource()" << endl;
    }

    virtual ~Resource() {
        cout << "~Resource()" << endl;
    }
};

class ResourceRAII {
private:
    Resource *m_resource;

public:
    ResourceRAII() {
        m_resource = new Resource();
    }

    Resource *getResource() {
        return m_resource;
    }

    ~ResourceRAII() {
        delete m_resource;
    }
};

int main() {
    ResourceRAII resourceRAII;
    auto resource = resourceRAII.getResource();
    resource->useResource();
}
atreus@MacBook-Pro % clang++ main.cpp -o main -w -std=c++11
atreus@MacBook-Pro % ./main                                
Resource()
UseResource()
~Resource()
atreus@MacBook-Pro % 

二、智能指针

智能指针是 RAII 机制的一个典型应用,使用时需要指定头文件 #include <memory>

智能指针是行为类似于指针的类对象,主要包括 auto_ptrunique_ptrshared_ptr。智能指针的思想就是给指针一个可以释放其指向的内存的析构函数,这样不管函数正常还是异常终止,指针及其指向的内存都能被正确回收。

#include <iostream>
#include <memory>

using namespace std;

class A {
private:
    int m_data;

public:
    explicit A(int data) : m_data(data) {}

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

int main() {
    {
        A *normal_p = new A(1); // 存在内存泄漏
    }
    {
        std::auto_ptr<A> auto_p(new A(2));
    }
    {
        std::unique_ptr<A> unique_p(new A(3));
    }
    {
        std::shared_ptr<A> shared_p(new A(4));
    }
    return 0;
}
atreus@MacBook-Pro % g++ -w main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
~2
~3
~4
atreus@MacBook-Pro % 

尽管 auto_ptrunique_ptrshared_ptr 均能实现内存的自动释放,但相互之间也有区别:

对于 auto_ptr,一个显而易见的问题就是如果将一个 auto_ptr 对象赋值给另一个 auto_ptr 对象,那么二者将会指向同一块内存,这将导致同一块内存释放两次。

为了解决 auto_ptr 的问题,shared_ptr 会跟踪引用特定对象的智能指针数,这被称为引用计数(类似于操作系统中的软链接),仅当最后一个 shared_ptr 过期时才会调用 delete 释放内存。

而 unique_ptr 则通过建立所有权的概念来应对这个问题,对于特定的对象,只能有一个 unique_ptr 拥有它,也只有拥有对象的 unique_ptr 才能删除该对象。unique_ptr 通过赋值操作转让所有权,但转移过程中的源 unique_ptr 必须为临时对象,如果源 unique_ptr 在转移后还会存在一段时间,编译器将禁止这种转移操作以避免对无效数据的访问。此外,unique_ptr 还有一个可用于数组的变体,因此在使用 new [] 分配内存时,只能使用 unique_ptr。


三、unique_ptr

unique_ptr 不共享它所指向的内容,它本身也无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何 STL 算法,只能移动 unique_ptr。这意味着在移动 unique_ptr 时内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 将不再拥有此资源

unique_ptr 实现了独享所有权的语义。一个非空的 unique_ptr 总是拥有它所指向的资源,拷贝一个 unique_ptr 将不被允许,因为如果你拷贝了一个 unique_ptr,那么拷贝结束后这两个 unique_ptr 都会指向相同的资源,它们都认为自己拥有这块资源,所以都会企图释放。因此 unique_ptr 是一个仅能移动(move only)的类型,当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用 unique_ptr 内部的原始指针的 delete 操作。

要想创建一个 unique_ptr,我们需要将一个 new 操作符返回的指针传递给 unique_ptr 的构造函数,或者通过 std::move 来实现。总之,unique_ptr 没有拷贝构造函数,不支持普通的拷贝和赋值操作

#include <iostream>
#include <memory>

using namespace std;

int main() {
    unique_ptr<int> p1(new int(1));
    cout << *p1 << endl; // 1

    unique_ptr<int> p2(p1); // error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)’
    unique_ptr<int> p2 = p1; // error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)’
    
    unique_ptr<int> p2 = move(p1);
    cout << *p2 << endl; // 1

    unique_ptr<int> p3(move(p2));
    cout << *p3 << endl; // 1
}

还可以通过 reset() 方法替换智能指针的管理对象,reset() 会释放智能指针原来指向的内存并为智能指针赋新值。

#include <iostream>
#include <memory>

using namespace std;

class A {
public:
    int m_data;

    A(int data) : m_data(data) {
        cout << "A(" << data << ")" << endl;
    }

    ~A() {
        cout << "~A(" << m_data << ")" << endl;
    }
};

int main() {
    unique_ptr<A> p(new A(0));
    cout << "----------" << endl;
    p.reset(new A(1));
    cout << "----------" << endl;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
A(0)
----------
A(1)
~A(0)
----------
~A(1)
atreus@MacBook-Pro % 

除了使用 std::move 显示转让所有权,从函数中返回时,所有权会会自动从一个局部变量中转让到调用函数中,因此可以通过函数返回 unique_ptr。

#include <iostream>
#include <memory>

using namespace std;

unique_ptr<int> clone(int x)
{
    unique_ptr<int> p(new int(x));
    return p; // 返回unique_ptr
}

int main() {
    unique_ptr<int> ret = clone(1);
    cout << *ret << endl; // 1
}

而当 unique_ptr 做函数参数时,如果只是简单传参,会导致形参和实参同时指向同一块内存,因此可以通过传引用或者 std::move 来实现 unique_ptr 作函数参数,但也要注意 std::move 会移除实参的所有权。

#include <iostream>
#include <memory>

using namespace std;

void show_ref(unique_ptr<int> &p) {
    cout << *p << endl;
}

void show_move(unique_ptr<int> p) {
    cout << *p << endl;
}

int main() {
    unique_ptr<int> p(new int(1));
    show_ref(p);
    show_move(move(p));
}

此外,标准库还提供了一个可以管理动态数组的 unique_ptr 版本 unique_ptr<类型[]>

#include <memory>
#include <iostream>

using namespace std;

int main() {
    unique_ptr<int[]> parr(new int[3] {1, 2, 3});
    cout << parr[0] << " " << parr[1] << " " << parr[2] << " " << endl;
}

四、智能指针与 pimpl 惯用法结合

智能指针的一个典型应用就是与 pimpl 惯用法结合。

pImpl(Pointer to implementation)是一种 C++ 编程技巧,它将类的实现细节从对象表示中移除,放到一个分离的类中,并以一个不透明的指针进行访问

举例来说,假如我们制作了一个库,库对应的头文件中有以下类定义:

class A {
public:
    void func();

private:
    int m_id;
    string m_password;
    string m_name;
};

实际上,我们仅为库的使用者提供成员函数就完全足够了,大量的成员变量很可能会暴露过多的实现细节。我们可以通过 pimpl 惯用法来改进这个类,将想要隐藏的成员变量都放到 m_pimpl 中。同时由于 m_pimpl 的生存周期与对象保持一致,因此可以通过智能指针实现内存管理

class Impl {
public:
    int m_id;
    string m_password;
    string m_name;
};

class A {
public:
    A() {
        m_pimpl.reset(new Impl);
    }

public:
    void func();

private:
    unique_ptr<Impl> m_pimpl;
};

参考:

https://zhuanlan.zhihu.com/p/34660259
https://www.cnblogs.com/DswCnblog/p/5628195.html

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值