Chapter 16 string类和标准模板库

本章内容包括:

  • 标准C++string类
  • 标准auto_ptr、unique_ptr和shared_ptr
  • 标准模板库STL
  • 容器类
  • 迭代器
  • 函数对象
  • STL算法
  • 模板initializer_list

16.1 string类

要使用类,关键在于知道它的公有接口。

16.1 构造字符串

构造函数隐藏了这样一个事实:string实际上是模板具体化basic_string<char>的一个typedef,同时省略了内存管理相关的参数。
size_type是一个依赖于实现的整形,是在头文件string中定义的。
string类将string::npos定义为字符串的最大长度,通常为unsigned int的最大值。
下面的表格使用NBTS(null-terminated string)来表示以空字符结束的字符串——传统的c字符串。
string类的构造函数

构造函数 描述
string(const char * s) 将对象初始化为s指向的NBTS
string(size_type n, char c) 创建一个包含n个元素的string对象,其中的每个元素都被初始化为字符c
string(const string & str) 将一个string对象初始化为string对象str(复制构造函数)
string() 创建一个默认的string对象,长度为0(默认构造函数)
string(const char * s, size_type n) 将string对象初始化为s指向的NBTS的前n个字符,即使超过了NBTS结尾
tmplate<class Iter>
string(Iter begin, Iter end)
将string对象初始化为区间[begin,end)内的字符,其中begin和end的行为就像指针,用于指定位置,范围包括begin在内,但不包括end
string(const string & str, string size_type
pos = 0, size_type n = npos)
将一个string对象初始化为str对象从位置pos开始到结尾的字符,或者从位置pos
开始的n个字符
string(string && str)noexcept 这是C++11新增的,它将一个string对象初始化为string对象str,并可能修改str
(移动构造函数)
string(initializer_list<char>il) 这是C++11新增的,他将一个string对象初始化为初始化列表il的字符

1.C++11新增的构造函数
string(string && str)类似复制构造函数,新创建的string为str的副本,与复制构造函数不同的是,该构造函数不保证str为const,可以修改str。
构造函数string(initializer_list<char>il)使我们可以将初始化列表语法用于string类。

16.1.2 string类输入

C-风格字符串,有三种方式: char info[100];

  • cin >> info; // read a word
  • cin.getline(info, 100) // read a line, discard \n
  • cin.get(info, 100) // read a line, leave \n in queue

对于string对象,有两种方式: string stuff;

  • cin >> stuff; // read a word
  • getline(cin, stuff); // read a line, discard \n

getline将自动调整目标string对象的大小,使之刚好可以存储输入的字符。
文件输入string对象:
string版本的getline()函数从输入中读取字符,并将其存储到目标string中,知道发生下列三种情况之一:

  • 到达文件尾,在该情况下,输入流的eofbit将被设置,这意味着fail()和eof()都将返回true;
  • 遇到分界字符(默认为\n),在这种情况下,将把分解字符从输入流删除,但不存储它;
  • 读取的字符达到允许最大值(string::npos和可供分配的内存字节数中较小的一个),在这种情况下,将设置输入流的failbit,这意味着方法fail()将返回true。

指定分界字符后,\n将被是为常规字符。

16.1.3 使用字符串

size()和length()的成员函数都返回字符串中的字符数。
重载的find()方法

方法原型 描述
size_type find(const string&str,
size_type pos = 0)const
从字符串的pos位置开始,查找子字符串str。如果找到,则返回该子字符串首次出现时
其首字符的索引;否则,返回string::pos
size_type find(const char * s,
size_type pos = 0)const
从字符串的pos位置开始,查找子字符串s。如果找到,则返回该子字符串首次出现时
其首字符的索引;否则,返回string::pos
size_type find(const char * s,
size_type pos = 0, size_type n)
从字符串的pos位置开始,查找s的前n个字符组成的子字符串。如果找到,则返回该
子字符串首次出现时其首字符的索引;否则,返回string::pos
size_type find(char ch,
size_type pos = 0)
从字符串的pos位置开始,查找子字符ch。如果找到,则返回该字符首次出现的位置;否
则,返回string::pos

除了以上的库方法,string库还提供了方法:rfind()、find_first_of()、find_last_of()、find_first_not_of()和find_last_not_of(),这几个函数的重载特征标都与find()方法相同。rfind(): 查找字符串或字符最后一次出现的位置;
find_first_of(): 在字符串中查找参数任何一个字符首次出现的位置;
find_last_of(): 在字符串中查找参数任何一个字符最后出现的位置;
find_first_not_of(): 在字符串中查找第一个不包含参数中的字符;
find_last_not_of(): PS: 后面补

16.1.4 string提供的其他功能

标准容量,为了方便string对象在结尾添加字符设计的,考虑了效率的因素,通常分配一个内存块,该内存块>=string对象所需的尺寸。
字符串不断增大,超过了内存块的大小时,程序将给对象重新分配一个大小为原来两倍的内存块。使用的最小容量为15个字符,比标准容量少1.
.c_str()方法可以返回一个C-风格字符串的指针,指针指向的内容与调用方法的对象的内容相同。

