C++ 第十六章-string类和标准库模板

C++ 第十六章-string类和标准库模板

本章内容包括:

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

string类:

string类的构造函数

构造函数 描述
string(const char *) 将string对象初始化为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指向的传统字符串的前n个字符,即使超过了字符串长度
template string(Iter begin,Iter end) 将string对象初始化为区间[begin,end)内的字符,其中begin和end的行为就像指针,用于指定位置
string(const string & str, 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_listil) 这是C++11中新增的,它将一个string对象初始化为列表il中的字符

str1.cpp

#include <iostream>
#include <string>

int main(){
    using namespace std;
    string one("one");
    cout << one << endl;
    string two(20,'$');
    cout << two << endl;
    string three(one);
    cout << three << endl;
    one += " Oops!";
    cout << one << endl;
    two = "Sorry! That was ";
    three[0] = 'p';
    string four;
    four = two + three;
    cout << four << endl;
    char alls[] = "All's well that ends well";
    string five(alls,20);
    cout << five << "!\n";
    string six(alls + 6,alls + 10);
    cout << six << ", ";
    string seven(&five[6],&five[10]);
    cout << seven << "...\n";
    string eight(four,7,16);
    cout << eight << " in motion!" << endl;
    return 0;
}

构造函数string(string && str)类似于复制构造函数,导致新创建的string以str为副本。但与复制构造函数不同的是,它不保证将str视为const。这种构造函数称为移动构造函数。在有些情况下,编译器可能使用它而不是复制构造函数。

构造函数string(initializer_listil)能够让我们将列表初始化语法用于string类。也就是说,它使得下面的声明是合法的

string piano = {'n','b','b'};
string类输入:

对于类有很帮的另一点是,知道哪些输入方式可用。对于C-风格字符串,有3种方式:

char info[100];
cin >> info;
cin.getline(info,100);
cin.get(info,100);

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

string stuff;
cin>>stuff;
getline(cin,stuff);	

两个版本的getline()都有一个可选参数,用于指定哪个字符来确定输入边界:

cin.getline(info,100,':')
getline(stuff,':')

在功能上,它们之间的主要区别在于,string版本的getline()将自动调整目标string对象的大小,使之刚好存储输入字符;

这两个函数都自动调整string的大小,使之与输入匹配。但也存在一些限制。第一个限制因素是string对象的最大允许长度,由常量string::npos指定。这通常是最大的unsigned int 值,因此对于普通的交互式输入,这不会带来实际的限制;但是,如果尝试将整个文件的内容读取到单个string对象中,这可能成为限制因素。第二个限制因素是程序可以使用的内存量。

string版本的getline()函数从输出中读取字符,将其存储到目标string中,直到发生下面三种情况之一

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

输入流对象有一个统计系统,用于跟踪流的错误状态。在这个系统中,检测到文件尾后将设置eofbit寄存器,检测到输入错误将设置failbit寄存器,出现无法识别的故障(如硬盘故障)时将设置badbit寄存器,一切顺利的情况下将设置goodbit寄存器。

string版本的operator>>()函数的行为与次类似,它只是不断读取,直到遇到空白字符并将其留在输入队列中,而不是不断读取,知道遇到分界字符,然后将其丢弃。

strfile.cpp

#include <iostream>
#include <string>
#include <fstream>
#include <cstdlib>

int main(){
    using namespace std;
    ifstream fin;
    fin.open("/home/luslin/c++/d1/main.cpp");
    if (!fin.is_open())
    {
        cerr << "Can't open file. Bye.\n";
        exit(EXIT_FAILURE);
    }
    string item;
    int count = 0;
    getline(fin,item,':');
    while (fin)
    {
        ++count;
        cout << count << ": " << item << endl;
        getline(fin,item,':');
    }
    cout << "Done.\n";
    fin.close();
    return 0;
}
使用字符串:

现在,可以以不同的方式来创建string对象、显式string对象的内容、将数据读取和附加到string对象中,其他啊功能呢?

  • 比较字符串

    string类对6个关系运算符都进行了重载。按ASCII码对字符大小进行比较 < <= > >= == !=

  • 确定字符串长度

    size()和length()成员函数都能返回字符串中的字符数。

  • 搜索给定的子字符串或字符。

重载的find()方法

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

