C++ Primer Plus 学习笔记(十三)

第 16 章 string 类和标准模板库

1. string 类

string 有长度限制,由 string::npos 指定,通常是 usigned int 的值。对于 string 类对象的输入,有两种方式:

string s;
cin >> s;
getline(cin, s);  // 遇到 \n 结束,并删掉\n

string 类会自动调整大小来容纳字符串,直到以下三种情况停止:

1. 达到文件尾

2. 遇到 \n 等分界字符,这种情况下会将分解字符从输入流中删除,不储存它

3. 读取达到最大允许值

string 类还有许多方法,用的时候再总结

2. 智能指针模板类

对于动态内存分配 new 的情况下,比如在 delete 之前 throw 了一个异常,而 catch 块没有 delete 的处理,或者其他的一些情况,导致 delete 无法执行,这种情况下,智能指针可以通过释放本身的情况下,delete 释放掉new 的内存(释放自身时,调用析构函数,释放 new 的内存)。

智能指针的主要作用是,在 new 一块内存时,无需再考虑释放这块内存。书里介绍的智能指针模板有三个:auto_ptr(已弃用)、unique_ptr、shared_ptr。

要创建智能指针,要包含头文件 memory,这个头文件有智能指针的模板定义。智能指针的用法:

std::auto_ptr<double> pd(new double);
std::auto_ptr<std::string> ps(new std::string);
std::unique_ptr<double> pdu(new double);
std::shared_ptr<std::dtring> pss(new std::string);

智能指针模板位于 std 空间内。所有智能指针的构造函数都由 explicit 修饰,即禁止隐式转换:

shared_ptr<double> pd;
double *p = new double;
pd = p;  // not allowed
pd = shared_ptr<double>(p);  // allowed
shared_ptr<double> ps = p;  // not allowed
shared_ptr<double> ppp(ps);  // allowed,构造函数有参数为指针的定义

智能指针的用法与常规指针差不多,常规指针的操作一般智能指针也一样,解引用,-> 访问成员等等。

智能指针的一个错误用法:

string s("I wandered lonely");
shared_ptr<string> ps(&s);  

指向了一个非堆内存。这样 delete 了一个非堆内存,这是错的。

下面记录智能指针的注意事项:

auto_ptr<string> pv;
auto_ptr<string> ps(new string("I reigned lonely"));
pv = ps;

这种情况下,new 的地址将被释放两次(ps 和 pv 过期时)。对于这种情况,主要有三种方法解决:

1. 定义赋值运算符时,采用深拷贝,这样赋值的时候,ps、pv 指向的 new 不同。

2. 建立所有权概念,即对于特定的对象,只能有一个智能指针拥有它,当智能指针赋值时,转让所有权,这就是 auto_ptr 和 unique_ptr 的策略,后者更严格。

3. 创建只能更高的指针,跟踪引用特定对象的智能指针数,也称为引用计数,如,赋值时,计数器加 1,指针过期时,计数器减 1,当最后一个智能指针过期时,才调用 delete。这是 shared_ptr 的策略。

弃用 auto_ptr 的原因:

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

int main()
{
    using namespace std;
    auto_ptr<string> film[5] = 
    {
        auto_ptr<string> (new string("Fowl"));
        auto_ptr<string> (new string("Duck"));
        auto_ptr<string> (new string("Chicken"));
        auto_ptr<string> (new string("Turkey"));
        auto_ptr<string> (new string("Goose"));
    };
    auto_ptr<string> pwin;
    pwin = film[2];
    
    for (int i = 0; i < 5; i++)
        cout << *film[i] << endl;
    cin.get();
    return 0;
}

该程序会在输出到第三个词 chicken 时报错。在 auto_ptr 赋值后,flim[2] 放弃对象的所有权,变成了空指针,再解引用就出现了错误。而换成 shared_ptr 时,程序就可以正常运行,赋值后,pwin 和 film[2] 依然指向同一个对象。当换成 unique_ptr 时,程序在编译阶段就会报错。虽然所有权概念是好事,但是在转让所有权以后,还调用丧失所有权的智能指针就是坏事,auto_ptr 在编译阶段不会报错,因此 unique_ptr 更安全。

有的时候智能指针复制后又不会留下悬挂指针:

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

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

这种情况下,temp 在转让完所有权后,会很快销毁,这样就不会再被调用,而且编译器也允许这样赋值,即,如果赋值的 unique_ptr 是一个临时右值,则编译器允许,若还存在一段时间,则编译器禁止:

