《C++ Primer Plus》第16章:string类和标准模板库(3)

智能指针模板类

智能指针是行为类似于指针的类对象,但这种对象还有其他功能。本节介绍其他三个可帮助管理动态内存分配的智能指针模板。先来看看需要哪些功能以及这些功能是如何实现的。请看下面的函数:

void remodel(std::string & str){
	std::string * ps = new std::string(str);
	...
	str = *ps;
	return;
}

您可能发现了其中的缺陷。每当调用时,该函数都分配堆中的内存,但从不收回,从而导致内存泄漏。您可能也知道解决之道——只要别忘了在 return 语句前添加下面的语句,以释放分配的内存即可:

delete ps;

然而,但凡涉及“别忘了”的解决方法,很少是最佳的。因为您有时可能忘了,也有时可能虽然记住了,但是不经意见删除或注释掉了这些代码。即使确实没有忘记,也可能有问题。请看下面的变体:

void remodel(std::string & str) {
	std::string * ps = new std::string(str);
	...
	if (weird_thing()){
		throw exception();
	}
	str = *ps;
	delete ps;
	return;
}

当出现异常时,delete 将不被执行,因此也将导致内存泄漏。
可以按 14 章介绍的方式修复这种问题,但如果有更灵巧的解决方法就好了。来看一些需要些什么。当 remodel() 这样的函数终止(不管是正常终止,还是由于出现了异常而终止),本地变量都将从栈内存中删除——因此指针 ps 占据的内存将被释放。如果ps指向的内存也被释放,那该有多好啊。如果ps有一个析构函数,该析构函数将在 ps 过期时释放它指向的内存。因此,ps 的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是 auto_ptr、unique_ptr 和 shared_ptr 背后的思想。模板 auto_ptr 是 C++98 提供的解决方案,C++11 已将其摒弃,并提供了另外两种解决方案。然而,虽然 auto_ptr 被摒弃,但它已使用了多年;同时,如果您的编译器不支持其他两种解决方案,auto_ptr 将是唯一的选择。

使用智能指针

这三个智能指针模板(auto_ptr、unique_ptr 和 shared_ptr)都定义了类似指针的对象,可以将 new 获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用 delete 来释放内存。因此,如果将 new 返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存将自动释放。

要创建智能指针对象,必须包含头文件 memory,该文件含有模板定义。然后使用通常的模板语法来实例化所需的类型的指针。例如,模板 auto_ptr 包含如下构造函数:

template<class X> class auto_ptr {
public:
	explicit auto_ptr(X* p = 0) throw();
	...
}

本书前面说过,throw() 意味着构造函数不会引发异常;与 auto_ptr 一样,throw() 也被摒弃。因此,请求 X 类型的 auto_ptr 将获得一个指向 X 类型的 auto_ptr:

auto_ptr<double> pd(new double); // pd an autp_ptr to double
								 // (use in place of double *pd
autp_ptr<string> ps(new string);// ps an auto_ptr to string
								// (use in place of string * ps)

new double 是 new 返回的指针,指向新分配的内存块。它是构造函数 auto_ptr<double> 的参数,即对应于原型中形参 p 的实参。同样,new string 也是构造函数的实参。其他两种智能指针使用同样的语法:

unique_ptr<double> pdu(new double);	// pdu an unique_ptr to double
shared_ptr<string> pss(new string);	// pss a shared_ptr to string

因此,要转换 remodel() 函数,应按下面3个步骤进行:

  1. 包含头文件 memory;
  2. 将指向 string 的指针替换为指向 string 的智能指针对象;
  3. 删除 delete 语句。

下面是使用 auto_ptr 修改该函数的结果:

#include<memory>
void remodel(std::string & str) {
	std::auto_ptr<std::string> ps (new std::string(str) );
	...
	if (weird_thing())
		throw exception();
	str = *ps;
	// delete ps; NO LONGER NEEDED
	return;
}

注意到智能指针模板位于名称空间 std 中。

