C++动态内存与智能指针:shared_ptr 类 及 C++ primer 5 练习 12.1.1


 使用动态内存很容易出现问题。例如:有时忘记释放内存,会产生 内存泄露;有时在尚有指针引用内存的情况下我们就释放了它,这种情况下会产生 引用非法内存的指针

​ 为了更容易(同时也更安全)的使用动态内存,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 元素。

使用了动态生存期的资源的类

​ 程序使用动态内存出于以下三种原因之一:

  1. 程序不知道自己需要使用多少对象
  2. 程序不知道所需对象的准确类型
  3. 程序需要在多个对象间共享数据

这里,将使用第三种原因举例。

定义 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 的有点显然是需要的时候可以自动执行隐式转换。缺点是列表中如果有非法值则会出现错误。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值