unique_ptr<string> ps1, ps2;
ps1 = demo("Uniquele special");
ps2 = move(ps1);  //  allowed
ps1 = demo(" and more");
cout << *ps2 << *ps1 << endl;

调用 move() 函数,使用了 C++ 11 新增的移动构造函数和右值引用。

相比于 auto_ptr,unique_ptr 的另一个优势是,它有 new [] 和 delete [] 的版本,而前者没有:

unique_ptr<double []>pda(new double[5]);

在选择智能指针时,当程序需要多个指针指向同一个对象时,使用 shared_ptr,这种情况包括:

一个数组指针,需要一些辅助指针标识特定元素,如指向最大、最小值;两个对象包含都指向第三个对象的指针;STL 容器包含指针。当不需要多个指针指向相同对象,使用 unique_ptr:

unique_ptr<int> make_ini(int n)
{
    return unique_ptr<int>(new int(n));
}

void show(unique_ptr<int> &p)
{
    cout << *p << endl;
}

int main()
{
...
    vector<unique_ptr<int>>vp(size);
    for (int i = 0; i < vp.size(); i++)
        vp[i] = make_ini(rand() % 1000);  
    vp.push_back(make_ini(rand() % 1000));
    for_each(vp.begin(), vp.end(), show);
...
}  

这里 make_ini 函数返回一个临时的 unique_ptr 对象,函数调用后,所有权转让完就会销毁,因此编译器允许。注意 show 函数,它的参数必须是引用,如果是按值传递,则 vector 里的 unique_ptr 对象在转让所有权给一个临时的函数参数后,还会存在,这种情况下,编译器不会允许。

模板 shared_ptr 包含一个显示的构造函数,可将右值 unique_ptr 转换为 shared_ptr,并接管 unique_ptr 所有的对象。

3. 标准模板库

标准模板库 STL(Standard Template Library),提供了一组表示容器、迭代器、函数对象和算法的模板。容器类似于数组,用来储存若干个类型相同的值。迭代器可以理解为指针,广义的指针,它的用法跟指针差不多。函数对象是类似函数的对象,可以是重载了 () 运算符的类对象或者是函数指针。

STL 属于泛型编程,理解起来挺费劲的,还是得多用用。

3.1. 模板类vector

vector 对应数组,存储了一组可随机访问的值,vector 有很多方便的操作,如对象间互相赋值等。

vector 有很多基本方法,size() 返回容器元素数;swap() 交换两个容器内容;begin() 返回指向容器第一个元素的迭代器;end() 返回超过容器尾的迭代器。

迭代器算是一种广义指针,可以对其执行解引用操作(*),以及递增(++)。声明一个 vector 的迭代器:

vector<int>::iterator pd;  // pd is a vector iterator
vector<int> scores;  // 创建空vector
pd = scores.begin();  // pd 指向 scores 第一个元素
*pd = 22;              // scores 第一个元素赋值为 22
++pd;                  //  让 pd 指向 scores的下一个元素

超尾指的是容器最后一个元素后面的那个元素。push_back() 方法将元素添加到容器末尾,该方法负责管理内存,自动增加 vector 的长度。erase() 方法删除 vector 指定区间的元素,接受两个迭代作为参数,左闭右开区间( [p1, p2) )。因此,参数若是begin()、end(),则删除整个 vector。

insert() 方法接受三个迭代器参数,第一个参数表示新元素要插入的位置,后两个迭代器(通常是另一个 vector 对象) 定义了新元素的区间,也是左闭右开:

vector<int> new_v;
vector<int> old_v;
...
old_v.insert(old_v.begin(), new_v.begin(), new_v.end());  // 将new_v 全部插入到 old_v 的第一个元素位置上

针对如搜索、排序等算法,STL 定义了非成员函数来执行对应操作,这些函数具有泛性,可以适用于多种容器对象。如for_each()、random_shuffle() 和 sort()。

for_each() 接受三个参数,两个迭代器和一个函数指针:

vector<int> book;
...
for_each(book.begin(), book.end(), showReview);  // showReview 是一个参数是 int 的函数指针

// 等价于
for (auto pr = book.begin(); pr != book.end(); pr++)
showReview(*pr);

Randow_shuffle() 接受两个指定区间的迭代器参数,并随机排列区间中的元素,左闭右开区间:

Random_shuffle(book.begin(), book.end());

该函数要求容器允许随机访问。