string库还提供类相关的方法,它们的重载特征与find()方法相同:

  • rfind()查找子字符串或字符最后一次出现的位置
  • find_first_of()在字符串中查找参数中任何一个字符首次出现的位置
  • find_last_of()在字符串中查找参数中任何一个字符最后出现的位置
  • find_first_not_of()在字符串中查找第一个不在参数中的字符的位置
  • find_flast_not_of()在字符串中查找最后一个不在参数中的字符的位置
智能指针模板类:

智能指针是行为类似于指针的类对象,但这种对象还有其他的功能。本节介绍三个可帮助管理动态内存分配的智能指针模板。先来看需要哪些功能及这些功能是如何实现的。下面的函数:

void remodel(std::string & str)
{
	std::string * ps = new std::string(str);
	...
	str = ps;
	return;
}

每当调用函数时,该函数都会分配堆中的内存,但从不回收,从而导致内存泄露;还有其他例子

void remodel(std::string & str)
{
	std::string * ps = new std::string(str);
	...
	if(weird_thing())
		throw exception();
	str = *ps;
	delete ps;
	return;
}

当出现异常时,delete将不执行,因此也将导致内存泄露。

当remodel()函数终止时,本地变量都将从栈内存中删除,因此指针ps占用的内存将被释放。但如果ps指向的内存也被释放掉就好了。如果ps有一个析构函数,该析构函数在ps过期时释放掉它指向的内存。因此,ps的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是auto_ptr、unique_ptr、 和shared_ptr背后的思想。

使用智能指针:

这三个智能指针模板(auto_ptr、unique_ptr、 和shared_ptr)都定义了类似指针的对象,可以将new获取的(直接或间接)赋值给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。

因此,要改造remodel()函数,分以下3部:

  • 包含头文件memory

  • 将指向string的指针替换成指向string的智能指针对象

  • 删除delete语句

    #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;
    }
    

smartptrs.cpp

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

class Reprt
{
private:
    std::string str;
public:
    Reprt(const std::string &s):str(s){
        std::cout << "object created" << std::endl;
    };
    ~Reprt(){ std::cout << "object deleted" << std::endl;}
    void comment(){std::cout << str << std::endl;}
};

int main(){
    using namespace std;
    {
        std::auto_ptr<Reprt> ps(new Reprt("using auto_ptr"));
        ps->comment();
    }
    {
        std::shared_ptr<Reprt> ps (new Reprt("using shared_ptr"));
        ps->comment();
    }
    {
        std:unique_ptr<Reprt> ps(new Reprt("using unique_ptr"));
        ps->comment();
    }
    return 0;
}

所有智能指针类都有一个explicit构造函数,该构造函数将指针作为参数。因此,不需要自动将转换为指针对象。

由于智能指针模板类的定义方式,智能指针对象的很多方面都类似于常规指针。例如ps是一个只能指针对象,则可以对它执行解除引用操作(*ps)、用它访问结构成员(ps->comment())、将它赋值给相同类型的常规指针。

有关智能指针注意事项:

auto_test.cpp

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


int main(){
    using namespace std;
    auto_ptr<string> films[5]{
        auto_ptr<string>(new string("f1")),
        auto_ptr<string>(new string("f2")),
        auto_ptr<string>(new string("f3")),
        auto_ptr<string>(new string("f4")),
        auto_ptr<string>(new string("f5")),
    };
    auto_ptr<string>pwin;
    pwin = films[2];
    cout << "films:" << endl;
    for (int i = 0; i < 5;i++){
        cout << *films[i] <<endl;
    }
    cout << "pwin:" << *pwin << endl;
    cin.get();
    return 0;
}
结果:
films:
f1
f2
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)

结果说明,错误的使用auto_ptr可能导致问题,pwin = films[2];将指针所有权从file[2]转移到了pwin,这导致films[2]不再引用该字符串。在auto_ptr放弃对象所有权后,便可能使用使用它来访问对象,这时,打印films[2]指向的字符串时,却发现这是一个空指针,导致意外。

如果将auto_ptr换成shared_ptr,将正常执行。shared_ptr使用引用计数。

unique_ptr为何优于auto_ptr:
auto_ptr<string> p1 (new string("p1"));
auto_ptr<string> p2;
p2 = p1;	

在最后一句中,p2接管了string对象的所有权后,p1的所有权将被剥夺。这是一件好事,可防止p1与p2的析构函数删除同一对象;但如果后面程序试图再访问p1,这将出现问题。因为p1不再指向有效数据。

