C++ 学习笔记——十、标准模板库

目录:点我

一、智能指针模板类

智能指针是行为类似于指针的类对象,但这种对象还有其他功能,下面介绍三个可帮助管理动态内存分配的智能指针模板:

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 
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)。

你可能感兴趣的:(C/C++,c++,学习,开发语言)