文章目录
使用动态内存很容易出现问题。例如:有时忘记释放内存,会产生 内存泄露;有时在尚有指针引用内存的情况下我们就释放了它,这种情况下会产生 引用非法内存的指针。
为了更容易(同时也更安全)的使用动态内存,C++11标准库提供了两种智能指针,它负责自动释放所指向的对象。shared_ptr 允许多个指针指向同一个对象,unique_ptr 则“独占”所指向的对象。标准库还定义了一个名为 weak_ptr 的伴随类,它是一种弱引用,指向 shared_ptr 所管理的对象。这三种类型都定义在 memory 头文件中。
这里介绍 shared_ptr 类。
shared_ptr 类
智能指针也是模板类。所以我们创建一个智能指针时,必须提供指针指向的类型。
shared_ptr<string> p1; // shared_ptr,可以指向 string
shared_ptr<list<int> >p2; // shared_ptr,可以指向 list<int>
默认初始化的智能指针保存着一个空指针。
智能指针的使用方式和普通指针类似。解引用一个指针指针返回它指向的对象。同理,在条件判断中使用智能指针就是检测它是否为空。
// 如果 p1 不为空,检测它是否指向一个空 string
if(p1 && p1 -> empty())
*p1 = "hi"; // p1 为空,赋予一个新值
下面是 shared_ptr 与 unique_ptr 都支持的操作:
shared_ptr<T> sp; // 空智能指针,可以指向类型为 T 的对象
unique_ptr<T> up;
p; // 将 p 用作条件判断,若 p 指向一个对象,则为 true
*p; // 解引用 p,获得它指向的对象
p -> mem; // 等价于 (*p).mem
p.get(); // 返回 p 中保存的指针。注意:若智能指针释放了其对象,返回的指针所指向的对象也消失了
swap(p, q); // 交换 p 和 q 中的指针
p.swap(q);
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 共享对象的智能指针的数量;可能很慢,主要用于调试。
make_shared 函数
最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptr。
要使用 make_shared 函数,必须指定想要创建的对象的类型。
// 指向一个值为 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>();
类似顺序容器的 emplace 成员,make_shared 用其参数来构造给定类型的对象。所以给定的实参要能和类型的某个构造函数匹配。如果不传递任何参数,对象就会执行值初始化。
通常情况下我们用 auto 定义一个对象来保存 make_shared 的结果。
shared_ptr 的拷贝和赋值
当进行拷贝或者赋值时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同对象:
auto p = make_shared<int>(42); // p 指向的对象只有 p 一个引用者
auto q(p); // p 和 q 指向相同对象,此对象有两个引用者
我们可以认为每个 shared_ptr 都有一个关联计数器,通常称其为引用计数。无论何时我们拷贝一个 shared_ptr,计数器都会递增。如:用一个 shared_ptr 初始化另一个 shared_ptr,或将它作为参数传递给一个函数,或者是作为函数的返回值。 当 shared_ptr 被赋予新值,(该 shared_ptr 原对象被引用的次数)计数器就会递减。
当一个 shared_ptr 的计数器为 0,它就会自动释放自己所管理的对象。
auto r = make_shared<int> 42;
r = q; // q 指向的对象引用计数会递增
// r 原来指向的对象引用计数会递减
// 显然 r 原来指向的对象没有引用者了,会自动释放(即上面的 int 对象)
shared_ptr 自动销毁所管理的对象,……
当指向一个对象的最后一个 shared_ptr 被销毁时,shared_ptr 类会自动销毁此对象。它是通过一个特殊的成员函数——析构函数完成销毁工作的。类似构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。
析构函数一般用来释放对象所分配的资源。如:vector 的若干操作会分配内存来保存其元素。vector 的析构函数就负责销毁这些元素,并释放它们所占的内存。
可以知道,shared_ptr 的析构函数会递减它所指向的对象的引用计数。如果引用计数变为 0,shared_ptr 的析构函数就会销毁对象,并释放它占用的内存。
……,shared_ptr 还会自动释放相关联的内存
当动态对象不再被使用时,shared_ptr 类会自动释放动态对象,这一特性使得动态内存的使用变得容易安全。例如,我们现在有函数 factory,它返回一个 shared_ptr:
shared_ptr<Foo> factory(T arg) {
// 恰当处理 arg
return make_shared<Foo>(arg);
}
此函数返回一个 shared_ptr,所以我们可以确保它分配的对象会在恰当的时刻被释放。例如,下面的函数将 factory 返回的 shared_ptr 保存在局部变量中:
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用 p
} // p 离开了作用域,它所指向的内存会被自动释放,且所指向的对象也将被销毁释放内存。
可以看到,p 是 use_factory 的局部变量。当函数结束时,此局部变量将被销毁,当 p 销毁时,将先递减其引用计数并检测其是否为 0,在此函数中, p 是唯一引用 factory 返回的内存的对象。所以函数结束时,p 被销毁,p 指向的对象也会被销毁。
但是如果有其他的 shared_ptr 也指向这块内存,它就不会被释放掉:
shared_ptr<Foo> use_factory(T arg) {
shared_ptr<Foo> p = factory(arg);
// 使用 p
return p;
} // p 离开作用域,p 被销毁,但其所指对象不会被销毁
这个函数与上面的 use_factory 不同,当调用这个函数时,会将 p 拷贝(其引用计数递增),所以最后调用结束时(引用计数递减),p 的引用计数大于 0(创建时引用计数为 1,所以最后引用计数为 1),所以 p 指向的对象不会被销毁。
由于在最后一个 shared_ptr 销毁前内存都不会释放,保证 shared_ptr 在无用之后不再保留就非常重要了。
你将 shared_ptr 存放在一个容器中,然后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用 unique 删除那些不再需要的 shared_ptr 元素。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
这里,将使用第三种原因举例。
定义 StrBlob 类
最终,我们会将 Blob 实现为一个模板。这里我们先定义一个管理 string 的类 StrBlob。
在本例中,我们将使用 vector 来保存元素。但是我们不能在 Blob 对象内直接保存 vector,因为一个对象的成员在对象销毁时也会被销毁。所以,这里将 vector 保存在动态内存中(因为我们要实现的是多个对象共享相同的数据)。
为了实现数据共享,我们为每个 StrBlob 设置一个 shared_ptr 来管理动态分配的 vector。此 shared_ptr 的成员将会记录有多少个 StrBlob 共享相同的 vector,并在 vector 的最后一个使用者被销毁时释放 vector。
当前,我们将实现一个 vector 操作的小的子集。我们会修改访问元素的操作:在我们的类中,如果用户试图访问不存在的元素,这些操作会抛出一个异常。
我们的类有一个默认构造函数和一个构造函数,接受单一的 initializer_list<string> 类型参数。此构造函数可以接受一个初始化器的花括号列表。
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> i1);
size_type size() const { return data -> size(); }
bool empty() const { return data -> empty(); }
// 添加和删除元素
void push_back(const std::string &t) { data -> push_back(t); }
void pop_back();
// 元素访问
std::string& front();
std::string& back();
private:
std::shared_ptr<std::vector<std::string>> data;
// 如果 data[i] 不合法,抛出一个异常
void check(size_type i,const std::string &msg) const ;
};
在此类中,我们实现了 size、empty 和 push_back 成员。这些成员通过指向底层 vector 的 data 成员来完成它们的工作。
StrBlob 构造函数
两个构造函数都是用初始化列表来初始化其 data 成员,令它指向一个动态分配的 vector。默认构造函数分配一个空 vector:
StrBlob::StrBlob() : data(std::make_shared<std::vector<std::string>>()) {}
StrBlob::StrBlob(std::initializer_list<std::string> i1) :
data(std::make_shared<std::vector<std::string>>(i1)) {}
元素访问成员函数
pop_back、front 和 back 操作访问 vector 中的元素。这些操作在试图访问元素之前必须检查元素是否存在。由于做的检查操作是相同的,所以我们为 StrBlob 定义了一个名为 check 的 private 函数,它检查一个给定索引是否在合法范围内。除了索引,check 还接受一个 string,它会将此参数传递给异常处理程序,这个 string 描述错误内容。
void StrBlob::check(StrBlob::size_type i, const std::string &msg) const {
if(i >= data -> size()) {
throw std::out_of_range(msg);
}
}
pop_back 和成员访问函数首先调用 check。如果 check 成功,这些成员函数继续利用底层 vector 操作来完自己的工作:
std::string& StrBlob::front() {
check(0,"front on empty StrBlob");
return data -> front();
}
std::string& StrBlob::back() {
check(0,"back on empty StrBlob");
return data -> back();
}
void StrBlob::pop_back() {
check(0,"pop_back on empty StrBlob");
data -> pop_back();
}
front 和 back 应该对 const 进行重载,在对象是 const 的时候调用。
const std::string &StrBlob::front() const {
check(0,"front on empty StrBlob");
return data -> front();
}
const std::string &StrBlob::back() const {
check(0,"back on empty StrBlob");
return data -> back();
}
当然上述代码并没有在 class 中声明,应该在 class 中添加声明。
StrBlob 的拷贝、赋值和销毁
StrBlob 使用默认版本的拷贝、赋值和销毁成员函数。当拷贝、赋值或销毁一个 StrBlob 对象时,它的 shared_ptr 成员会被拷贝、赋值或销毁,当然会涉及到引用计数的问题。当最后一个指向 data 的 StrBlob 对象被销毁时,此 vector(data) 也会被销毁。
12.1.1 节练习
练习 12.1:很显然,当执行完 b1 = b2 这一句话后,b1 与 b2 便指向动态内存中的同一个 vector。故最后 b1 与 b2 都包含 4 个元素。
练习12.2:
const std::string &StrBlob::front() const {
check(0,"front on empty StrBlob");
return data -> front();
}
const std::string &StrBlob::back() const {
check(0,"back on empty StrBlob");
return data -> back();
}
练习12.3:不需要。一个 const 对象本身就要求不可写,可读。如果添加了 const 版本的 push_back 与 pop_back 便为一个 const 对象提供了写的手段,那么对象本身是 const 的就没意义了。
练习12.4:可以直接忽略,因为 size_type 本身就是 unsigned 的。
练习12.5:不编写 explicit 的有点显然是需要的时候可以自动执行隐式转换。缺点是列表中如果有非法值则会出现错误。