当使用unique_ptr时

最后一句将非法不能通过编译,unique_ptr比auto_Ptr更安全(编译阶段错误比潜在的程序崩溃更安全)

但作为函数返回值时,将一个智能指针赋值给另一个并不会留下危险的悬挂指针。假设如下函数定义。

unique_ptr<string> demo(const char * s){
	unique_ptr<string> temp(new string(s));
	return temp;
}
unique_ptr<string> ps;
ps = demp("uniquely special");

demo()函数返回一个临时unique_ptr,然后ps接管了原本的unique_ptr,而却,编译器允许这种做法。

标准模板库:

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

模板类vector:

vect2.cpp

#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 namespace std;
    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;
        vector<Review> oldlist(books); // copy consturctor used
        for (pr = books.begin(); pr != books.end(); pr++)
            ShowReview(*pr);
        if (num > 3) {
            // remove 2 items
            books.erase(books.begin() + 1, books.begin() + 3);
            cout << "After erase:\n";
            for (pr = books.begin(); pr != books.end(); pr++)
                ShowReview(*pr);
            // insert 1 item;
            // insert 接受 3 个参数,第一个参数是插入位置,第二,第三个参数是另一个容器的一部分
            books.insert(books.begin(),oldlist.begin()+1,oldlist.begin()+2);
            cout << "After insert:\n";
            for (pr = books.begin(); pr != books.end(); pr++)
                ShowReview(*pr);
        }
    } else {
        cout << "Nothing entered,nothing gained";
    }
    return 0;
}

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

void ShowReview(const Review & rr){
    std::cout << rr.rating << '\t' << rr.title << '\n';
}
对矢量的其他操作:

vect3.cpp

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

struct Review{
    std::string title;
    int rating;
};
bool FillReview(Review & rr);
void ShowReview(const Review &rr);
bool operator<(const Review &r1, const Review &r2);
bool worseThan(const Review &r1, const Review &r2);

int main(){
    using namespace std;
    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_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 shuffing:\nRating\tBook\n";
        for_each(books.begin(),books.end(),ShowReview);
    } else {
        cout << "Nothing entered,nothing gained";
    }
    return 0;
}

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

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


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

bool worseThan(const Review &r1, const Review &r2){
    return  r1.rating < r2.rating;
}
基于范围的for循环:
for_each(books.begin(),books.end(),ShowReview);
可替换为
for(auto book : books) ShowReview

泛型编程:

泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而STL通过通用算法更近了一步。模板让这一切成为可能,但必须对元素进行设计

迭代器类型:

STL定义了5种迭代器,并根据所需要的迭代器类型对算法进行了描述。这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。例如,find()的原型与下面类似

template<class InputIterator, class T>
InputIterator find(InputIterator first,  InputIterator last, const T & value)

这指出,这种算法需要一个输入迭代器。同样,下面的原型指出排序算法需要一个随机访问迭代器;

template<class RandomAccessIterator>
void sort(RandomAccessIterator first,RandomAccessIterator last);

对于这5种迭代器,都可以执行解除引用操作(即为它们定义了*运算符),也可进行比较,看其是相等(使用==运算符,可能被重载了)还是不等(!=运算符,可能被重载了)。如果两个迭代器相同,则对它们执行解除引用操作得到的值相同。

  • 输入迭代器

    术语“输入”是从程序的角度说的,即来自容器的信息被视为输入,就像来自键盘的信息对程序来说来说是输入一样。因此,输入迭代器可被程序用来读取容器信息。具体来说,对输入迭代器解除引用能够使程序读取到容器的值,但不一定能让程序修改

  • 输出迭代器

    指信息从程序输出给容器的迭代器,解除引用能让程序修改容器值,而不能读取。

  • 正向迭代器

    与输出迭代器类似,正向迭代器只能使用++来遍历容器,所以它每次沿容器向前移动一个元素;然而,与输出迭代器不同的是,它总能按相同的顺序遍历一系列值。

  • 双向迭代器

  • 随机迭代器