16.1.5 字符串种类

本节将string类看作基于char类型的。string库是基于一个模板类的:

tempalte<class charT,class traits = char_traits<charT>,
		class Allocator = allocator<charT> >
basic_string{...};
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string;		// C++11
typedef basic_string<char32_t> u32string;		// C++11

traits类描述关于选定字符的特定情况,如如何对值进行比较等。
Allocator是一个管理内存分配的类,对于各种字符类型,都有预定义的allocator模板具体化。它们都是默认的,使用new和delete。

16.2 智能指针模板类

智能指针的行为类似于指针的类对象,但该对象还有其他功能。本节介绍了三个可帮助管理动态内存分配的智能指针模板。分别是:

  • auto_ptr // C++98 C++11已抛弃
  • unique_ptr // C++11
  • shared_ptr // C++11

16.2.1 使用智能指针

要使用智能指针,需要包含头文件memory,删除delete语句。

所有的智能指针都有一个explicit的构造函数,因此不能自动将指针转换为智能指针对象,需要显式转换。
三种智能指针都应该避免的一点是不能将构造函数用于非堆内存,例如:
string vacation("I wandered lonely as a cloud.");
shared_ptr<string>pavc(&vacation); // NO!

16.2.2 有关智能指针的注意事项

智能指针引起的问题,赋值语句让一个智能指针与另一个智能指针相等,会使程序删除一个对象两次。解决该问题:

  • 定义赋值运算符,使之执行深复制;
  • 建立所有权概念,对于特定对象,只能有一个指针拥有它,赋值操作转让所有权;auto_ptr和unique_ptr的策略
  • 创建更智能的指针,跟中引用特定对象的智能指针数。仅当最后一个指针过期时,调用delete,shared_ptr的策略。

16.2.3 unique_ptr为何优于auto_ptr

unique_str会指出赋值将留下悬挂指针的错误。
创建临时对象赋值时并不会留下悬挂指针,因此当源unique_ptr是临时右值时,unique_ptr将允许这种赋值。
如果非要使用危险赋值,可以使用C++的标准库函数move(),例如:

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

C++11通过新增的移动构造函数和右值引用来实现unique_ptr的功能。
unique_ptr可以使用new[]和delete[]分配内存,而shared_str和auto_ptr只能用new和delete分配内存。

16.2.4 选择智能指针

STL容器包含指针,且很多STL算法都支持复制和赋值操作,这些操作可以用于shared_ptr,不能用于unique_str。
如果函数使用new分配内存,并返回指向该内存的指针,将返回类型声明为unique_ptr是不错的选择。
当unique_str为右值时,可以将其赋给shared_ptr,这与unique_ptr赋给另一个需要满足的条件相同,即unique_ptr为临时右值。
BOOST库的scoped_str优于auto_ptr。

16.3 标准模板库

STL提供了一组表示容器、迭代器、函数对象和算法的模板。容器是一个与数组类似的单元,可以存储若干个值。
STL容器是同质的,即存储的值的类型相同;
算法是完成特定任务的处方;
迭代器是能够遍历容器的对象,与能够遍历数组的指针类似,是广义指针;
函数对象是类似于函数的对象,可以是类对象或函数指针。
STL使得可以构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。STL不是面向对象的编程,而是泛型编程。

16.3.1 模板类vector

计算机中,矢量vector对应数组。
要创建vector模板对象,可使用<type>表示法来指出要使用的类型。vector模板使用动态内存分配。分配器
与string类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器来管理内存,默认使用动态分配。

16.3.2 可对矢量执行的操作

所有的STL容器提供了一些基本方法:

  • size()——返回容器中的元素数目
  • swap()——交换两个容器的内容
  • begin()——返回一个指向容器中第一个元素的指针
  • end()——返回一个表示超过容器尾的迭代器

迭代器是一个广义指针,它可以是指针,也可以是一个可对其执行类似指针操作的对象。将指针广义化为迭代器,让STL能够为各种不同的容器类提供统一的接口。迭代器的类型是一个名为iterator的typedef,其作用域为整个类。

vector模板类也包含一些某些STL容器特有的方法:

  • push_back(); // 在队尾添加一个元素
  • erase(iterator,itetator); // 两个迭代器指名了要删除的区间
  • insert(iterator,iterator,iterator) // 第一个参数新元素插入的位置,第二和第三个元素定义了插入区间

16.3.3 对矢量可执行的操作

STL从更广泛的角度定义了非成员(non-member)函数来执行搜索、排序和随机排序的操作。优势是只需定义少量的非成员函数即可,缺点通用算法是没有特定算法的效率高。
3个有代表性的STL函数:

  • for_each(): 三个参数,前两个参数是定义容器区间的迭代器,第三个参数是指向函数的指针
  • random_shuffle(): 两个参数,都是定义函数区间的迭代器
  • sort(): 有两个版本,第一个接受两个定义区间的迭代器参数,必须为对象提供成员或非成员的operator<(),另一个版本三个参数,最后一个参数指向要使用的函数的指针。