sort() 函数同样要求容器支持随机访问,它有两种函数调用版本。第一种,参数是两个表示区间的迭代器( 左闭右开 ),并使用容器中从定义的 小于( < ) 运算符重载,将该区间进行升序排列:

sort(book.begin(), book.end());

如果容器的数据类型是自定义的,要使用 sort() 第一种形式,需要定义一个处理该数据类型的 operator<() 函数:

struct Review {
    std::string tile;
    int rating;
};
bool operator<(const Review & r1, const Review & r2)
{
    if (r1.tile < r2.tile)
        return true;
    else if (r1.tile == r2.tile && r1.rating < r2.rating)
        return true;
    else
        return false;
}


vector<Review> books;
...  // 赋值
sort(books.begin(), books.end());

如果想要升序排列,可以使用 sort() 函数的第二个版本,接受三个参数,前两个参数是表示范围的迭代器,第三个参数是一个函数指针,该指针可以指向一个自定义的升序函数:

bool worseThan(const Review & r1, const Review & r2)
{
    if ( r1.rating < r2.rating)
        return true;
    else 
        return false;
}

sort(books.begin(), books.end(), worseThan);

自定义函数的返回值要是 bool。

C++ 11 提供了一种基于范围的 for 循环:

double prices[3] = { 4.99, 10.99, 6.87 };
for (double x : prices)
    cout << x << endl;

在这种 for 循环中,声明一个与容器存储的类型相同类型的变量,然后指出容器的名称,然后循环体就会一次访问该容器的每一个元素:

for(auto x : books)
    showReview(x);

这种基于范围的 for 循环,可以通过 声明一个引用变量,改变容器元素:

void inflate(Review & r) { r.rating++; }
for (auto &x : books)
{
    inflate(x);         
}

4. 泛型编程

4.1 迭代器类型

泛型编程主要的关注点是算法,旨在编写独立于数据类型的代码,一般是工具模板。

容器作为数据结构的通用模板,可以使算法不特定于具体的数据类型,而迭代器可以使算法不特定于具体的容器。STL中,每个容器都定义了相应的迭代器,有的是指针,有的是对象,每个容器都有一个超尾标记,容器也会定义相应 +、== 等运算符重载。

STL 定义了 5 种迭代器:输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。

输入迭代器的输入指,信息来自于容器,例如读取容器的内容,对输入迭代器解引用来读取容器元素的值,它不一定允许程序修改容器的值。输入迭代器是单向的,只能递增,不能递减。它也是单通行(不理解,以后看书理解了再总结)。

输出迭代器的输出只将信息从程序传输到容器,即写操作。解引用输出迭代器能让程序修改容器的值,但不能读取(类似 cout)。它也是单通行的。

正向迭代器类似于输出迭代器,它只使用 ++ 运算符向前遍历容器,与输入、输出迭代器不同,它是多次通行的(不理解,以后看书再总结)。它可以读取或写入容器信息。

双向迭代器具有正向迭代器的所有特性,并支持递减运算符。

随机访问迭代器能够直接跳到容器的任一元素,它具有双向迭代器的所有特性,并支持 +、- 等操作(a、b 表示迭代器;n 表示整数):

表达式描述
a + na向后n个元素
n + a同上
a - na向前n个元素
a += na = a + n
a -= na = a - n
a[n]等价于 *(a + n)
b - a结果为 n 这样的值:b = a + n
a < b返回 bool,a-b<0
a > b同上
a >= b
a <= b

类似 a + n 这种表达式,a 和 a+n 必须在容器范围内(包括超尾)。

C++ 超尾的特性适用于数组,,且 STL 算法是基于迭代器的,而指针可以作为一种迭代器,因此 STL 算法适用于常规数组。

4.2 概念、改进和模型

有一些将其他接口转换为STL接口的适配器(类或函数),通过头文件 iterator 创建这种迭代器,比如 ostream_iterator 模板:

#include <iterator>
vector<int> dice[10];
...
ostream_iterator<int, char> out_iter(cout, " ");

out_iter 迭代器现在是一个接口,模板的第一个参数 ”int" 表示被发送到输出流的数据类型,第二个参数表示输出流输出的类型(也可以是 wchar_t)。构造函数的的一个参数表示要使用的输出流,也可以是用于文件的输出流,第二个参数表示每个输出流数据项显示的分隔符。

*out_iter++ = 15;  // 等价于 cout << 15 << " "

上述表示将 15 和 空格组成的字符串发送到 cout,并为下一个输出做准备。

可以使用 STL 方法 copy() 将容器的元素依次输出:

copy(dice.begin(), dice.end(), out_iter);
// 或者使用匿名迭代器
copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, " "));

iterator 头文件还有输入流迭代器模板 istream_iterator,使输入流作为迭代器接口,使用 STL 方法。

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

模板的第一个参数表示要读取的数据类型,第二个参数表示输入流使用的字符类型。构造函数参数 cin 表示通过 cin 管理输入流,省略构造函数表示输入失败。因此,上述表示遇到文件结尾、类型不匹配或其他输入错误才停止,否则从dice开始一直输入。

iterator 头文件还提供了 reverse_iterator、back_insert_iterator、front_insert_iterator 和 insert_ierator 等预定义迭代器类型。

reverse_iterator 的功能是执行递增操作将导致它递减,这么做的主要原因是为了简化对已有函数的使用。像 vector 容器中有 rbegin() 和 rend() 的成员函数,前者返回一个指向超尾的反向迭代器,后者返回一个指向第一个元素的反向迭代器,对他俩执行递增(++)操作会导致它们递减,要反向输出 vector 元素,可以:

ostream_iterator<int, char> out_iter(cout, " ");
copy(book.rbegin(), book.rend(), out_iter);

rbegin() 和 end() 返回值相同(超尾),但是类型不同,前者是反向迭代器,后者不是。

反向迭代器有特殊的补偿模式,如果 rp 是一个 rbegin() 的返回值,*rp 是超尾,不能直接解引用,同样,在上述 copy 中,rend() 是开区间,输出不到第一个元素。因此,反向迭代器通过先递减再解引用的方式,进行补偿。

copy() 函数必须要保证第三个参数有足够的空间容纳要复制的区间,copy() 不能自动改变容器的空间,而且也会覆盖已有数据。三种插入迭代器将覆盖转为插入,且能够自动调节容器空间来容纳数据。back_insert_iterator 将数据插入容器尾部,front_insert_iterator 将元素插入容器前端,insert_ierator 迭代器姜将元素插入到构造函数的参数指定的位置前。前两个迭代器有限制,要求容器能够在头或尾支持快速插入的算法,因此后者就不适用于vector。而 insert_iterator 就没有限制。

这三种迭代器将容器类型作为模板,创建这种迭代器需要:

vector<int> dive;
back_insert_iterator<vector<int>> back_iter(dice);

指明 back_insert_iterator 的模板类型的原因是,假设该迭代器要扩充容器的空间时,copy() 本身不能改变容器空间,但是模板类型对应的容器 vector<int> 有 push_back() 方法,可以扩充容器大小。

上面创建了一个 dice 的尾部插入迭代器。front_insert_iterator 的创建方式相同。而 insert_iterator 的构造函数需要一个指示插入位置的参数:

insert_iterator<vector<int>> insert(dive, dive.begin());

vector<string> words(4) = { "fine", "fish", "fashion", "fate" };
void out_put(const std::string& s) { std::cout << s << " "; }
string s2[2] = { "silly", "singers" };
copy(s2, s2 + 2, insert_iterator<vector<string>> (words, words.begin()));
for_each(words.begin(), words.end(), out_put);
cout << endl;
// words会输出 silly singers fine fish fashion fate
//不是 singers silly fine fish fashion fate

STL 具有容器概念和容器类型,概念是具有名称(如容器、序列容器、关联容器等)的通用类别,容器类型是可用于创建具体容器对象的模板,到了 C++11,容器类型有 deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset、bitset、forward_list、unordered_map、unordered_multimap、unordered_set 和 unordered_multiset,且不将 bitset 视为容器,将其视为一种独立的类型。

容器概念指定了 STL 容器类都必须满足的一系列要求。容器的元素类型必须相同,且不是任意的对象都可以储存到容器中,换句话说,能存储在容器中的元素必须是可复制构造和可赋值的,基本类型可以,类定义中没有将复制构造函数和赋值运算符声明为保护或私有的也可以。C++ 11 改进了概念,添加了可复制插入和可移动插入(不懂)。

4.3 容器种类

一些基本的容器特征(rv 表示容器的非常量右值,如函数的返回值):