下面的程序是一个简单的程序,演示了如何使用全部三种智能指针。要编译该程序,您的编译器必须支持 C++11 新增的类 shared_ptr 和 unique_ptr。每个智能指针都放在一个代码块内,这样离开代码时,指针将过期。Report 类使用方法报告对象的创建和销毁。

// smrtptrs.cpp -- using three kinds of smart pointers
// requires support of C++11 shared_ptr and unique_ptr

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

class Report {
private:
	std::string str;
public:
	Report(const std::string s) : str(s) {
		std::cout << "Object created!\n";
	}
    ~Report() {
        std::cout << "Object deleted!\n";
    }
    void comment() const {
        std::cout << str << "\n";
    }
};

int main(){
    {
        std::auto_ptr<Report> ps(new Report("using auto_ptr"));
        ps->comment();
    }
    {
        std::shared_ptr<Report> ps (new Report("using shared_ptr"));
        ps->comment();
    }
    {
        std::unique_ptr<Report> ps (new Report("using unique_ptr"));
        ps->comment();
    }
    return 0;
}
Object created!
using auto_ptr
Object deleted!
Object created!
using shared_ptr
Object deleted!
Object created!
using unique_ptr
Object deleted!

所有智能指针类都有一个 explicit 构造函数,该构造函数将指针作为参数。因此不需要自动将指针转换为智能指针对象:

shared_ptr<double> pd;
double * p_reg = new double;
pd = p_reg;				// not allowed (implicit conversion)
pd = shared_ptr<double>(p_reg);	// allowed (explicit conversion)
shared_ptr<double> pshared = p_reg;	// not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg);	// allowed (explicit conversion)

由于智能指针模板类的定义方式,智能指针对象的很多方面都类似于常规指针。例如,如果 ps 是一个智能指针对象,则可以对它执行解除引用操作(* ps)、用它来访问结构成员(ps->puffIndex)、将它赋给指向相同类型的常规指针。还可以将智能指针对象赋给另一个同类型的智能指针对象,但将引起一个问题,这将在下一节进行讨论。

但在此之前,先说说对全部三种智能指针都应避免的一点:

string vacation("I wandered lonely as a Cloud.");
shared_ptr<string> pvac(&vacation);	// NO!

pvac 过期时,程序将把 delete 运算符用于非堆内存,这是错误的。
就上面的程序演示的情况而言,三种智能指针都能满足要求,但情况并非总是这样简单。

有关智能指针的注意事项

为何又三种智能指针呢?实际上有4种,但本书不讨论 weak_ptr。为何摒弃 auto_ptr 呢?

先来看看下面的赋值语句:

auto_ptr<string> ps (new string("I reigned lonely as cloud." ) );
auto_ptr<string> vocation;
vocation = ps;

上述赋值语句将完成什么工作呢?如果 ps 和 vocation 是常规指针,则两个指针将指向同一个 string 对象。这是不能接受的,因为程序将试图删除同一个对象两次——一次是 ps 过期时,另一次是 vocation 过期时。要避免这种问题,方法有多种。

  • 定义赋值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本。
  • 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的构造函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这成为引用计数(reference counting)。例如,赋值时,计数将加1,而指针过期时,计数将减1。仅当最后一个指针过期时,才调用 delete。这是 shared_ptr 采用的策略。

当然,同样的策略也适用于复制构造函数。

每种方法都有其用途。下面的程序是一个不适合使用 auto_ptr 的示例。

// fowl.cpp -- auto ptr a poor choice

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

int main(){
    using namespace std;
    auto_ptr<string> films[5] = {
        auto_ptr<string> (new string("Fowl Balls")),
        auto_ptr<string> (new string("Duck Walks")),
        auto_ptr<string> (new string("Chicken Runs")),
        auto_ptr<string> (new string("Turkey Errors")),
        auto_ptr<string> (new string("Goose Eggs"))
    };
    auto_ptr<string> pwin;
    pwin = films[2];    // films[2] loses ownership

    cout << "The nominees for best avian baseball film are\n";
    for (int i = 0; i < 5; i++){
        cout << *films[i] << endl;
    }
    cout << "The winner is " << *pwin << "!\n";
    cin.get();
    return 0;
}