排序方式有两种:

  • 全排序,如果a<b和a>b都不成立,则a和b必定相同;
  • 完整弱排序,可能相同,不能不相同。

16.3.4 基于范围的for循环

对于:
for_each(books.begin(), books.end(), ShowReview);
可以用范围循环替换成:
for (auto x : books) ShowReview(x);
基于范围的for循环可修改容器的内容,使用带引用参数的函数。

16.4 泛型编程

STL是一种泛型编程(generic programming),面向对象编程关注的是数据方面,而反省编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但他们的理念绝然不同。
泛型编程旨在编写独立于数据类型的代码。

16.4.1 为何使用迭代器

模板使得算法独立于存储的数据类型,而迭代器使算法独立于所使用的容器类型。

泛型编程旨在使用同一个find()函数来处理数组、链表或任何其他数据类型。即函数不仅独立于容器中的存储类型,而且独立于容器本身的数据结构。模板提供了存储在容器的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。

迭代器应具备的特征:

  • 应能够对迭代器执行解除引用的操作,以便能够访问它引用的值,p是一个迭代器,则应对*p进行定义。
  • 应能够将一个迭代器赋给另一个,即如果p和q都是迭代器,则应对表达式p=q进行定义。
  • 应能够将一个迭代器与另一个进行比较,看他们是否相等。
  • 应能够使用迭代器遍历容器中的所有元素,这可以通过为迭代器p定义++p和p++实现。

STL遵循上面的方法:

  • 每个容器类定义了相应的迭代器类型,对于其中某个类,迭代器可能是指针,对于另一个类,迭代器可能是对象。
  • 每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。
  • 每个容器类都有begin()方法和end()方法
  • 每个容器类都使用++操作,让迭代器从指向第一个元素和超尾位置的迭代器。

STL通过为每个类定义适当的迭代器,并以统一的风格设计类,能够对内部表示绝然不同的容器,编写相同的代码。
最好避免直接使用迭代器,而应尽可能使用STL函数(如for_each())来处理细节,或者使用基于范围的for循环。

16.4.2 迭代器类型

STL定义了五种迭代器,分别是:输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。
1.输入迭代器
”输入“是从程序的角度来说的,即读取容器的信息。输入迭代器可以访问容器中所有的值,通过++运算符来实现的。基于输入迭代器的任何算法都是单同行的,即不依赖于前一次遍历时迭代器的值,也不依赖本次遍历中前面的迭代器的值。单向迭代器,可以递增,但不能倒退
2.输出迭代器
”输出“只信息从程序传输至容器。输出迭代器与输入迭代器相似,只是解除引用,让程序可以修改容器值,而不能读取。
STL足够通用,其容器可以表示输出设备。
对于单同行、只读算法,可以使用输入迭代器,对于单同行只写算法,可以使用输出迭代器。
3.正向迭代器
正向迭代器只使用++运算符来遍历容器。所以他每次沿容器向前移动一个元素;与输入输出迭代器不同的是,他总是按相同的顺序遍历一系列值;将正向迭代器递增后,仍可以对前面的迭代器的值解除引用,并得到相同的值。这些特征使得多次通行算法称为可能。
正向迭代器既可以使能读取和修改数据,也可以只能读取数据:

  • int * pirw;
  • const int * pir

4.双向迭代器
双向迭代器具有正向迭代器的所有特征,同时支持两种递减运算符。

5.随机访问迭代器
有些算法(如标准排序和二分检索)要求能够直接跳到容器种的任何一个元素,这叫做随机访问,需要随机访问迭代器。随机访问迭代器具有双向迭代器的所有特性,同时增加了支持随机访问的操作。下表是随机访问增加的操作,其中,X表示随机迭代器类型,T表示被指向的类型,a和b都是迭代器值,n为整数,r为随机迭代器的变量或引用。
随机访问迭代器操作

表达式 描述
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), 则为真

a+n这样的表达式仅当a和a+n都位于容器区间内时才合法。

16.4.3 迭代器的层次结构

五种迭代器类型形成了一个层次结构,依次功能越来越多。
迭代器性能

迭代器功能 输入 输出 正向 双向 随机访问
解除引用读取
解除引用写入
固定和可重复排序
++i i++
--i i--
i[n]
i + n
i - n
i += n
i -= n

需要这么多迭代器的原因时编写算法尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。注意,各种迭代器的类型并不是确定的,只是一种概念性描述。每个容器类都定义了一个类级typedef名称——iterator。

16.4.4 概念、改进和模型