表达式返回类型说明复杂度
X::iterator指向X容器的迭代器满足正向迭代器要求的任意迭代器编译时间
X::value_type容器X的数据类型编译时间
X u创建一个名为u的空容器固定
X()创建匿名容器固定
X u(a)复制构造 u==a线性
X u = a同上线性
r = a赋值运算符线性
(&a)->~X()void容器每个对象析构线性
a.begin()迭代器指向第一个元素的迭代器固定
a.end()迭代器指向超尾的迭代器固定
a.size()无符号整型元素个数,等价于 a.end()-a.begin()固定
a.swap(b)voida 和 b 交换内容固定
a == bboola 和 b 长度相同,且a中每个元素都等于b相应的元素,为真线性
a != bbool返回 !(a == b)线性
X u(rv)调用移动构造函数后,u 的值与 rv的原始值相同线性
X u = rv同上
a = rv调用移动构造函数后,u 的值与 rv的原始值相同线性
a.cbegin()const_iterator返回容器第一个元素的 const 迭代器固定
a.cend()const_iterator返回容器超尾的 const 迭代器固定

复制构造和移动构造的区别在于,复制操作保留源对象,而移动操作可以修改源对象,或者转让所有权,而并不进行复制。

4.4 序列容器

在基本的容器特征下,可以添加特性来改进容器,如序列特性和关联特性等。

序列是一种很重要的容器特性,C++ 11 下,有 7 种序列容器:deque、forward_list、list、queue、priority_queue、stack、vector。序列的特性要求迭代器至少要达到正向迭代器,保证容器中元素按顺序排列,不会在两次迭代间发生变化。array 也被归类到顺序容器,即使它并不满足序列容器的所有要求。序列要求元素按严格的线性顺序排列,即存在第一个元素、最后一个元素,除最前、最后两个元素外,每个元素都有前后两个元素。数组和链表都是序列,分支结构( 每个节点都指向两个子节点 ) 不是。

序列由于元素有确定的顺序,因此可以执行插入到特定位置、删除特定区间等操作。下面列出序列必须完成的操作(n 表示 类型为 int 的值,p、q、i、j 表示迭代器,t 表示容器元素类型的具体的值 ):

