一、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_ptr
、unique_ptr
和 shared_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_ptr
、unique_ptr
、shared_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