Chapter16.2 : 智能指针模板类
- 1.指针指针现实需求
- 1.1 普通指针弊端
- 1.2 智能指针现实需求
- 2. 智能指针使用范式
- 2.1 下面演绎三种智能指针用法
- 注意一个问题
- 3. 三种智能指针特点
- 3.1 为何摒弃 auto_ptr
- 3.2 选用 unique_ptr
- 3.3 选用 shared_ptr
- 4. 应该使用哪种智能指针
本章节我们主要讲解 智能模板类,并主要从下面几个方面入手:
- 智能指针出现背景和现实需求
- 智能指针 使用范式
- 智能指针种类以及该如何选型
1.指针指针现实需求
智能指针是行为类似于指针的类对象,但这种对象还有其他功能,它可以管理给指针分配的对象
1.1 普通指针弊端
先看下面函数
#include<string>
void remodel(std::string& str)
{
std::string* ps = new std::string(str);
....
std = ps;
return;
}
上面的代码呢可以已经发现了这样一个现象:
该函数分配在堆中的随想,从来不收回,这会导致内存泄漏,你可能也知道解决之道: 在 return 前添加下面语句,释放内存即可
delete ps
但是这仍然会有问题,情况下面的变体。
#include<string>
void remodel(std::string & str)
{
std::string *ps = new std::string(str);
...
if (weird_thing())
throw exception();
str = *ps;
delete ps;
return;
}
很显然,出现异常时,delete将不再执行,因此还是会导致内存泄漏。
1.2 智能指针现实需求
- 基于1.1章节,我们知道,你可能会在函数执行完毕,忘记 delete ps 或者在 delete ps 执行之前,代码发生异常。这两点都会无法释放内存,造成内存泄漏。
- 所以,我们需要有这样一种情况:函数栈在执行完毕后,指针ps 占的内存被释放,指针ps 指向的堆内存也会自动释放(这个可以通过析构函数释放)
- 但是 ps 只是一个常规指针,不是有析构函数的类对象,如果它是对象,那么可以在对象过期时,让它的析构函数删除指向的内存。
- 这就是 auto_ptr , unique_ptr和 shared_ptr 背后的思想所在。
- auto_ptr 是C++98 提供的解决方案,C++11 已经将摒弃,并提供了另外两种解决方案, 如果您的编译器不支持其他两种方案,那么 auto_ptr 是唯一的选择。
💚💚💚
下面来看一个使用智能指针的例子
很显然 auto_ptr 定义了类似指针的对象,可以将 new 获取(直接或间接)的地址赋给对象,当智能指针过期时,对象其析构函数 将使用 delete 来释放内存。
- 如果将 new返回的地址赋给这些对象,将无需记住稍后需要释放的内存。
- 智能指针在过期时,这些内存会自动释放。
2. 智能指针使用范式
- 要创建智能指针对象,必须包含头文件 memory, 下面是 auto_ptr 文件模板定义如下。
// 下面是伪代码
#include<memory>
#include<string>
template<class X>
class auto_ptr
{
public:
eplicit auto_ptr(X* p = 0);
}
void create_auto_ptr()
{
auto_ptr<double> pd(new double); // pd an auto_ptr to double
auto_ptr<string> ps(new string); // ps an auto_ptr string
}
- new double/ new string 是new 返回的指针,指向新分配的内存块,它是构造函数 auto_ptr 的参数,对应原型中形参 p 的实参。
因此第一节的例子就可以这样转换
💚 包含头文件 memory
💚 指向 string 的指针 替换为 指向 string 的智能指针对象
💚 删除 delete语句
#include<string>
#include<memory>
void remodel(std::string & str)
{
// std::string *ps = new std::string(str);
std::auto_ptr<str::string> ps (new std::string(str));
...
if (weird_thing())
throw exception();
str = *ps;
// delete ps; no longer needed
return;
}
2.1 下面演绎三种智能指针用法
#incdlue<iostream>
#include<string>
#include<memory>
class Report
{
private:
std::string str;
public:
Report(const std::string s):str(s)
{
std::cout<< "Objcet create \n";
}
~Report()
{
std::cout << "Object deleted\n";
}
void comment() const
{
std::cout <<str << std::endl;
}
}
int main()
{
{
// 将 new Report 对象地址 赋值给 auto_ptr 构造函数 实参p
std::auto_ptr<Report> ps(new Report("using auto_ptr"));
ps->comment(); // use ->invoke a member function
}
{
// 将 new Report 对象地址 赋值给 shared_ptr 构造函数 实参p
std::shared_ptr<Report> ps(new Report("using shared_ptr"));
ps->comment(); // use ->invoke a member function
}
{
// 将 new Report 对象地址 赋值给 unique_ptr 构造函数 实参p
std::unique_ptr<Report> ps(new Report("using unique_ptr"));
ps->comment(); // use ->invoke a member function
}
return 0;
}
// 打印结果
Objcet create
using auto_ptr
Object deleted
Objcet create
using shared_ptr
Object deleted
Objcet create
using unique_ptr
Object deleted
- 所有智能指针类都有一个 explicit 构造函数,该构造函数将指针作为参数,因此不需要自动将指针转换为 智能指针对象,更不要将指针转换成智能指针。如
shared_ptr<double> pd;
double *p_reg = new double;
pd = p_reg; // not allow (implicit conversion)
pd = shared_ptr<double>(p_reg); // allow (explicit conversion)
shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion)
shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
注意一个问题
下面这个问题,全部三种智能指针都应该避免
string vacation("I wandeed lonely as a child");
shared_ptr<string> pvac(&vacation); // error
// pvac 过期时,程序将把 delete 运算符用于非 堆内存,这是 错误的
3. 三种智能指针特点
3.1 为何摒弃 auto_ptr
先看下面语句
string str = new string("I wandeed lonely as a child");
string *ps = &str;
string* vocation;
vocation = ps; // 将指针赋值给另一个指针
delete ps;
delete vocation;
💚 上面这段语句会完成什么操作了 ?
- 这两个指针都指向一个string 对象,这是不能接受的,因为程序将视图删除同一个对象 两次,一次是 ps 过期时,另一次是 vocation过期时,要避免这种问题方法有很多 。
🧡 - 定义赋值运算符:使之执行深度赋值,这样两个指针指向不同的对象,其中的一个对象是另一个对象的副本。
- 建立所有权(ownership) 概念:对于特定对象,只能有一个指针拥有它,这样只有拥有对象的指针指针的析构函数才可以删除,然后,让赋值操作转让所有权,这就是 auto_ptr 和unique_ptr 的策略,但是 unique_ptr 的策略更严格
- 创建智能更高的指针,跟踪引用特定对象的智能指针,这被称为引用计数 (reference counting)。
🚀 下面我们来通过另一个例子详细说明:为何摒弃 auto_ptr。
#include<iostream>
#include<string>
#include<memory>
int main()
{
using namespace std;
auto_ptr<string> film[5]=
{
auto_ptr<string> (new string("Flowl Bals")),
auto_ptr<string> (new string("Duck walks")),
auto_ptr<string> (new string("Chicken run")),
auto_ptr<string> (new string("Turkey errors")),
auto_ptr<string> (new string("Goose Eggs"))
};
auto_ptr<string> pwin;
pwin = film[2]; // film[2] loses ownership(将对象的所有权转让给 pwin)
for(int i = 0;i<5;i++)
{
cout << *film[i] << endl;
}
cout<< "the winner is: " << *pwin << "\n";
return 0;
}
// 打印结果
Flowl Bals
Duck walks
Segmentation fault
很显然,这是由于错误的使用 auto_ptr 可能导致的问题(但是这种行为是不确定的,其行为可能随系统而定),这里的问题是由于下面的语句将 :所有权 从 films[2] 转让给 pwin
pwin = films[2] // films[2] loses ownership
这导致 films[2] 不再引用 该字符串,在 auto_ptr 放弃对象所有权后,你又通过 *films[2] 访问指向的对象,却发现这是一个空指针。
这便是 auto_ptr 令人讨厌的地方。
3.2 选用 unique_ptr
如果我们将 auto_ptr 修改成 unipue_ptr 了 ?
#include<iostream>
#include<string>
#include<memory>
int main()
{
using namespace std;
unique_ptr<string> film[5]=
{
unique_ptr<string> (new string("Flowl Bals")),
unique_ptr<string> (new string("Duck walks")),
unique_ptr<string> (new string("Chicken run")),
unique_ptr<string> (new string("Turkey errors")),
unique_ptr<string> (new string("Goose Eggs"))
};
unique_ptr<string> pwin;
pwin = film[2]; // film[2] loses ownership(将对象的所有权转让给 pwin)
for(int i = 0;i<5;i++)
{
cout << *film[i] << endl;
}
cout<< "the winner is: " << *pwin << "\n";
return 0;
}
好家伙,直接报错,都不能编译通过,
💚💚💚这便是 unique_str 与 auto_ptr 的区别
- 它避免了 films[2] 指针不再指向有效数据的问题,即悬挂指针问题。
3.3 选用 shared_ptr
#include<iostream>
#include<string>
#include<memory>
int main()
{
using namespace std;
shared_ptr<string> film[5]=
{
shared_ptr<string> (new string("Flowl Bals")),
shared_ptr<string> (new string("Duck walks")),
shared_ptr<string> (new string("Chicken run")),
shared_ptr<string> (new string("Turkey errors")),
shared_ptr<string> (new string("Goose Eggs"))
};
shared_ptr<string> pwin;
pwin = film[2]; // film[2] loses ownership(将对象的所有权转让给 pwin)
for(int i = 0;i<5;i++)
{
cout << *film[i] << endl;
}
cout<< "the winner is: " << *pwin << "\n";
return 0;
}
// 打印结果
Flowl Bals
Duck walks
Chicken run
Turkey errors
Goose Eggs
the winner is: Chicken run
这次 pwin和films[2] 指向同一个对象。
- 引用计数从 1 增加到 2
- 在程序结尾,后声明的 pwin先调用析构函数,该析构函数将引用计数降低到1
- 对 films[2] 调用析构函数时,引用计数降低到0 ,这个时候就可以释放以前分配的空间。
4. 应该使用哪种智能指针
- 如果程序使用多个指向同一个对象的指针,应选择 shared_ptr
- STL容器包含指针,很多STL算法都支持 复制和赋值操作,这些操作可用于 shared_ptr
- 如果您的编译器没有体用 shared_ptr 可以使用 Boost库提供的 shared_ptr
- 如果程序不需要多个指向同一个对象的指针,则可以使用 unique_ptr 。