目录:点我
一、智能指针模板类
智能指针是行为类似于指针的类对象,但这种对象还有其他功能,下面介绍三个可帮助管理动态内存分配的智能指针模板:
void remodel(string & str) {
string * ps = new string(str);
...
str = *ps;
return;
}
对于上面的函数,每当调用时,该函数都分配堆中的内存,但从不收回,导致内存泄漏,只需在 return
前添加 delete ps
即可释放内存。但是通常会忘记这一步,而且如果存在其他异常情况导致没有执行它,也会导致内存泄漏:
void remodel(string & str) {
string * ps = new string(str);
...
if(weird_thing()) // 可能由于抛出异常导致未执行内存泄漏
throw exception();
str = *ps;
delete ps;
return;
}
遇到这种情况,就期望指针 ps 是一个对象,这样当它过期时就可以使用析构函数删除指向的内存,为此 C++ 提供了智能指针模板,其中 auto_ptr
是 C++ 98 提供的方案,C++ 11 已将其摒弃,并提供了另外两种:unique_ptr, shared_ptr
,它们都定义了类似指针的对象,可以将 new 获得的地址赋给它,并且当智能指针过期时,这些内存将自动被释放。
要创建智能指针对象,必须包含头文件 memory ,然后使用通常的模板语法来实例化所需类型的指针:
template<class X> class auto_ptr {
public:
explicit auto_ptr(X* p = 0) throw(); // 必须包含这个构造函数
...
}
auto_ptr<double> pd(new double); // double类型的智能指针
auto_ptr<string> pd(new string); // string类型
unique_ptr<double> pdu(new double); // 相似的用法
shared_ptr<string> pss(new string); // 相似的用法
从写 remodel 方法:
#include <memory>
void remodel(std::string & str) {
std::auto_ptr<std::string> ps(new std::string(str));
...
if(weird_thing()) // 可能由于抛出异常导致未执行内存泄漏
throw exception();
str = *ps;
return;
}
注意,智能指针模板位于名称空间 std 中,下面是一个使用例子:
class Report {
private:
string str;
public:
Report(const string s) : str(s) {cout << "created\n";}
~Report() {cout << "deleted\n";}
void comment() const {cout << str << endl;}
};
int main() {
{
auto_ptr<Report> ps(new Report("auto_ptr"));
ps->comment();
}
{
shared_ptr<Report> ps(new Report("shared_ptr"));
ps->comment();
}
{
unique_ptr<Report> ps(new Report("unique_ptr"));
ps->comment();
}
}
// outputs
created
auto_ptr
deleted
created
shared_ptr
deleted
created
unique_ptr
deleted
所有智能指针都有一个 explicit 构造函数,它将指针作为参数,因此不需要自动将指针转换为智能指针对象。
需要避免下面这种情况:
string v("example");
shared_ptr<string> p(&v);
当 p 过期时,程序将把 delete 运算符用于非堆栈内存,这是错误的。
相同智能指针之间赋值也需要注意:
auto_ptr<string> pa(new string("example")), pb;
pb = pa;
这样做会导致两个指针指向同一个内存,然后当它们相继过期时会对其进行两次 delete ,解决方法有多种:
- 定义赋值运算符:使之成为深复制,即再创建一个副本,使两个指针指向不同的副本;
- 建立所有权概念:对于特定的对象,只有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。unique_ptr 和 auto_ptr 采用此策略,但前者更严格;
- 创建智能更高的指针,根据引用特定对象的智能指针数,这称为引用计数(reference counting)。shared_ptr 采用此策略。
下面举一个不适用于 auto_ptr 的例子:
auto_ptr<string> p1(new string("example")), p2;
p2 = p1; // p1丧失所有权
cout << *p1; // 发生错误:Segmentation fault (core dumped)
由于 p1 丧失所有权,因此变成一个空指针,此时访问 p1 发生错误;如果采用 shared_ptr 则能正常工作,因为二者都指向同一块区域;如果采用 unique_ptr 则会在编译过程中察觉错误,因此这样更安全。
再来看下一种情况:
unique_ptr<string> demo(const string s) {
unique_ptr<string> tmp(new string(s));
return tmp;
}
unique_ptr<string> ps = demo("example");
此时由于 demo 函数返回一个临时的 unique_ptr ,然后 ps 接管了原本返回的 unique_ptr 所有的对象,而返回的 unique_ptr 被销毁,这没有问题,因为 ps 拥有了 string 对象的所有权。这样做的另一个好处是 demo 函数返回的临时 unique_ptr 很快被销毁,没有机会使用它访问无效数据,因此编译器允许这种赋值。
编译器是如何区分两种情况的呢?答案是判断右值:
unique_ptr<string> p1(new string("example"));
unique_ptr<string> p2;
p2 = p1; // not allow
p2 = unique_ptr<string>(new string("example")); // allow
也就是说,如果源 unique_ptr 是一个临时右值,编译器允许赋值,反之不行。因此 unique_ptr 优于 auto_ptr ,因为后者将允许这两种赋值。如果一定要使用第一种方式赋值,可以采用 std::move()
,该函数类似于 demo ,将返回一个 unique_ptr 对象:
unique_ptr<string> p1(new string("example"));
unique_ptr<string> p2;
p2 = std::move(p1); // allow
unique_ptr 优于 auto_ptr 的另一个原因是 unique_ptr 可以用于数组,而 auto_ptr 不能。因此 unique_ptr 可与 new [] 配套使用,而 auto_ptr 只能与 new 配套使用。
根据不同智能指针的特点,若有多个智能指针同时指向同一个对象,则使用 shared_ptr ,否则建议使用 unique_ptr 。
二、泛型编程
STL 是一种泛型编程,面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但理念决然不同。泛型编程旨在编写独立于数据类型的代码。
1. 迭代器
顾名思义,迭代器就是用于迭代获取 STL 中不同容器的方式:
// 数组迭代访问,相应元素的引用就是迭代器
for(int i = 0; i < n; ++i)
return &arr[i];
// 链表迭代访问,start实际上就是迭代器
for(start = head; start != nullptr; start = start->next)
return start;
从细节上看,二者的实现存在差异;但从功能上看,二者是相同的。因此泛型编程注重的就是算法的功能,而不关注具体实现,对于相同的功能,要求给出一个相同的函数接口和相同的使用方法,因此迭代器需要具备以下特征:
- 应能够对迭代器执行解除引用的操作,以便能够访问它引用的值。
- 应能够将一个迭代器赋给另一个;
- 应能够将一个迭代器与另一个进行比较,判断是否相等;
- 应能够使用迭代器遍历容器中的所有元素;
这样对于查找函数来说,可以如下编写:
typedef double * iterator;
iterator find_arr(iterator ar, int n, const double & val) {
for(int i = 0; i < n; ++i, ++ar)
if(*ar == val)
return ar;
return nullptr;
}
还可以使用两个指针标记容器的起始与结束:
typedef double * iterator;
iterator find_arr(iterator begin, iterator end, const double & val) {
iterator ar;
for(ar = begin; ar != end; ++ar)
if(*ar == val)
return ar;
return nullptr;
}
此处只给出了指针的迭代器版本,对于不同的容器,所使用的迭代器不一定是指针,也可能是对象,但必须要满足泛型编程的准则。
2. 迭代器类型
不同算法对迭代器要求不同,例如查找算法需要定义 ++ 运算符,以便能够遍历整个容器,但它要求只能读取数据而不能修改。而排序算法要求能随机访问,以便能交换两个不相邻的元素。STL 定义了 5 种迭代器:
- 输入迭代器:针对程序的角度,从容其中获取数据称为输入,因此输入迭代器可被程序用来读取容器中的信息,支持 ++ 运算符。但是对于两次迭代器遍历,它们的顺序可能会发生改变;输入迭代器被递增后,也不能保证其先前的值仍然可以被解除引用。该算法应当是单同行的,不依赖于前一次遍历时的迭代器值,也不依赖于本次遍历中前面的迭代器值。
- 输出迭代器:用于将信息从程序传输给容器的迭代器,允许引用让程序能够修改容器值,而不能读取。也就是说,对于单同行只读算法,可以使用输入迭代器;对于单同行只写算法,可以使用输出迭代器。
- 正向迭代器:与前两种相似,只能使用 ++ 运算符来遍历容器,但是它总是按照相同的顺序遍历值。另外,将正向迭代器递增后,仍然可以对前面的迭代器值解除引用,并可以得到相同的值。正向迭代器既可以读取数据,也可以修改数据:
int * p; // 可读写 const int * p; // 只读
- 双向迭代器:支持递增、递减两种运算符。
- 随机访问迭代器:
表达式 描述 a + n 指向 a 所指向的元素后的第 n 个元素 n + a 与 a + n 相同 a - n 指向 a 所指向的元素前的第 n 个元素 r += n 等价于 r = r + n r -= n 等价于 r = r - n a[n] 等价于 *(a + n) b - a 结果为这样的 n 值,即 b = a + n a < b 如果 b - a > 0 ,则为真 a > b 如果 b < a ,则为真 a >= b 如果 !(a < b) ,则为真 a <= b 如果 !(a > b) ,则为真
三、函数对象
很多 STL 算法都使用函数对象——也叫函数符(functor),它是可以以函数方式与 () 结合使用的任意对象。这包括函数名、指向函数的指针和重载了 () 运算符的类对象:
class Linear {
private:
double slope;
double y0;
public:
Linear(double sl_ = 1, double y_ = 0) : slope(sl_), y0(y_) {}
double operator()(double x) {return y0 + slope * x;} // 重载()
};
Linear f1;
Linear f2(2.5, 10.0);
double y1 = f1(12.5);
double y2 = f2(0.4);
这样重载 () 运算符将使得能够像函数那样使用 Linear 对象。对于下面这个函数存在一个问题,如何定义第三个参数?
for_each(books.begin(), books.end(); showReview); // 对books容器中每个元素使用showReview函数处理
首先考虑使用函数指针,但是函数指针需要制定参数类型,因此不可用。然后考虑模板:
template<class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f); // 一元函数
void ShowReview(const Review &); // ShowReview原型
此时,便完成了对第三个参数的定义。由上面的例子便可得到函数符的概念:
- 生成器(generator)是不用参数就可以调用的函数符;
- 一元函数(unary function)是用一个参数可以调用的函数符;
- 二元函数(binary function)是用两个参数可以调用的函数符;
- 返回 bool 值的一元函数是谓词(predicate);
- 返回 bool 值的二元函数是二元谓词(binary predicate)。