表达式返回类型说明
X a(n, t);声明一个名为a的,n个 t 的序列容器
X(n, t);创建匿名序列
X a(i, j)声明一个 a序列,初始化为区间 [ i, j) 的内容
X(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等价于 a.erase(a.begin(), a.end());

其中,deque、list、queue、priority_queue、stack 和 vector 还支持:

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

a[n] 与 a.at(n) 的差别在于,后者会执行边界检查,并引发 out_of_range 异常。

下面详细记录各个序列容器:

(1)vector

vector 可以动态改变自身长度,随着删除、插入等操作而增大或缩小。除了序列特性,它也是可翻转容器,方法 rbegin()、rend(),二者返回反转后的第一和最后一个迭代器。

(2)deque

它表示双端队列( double-ended queue ),从 deque 对象的开始位置插入和删除元素的时间是固定的,不像 vector 是线性时间,如果多次操作发生在容器起始和结尾处,可以考虑它。deque 固定时间特性使得它的设计比 vector 复杂,因此随机访问或中部插入、删除等操作,vector 快些。

(3)list

list 表示双向链表,除头尾元素外,每个元素都与前后元素链接,这就可以双向遍历链表。list 在任意位置的插入和删除时间都是固定的( vector 提供除结尾外的线性插入和删除,结尾处插入和删除是固定时间 )。vector 强调快速随机访问,list 强调快速插入、删除。

list 支持反转容器,但不支持数组表示法和随机访问。list的迭代器在执行插入、删除操作后,迭代器指向的元素不变,只是改变了链表信息,而vector 的迭代器指向的元素会改变。

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()连续的相同元素压缩为单个元素,复杂度为线性时间

调用splice() 函数后,指向变为空的 list 的迭代器依然有效,只是变为指向新 list 的相同元素了。unique() 函数只能压缩相邻的相同元素,要想完全消重,需要先排序。list 不能作为 非成员函数的 sort() 函数的参数,因为非 list 成员函数的 sort() 要求容器能随机访问。

(4)forward_list

该容器实现单向链表,单向链表的每个元素只链接下一个节点,没有链接前一个节点,因此,它只需要正向迭代器,且不可反转。

(5)queue

queue 模板类是一个适配器,默认改造了deque。queue 不允许随机访问,甚至不允许遍历。它实现队尾添加元素,队首删除元素,查看队首、队尾的值,检查元素数目,测试是否为空:

方法说明
bool empty() const队列空,返回true
size_type size() const返回队列元素数目
T & front()返回队首元素的引用
T & back()返回队尾元素的引用
void push(const T & x)队尾插入x
void pop()删除队首元素

(6)priority_queue

它同 queue 一样,也是个适配器类,支持的操作与 queue 相同,但是最大的元素会被移到队首。内部的区别是,底层的默认类是 vector。

(7)stack

它也是一个适配器类,底层默认类是vector。stack 不允许随机访问,不允许遍历,可以在栈顶压入、弹出元素,检查栈顶的值,检查元素数目和判断栈是否为空:

方法说明
bool empty() const栈空,返回true
size_type size() const返回栈的元素数目
T & top()返回栈顶元素的引用
void push(const T & x)栈顶压入x
void pop()栈顶弹出元素

(8)array

array 并非 STL 容器,因为它的长度是固定的,无法自动调节内存大小,push_back()。insert() 操作无法执行。但是有很多的 STL 算法可以应用,如 copy()、for_each() 等。

4.5 关联容器

关联容器将值与键关联在一起,并通过键来查找值。X::value_type 表示值的类型,关联容器中,X::key_type 表示键的类型。关联容器能够快速访问元素,允许插入新元素,但是不能指定插入位置,因为它有确定数据放置位置的算法。关联容器由某种树实现,类似链表的节点分支结构,使得删除、添加数据项比较简单,且相比于链表,树的查找更快。STL 提供 4 种关联容器:set、multiset、map 和 multimap。前两种在头文件 set 中定义,后两种定义在 头文件 map 中。

set 是最简单的关联容器,值类型与键类型相同,键是唯一的,set 中不会有多个相同的键。multiset 类似于 set,不过它可能多个值的键相同,例如 int 类型的 multiset的多个值:1、2、2、2、2。map的值与键类型不同,键是唯一的,每个键只对应一个值,multimap 类似 map,只是一个键可以关联多个值。

(1)set

set 可反转。可排序,键值唯一,不能存储多个相同的值,实际 value 就是它的 key:

const int n = 6;
string s1[n] = { "buff", "think", "for", "heavy", "can", "for" };
set<string> A(s1, s1 + n);  // 用一个数组的范围初始化set
copy(A.begin(), A.end(), ostream_iterator<string, char>(cout, " "));
// 输出 buff can for heavy think

set 模板的第二个参数是可选的,用来指示对键排序的函数或对象,默认是 less<>。

set 内容会被排序,由于键值唯一,for 在集合中只出现一次。STL 提供了数学上对集合的标准操作的支持。两个 set 相减是第一个集合减去两个集合交集的元素,相加就是取交集。STL 提供的对 set 的支持是通用函数,不是 set 的成员函数。这些算法的先决条件,set 是满足的,即容器是经过排序的。set_union() 支持 5 个参数,前四个是两个 set 的范围,第 5 个参数是结果的位置:

set_union(a.begin(), a.end(), b.begin(), b.end(),
            ostream_iterator<string, char>(cout, " "));

将 set a 和 b 的并集结果打印到标准输出。如果是要放到 set c 中,需要使用 insert_iterator :

set_union(a.begin(), a.end(), b.begin(), b.end(),
            insert_iterator<set<string>>(c, c.begin()));

函数 set_intersection() 和 set_difference() 分别查找交集和得到两个集合的差,接口与 set_union()相同。set 的两个方法 lower_bound() 和 upper_bound() 将键作为参数,返回一个迭代器,该迭代器指向第一个不小于或者大于键参数的成员。

由于 set 会排序,因此插入只需要内容,不需要指定位置:

string s("tennis");
a.insert(s);
b.insert(a.begin(), a.end());

(2)multimap

它也是可反转,经过排序的容器,但是,multimap 同一个键可以与多个值关联。

multimap<int, string> codes;  // 键类型 int,值类型 string

模板的第三个参数被隐藏了,默认为 less<>,表示对键排序的比较函数或对象。实际的值类型是将键类型与数据类型结合为一对,STL 使用模板类 pair<class T, class U> 将两种值存储到一个对象中。上面 codes 的值类型,实际为:pair<int, string>。

pair<const int, string> item(213, "Los Angeles");
codes.insert(item);  
// 匿名插入
codes.insert(pair<const int, string>(224, "New York"));

因为容器会按键排序,所以不用指出插入位置。可以通过 first 和 second 访问 pair 的两部分:

cout << item.first << ' ' << item.second <<endl;

成员函数 count() 接受键作为参数,返回具有该键的元素数目。成员函数 lower_bound() 和 upper_bound() 将键作为参数,结果与 set 的相同。成员函数 equal_range() 用键做参数,返回两个迭代器,它俩表示的区间与该键匹配,为返回两个值,将他们封装到 pair 中:

pair<multimap<keytype,string>::iterator,
    multimap<keytype,string>::iterator> range
                         = codes.equal_range(718);
std::multimap<keytype,string>::iterator it;
for(it = range.first; it != range.second; it++)
    cout << (*it).second << endl;  // 输出键为718的值

4.6 无序关联容器

无序关联容器也是将值与键关联起来,使用键查找值,但是底层使用哈希表实现,提高添加、删除以及查找算法的效率。4 中无序关联容器:unordered_set、unordered_multiset、unordered_map 以及 unordered_multimap。

5.函数对象

很多 STL 算法都使用函数对象,也叫函数符。函数符是可以与括号”()“ 结合使用的任意对象。包括函数名、函数指针和重载了 ”()“ 运算符的类对象。

STL 定义了函数符的概念:

生成器——不用参数就可以调用的函数符。

一元函数——一个参数就可以调用的函数符。

二元函数——两个参数可以调用的函数符。

谓词——返回 bool 的一元函数。

二元谓词——返回 bool 的二元函数。

list 的成员函数 remove_if() ,以谓词作为参数,删除返回 true 的元素:

bool tooBig(int n) {return n > 100; }
list<int> scores;
...
scores.remove_if(tooBig); //  删除 scores 中所有大于100的元素

可以通过类对象重载 ()运算符的方法,将二元谓词转换成谓词:

template <class T>
bool tooBig(const T & val, const T & lim)
{
    return val > lim;
}

template <class T>
class tooBig2
{
private:
    T cutoff;
public:
    tooBig2(const T & t) : cutoff(t) {}
    bool operator()(const T & v) { return tooBig<T>(v, cutoff); }
};

tooBig2<int> tB100(100);
int x;
cin >> x;
if (tB100(x))  // tB100(200) 等价于 tooBig(200, 100)
    ...

STL 预定义了多个基本函数符,在头文件 functional 中。它们执行两个值相加或者比较两个值是否相等等操作:

运算符相应的函数符
+plus
-minus
*multiplies
/divides
%modulus
-negate
==equal_to
!=not_equal_to
>greater
<less
>= greater_equal
<=less_equal
&&logical_and
||logical_or
!logical_not

例如,STL 的 transform() 函数,第一个版本接受四个参数,前两个参数是用迭代器表示一个容器的范围,第三个参数是结果复制到容器的起始位置迭代器,最后一个参数是函数符,应用于容器范围内的每个元素,该函数符表示一元函数:

const int LIM = 5;
double arr1[LIM] = {36, 39, 42, 45, 48};
vector<double> gr8(arr1, arr1 + LIM);
ostream_iterator<double, char> out(cout, " ");
transform(gr8.begin(), gr8.end(), out, sqrt);

第二个版本接受二元函数符,并将该函数符应用在两个容器区间,前两个参数同上,第三个参数表示另一个容器的起始迭代器,第四个参数表示结果存储的容器的起始迭代器,第五个参数是二元函数符:

double add(double x, double y) {return x + y;}
...
transform(gr8.begin(), gr8.end(), m8.begin(), out, add);

上面的 add 函数,就可以用 functional 头文件中的 plus<double>() 代替:

#include <functional>
plus<double> add;
double y = add(2.2, 3.3);
transform(gr8.begin(), gr8.end(), m8.begin(), out, plus<double>());  // 匿名的函数符

函数适配器的作用是,当函数符作为 STL 方法的参数时,若该方法的函数符参数是一元的,而现有的函数符是二元的,则通过函数适配器,可以将函数符适配成一元。如函数符 multiplies,通过STL 函数 bind1st() 或 bind2nd(),可以将其适配成一元函数符:

bind1st(multiplies<double>(), 2.5);  // 等价于 multiplies<double>(2.5, x)
transform(gr8.begin(), gr8end(), out, 
    bind1st(multiplies<double>(), 2.5)  // gr8每个元素增大2.5倍

bind2nd() 函数,将被适配的函数符第二个参数绑定成常数。

6. 算法

STL 将算法库分成4组:非修改式序列操作、修改式序列操作、排序和相关操作、通用数字运算。前三个在头文件 algorithm中,第四组专用于数值数据,在头文件 numeric 中。

算法的分类形式之一,是按将结果放置的位置分。如 sort() 函数,是将结果放在原位的,它属于就地算法(in-place algorithm)。而 copy() 函数是将结果发送的另一位置,它属于复制算法(copying algorithm)。transform() 函数有两个版本,STL 约定,复制版本以 _copy 结尾。对于复制算法,统一约定是,返回一个迭代器,该迭代器指向复制的最后一个值后面的一个位置。

6.1 string 和 STL

string 类并不是 STL 的组成部分,但设计它时考虑到了 STL,它包含了 begin()、end()、rbegin() 和 rend() 等成员,所以 string 可以使用 STL 接口。next_permutation() 算法将区间内容转换为下一种排列方式,即当前排列的下一个排列,对于字符串,按字符升序。成功,返回 true,若已经属于最后一个排列,返回 false:

string s = "bac";
while (next_permutation(s.begin(), s.end()))
    cout << s << endl;  // 输出 bca cab cba

要想得到全部排序,要先调用 sort() 函数,排成 abc。

6.2 使用 STL

map 类可以使用数组表示法,将键作为索引来访问存储的值:

map<string, int> wordmap;
....
cout << wordmap["the"] << endl;  // 输出键值为 the 的值
wordmap["hi"] = 3;  // 为 键值为 hi 的元素赋值

7. 其他库

7.1 vector、array 和 valarray

vector、valarray 和 array三个数组模板,vector 属于容器类和算法系统的一部分;而 valarray 是面向数值计算设计的,不是 STL 的一部分,它没有 push_back() 和 insert() 方法,但是有很多数学运算的简单接口,如重载了所有算数运算符等;array 是为了替代数组而设计的,提供了更好的接口,让数组效率更高,包括 begin()、rbegin() 等方法,这样 STL 算法也可以应用在 array:

vector<double> vecdl(10), vecd2(10), vecd3(10);
array<double, 10> arrd1, arrd3, arrd3;
valarray<double> vald1(10), vald2(10), vald3(10);

//两个数组和存到第三个数组
transform(vecd1.begin(), vecd1.end(), vecd2.begin(),
            vecd3.begin(), plus<double>());  // vector 类

transform(arrd1.begin(), arrd1.end(), arrd2.begin(),
            arrd3.begin(), plus<double>());  // array 类

vald3 = vald1 + vald2;  // valarray 类

// 数组元素扩大2.5倍
transform(vecd1.begin(), vecd1.end(), vecd1.begin(), 
                bind1st(multiplies<double>(), 2.5));  // vector、array 类

vald3 *= 2.5;  // valarray 类,等价于 vald3 = 2.5 * vald3;

valarray 专注于数值计算,支持很多数学函数:

vald3 = log(vald1);  //  vald1 每个元素的对数结果存在 vald3
vald3 = vald1.apply(log);  // 等价于上面的

apply() 方法不修改调用对象,而是返回新对象。valarray 类还提供了 sum()、size()、max() 和 min() 等方法,计算所有元素和,元素个数,最大、最小值等。由于 valarray 的对象并不是指针或者迭代器,它没有 begin() 等方法,很多 STL 算法并不支持, C++ 11 提供了函数 begin() 和 end(),函数的返回值支持 STL:

sort(begin(vald1), end(vald1));

valarray 还有很多其他特性,如果有 bool valarray 对象:

valarray<bool> vbool = vald1 > 9; // vbool[i] = vald1[i] > 9
                                  // 即vbool元素个数和vald1相同

slice 类是一个索引类,slice 对象被初始化为三个整数值:起始索引、索引数、跨距。valarray 对象下标可以是 slice 类对象:

// varint 的 1, 4, 7, 10 号元素赋值为10
valarray<int> varint[slice(1, 4, 3)] = 10;

valarray 可以通过这种形式,用一维对象表示二维数组。如一个12个元素的valarray对象 valint,valint[slice(0,3,1)],可以表示三行四列的数组的第一行。

7.2 模板 initializer_list

C++ 11 可以是用初始化列表的方式初始化容器:

vector<int> num {10, 12, 18, 22};

这是因为容器类包含将 initializer_list<T> 作为参数的构造函数。因此,上面等价为:

vector<int> num ({10, 12, 18, 22});  // 通过小括号显示将初始化列表表示成参数

initializer_list 旨在让用户能够将一系列值传递给构造函数或其他函数。它有 begin() 和 end() 方法,可以使用 STL 相关方法,但迭代器是 const 类型的,不允许解引用更改迭代器指向的值,但是不同的对象可以赋值:

intializer_list<int> d1 {1, 2, 3, 4};
*d1.begin() = 8; // 不允许
d1 = {4, 5, 8, 10};  // 可以
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值