概念、改进、模型:
  • 将指针用作迭代器

    迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是STL算法的接口,而指针是迭代器,引出STL算法可以使用指针来对基于指针的非STL容器进行操作。比如数组。

    假设Receipts是一个double数组,并且要按升序对它进行排列

    const int SIZE = 100;
    double Receipts[SIZE];	
    

    STL sort()函数接受指向容器第一个元素的迭代器和指向超尾的迭代器作为参数.&Receipts[0]是数组的第一个元素,&Receipts[SIZE]是数组最后一个元素后面的元素。因此,排序

    sort(Receipts,Receipts + SIZE);
    

    STL提供了一些预定义迭代器:

    int case[10]{6,4,1,23,5,6,8,1,4,5};
    vector<int> dice[10];
    copy(casts,casts+10,dice.begin());
    

    copy 的前两个参数表示复制范围,最后一个参数表示复制到的起始位置。前两个参数最好是输入迭代器,最后一个参数最好是输出迭代器。

    现在,假设要将信息复制到显示器上,如果有一个表示输出流的迭代器,则可以使用copy()。ST为这种迭代器提供了ostream_iterator模板

    #include <algorithm>
    #include <iterator>
    int nums[5] {4,12,5,24,2};
    sort(nums,nums+5);
    ostream_iterator<int,char> out_iter(cout," ");
    copy(nums,nums+5,out_iter);
    

    out_iter迭代器现在是一个接口,第一个模板参数指出了被发送给输出流的数据类型;第二个模板指出了输出流使用的字符串类型(另一个可能的值是wchar_t)。构造函数的第一个参数(这里是cout)指出了要使用的输出流,它也可以是文件输出流;最后一个参数是在发送给输出流每个数据项后显式的分隔符。

    可以这样使用迭代器:

    *out_iter++= 16
    

    对于常规指针,这意味着将15赋值给指针指向的位置,然后指针加1.但对于ostream_iterator,这意味着将15和由空格组成的字符串发送到cout管理的输出流中,并为下一个输出操作做好准备。iterator头文件还定义了一个istream_iterator模板,使istream输入可用作迭代器接口。它是一个输入迭代器的概念模型,可以使用两个istream_iterator对象来定义copy()的输入范围:

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

    与ostream_iterator相似,istream_iterator也使用两个模板参数,第一个参数指出要读取的数据类型,第二个参数表示输入流使用的数据类型。使用构造参数cin表示使用由cin管理的输入流,省略构造参数表示输入失败,因此,上述代码从输入流中读取,直到文件结尾、类型不匹配或出现其他输入故障为止。

其他有用的迭代器:
  • reverse_iterator

    对reverse_iterator执行增操作将导致它被递减。

    copyit.cpp

    #include <iostream>
    #include <vector>
    #include <iterator>
    int main(){
        using namespace std;
        int casts[10]{5,2,1,5,7,1,4,2,1,6};
        vector<int>dice(10);
        copy(casts,casts+10,dice.begin());
        cout << "let the dice be casts!\n";
        ostream_iterator<int,char> out_iter(cout," ");
        copy(dice.begin(),dice.end(),out_iter);
        cout << endl;
        cout << "Implicit use of reverse iterator.\n";
        copy(dice.rbegin(),dice.rend(),out_iter);
        cout << endl;
        cout << "Explicit use of reverse iterator.\n";
        vector<int>::reverse_iterator ri;
        for (ri = dice.rbegin();ri != dice.rend();ri++)
            cout << *ri << ' ';
        cout << endl;
        return 0;
    }
    
  • back_insert_iterator 将元素插入到容器尾部

  • front_insert_iterator 将元素插入到容器前端

  • insert_iterator没有这些限制

    inserts.cpp

    void output(const std::string & s){std::cout << s << " ";};
    int main(){
        using namespace std;
        string s1[4]{"fine","fish","fashion","fate"};
        string s2[2]{"busy","bats"};
        string s3[2]{"silly","singers"};
        vector<string> words(4);
        copy(s1,s1+4,words.begin());
        for_each(words.begin(),words.end(),output);
        cout << endl;
        copy(s2,s2+2,back_insert_iterator<vector<string>>(words));
        for_each(words.begin(),words.end(),output);
        cout << endl;
        copy(s3,s3+2,insert_iterator<vector<string>>(words,words.begin()));
        for_each(words.begin(),words.end(),output);
        cout << endl;
        return 0;
    }
    结果
    fine fish fashion fate 
    fine fish fashion fate busy bats 
    silly singers fine fish fashion fate busy bats 
    
容器种类:

序列的要求:

n表示整数,t表示类型为T的值,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指向的元素
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() void a.erase(a.begin()) list deque
a.pop_back() void a.erase(–a.end()) vector list deuque
a[n] T& *(a.begin()+n) vector deque
a.at(n) T& *(a.begin()+n) vector deque

a[n] 与a.at(n)都返回一个指向容器第n个元素的引用,区别在于at将执行边界检查,并引发out_of_range异常

  • vector

    前面有很多vector模板的例子,该模板是在vector头文件呢中声明的。简单地说,vector是数组的一种类表示,它提供了自动内存管理功能,可以动态地 改变vector对象的长度,并随着元素的增加和删除而增大和缩小。它提供了对元素的随机访问。在尾部添加和删除的时间是固定的,在头部或中间插入和删除元素的复杂度为线性时间

    除序列外,vector还是可反转容器概念的木星。增加了这两个类方法:rbegin()和rend(),前者返回一个指向反转序列的第一个元素的迭代器,后者返回反转序列的超尾序列。

    vector模板类是最简单的序列类型,除非其他类型的特殊优点能够更好地满足程序的要求,否则应默认使用这种类型

  • deque

    deque模板类(在deque头文件中)表示双端队列,在STL中,其实现类似于vector容器,支持随机访问。主要区别在于,从deque对象的开始位置插入和删除元素的时间是固定的,而不像vector那样是线性时间的。所以,如果多数操作发生在序列的起始和结束位置,则应考虑使用deque数据结构。

    为实现在deque两端执行插入和删除操作的时间为固定的这一目的,deque对象的设计比vector对象更为复杂。尽管两者都提供对元素的随机访问和在序列中执行线性时间的插入和删除操作,但vector容器执行这些操作时速度要快一些。

  • list

    list模板类(在list)头文件中)表示双向链表list与vector之间关键区别在于,list在链表中任意一个位置进行插入和删除的时间都是固定的。因此,vector强调的是随机访问,而list强调的是元素的快速插入和删除。

    除了序列和反转容器函数书外,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() 将连续相同的元素压缩为单个元素,复杂度为线性时间

    list.cpp

    void outint(int n){std::cout << n << " ";}
    int main(){
        using namespace std;
        list<int> one(5,2); // 创建 5 个 2 的list
        int stuff[5]{1,2,4,7,5};
        list<int>two;
        two.insert(two.begin(),stuff,stuff+5);
        int more[6]{6,4,2,6,2,12};
        list<int>three(two);
        three.insert(three.end(),more,more+6);
        cout << "List one: ";
        for_each(one.begin(),one.end(),outint);
        cout << endl << "List two: ";
        for_each(two.begin(),two.end(),outint);
        cout << endl << "List three: ";
        for_each(three.begin(),three.end(),outint);
        three.remove(2);
        cout << endl << "List three after remove 2: ";
        for_each(three.begin(),three.end(),outint);
        three.splice(three.begin(),one);
        cout << endl << "List three after splice one: ";
        for_each(three.begin(),three.end(),outint);
        cout << endl << "List one after splice : ";
        for_each(one.begin(),one.end(),outint);
        three.unique();
        cout << endl << "List three after unique: ";
        for_each(three.begin(),three.end(),outint);
        three.sort();
        three.unique();
        cout << endl << "List three after sort & unique: ";
        for_each(three.begin(),three.end(),outint);
        two.sort();
        three.merge(two);
        cout << endl << "Sorted two merged into three: ";
        for_each(three.begin(),three.end(),outint);
        cout << endl;
        return 0;
    }
    结果
    List one: 2 2 2 2 2 
    List two: 1 2 4 7 5 
    List three: 1 2 4 7 5 6 4 2 6 2 12 
    List three after remove 2: 1 4 7 5 6 4 6 12 
    List three after splice one: 2 2 2 2 2 1 4 7 5 6 4 6 12 
    List one after splice : 
    List three after unique: 2 1 4 7 5 6 4 6 12 
    List three after sort & unique: 1 2 4 5 6 7 12 
    Sorted two merged into three: 1 1 2 2 4 4 5 5 6 7 7 12 
    

    sort()、merge()、unique()方法还各自拥有接受另一个参数的版本,该函用来指定用于比较的函数。同样,remove()方法也有一个接受另一个参数的版本,该参数用于指定确定是否删除元素的函数。这些参数都是谓词函数,稍后介绍。

  • forward_list

    C++11新增了容器类forward_list,它实现了单链表。在这种链表中,每个节点都只链接到下一个节点,而没有链接到前一个节点

  • queue

    queue模板类(在头文件queue中声明)是一个适配类。由前所述,ostream_iterator模板就是一个适配器,让输出流能够使用迭代器接口。同样,queue模板让底层类(默认是deque)再是典型的队列接口

    queue模板的限制比deque更多。它不仅不允许随机访问队列元素,甚至不允许遍历队列。它把使用限制在定义队列的基本操作上。可以将元素添加到队尾、从队首删除元素、查看队首和队尾的值、检查元素数目和测试队列是否为空。

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

    prioity_queue模板类(在queue文件中声明)是另一个适配类,它支持的操作与queue相同。两者之间的主要区别在于,在prioity_queue中,最大的元素被移到队首。内部区别在于,默认的底层类是vector。可以修改用于确定哪个元素放到队首的比较方式,方法是提供在一个可选的构造函数参数:

    prioity_queue<int> pq1;
    prioity_queue<int> pq2(greater<int>);
    

    greater<>()函数是一个预定义的函数对象,稍后讨论

  • stack

    与queue相似,stack(在头文件stack中声明)也是一个适配器类,它给底层类(默认是vector)提供了典型的栈接口

    方法 说明
    bool empty() const 如果栈为空,返回true
    size_type size() const 返回栈中元素个数
    T& top() 返回指向栈首元素的引用
    void push(const T& t) 在栈顶插入t
    void pop() 删除栈顶元素
  • array

    类模板array是头文件array中定义的,它并非STL容器,因为其长度是固定的。因此,array没有定义调整容器大小的操作,如push_back()和insert(),但定义了对它来说有意义的成员函数,如operator[] ()和at()。可以将很多标准STL算法用于array对象,如copy(),for_each().