STL有若干个C++语言无法表达的特性,如迭代器种类。正向迭代器是一系列要求,而不是一种类型。所设计的迭代器类可以满足这种要求。STL算法可以使用任何满足其要求的迭代器实现。
STL文献使用概念(concept)来描述一系列的要求。因此存在输入迭代器概念、正向迭代器概念。
概念可以有类似继承的关系,例如双向迭代器继承了正向迭代器的功能。然而,不能将C++继承机制用于迭代器,因为双向迭代器是一种内置类型,不能从类派生出来。
从概念上看,他确实能够继承。使用改进(refinement)来表示这种概念上的继承。双向迭代器是对正向迭代器的一种改进。
概念的具体实现被称为模型。指向int的常规指针是一个随机迭代器模型,同时也是一个正向迭代器模型。
1.将指针用作迭代器
迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是STL算法的接口,而指针是迭代器,因此STL算法可以使用指针来对基于指针的非STL容器进行操作。
STL sort()函数接受指向容器第一个元素的迭代器和指向超尾的迭代器作为参数。
C++支持将超尾概念用于数组,使得可以将STL算法用于常规数组。
可以将STL算法用于自己设计的数组形式,只要提供适当的迭代器(可以是指针,也可以是对象)。

STL提供了一些预定义迭代器。copy()算法可以将数据从一个容器复制到另一个容器中,该算法是以迭代器方式实现的,它可以从一种容器到另一种容器进行复制,甚至可以在数组之间复制。
copy()算法有三个参数:分别是两个迭代器参数表示要复制的范围,第三个迭代器参数表示将第一个元素复制到的位置。
copy()函数将覆盖目标容器中已有的数据,同时必须保证目标容器足够大,能够容纳被复制的元素。
因此不能使用copy将数据放到空矢量中。
假设要将信息复制到显示器上,如果有一个表示输出流的迭代器,则能使用copy()。STL为这种迭代器提供了ostream_iterator模板。用STL的话说,改模板是输出迭代器概念的一个模型,它也是一个适配器(adapter)——一个类或函数,可以将一些其他接口转换为STL使用的接口。可以使用包含头文件iterator并使用声明来创建该迭代器,如下:

#include <iterator>
...
ostream_iterator<int, char> out_iter(cout, " ");

out_iter迭代器是一个接口,模板参数有两个,第一个是发送给输出流的数据类型,第二个模板参数指出了输出流使用的字符类型。构造函数参数有两个,第一个参数指出了要使用的输出流,第二个参数指出了输出流的每个数据项后显式的分隔符。
可以将copy()用于该迭代器,这意味着将容器的整个区间复制到输出流中,显式容器的内容。

iterator头文件还定义了一个istream_iterator的模板,使是输入流可用作迭代器接口。使用两个istream_iterator对象来定义copy()的输入范围:

copy (istream_itterator<int, char>(cin),
	  istream_iterator<int, char>(), dice.begin());

2.其他有用的迭代器
头文件iterator提供了其他专用的预定义迭代器类型:

  • reverse_iterator
    vector类的rbegin()的成员函数和rend()的成员函数,返回的是反向迭代器,前者返回指向超尾的,后者返回指向第一个元素的。
    **rbegin()和end()都返回超尾,但类型不同。
  • back_insert_iterator
    该迭代器将元素插入到容器尾部,只能用于允许尾部快速插入的容器,例如vector
  • front_insert_iterator
    该迭代器将元素插入到容器前端,只能用于在起始位置做时间固定插入的容器类型,如queue,vector不满足
  • insert_iterator
    该迭代器将元素插入到参数指定位置的前面,该迭代器没有这些限制。

如果可以在显式声明迭代器和使用STL函数处理内部问题之间选择,选择STL函数处理更好。
必须声明容器类型的原因是迭代器必须使用合适的容器方法,back_insert_iterator的构造函数将假设传递给它的类型有一个push_back的方法。
copy()是一个独立函数,没有调整容器大小的权限,但插入迭代器参数有这样的权限。

16.4.5 容器种类

STL具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。
以前的11个容器类型分别是:

  • deque
  • list
  • queue
  • priority_queue
  • stack
  • vector
  • map
  • multimap
  • set
  • multiset
  • bitset(不讨论,它是在比特级处理数据的容器)
    C++11新增的容器:
  • forward_list
  • unordered_map
  • unorderes_multimap
  • unordered_set
  • unordered_multiset
    同时,不将bitset视为容器。

1.容器概念
没有于基本容器概念对应的模型,但概念描述了所有容器类都通用的元素。它是一个概念化的抽象基类——概念化是因为容器类并不真正使用继承继续。
容器是存储其他对象的对象。
存储在容器中的对象必须是可复制构造和可赋值的才可以存储在容器中。
C++11改进了这些概念,添加了可复制插入和可移动插入。
基本容器不能保证元素都按特定的顺序存储,也不能保证元素的顺序不变,但对概念进行改进后,则可以增加这样的保证。
容器的通用特征总结如下,其中X表示容器类型,如vector;T表示存储在容器中的对象类型;a和b表示类型为X的值;r表示类型为X&的值,u表示类型为X的标识符, u是一个X对象。
一些基本的容器特征

