《C++ Primer Plus》第16章:string类和标准模板库(4)

标准模板库

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

Alex Stepanov 和 Meng Lee 在 Hewlett-Packard 实验室开发了 STL,并于 1994年发布其实现。ISO/ANSI C++ 委员会投票同意将其作为 C++ 标准的组成部分。STL 不是面向对象的编程,而是一种不同的编程模式——泛型编程(generic programming)。这使得 STL 在功能和方法方面都很有趣。关于 STL 的信息很多,无法用一章的篇幅全部介绍,所以这里将介绍一些有代表性的例子,并领会泛型编程方法的精神。先来看几个具体的例子,让您对容器、迭代器和算法有一些感性的认识,然后再介绍底层的设计理念,并简要地介绍 STL。附录 G 对各种 STL 方法和函数进行了总结。

模板类 vector

第4章简要地介绍了 vector 类,下面更详细地介绍它。在计算中,矢量(vector)对应数组,而不是第11章介绍的数学矢量(在数学中,可以使用 N 个分量来表示 N 维数学矢量,因此从这方面讲,数学矢量类似一个 N 维数组。然而,数学矢量还有一些计算机矢量不具备的其他特征,如内乘积和外乘积)。计算矢量存储了一组可随机访问的值,即可以使用索引来直接访问矢量的第10个元素,而不必首先访问前面第9个元素。所以 vector 类提供了与第14章介绍的 valarray 和 ArrayTP 以及第4章介绍的 array 类似的操作,即可以创建 vector 对象,将一个 vector 对象赋给另一个对象,使用 [ ] 运算符来访问 vector 元素。要使类成为通用的,应将它设计为模板类,STL 正是这样做的——在头文件 vector(以前为 vector.h)中定义了一个 vector 模板。

要创建 vector 模板对象,可使用通常的<type> 表示法来指出要使用的类型。

另外,vector 模板使用动态内存分配,因此可以用初始化参数来指出需要多少矢量:

#include <vector>
using namespace std;
vector<int> ratings(5);			// a vector of 5 ints
int n;
cin >> n;
vector<double> scores(n);	// a vector of n doubles

由于运算符[] 被重载,因此创建 vector 对象后,可以使用通常的数组表示法来访问各个元素:

ratings[0] = 9;
for (int i = 0; i < n; i++){
	cout << scores[i] << endl;
}

分配器

与 string 类相似,各种 STL 容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。例如,vector 模板的开头与下面类似:

template<class T, class Allocator = allocator<T> >
	class vector { ...

如果省略该模板参数的值,则容器模板将默认使用 allocator<T> 类。这个类使用 new 和 delete。

下面的程序是一个要求不高的应用程序,它使用了这个类。该程序创建了两个 vector 对象——一个是 int 规范,另一个是 string 规范,它们都包含 5 和元素。

// vect1.cpp -- introducing the vector template

#include<iostream>
#include<string>
#include<vector>

const int NUM = 5;

int main(){

    using std::vector;
    using std::string;
    using std::cin;
    using std::cout;
    using std::endl;

    vector<int> ratings(NUM);
    vector<string> titles(NUM);

    cout << "You will do exactly as told. You will enter\n"
         << NUM << " book titles and your ratings (0-10).\n";

    int i;
    for ( i=0; i < NUM; i++){
        cout << "Enter title #" << i + 1 << ": ";
        getline(cin, titles[i]);
        cout << "Enter your rating (0-10): ";
        cin >> ratings[i];
        cin.get();
    }
    cout << "Thank you. You entered the following:\n"
         << "Raintg\tBook\n";
    for (i = 0; i < NUM; i++){
        cout << ratings[i] << "\t" << titles[i] << endl;
    }
    
    return 0;
}

上面的程序的运行情况如下:

You will do exactly as told. You will enter
5 book titles and your ratings (0-10).
Enter title #1: jiej
Enter your rating (0-10): 3
Enter title #2: eieiieie
Enter your rating (0-10): 20
Enter title #3: jjjjjjj
Enter your rating (0-10): 2
Enter title #4: ijij
Enter your rating (0-10): 2
Enter title #5: 3jjj
Enter your rating (0-10): 93
Thank you. You entered the following:
Raintg  Book
3       jiej
20      eieiieie
2       jjjjjjj
2       ijij
93      3jjj

该程序使用 vector 模板只是为方便创建动态分配的数组。下一节将介绍一个使用更多类方法的例子。

可对矢量执行的操作

除分配存储空间外,vector 模板还可以完成哪些任务呢?所有的 STL 容器都提供了一些基本方法,其中包括 size()——返回容器中元素数目、swap() ——交换两个容器的内容、begin() —— 返回一个指向容器中第一个元素的迭代器、end()——返回一个表示超过容器尾的迭代器。

什么是迭代器?它是一个广义指针。事实上,它可以是指针,也可以是一个可对其执行类似指针的操作——如解除引用和递增的对象。稍后将指针广义化为迭代器,让 STL 能够为各种不同的容器类(包括那些简单指针无法处理的类)提供统一的接口。每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为 iterator 的 typedef,其作用域为整个类。例如,要为 vector 的 double 类型规范声明一个迭代器,可以这样做:

vector<double> scores;

则可以使用迭代器 pd 执行这样的操作:

pd = scores.begin();		// have pd point to the first element
*pd = 22.3;					// dereference pd and assign value to first element
++pd;						// make pd point to the next element

正如您看到的,迭代器的行为就像指针。顺便说一句,还有一个 C++11 自动类型推断很有用的地方。例如,可以不这样做:

vector<double>::iterator pd = scores.begin();

而这样做:

auto pd = scores.begin();		// C++11 automatic type deduction

回到前面的示例。什么是超过结尾(past-the-end)呢?它是一种迭代器,指向容器最后一个元素后面的那个元素。这与 C-风格字符串最后一个字符后面的空字符类似,只是空字符是一个值,而“超过结尾”是一个指向元素的指针(迭代器)。end() 成员函数表示超过结尾的位置。如果将迭代器设置为容器的第一个元素,并不断地递增,则最终它将到达容器结尾,从而遍历整个容器的内容。因此,如果 scores 和 pd 的定义与前面的示例中相同,则可以用下面的代码来显式容器的内容:

for(pd = scores.begin(); pd != scores.end(); pd++){
	cout << *pd << endl;
}

所有容器都包含刚才讨论的那些方法。vector模板类也包含一些只有某些 STL 容器才有的方法。 push_back() 是一个方便的方法,它将元素添加到矢量末尾。这样做时,它将负责内存管理,增加矢量的长度,使之能够容纳新的成员。这意味着可以编写这样的代码:

vector<double> scores;		// create an empty vector
double temp;
while (cin >> temp && temp >= 0) {
	scores.push_back(temp);
}
cout << "You entered " << scores.size() << " scores.\n";

每次循环都给 scores 对象增加一个元素。在编写或运行程序时,无需了解元素的数目。只要能够取得足够的内存,程序就可以根据需要增加 scores 的长度。

erase() 方法删除矢量中给定区间的元素。它接受两个迭代器参数,这些参数定义了要删除的区间。了解 STL 如何使用两个迭代器来定义区间至关重要。第一个迭代器指向区间的起始处,第二个迭代器位于区间终止处的后一个位置。例如,下述代码删除第一个和第二个元素,即删除 begin() 和 begin()+1 指向的元素(由于 vector 提供了随机访问功能,因此 vector 类迭代器定义了诸如 begin() + 2 等操作):

scores.erases(scores.begin(), scores.begin() +2);

如果 it1 和 it2 是迭代器,则 STL 文档使用 [p1,p2) 来表示从 p1 到 p2 (不包括p2 )的区间。因此,区间[begin(), end() ) 将包括集合的所有内容,而区间[p1,p1)为空。[) 表示法并不是 C++ 的组成部分,因此不能在代码中使用,而只能出现在文档中。

注意:区间[it1, it2)由迭代器 it1 和 it2 指定,其范围为 it1 到 it2(不包括 it2)。

insert() 方法的功能与 erase()相反。它接受3个迭代器参数,第一个参数指定了新元素的插入位置,第二个和第三个迭代器参数定义了被插入区间,该区间通常是另一个容器对象的一部分。例如,下面的代码将矢量 new_v 中除第一个元素外的所有元素插入到 old_v 矢量的第一个元素前面:

vector<int> old_v;
vector<int> new_v;
...
old_v.insert(old_v.begin(), new_v.begin()+1, new_v.end());

顺便说一句,对于这种情况,拥有超尾元素是非常方便的,因为这使得在矢量尾部附加元素非常简单。下面的代码将新元素插入到 old.end() 的地方,即矢量最后一个元素的后面。

old_v.insert(old_v.end(), new_v.begin(), new_v.end());

下面的程序演示了 size()、begin()、end()、push_back()、erase() 和 insert() 用法。为简化数据处理,将之前程序中的 rating 和 title 组合成了一个 Review 结构,并使用 FillReview() 和 ShowReview() 函数来输入和输出 Review 对象。

// vect2.cpp -- methods and iterators
#include<iostream>
#include<string>
#include<vector>

struct Review{
    std::string title;
    int rating;
};

bool FillReview(Review & rr);
void ShowReview(const Review & rr);

int main(){
    using std::cout;
    using std::vector;
    vector<Review> books;
    Review temp;
    while(FillReview(temp)){
        books.push_back(temp);
    }
    int num = books.size();
    if(num>0){
        cout << "Thank you. You entered the following:\n"
             << "Rating\tBook\n";
        for (int i = 0; i < num; i++){
            ShowReview(books[i]);
        }
        cout << "Reprising:\n"
             << "Rating\tBook\n";
        vector<Review>::iterator pr;
        for (pr=books.begin(); pr!=books.end(); pr++){
            ShowReview(*pr);
        }
        vector<Review> oldlist(books);  // copy constructor used
        if(num>3){
            // remove 2 items
            books.erase(books.begin()+1,books.begin()+3);
            cout << "After erasure:\n";
            for(pr = books.begin(); pr!=books.end(); pr++){
                ShowReview(*pr);
            }
            // insert 1 item
            books.insert(books.begin(), oldlist.begin()+1, oldlist.begin()+2);
            cout << "After insertion:\n";
            for (pr = books.begin(); pr!=books.end(); pr++){
                ShowReview(*pr);
            }
        }
        books.swap(oldlist);
        cout << "Swapping oldlist with books:\n";
        for (pr = books.begin(); pr!= books.end(); pr++){
            ShowReview(*pr);
        }
    }
    else{
        cout << "Nothing entered, nothing gained.\n";
    }
    return 0;
}

bool FillReview(Review & rr){
    std::cout << "Enter book title (quit to quit): ";
    std::getline(std::cin, rr.title);
    if(rr.title == "quit"){
        return false;
    }
    std::cout << "Enter book rating: ";
    std::cin >> rr.rating;
    if (!std::cin){
        return false;
    }
    // get rid of rest of input line
    while(std::cin.get() != '\n'){
        continue;
    }
    return true;
}

void ShowReview(const Review & rr){
    std::cout << rr.rating << "\t" << rr.title << std::endl;
}

上面的程序的运行情况如下:

Enter book title (quit to quit): jij
Enter book rating: 12
Enter book title (quit to quit): 2jij
Enter book rating: 99o
Enter book title (quit to quit): jij
Enter book rating: iii
Thank you. You entered the following:
Rating  Book
12      jij
99      2jij
Reprising:
Rating  Book
12      jij
99      2jij
Swapping oldlist with books:
12      jij
99      2jij

对vector可执行的其他操作

程序员通常要对数组执行很多操作,如搜索、排序、随机排序等。vector 模板类包含了执行这些常见的操作的方法吗?没有!STL 从更广泛的角度定义了非成员(non-member)函数来执行这些操作,即不是为每个容器类定义 find() 成员函数,而是定义了一个适用于所有容器类的非成员函数 find()。这种设计理念省去了大量重复的工作。例如,假设由8个容器类,需要支持10种操作。如果每个类都有自己的成员函数,则需要定义 80(8*10)个成员函数。但采用 STL 方式时,只需要定义 10 个非成员函数即可。在定义新的容器类时,只要遵循正确的指导思想,则它也可以使用已有的 10 个非成员函数来执行查找、排序等操作。

另一方面,即使有执行相同任务的非成员函数,STL 有时也会定义一个成员函数。这是因为对有些操作来说,类特定算法的效率比通用算法高,因此,vector 的成员函数 swap() 的效率比非成员函数 swap() 高,但非成员函数让您能够交换两个类型不同的容器的内容。

下面来看 3 个具有代表性的 STL 函数:for_each()、random_shuffle() 和 sort()。for_each() 函数可用于很多容器类,它接受3个参数。前两个是定义容器中区间的迭代器,最后一个是指向函数的指针(更普遍地说,最后一个参数是一个函数对象,函数对象将稍后介绍)。for_each() 函数将被指向地函数应用于容器区间中的各个元素。被指向的函数不能修改容器元素的值。可以用 for_each() 函数来代替 for 循环。例如,可以将代码:

vector<Review>::iterator pr;
for (pr = books.begin(); pr != books.end(); pr++){
	ShowReview(*pr);
}

替换为:

for_each(books.begin(), books.end(), ShowReview);

这样可避免显式地使用迭代器变量。
Random_shuffle() 函数接受两个指定区间的迭代器参数,并随机排列该区间中的元素。例如,下面的语句随机排列books矢量中的所有元素:

random_shuffle(books.begin(), books.end() );

与可用于任何容器类的 for_each 不同,该函数要求容器类允许随机访问,vector类可以做到这一点。
sort()函数也要求容器支持随机访问。该函数有两个版本,第一个版本接受两个定义区间的迭代器参数,并使用为存储在容器中的类型元素定义的<运算符,对区间中的元素进行操作。例如,下面的语句按升序对 coolstuff 的内容进行排序,排序时使用内置的<运算符对值进行比较:

vector<int> coolstruff;
...
sort(coolstuff.begin(), coolstuff.end());

如果容器元素是用户定义的对象,则要使用 sort(),必须定义能够处理该类型对象的 operator<() 函数。例如,如果为 Review 提供了成员或非成员函数 operator<(),则可以对包含 Review 对象的矢量进行排序。由于 Review 是一个结构,因此其成员是公有的,这样的非成员函数将为:

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

有了这样的函数后,就可以对包含 Review 对象(如 books)的矢量进行排序了:

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

上述版本的 operator<() 函数按title 成员的字母顺序排序。如果title成员相同,则按照 rating 排序。然而,如果想按降序或是按 rating (而不是 title )排序,该如何办呢?可以使用另一种格式的 sort()。它接受 3 个参数,前两个参数也是指定区间的迭代器,最后一个参数是指向要使用的函数指针(函数对象),而不是用于比较的 operator<()。返回值可转换为 bool,false 表示两个参数的顺序不正确。下面是一个例子:

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

有了这个函数后,就可以使用下面的语句将包含 Review 对象的 books 矢量按 rating 升序排列:

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

注意,与 operator<() 相比,WorseThan() 函数执行的对 Review 对象进行排序的工作不那么完整。如果两个对象的 titile 成员相同,operator<() 函数将按 rating 进行排序,而 WorseThan() 将它们视为相同。第一种排序成为全排序(total ordering),第二种排序称为完整弱排序(strict weak ordering)。在全排序中,如果 a<b 和 b<a 都不成立,则 a 和 b 必定相同。在完整弱排序中,情况就不是这样了。它们可能相同,也可能只是在某方面相同,如 WorseThan() 示例中的 rating 成员。所以在完整弱排序中,只能说它们等价,而不是相同。

下面程序演示了这些 STL 函数的用法。

// vect3.cpp -- using STL functions
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>

struct Review{
    std::string title;
    int rating;
};

bool operator<(const Review & r1, const Review & r2);
bool worseThan(const Review & r1, const Review & r2);

bool FillReview(Review & rr);
void ShowReview(const Review & rr);

int main(){

    using namespace std;

    vector<Review> books;
    Review temp;
    while (FillReview(temp)){
        books.push_back(temp);
    }
    if(books.size()>0){
        cout << "Thanke you. You entered the following "
             << books.size() << " ratings:\n"
             << "Rating\tBook\n";
        for_each(books.begin(), books.end(), ShowReview);

        sort(books.begin(), books.end());
        cout << "Sorted by title:\nRating\tBook\n";
        for_each(books.begin(),books.end(),ShowReview);

        sort(books.begin(), books.end(), worseThan);
        cout << "Sorted by rating:\nRating\tBook\n";
        for_each(books.begin(), books.end(), ShowReview);

        random_shuffle(books.begin(), books.end());
        cout << "After shuffling:\nRating\tBook\n";
        for_each(books.begin(), books.end(), ShowReview);
    }
    else{
        cout << "No entries. ";
    }
    cout << "Bye.\n";

    return 0;
}

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

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

bool FillReview(Review & rr){
    std::cout << "Enter book title (quit to quit): ";
    std::getline(std::cin, rr.title);
    if(rr.title=="quit"){
        return false;
    }
    std::cout << "Enter book rating: ";
    std::cin >> rr.rating;
    if (!std::cin){
        return false;
    }
    // get rid of rest of input line
    while(std::cin.get() != '\n'){
        continue;
    }
    return true;
}

void ShowReview(const Review & rr){
    std::cout << rr.rating << "\t" << rr.title << std::endl;
}

程序运行情况如下:

Enter book title (quit to quit): The cat Who Can Teach You Weight Loss
Enter book rating: 8
Enter book title (quit to quit): The Dogs of Dharma
Enter book rating: 6
Enter book title (quit to quit): The Wimps of Wonk
Enter book rating: 3
Enter book title (quit to quit): FareWell and Delete
Enter book rating: 7
Enter book title (quit to quit): quit
Thanke you. You entered the following 4 ratings:
Rating  Book
8       The cat Who Can Teach You Weight Loss
6       The Dogs of Dharma
3       The Wimps of Wonk
7       FareWell and Delete
Sorted by title:
Rating  Book
7       FareWell and Delete
6       The Dogs of Dharma
3       The Wimps of Wonk
8       The cat Who Can Teach You Weight Loss
Sorted by rating:
Rating  Book
3       The Wimps of Wonk
6       The Dogs of Dharma
7       FareWell and Delete
8       The cat Who Can Teach You Weight Loss
After shuffling:
Rating  Book
3       The Wimps of Wonk
6       The Dogs of Dharma
8       The cat Who Can Teach You Weight Loss
7       FareWell and Delete
Bye

基于范围的 for 循环(C++11)

第5章说过,基于范围的 for 循环是为用于 STL 而设计的。为复习该循环,下面是第5章的第一个示例:

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

在这种 for 循环中,括号内的代码声明一个类型与容器存储的内容相同的变量,然后指出了容器的名称。接下来,循环体使用指定的变量依次访问容器的每个元素。例如,对于下述摘自程序的语句:

for_each(books.begin(), books.end(), ShowReview);

可将其替换为下述基于范围的 for 循环:

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

根据 book 的类型(vector<Review>),编译器将推断出 x 的类型为 Review,而循环将依次将 books 中的每个 Review 对象传递给 ShowReview()。

不同于 for_each(),基于范围的 for 循环可修改容器的内容,诀窍是指定一个引用参数。例如,加入有如下函数:

void InflateReview(Review &r) {
	r.rating ++;
}

可使用如下循环对 books 的每个元素执行该函数:

for (auto & x : books) InflateReview(x);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值