关联容器:

关联容器是对容器概念的另一个改进。关联容器将值与键关联在一起,并使用键来查找值。对于容器X,表达式X::value_type 通常指出了存储在容器中的值类型。对于关联容器来说,表达式X::key_type 指出键的类型

STL提供4种关联容器:set、multiset、map和multimap。前两种在头文件set中定义,而后两种定义在头文件map中

  • set

    值类型与键相同,键是唯一的,值就是键。

    set<string> A;
    

    第二个模板参数是可选的,可用于指示用来对键进行排序的比较函数或对象,默认情况下,将使用模板less<>(稍后讨论)

    set<string,less<string>> A;
    

    请看下面代码:

    const int N = 6;
    string s1[N]{"buff","think","for","for","for","heavy"};
    set<string> A(s1,s1+N);
    ostream_iterator<string,char> out(cout," ");
    copy(A.begin(),A.end(),out);
    结果
    buff for heavy think
    

    结果表明键是唯一,且集合被排序

    setops.cpp

    int main(){
        using namespace std;
        const int N = 6;
        string s1[N]{"buff","thinkers","for","heavy","can","for"};
        string s2[N]{"metal","any","food","elegant","deliver","for"};
    
        set<string> A(s1,s1+N);
        set<string> B(s2,s2+N);
        ostream_iterator<string,char> out(cout," ");
        cout << "Set A: ";
        copy(A.begin(),A.end(),out);
        cout << endl << "Set B: ";
        copy(B.begin(),B.end(),out);
        cout << endl << "Union of A and B: ";
        // 求两个集合的并集
        set_union(A.begin(),A.end(),B.begin(),B.end(),out);
        cout << endl << "Intersection of A and B: ";
        // 求两个集合的交集
        set_intersection(A.begin(),A.end(),B.begin(),B.end(),out);
        cout << endl << "Difference of A and B: ";
        // 求两个集合的差集(A结合减去A与B的交集)
        set_difference(A.begin(),A.end(),B.begin(),B.end(),out);
    
        set<string> C;
        cout << endl << "Set C: ";
            set_union(A.begin(),A.end(),B.begin(),B.end(),insert_iterator<set<string>>(C,C.begin()));
        copy(C.begin(),C.end(),out);
    
        string s3("grungy");
        C.insert(s3);
        cout << endl << "Set C after insertion: ";
        copy(C.begin(),C.end(),out);
        cout << endl << "Showing a range: ";
        // lower_bound返回大于ghost 的迭代器,upper_bound返回第一个小于spook的迭代器
        copy(C.lower_bound("ghost"),C.upper_bound("spook"),out);
        cout << endl;
        return 0;
    }
    结果
    Set A: buff can for heavy thinkers 
    Set B: any deliver elegant food for metal 
    Union of A and B: any buff can deliver elegant food for heavy metal thinkers 
    Intersection of A and B: for 
    Difference of A and B: buff can heavy thinkers 
    Set C: any buff can deliver elegant food for heavy metal thinkers 
    Set C after insertion: any buff can deliver elegant food for grungy heavy metal thinkers 
    Showing a range: grungy heavy metal 
    
  • multimap

    int main(){
        using namespace std;
        typedef int KeyType;
        typedef pair<const KeyType,string> Pair;
        typedef multimap<const KeyType,string> MapCode;
    
        MapCode codes;
        codes.insert(Pair(415,"San"));
        codes.insert(Pair(415,"Okland"));
        codes.insert(Pair(510,"Staten"));
        codes.insert(Pair(510,"Berk"));
        codes.insert(Pair(510,"Jess"));
    
        cout << "Number of cities 415: " << codes.count(415) << endl;
        cout << "Area Code\tCity\n";
        MapCode::iterator it;
        for(it=codes.begin();it!=codes.end();++it)
            cout << "\t" << (*it).first << "\t" << (*it).second << endl;
        cout << "Area Code 510\tCity\n";
        auto range = codes.equal_range(510);
        for(it=range.first;it!= range.second;++it)
            cout << "\t" << (*it).first << "\t" << (*it).second << endl;
        return 0;
    }
    
    