表达式 返回类型 说明 复杂度
X::iterator 指向T的迭代器类型 满足正向迭代器要求的任何迭代器 编译时间
X::value_type T T的类型 编译时间
X u; 创建一个名为u的空容器 固定
X(); 创建一个匿名的空容器 固定
X u(a); 调用复制构造函数后u == a 线性
X u = a; 作用同X u(a) 线性
r = a; X& 调用赋值运算符后r == a 线性
(&a)->~X() void 对容器中的每个元素应用析构函数 线性
a.begin() 迭代器 返回指向容器第一个元素的迭代器 固定
a.end() 迭代器 返回超尾值迭代器 固定
a.size() 无符号整形 返回元素个数,a.end() - a.begin() 固定
a.swap(b) void 交换a和b的内容 固定
a == b 可转换为bool 如果a和b长度相同,且a中每个元素都等于(==为真)b中相应的元素,则为真 线性
a != b 可转换为bool 返回!(a == b) 线性

复杂度描述了执行操作所需的时间:

  • 编译时间,操作在编译时执行,执行时间为0;
  • 固定时间,操作发生在运行阶段,时间独立于对象中的元素数目
  • 线性时间,时间与元素数目成正比

复杂度要求是STL特征,其实现细节可以隐藏,但性能规格应公开,以便计算完成操作的计算成本。

2.C++11新增的容器要求
rv表示类型为X的非常量右值,如函数返回值
C++11新增的基本容器要求

表达式 返回类型 说明 复杂度
X u(rv) 调用移动构造函数后,u的值与rv的原始值相同 线性
X u = rv 作用同X u(rv)
a = rv X& 调用移动赋值运算符后,a的值与rv的原始值相同 线性
a.cbegin() const_iterator 返回指向容器第一个元素的const迭代器 固定
a.cend() const_iterator 返回超尾值const迭代器 固定

3.序列
可以通过添加要求来改进基本的容器概念。序列是一种重要的改进。
7种STL容器类型(deque、list、queue、priority_queue、stack、vector和C++11新增的forward_list)都是序列。
deque表示双端队列,序列增加了迭代器至少是正向迭代器这样的要求,保证了元素将按特定顺序排列,不会再两次迭代之间发生变化,array类也被归类到序列容器。
序列还要求其元素按严格的线性顺序排列,即存在第一个元素,最后一个元素,除第一个和最后一个元素外,每个元素前后都分别有一个元素。
序列中的元素有特定的顺序,因此可以执行诸如将值插入到特定位置、删除特定区间等操作。
下表列出了序列必须完成的操作,t表示类型为T的值,n表示整数,p、q、i和j表示迭代器
序列的要求

表达式 返回类型 说明
X a(n,t); 声明一个名为a的有n个t值组成的序列
X (n,t) 创建一个由n个t值组成的匿名序列
X a(i,j) 声明一个名为a的序列,并将其初始化为区间[i,j)的内容
X (i,j) 创建一个匿名序列,并将其初始化为区间[i,j)的内容
a.insert(p,t) 迭代器 将t插入到p的前面
a.insert(p,n,t) void 将n个t插入到p的前面
a.insert(p,i,j) void 将区间[i,j)插入到p的前面
a.erase(p) 迭代器 删除指向p的元素
a.erase(p,q) 迭代器 删除区间[p,q)中的元素
a.clear() void 等价于erase(begin(),end())

序列的可选要求

表达式 返回类型 含义 容器
a.front() T& *a.begin() vector、list、deque
a.back() T& *--a.end() vector、list、deque
a.push_front(t) void a.insert(a.begin(),t) list、deque
a.push_back(t) void a.insert(a.end(),t) vector、list、deque
a.pop_front(t) void a.erase(a.begin()) list、deque
a.pop_back(t) void a.erase(--a.end()) vector、list、deque
a[n] T& *(a.begin()+n) vector、deque
a.at(n) T& *(a.begin()+n) vector、deque

(1)vector
vector是数组的一种类表示,他提供了自动内存管理的功能,可以动态的改变vector对象的长度;提供了对元素的随机访问;在尾部添加删除元素时间是固定的,但在头部或中间插入删除元素的复杂度为线性时间;vector还是可反转容器。
vector模板类是最简单的序列类型,除非其他类型的优点能够更好的满足程序的需求,否则应默认使用这种类型。

(2)deque
deque模板类表示双端队列,(double-ended queue),其实现类似于vector容器,从deque对象的起始位置插入删除元素的时间是固定的。其随机访问的速度要比vector随机访问的速度慢一些。

(3)list
list模板类表示双向链表。list和vector的区别在于,list在链表中任意一位置插入删除的时间都是固定的。vector强调的是通过随机访问进行快速访问,list强调的是元素的快速插入和删除。
list是可反转容器,list不支持数组表示法和随机访问。list插入和删除元素不会移动已有的元素。