下面是该程序的输出:

The nominees for best avian baseball film are
Fowl Balls
Duck Walks

输出表明,错误地使用 auto_ptr 可能导致问题(这种代码的行为是不确定的,其行为可能随系统而异)。这里的问题在于,下面的语句将所有权从 films[2] 转让给 pwin:

pwin = films[2];	// films[2] loses ownership

这导致 films[2] 不再引用该字符串。在 auto_ptr 放弃对象的所有权后,便可能使用它来访问该对象。当程序打印 films[2] 指向的字符串时,却发现这是一个空指针,这显然是个令人讨厌的意外。

如果在上面的程序中用 shared_ptr 代替 auto_ptr,则程序将正常运行,其输出如下:

The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Chicken Runs
Turkey Errors
Goose Eggs
The Winner is Chicken Runs!

差别在于程序的如下部分:

shared_ptr<string> pwin;
pwin = films[2];

这次 pwin 和 film[2] 指向同一个对象,而引用计数从1增加到2.在程序末尾,后声明的 pwin 首先调用其析构函数,该析构函数将引用计数降低到1.然后,shared_ptr 数组的成员被释放,对 filmsp[2] 调用析构函数时,将引用计数降低到0,并释放以前分配的空间。

因此使用 shared_ptr 时,上面的程序将运行正常;而使用 auto_ptr 时,该程序在运行阶段崩溃。如果使用 unique_ptr,结果将如何呢?与 auto_ptr 一样,unique_ptr 也采用所有权模型。但使用 unique_ptr 时,程序不会等到运行阶段崩溃,而在编译器因下述代码行出现错误:

pwin = films[2];

显然,该进一步探索 auto_ptr 和 unique_ptr 之间的差别。

unique_ptr 为何优于 auto_ptr

请看下面的语句:

auto_ptr<string> p1(new string("auto"));	// #1
auto_ptr<string> p2;			// #2
p2 = p1;

在语句 #3 中,p2 接管 string 对象的所有权后,p1 的所有权将被剥夺。前面说过,这是件好事,可防止 p1 和 p2 的析构函数试图删除同一个对象;但如果程序随后试图使用 p1,这将是坏事,因为 p1 不再指向有效的数据。

下面来看使用 unique_str 的情况:

unique_ptr<string> p3(new string("auto");	// #4
unique_ptr<string> p4;						// #5
p4 = p3;

编译器认为语句#6非法,避免了p3不再指向有效数据的问题。因此,unique_ptr 比 auto_ptr 更安全(编译阶段错误比潜在的程序崩溃更安全)。

但有时候,将一个智能指针赋给另一个并不会留下危险的悬挂指针。假设有如下函数定义:

unique_ptr<string> demo(const char *s) {
	unique_ptr<string> temp(new string(s));
	return temp;
}

并假设编写了如下代码:

unique_ptr<string> ps;
ps = demo("Uniquely special");

demo() 返回一个临时 unique_ptr,然后 ps 接管了原本返回的 unique_ptr 所有的对象,而返回的 unique_ptr 被销毁。这没有问题,因为 ps 拥有了 string 对象的所有权。但这里的另一个好处是,demo() 返回的临时 unique_ptr 很快被销毁,没有机会使用它来访问无效的数据。换句话说,没有理由禁止这种赋值。神奇的是,编译器确实允许这种赋值!

总之,程序试图将一个 unique_ptr 赋给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这样做;如果源 unique_ptr 将存在一段时间,编译器将禁止这样做:

using namespace std;
unique_ptr<string> pu1(new string "Hi ho!");
unique_ptr<string> pu2;
pu2 = pu1;		// #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string> (new string "Yo!");	// #2 allowed

语句#1将留下悬挂的 unique_ptr(pul),这可能导致危害。语句#2 不会留下悬挂的 unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权装让给 pu3 后就会被销毁。这种随情况而异的行为表明,unique_ptr 优于允许两种赋值的 auto_ptr。这也是禁止(只是一种建议,编译器并不禁止)在容器对象中使用 auto_ptr,但允许使用 unique_ptr 的原因。如果容器算法试图对包含 unique_ptr 的容器执行类似于语句#1的操作,将导致编译错误;如果算法试图执行类似于语句#2的操作,则不会有任何问题。而对于 auto_ptr,类似于语句#1的操作可能导致不确定的行为和神秘的崩溃。

当然,您可能确实想执行类似于语句#1的操作。仅当以非智能的方式使用遗弃的智能指针(如解除引用时),这种赋值才不安全。要安全地重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让您能够将一个 unique_ptr 赋给另一个。下面是一个使用前述 demo() 函数的例子,该函数返回一个 unique_ptr<string> 对象:

using namespace std;
unique_ptr<string> ps1, ps2;
ps1 = demo("Uniquely special");
ps2 = move(ps1);		// enable assignment
ps1 = demo(" and more");
cout << *ps2 << *ps1 << endl;

您可能会问,unique_ptr 如何能够区分安全和不安全的用法呢?答案是它使用了 C++11 新增的移动构造函数和右值引用,则将在第18章讨论。

相比于 auto_ptr,unique_ptr 还有另一个优先。它有一个可用于数组的变体。别忘了,必须将 delete 和 new 配对,将 delete[] 和 new[] 配对。模板 auto_ptr 使用 delete 而不是 delete[],因此只能与 new 一起使用,而不能与 new[] 一起使用。但 unique_ptr 有使用 new[] 和 delete[] 的版本:

std::unique_ptr<double[]> pda(new double[5]);	// will use delete[]

警告:使用 new 分配内存时,才能使用 auto_ptr 和 shared_ptr,使用 new[]分配内存时不能使用它们。

选择智能指针

应使用哪种智能指针呢?如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出警告)和 auto_ptr(行为不确定)。如果您的编译器没有提供 shared_ptr,可使用 Boost 库提供的 shared_ptr。

如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_pr 是不错的选择。这样,所有权将转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋给另一个的方法或算法(如sort() )。例如,可在程序中使用类似于下面的代码段,这里假设程序包含正确的 include 和 using 语句:

unique_ptr<int> make_int(int n) {
	return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> & pi) {
	cout << *pi << '  ';
}
int main(){
..
	vector<unique_ptr<int> > vp(size);
	for (int i = 0; i < vp.size(); i++){
		vp[i] = make_int( rand() % 1000); 		// copy temporary unique_ptr
	}
	vp.push_back(make_int(rand() %1000));		// ok because arg is temporary
	for_each(vp.begin(), vp.end(), show);		// use for_each()
...
}

其中的 push_back() 调用没有问题,因为它返回一个临时 unique_ptr,该 unique_ptr 被赋给 vp 中的一个 unique_ptr。另外,如果按值而不是按引用给show() 传递对象,for_each() 语句将非法,因为这将导致使用一个来自vp的非临时 unique_ptr 初始化 pi,而这是不允许的。前面说过,编译器将发现错误使用 unique_ptr 的企图。

在 unique_ptr 为右值时,可将其赋给 shared_ptr,这与将一个 unique_ptr 赋给另一个需要满足的条件相同。与前面一样,在下面的代码中,make_int() 的返回类型为 unique_ptr<int>:

unique_ptr<int> pup(make_int(rand() % 1000);		// ok
shared_ptr<int> spp(pup);			// not allowed, pup an lvalue
shared_ptr<int> spr(make_int(rand()%1000);		// ok

模板 shared_ptr 包含一个显式构造函数,可用于将右值 unique_ptr 转换为 shared_ptr。shared_ptr 将接管原来归 unique_ptr 所有的对象。

在满足 unique_ptr 要求的条件时,也可使用 auto_ptr,但 unique_ptr 是更好的选择。如果您的编译器没有提供 unique_ptr,可考虑使用 BOOST 库提供的 scoped_ptr,它与 unique_ptr 类似。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值