函数对象:

很多STL算法都使用函数对象—也叫函数符。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象(即定义了函数operator()()的类)

class Linear
{
private:
	double slope;
	double y0;
public:
	Linear(double s1_ = 1,double y_ = 0):slope(s1_),y0(y_){};
	double operator()(double){return y0 + slope *x;}
}

这样,重载的()运算符将使得能够像函数那样使用Linear对象:

 Linear f1;
 Linear f2(2.5,10.0);
 double y1 = f1(12.0); 
 double y2 = f2(4.5);

其中 y1 将使用表达式0+1 × 12.0 ,y2将使用表达式 10.0 + 4.5 × 2.5计算

函数for_each

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

通常第3个参数可以是函数符。实际上,这提出了一个问题:如何声明第3个参数?不能把它声明为函数指针,因为函数指针指定了参数类型。由于容器可以包含任意类型,所以预先无法知道应使用哪种参数类型。STL通过模板解决了这个问题。for_each的原型看上去就像这样:

template<class Inputerator,class Function>
Function for_each(Inputerator first,Inputerator last,Function f)

ShowReview()的原型如下:

void ShowReview(const Review &)

这样,标识符ShowReview 的类型为void(*)(const Review &),这也是赋给模板参数Function的类型。对于不同的函数调用,Function参数可以表示具有重载的()运算符

函数符概念:
  • 生成器:不用参数就可以调用的函数符
  • 一元函数:用一个参数可以调用的函数符
  • 二元函数:用两个参数可以调用的函数符

当然,这些概念都有相应的改进版

  • 返回bool值的一元函数是谓词
  • 返回bool值的二元函数是二元谓词

fuctor.cpp

#include <iostream>
#include <string>
#include <algorithm>
#include <list>

template <class T>
class TooBig
{
private:
    T cutoff;
public:
    TooBig(const T & t) : cutoff(t){};
    bool operator()(const T & v) { return v > cutoff;}
};
void outint(int n) {std::cout << n << " ";}
int main(){
    using namespace std;

    TooBig<int> f100(100);
    int vals[10] = {50,100,90,180,60,210,415,88,188,201};
    list<int> tadatada(vals,vals+10);
    list<int> etctera(vals,vals+10);
    cout << "Original list:\n";
    for_each(tadatada.begin(),tadatada.end(),outint);
    cout << endl;
    for_each(etctera.begin(),etctera.end(),outint);
    cout << endl;
    // remove_if 接受谓词,如果返回值为true,将删除该对象
    tadatada.remove_if(f100);
    etctera.remove_if(TooBig<int>(200));
    cout << "Trimmed list:\n";
    for_each(tadatada.begin(),tadatada.end(),outint);
    cout << endl;
    for_each(etctera.begin(),etctera.end(),outint);
    cout << endl;
    return 0;
}
结果
Original list:
50 100 90 180 60 210 415 88 188 201 
50 100 90 180 60 210 415 88 188 201 
Trimmed list:
50 100 90 60 88 
50 100 90 180 60 88 188 
预定义的函数符:

STL定义了多个基本函数,它们执行诸如将两个值相加、比较两个值是否相等操作。提供这些函数对象是为了支持将函数作为参数的STL函数。例如,考虑函数transform()。它有两个版本。第一个版本接受4个参数,前两个参数是指定容器区间的迭代器,第3个参数是指定将结果复制到哪里的迭代器,最后一个参数是一个函数符,它将被应用于区间中的每个元素,生成结果中的新元素。

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

上述代码计算每个元素的平方根,并将结果发送到输出流。目标迭代器可以位于原始区间中。例如,将out换成gr8.begin()后,将覆盖掉原来的值。

第二个版本使用一个接受两个参数的函数,并将该函数用于两个区间中元素。它用另一个参数标识第二个区间的起始位置,如m8是vector对象,mean(double,double)返回两个值的平均值

transform(gr8.begin(),gr8.end(),mr8.begin(),out,mean);

现在假设要将两个数组相加。不能将+作为参数,因为对于类型double来说,+是内置运算数,而不是函数。可以定义一个将两个值相加的函数add()

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

然而,这样必须为每种类型单独定义一个函数。更好的办法是定义一个模板。头文件functional定义了多个模板类函数对象,其中包括plus<>().

transform(gr8.begin(),gr8.end(),mr8.begin(),out,plus<double>());

运算符和相应的函数符

运算符 相应的函数符
+ plus
- minus
* mulyiplies
/ divides
% modulus
- negate(否定)
== equal_to
!= not_equal_to
> greater
< less
>= greater_equal
<= less_equal
&& logical_and
|| logical_or
! logical_not
自适应函数符和函数适配器:

例如,假设要将矢量gr8的每个元素都增加2.5倍,则需要使用一个一元函数参数的transform()版本,就像前面的例子

transform(gr8.begin(),gr8.end(),out,sqrt);

multiplies()函数符可以执行乘法运行,但它是二元函数。因此需要一个函数适配器,将接受两个参数的函数符转换为接受1个参数的函数符,STL使用bind1st 和bind2nd类自动完成这一过程。它们将自适应二元函数转换为自适应一元函数

bind1st(f2,val)f1;

这样,使用单个参数调用f1(x),返回的值将与val作为第一参数、将f1()的参数作为第二参数调用f2()返回的值相同,即f1(x)等价于f2(val,x)

funadap.cpp

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <functional>

void Show(double);
const int LIM = 6;
int main(){
    using namespace std;

    double arr1[LIM]{28,29,30,35,38,59};
    double arr2[LIM]{63,65,69,75,80,99};

    vector<double> gr8(arr1,arr1+LIM);
    vector<double> m8(arr2,arr2+LIM);
    cout.setf(ios_base::fixed);
    cout.precision(1);
    cout << "gr8:\t";
    for_each(gr8.begin(),gr8.end(),Show);
    cout << endl << "m8: \t";
    for_each(m8.begin(),m8.end(),Show);
    cout << endl;

    vector<double> sum(LIM);
    transform(gr8.begin(),gr8.end(),m8.begin(),sum.begin(),plus<double>());
    cout << "sum:\t";
    for_each(sum.begin(),sum.end(),Show);
    cout << endl;

    vector<double> prod(LIM);
    transform(gr8.begin(),gr8.end(),prod.begin(),bind1st(multiplies<double>(),2.5));
    cout << "prod:\t";
    for_each(prod.begin(),prod.end(),Show);
    cout << endl;
    return 0;
}


void Show(double v)
{
    std::cout.width(6);
    std::cout << v << ' ';
}
结果
gr8:	  28.0   29.0   30.0   35.0   38.0   59.0 
m8: 	  63.0   65.0   69.0   75.0   80.0   99.0 
sum:	  91.0   94.0   99.0  110.0  118.0  158.0 
prod:	  70.0   72.5   75.0   87.5   95.0  147.5 

猜你喜欢

转载自blog.csdn.net/luslin1711/article/details/102489160