list模板类包含了链表专用的成员函数。通常不必担心Alloc模板参数,因为它有默认值。
list 成员函数

函数 说明
void merge(list<T,Alloc>&x) 将链表x与调用链表合并。两个链表必须已经排序。合并后的经过排序的链表保持在调用链表中,x为空。这个函数的复杂度为线性时间
void remove(const T & val) 从链表中删除val的所有实例,这个函数的复杂度为线性时间
void sort() 使用<运算符对链表进行排序;N个元素的复杂度为NlogN
void splice(iterator pos, list<T,Alloc>&x) 将链表x的内容插入到pos的前面,x将为空。这个函数的复杂度为固定时间
void unique() 将连续的相同元素压缩为单个元素,这个函数的复杂度为线性时间

(4)list工具箱
list有一个方便的工具箱。
sort()、merge()和unique()方法还各自拥有接受另一个参数的版本,该参数用于指定用来比较元素的函数。
remove()方法也有一个接受另一个参数的版本,该参数用来确定是否删除元素的函数。
这些参数都是谓词函数。
(5) forward_list
C++11新增的容器类,它实现了单链表,每个节点只连接到下一个节点,forward_list只需要单向迭代器,它不可反转,但更加简单紧凑、但功能也更少。
(6) queue
queue模板类是一个适配器类。queue模板让底层类(默认为deque)展示典型的队列接口。
queue的限制比deque多,不能随机访问队列元素,也不允许遍历队列。
queue的操作

方法 说明
bool empty()const 如果队列为空,则返回true;否则返回false
size_type size()const 返回队列中元素的数目
T&front() 返回指向队首元素的引用
T&back() 返回指向队尾元素的引用
void push(const T &x) 在队尾插入x
void pop() 删除队首元素

(7)priority_queue
在priority_queue中,最大的元素被移到队首。内部区别在于其默认底层类是vector。可以修改用于确定哪个元素放到队首的比较方式,方法提供了一个可选的构造参数:
priority_queue<int>pq1; // default version
priority_queue<int>pq2(greater<>); // use greater<int> to order
greater<>()是一个预定义函数对象。

(8)stack
与queue相似,stack也是一个适配器类,他给底层类(默认情况下为vector)提供了典型的栈接口。stack的操作

方法 说明
bool empty()const 如果栈为空,则返回true;否则返回false
size_type size()const 返回栈中元素的数目
T&top() 返回指向栈顶元素的引用
void push(const T &x) 在栈顶部插入x
void pop() 删除栈顶元素
(9)array
array并非STL容器,因为其长度是固定的,array没有定义调整容器大小的操作。可以将很多标准STL算法用于array对象,如copy()和for_each()

16.4.4 关联容器

关联容器是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。如值可以表示雇员信息,而键是唯一的员工编号。对于关联容器来说,表达式X::key_type指出了键的类型。
关联容器的优点在于,它提供了对元素的快速访问。与序列类似,关联容器也允许插入新元素,但指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便可以快速检索信息。
关联容器通常是使用某种树实现的。树的查找速度比链表快。
STL提供了四种关联容器:set、multiset、map和multimap。前两种在set文件定义,后两种在map文件中定义。
最简单的关联容器是set,其值类型与键相同,键是唯一的,意味着集合中不会有多个相同的键。对于set来说,值就是键。multiset类似于set,只是一个键可能对应多个值。
map中,值与键的类型不同,键是唯一的,每个键只对应一个值,multimap一个键可以对应多个值。
1.set示例
set是可反转的经过排序的容器。
set_union(A.begin(),A.end(),B.begin(),B.end(),迭代器参数)
set_intersection()和set_difference()是获得集合的交集和集合的差。

2.multimap示例
为了将信息结合在一起,使用pair<const keytype,datatype>来将键类型和值类型结合成一对。
equal_range()用键作为参数,返回两个迭代器,它们表示的区间与该键匹配。

16.4.5 无序关联容器

无序关联容器是基于数据结构哈希表的,目的是提高添加和删除元素的速度以及提高查找算法的效率。

16.5 函数对象

很多STL算法都使用函数对象——函数符。函数符是可以以函数方式结合使用的任意对象。包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数operator()()的类)。
for_each()的第三个参数可以是是常规函数,也可以是函数符,函数指针。如何声明第三个参数?STL使用模板解决了这个这个问题,函数的模板 class Function,f()将调用其重载的()运算符的对象。

16.5.1 函数符概念

函数符概念包括:

  • 生成器(generator)是不用参数就可以调用的函数符
  • 一元函数(unary function)是用一个参数可以调用的函数符
  • 二元函数(binary function)是用两个参数可以调用的函数符

概念有相应的改进版:

  • 返回bool值的一元函数是谓词(predicate)
  • 返回bool值的二元函数是二元谓词(binary predicate)
    一些STL函数需要谓词参数或二元谓词参数,例如sort()有一个版本,将二元谓词作为其第三个参数。list模板有一个谓词作为参数的remove_if()成员。
    函数适配器可以是函数满足不同的接口。

16.5.2 预定义的函数符

STL定义了多个基本函数符,它们执行诸如两个值相加等操作,提供这些函数对象是为了支持函数作为参数的STL函数,例如考虑函数transform()。它有两个版本:

  • transform(gr.begin(), gr.end(), result.begin(), Function f)
    这里的函数符f接受一个参数。
  • transform(gr.begin(), gr.end, m.begin(), result.begin(), Function f)
    这是的函数符f接受两个参数。

C++的头文件functional定义了多个模板类函数对象,代码没有创建构造的对象,而是使用构造函数构造了一个函数符。这使得将函数对象作为参数很方便:
transform(gr.begin(), gr.end, m.begin(), result.begin(), plus<double>())
运算符和相应的函数符

运算符 相应的函数符
+ plus
- minus
* multiplies
/ divides
% modulus
- negate
== equal_to
!= not_equal_to

|greater
<|less
=|greater_equal
<=|less_equal
&&|logical_and
|||logical_or
!||logical_not

老式C++实现使用函数符名times,而不是multiplies

16.5.3 自适应函数符和函数适配器

上表列出的预定义函数符都是自适应的。STL有五个相关的概念:

  • 自适应生成器
  • 自适应一元函数
  • 自适应二元函数
  • 自适应谓词
  • 自适应二元谓词

使函数称为自适应的原因,它携带了标识参数类型和返回类型的typedef成员。这些成员分别是:
result_type、first_argument_type、second_argument_type。
函数符自适应的意义在于:函数适配器对象可以使用函数对象,并认为存在这些typedef成员。
STL提供了使用这些工具的函数适配器类。binder1st和binder2nd类自动完成这一过程,这两个适配器类将自适应二元函数转换为自适应一元函数。
binder1st(f2, val) f1;
f2()是一个自适应二元函数对象,创建一个binder1st对象,该对象与一个被用于f2()的第一个参数与特定值val关联。
STL提供了函数bind1st(),以简化binder1st类的使用。

binder2nd类与binder1st类似,只是将常数赋给了第二个参数,而不是第一个参数。

16.6 算法

STL包含很多处理容器的非成员函数。这些非成员函数总体设计相同,使用迭代器来标识要处理的数据区间和结果的放置位置,有些函数还接受一个函数对象参数,并使用它来处理数据。
对于算法函数设计,有两个主要的通用部分:

  • 使用模板来提供泛型;
  • 使用迭代器来提供访问容器中数据的通用表示

**如果deque对象和vector对象的内容相同,并且排列顺序也相同,则它们是相等的。

16.6.1 算法组

STL将算法库分成4组:

  • 非修改式序列操作:对区间的每个元素进行操作,不修改容器的内容,例如find()和for_each()
  • 修改式序列操作:对区间的每个元素进行操作,修改容器的内容(值和值的排列顺序),例如transform()、random_shuffle()和copy()
  • 排序和相关操作:排序函数和其他各种函数,包括集合操作sort()
  • 通用数字计算:包括将区间的内容累积、计算两个容器的内部乘积、计算小计、计算相邻对象差的函数。

前三组在头文件algorithm中描述,第4组专用于数值数据,头文件为numeric。

16.6.2 算法的通用特征

从函数原型可知有关迭代器的假设。使用该函数时,应该使用该迭代器或高于该迭代器层次的迭代器。
算法按结果放置的位置进行分类:

  • 就地算法
  • 创建拷贝

有些算法有两个版本,复制版本的名称以_copy结尾,复制版本通常会多一个输出迭代器参数,且会返回一个迭代器,该迭代器指向复制的超尾位置。

算法还有一个常见的变体:根据谓词函数符应用于容器元素得到的结果来执行操作。
这些版本通常以_if结尾。

虽然文档可指出迭代器或函数符需求,但编译器不会对此进行检查。如果使用了错误的迭代器,编译器试图实例化模板时,将产生大量的错误信息。

16.6.3 STL和string类

string类不是STL的组成部分,但设计它时考虑了STL。
next_permutation()算法将区间内容转换为下一种排列方式,对于字符串,排列按照字母递增的顺序进行。

16.6.4 函数和容器方法

当STL函数和STL方法都可以时,方法通常是更好的选择。方法更适合于特定的容器,且可以使用模板类的内存管理工具。

16.6.5 使用STL

STL是一个库,其组成部分被设计成协同工作。STL组件是工具,但也是创建其他工具的基本部件。
使用STL时应尽可能减少要编写的代码。STL通用灵活的设计将节省大量工作。

16.7 其他库

16.7.1 vector、valarray、array

C++提供了三个数组模板:vector、valarray和array。这些类由不同的小组开发,用于不同的目的。
vector模板类时容器类和算法系统的一部分,支持面向容器的操作,如排序、插入、重新排列、搜索;valarray类模板是面向数值计算的,不是STL的一部分;array是为代替内置数组而设计的,它通过更好、更安全的接口,让数组更紧凑和高效。
执行多步计算时,valarray类的通用性低,但对数学运算的表示方式更清晰。
sort可以用于valarray数组,如下:
sort(begin(valarray),end(valarray));

valarray的slice类。slice类对象可用作数组索引。
slice对象被初始化为三个整数值(起始索引,索引数,跨距)

16.7.2 模板 initializer_list(C++11)

初始化列表语法将STL容器初始化为一系列值。
std::vector<int> vi{10}将调用std::vector<int> vi({10});
类要用于处理长度不同的列表,才应提供接受intializer_list作为参数的构造函数。

16.7.3 使用initializer_list

需要包含头文件initializer_list,该模板类包含成员函数begin()和end(),还包括成员函数size()。
initializer_list的迭代器类型为const,因此不能使用解除引用修改initializer_list中的值

16.8 复习题

1.考虑下面的类声明:

class RQ1
{
private:
	char * st;
public:
	RQ1() { st = new char[1]; strcpy(st,"");}
	RQ1(const char * s)
	{ st = new char[strlen(s) + 1]; strcpy(st, s); }
	RQ1(const RQ1 & rq)
	{ st = new char[strlen(rq.st) + 1]; strcpy(st, rq.st); }
	~RQ1() {delete [] st};
	RQ & operator=(const RQ & rq);
	// more stuff
};

将它转换为使用string对象的声明。哪些方法不需要再显式定义?
复制构造函数、析构函数、赋值函数重载,string对象自己提供了内存管理的功能
2.再易于使用方面,指出string对象至少两个优于C-风格字符串的地方。
string对象进行比较比C-风格字符串方便;string可以字符串所需空间进行动态分配,节约存储空间。
3.编写一个函数,用string对象作为参数,将string对象转换为全部大写。

char ch toLower(char ch) { return tolower(ch); }
string & ToLower(string & s)
{
	for_each(s.begin(), s.end(), toLower);
}

4.从概念或者语法上说,下面哪个不是正确使用auto_ptr的方法(假设已经包含了所需的头文件)?auto_ptr<int>pia(new int[20]);
auto_ptr<string>(new string);
int rigue = 7;
auto_ptr<int>pr(& rigue);
auto_ptr dbl(new double);
第一个错误,auto_ptr不允许使用new[],最后一个错误,没有指名只能指针的类型。**第二个错误,没有只能指针命名,第三个错误,智能指针分配空间必须使用new的空间区域。
5.如果可以生成一个存储高尔夫球棍(而不是数字)的栈,为何它(从概念上说)是一个坏的高尔夫袋子?
栈只能拿走栈顶的高尔夫球棍,也只能将高尔夫球棍放到栈顶,这与可以从高尔夫袋子里拿任何一个高尔夫球棍的要求相违背。
6.为什么说对于逐洞记录高尔夫乘积来说,set容器是糟糕的选择。
因为set容器会自动给成绩排序,而不是按照洞的顺序记录,而且相同的成绩会被合并成一个。
7.既然指针是一个迭代器,那为什么STL设计人员没有简单的使用指针来代替迭代器呢?
指针是一个迭代器,但指针并不能对比如说链表结构等的数据进行通用的操作,为了代码的通用性,因此使算法独立于数据结构,因此使用迭代器。
8.为什么STL设计人员仅定义了迭代器基类,而使用继承来派生其他迭代器类型的类,并根据这些迭代器来标识算法?
因为不同的容器支持的迭代器种类是不一样的,而不同的算法所要求的迭代器种类也是一样的,C++的要求使迭代器用最低的层次适用容器最广大的区间范围。
STL方法使得可以将STL函数用于指向常规数组的常指针以及指向STL容器类的迭代器,因此提高了通用性。

9.给出vector对象比常规数组方便的三个例子。

  • 执行添加数据的操作vector管理自己的内存,自动调整长度
  • 执行删除数据的操作可以将一个vector对象赋给另一个
  • 排序等操作比常规数组方便可以使用at()方法,检查边界。

10.如果程序清单16.9是使用list(而不是vector)实现的额,则程序的哪些部分将是非法的?非法部分能轻松修复吗?如果可以,如何修复呢?
sort()排序部分,和random_shuffle()部分,这两个部分都要求使用随机访问迭代器,而list的迭代器是双向迭代器,不满足要求。可以修复,将list的值调用copy复制给vector,排序后,再将vector的值复制给list来解决。可以让list使用自己的sort方法进行解决。

11.假设有程序清单16.15所示的函数符Toobig,下面的代码将有何功能?赋给bo的是什么值?
bool bo = TooBig<int>(10)(15);
实现将15与10比较大小,返回true。

posted @ 2022-01-12 14:52  Fight!GO  阅读(78)  评论(0编辑  收藏  举报