C++ Primer plus学习笔记-第十六章:string类和标准模板库

第十六章:string类和标准模板库

前言:这一章已经相当靠近全书的后面部分了;这一章我们会深入探讨一些技术上的细节,比如string的具体构造函数,比如适用于string类的几个函数,比如我们还会介绍一下标准模板库STL的一些细节;后面还会涉及智能指针的具体实现方式,总之这一章将会对细节有相当程度的深入;

1.string类

这个类是头文件string支持的,实际上是模板basic_string<char>的一个具体化,同时省略了和内存管理相关的参数,下面让我们看看string类的七个构造函数:

构造函数描述
string(const char * s)将string对象初始化为C风格字符串S
string(size_type n,char c)创建一个包含n个元素的string对象,每个对象都是字符c
string(const string & str)使用参数列表中的str初始化string对象
string()默认构造函数,创造一个空的string对象
string(const char * s,size_type n)使用字符串s的前n个元素初始化string对象
template<class Iter>string(Iter begin, Iter end)将string对象初始化为区间[begin,end)内的字符
string(const string & str,string size_type pos = 0, size_type n = npos)将string对象初始化为从pos到结尾或从pos开始的n个字符
string(string && str)noexcept和第三个函数相同,但是可能修改原函数,是移动构造函数
string(initializer_lis<char> il)将string对象初始化为il列表中的字符

另外string还重在了基本运算符,也就是说可以像普通数据类型一样使用==来比较两个对象是否相等,也可以使用=赋值,使用+链接等;

另外你可能对最后一个构造函数有一点懵逼,什么是列表?实际上我们早在前面就接触过了它:

string piano_man = {'L','i','s','z','t'};
string comp_lang = {'L','i','s','p'};

另外我们还需要比较一下普通字符串和string的输入区别,首先让我们写一点代码:

 char info[100];
cin >> info;
cin.getline(info,100);//读一行不保留换行符
cin.get(info,100);//读一行保留换行符

string stuff;
cin >> stuff;//遇到空白符之前一直输入,空白符被留在输入队列中
getline(cin,stuff);//这是一个单独的函数,使用cin作为输入源,使用stuff接受

cin.getline(info, 100, ':');//使用:作为分隔符
getline(stuff, ':')//使用stuff接收默认从cin输入,认定:为分隔符

string的最大长度是头文件string中定义的string::npos,它的大小通常是unsigned int的最大值;string版本的getline()函数将从输入中读入字符并将其存储到对象string中,除非发生:

  • 到达文件尾,此时eofbit将被设置,方法fail()和eof()都会返回true
  • 遇到分解字符,这种情况会将该分界字符从输入流中删除但是不存储它
  • 读取达到最大值,是string:npos和最大允许内存中较小的一个,设置failbit,方法fail()将返回true

输入流对象有一个统计系统,检测到文件尾后将设置eofbit寄存器,检测到输入错误时设置failbit寄存器,出现无法识别的故障时设置badbit寄存器,一切顺利时设置goodbit寄存器;

下面的程序是一个从文件中读取数据的例子:

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

int main()
{
    using namespace std;
    ifstream fin;
    fin.open("tobuy.txt");
    if (fin.is_open() == false)
    {
        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;
}

和源文件位于同一个目录下时,打开文件只需要文件名本身就足够了,但是要打开其他位置的文件就必须使用路径的全名,比图C::\\CPP\\Progs||tobuy.txt,因为双斜杠转义之后是单斜杠;

string对象中还内置了查找的方法,大小的判断依据是:

  • 如果在机器排列序列中,一个对象位于另一个对象的前面,则视为前者小于后者;如果机器排列顺序为ASCII码,则数字将小于大写字符,二大写字符小于小写字符;

可以对string使用比较符号,还可以使用string对象.length()string对象.size()计算对象的长度;

这是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位置开始查找往后n个字符的范围,查找s并返回第一次出现的位置;查找失败返回string::npos;
size_type find(char ch, size_type pos = 0)const从pos位置开始查找字符ch第一次出现的位置;查找失败返回string::npos;

当然关于string的查找函数并不只这么些,比如:

  • rfind()方法和find()类似只不过查找最后一次出现的位置
  • find_first_of()查找参数中任何一个字符第一次出现的位置
  • find_last_of()查找参数中任何一个字符最后一次出现的位置
  • find_first_not_of()查找不在参数中的任何一个字符第一次出现的位置
  • find_last_not_of()查找不在参数中的任何一个字符最后一次出现的位置

另外还有两个特殊的函数:

  • capacity()返回当前分配给字符串的内存块的大小
  • reserve()让用户能够请求内存块的最小长度

为了详细阐明这两个函数的作用,我们需要写一段测试用的代码:

#include<iostream>
#include<string>

int main()
{
    using namespace std;
    string empty;
    string small = "bit";
    string larger = "Elephants are a girl's best friend";
    cout << "Sizes:\n";
    cout << "\tempty:" << empty.size() << endl;
    cout << "\tsmall:" << small.size() << endl;
    cout << "\tlarger:" << larger.size() << endl;
    cout << "Capacities:\n";
    cout < "\tenpty:" << empty.capacity() << endl;
    cout < "\small:" << empty.capacity() << endl;
    cout < "\tlarger:" << empty.capacity() << endl;
    empty.reserve(50);
    cout << "Capacity after empty.reserve(50):" << enpty.capacity() << endl;
    return 0;
}

下面是输出:

Sizes:
	empty: 0
    small: 3
    larger: 34
Capacities:
	empty: 15
    small: 15
    larger: 47
Capacity after empty.reserve(50): 63

另外,文件对象的open()方法只能使用C风格字符串,因此要对string调用string.c_str()方法让它返回一个C风格字符串;

string类是基于char类型的具体化,因为它的模板是这样的:

template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT>>
basic_string{...};

并且它有4个具体化:

typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string;
typedef basic_string<char32_t> u32string;

2.智能指针模板类

智能指针是行为和指针类似的类对象,它存在的意义是防止开发者忘记将申请到的内存释放,因此可以避免内存泄漏;智能指针类共有三种,分别是auto_ptrunique_ptrshared_ptr;三种智能指针的使用方法是相同的,唯一的区别只体现在当多个指针对象同时指向一个堆中的内容时;

要使用智能指针必须包含头文件<memory>,比如这是模板auto_ptr的构造函数:

template<class X> class auto_ptr 
{
public:
    explicit auto_ptr(X * p = 0) throw();//该函数不会引发异常
    ...
};

比如让我们创建几个auto_ptr对象:

auto_ptr<double> pd(new double);//使用指向double的内存地址来初始化智能指针对象
auto_ptr<string> ps(new string);

另外两种智能指针对象的使用方法是相同的:

unique_ptr<double> pdu(new double);
shared_ptr<string> pss(new string);

智能指针虽然是一个类对象,但对绝大多数运算符都进行了重载:

  • 可以使用*进行解引用
  • 可以使用->访问结构体成员
  • 可以赋值给常规指针
  • 可以赋值给另一个智能指针对象(实现上有不同)

另外还有一点要注意,智能指针的自动回收只是针对堆内存,不要对栈内存使用自动回收!

所有权是auto_ptr和umique_ptr的实现方式,也就是说可能有多个智能指针同时指向同一个堆中的对象,但是只有其中拥有所有权的那个不可以用来访问对象而其它都可以;引用计数是shared_ptr的实现方式,这在其它高级语言中也有应用,也就是说只有引用次数从1到0变化时才会释放内存;

auto_ptr和unique_ptr的区别是,后者对于某个内存块多次释放的错误能够在编译中发现,而前者就只能等到程序崩溃;不过这两者都比不上使用引用计数的shared_ptr智能指针,它是绝对稳定安全的;

并不是所有的编译器都提供了对unique_ptr的支持,如果没有支持你可以换用BOOST库的scoped_ptr;

注意:auto_ptr只能使用new和delete,不能使用new[]和delete[];而另外两种指针都是可以使用new[]和delete[]的;

3.标准模板库

标准模板库STL提供了一组关于容器,迭代器,函数对象和算法的模板;容器和数组类似,存储的都是同质的数据;迭代器能够用来遍历数组对象,与能够遍历数组的指针类似,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针;

STL引出的是一种新的编程方式:泛型编程;

模板类vector是类似于数组的类,具体而言是这样使用的:

#include<vector>
using namespace std;
vector<int> rating(5);//构建一个基于vector模板的数组,元素是int,一共有五个元素;
int n;
cin >> n;
vector<double> scores(n);//创建一个有n个数据的double类型数组;


//遍历元素
ratings[0] = 9;
for (int i = 0;i < n; i++)
    cout << scores[i] << endl;

实际上每个模板都接受一个这样的参数,它被成为分配器,是管理内存的工具;该参数指定使用哪个分配器来管理内存,而我们之前使用的new和delete实际上是基于默认分配器进行的;完整的模板定义是这样的:

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

模板的特征是通用行,这些函数每种模板都具有:

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

迭代器是个广义的指针,支持的操作有:

  • 使用*符号解引用
  • 使用++递增来指向下一个元素

每个容器都有它们自己的迭代器类型变量,这样声明一个迭代器变量:

vector<double>::iterator pd;
vector<double> scores;

pd = scores.begin();

*pd = 22.3;
++pd;

另外实际上我们也可以使用自动推断变量类型的特性:

vector<double>::iterator = pd;
auto pd = scores.begin();//使用自动推断

我们前面提到过“超过结尾迭代器”,它的意思是这个迭代器是指向最后一个元素的;方法容器.end()可以获得这样一个迭代器;

关于容器,还有一些方法我们之前没有提到:

  • push_back()将参数中的元素添加在末尾并负责内存管理
  • erase()接受两个迭代器参数,删除[begin,end)之间的元素
  • insert()接受三个参数,第一个迭代器参数指定了要插入的区间,第二和第三个迭代器参数指定了要插入的内容;

下面这个程序演示了size(),begin(),end(),push_back(),erase()insert()的用法:

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

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

bool FillReviev(Reviev & rr);
bool ShowReview(const Reviev & rr);

int main()
{
    using std::cout;
    using std::vector;
    vector<Reviev> 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);
        if (num > 3)
        {
            books.erase(books.begin()+1,books.begin() + 3);
            cout << "After erasure:\n";
            for (pr = books.begin();pr != books.end();pr++)
                ShowReview(*pr);
            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;
    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 Knew Vectors
Enter book rating:5
Enter book title (quit to quit):Candid Canines
Enter book rating:7
Enter book title (quit to quit):Warriors of Wonk
Enter book rating:4
Enter book title (quit to quit):Quantun Manners
Enter book rating:8
Enter book title (quit to quit):quit
Thank you.You entered the following:
Rating Bok:
5	The Cat Who Knew Vectors
7	Candid Canines
4	Warriors of Wonk
8	Quantun Manners
Reprising:
Rating Bok:
5	The Cat Who Knew Vectors
7	Candid Canines
4	Warriors of Wonk
8	Quantun Manners
After erasure:
5	The Cat Who Knew Vectors
8	Quantun Manners
After insertion:
7	Candid Canines
5	The Cat Who Knew Vectors
8	Quantun Manners
Swapping oldist with books:
5	The Cat Who Knew Vectors
7	Candid Canines
4	Warriors of Wonk
8	Quantun Manners

下面让我们看看STL中有代表性的3个函数:

  • for_each()接受三个参数,前两个是迭代器第三个是指针,对迭代器之间的参数全部调用第三个参数指定的函数(被指定的函数不能修改容器元素的值!)
  • random_shuffle()接受两个迭代器参数,将两个迭代器之间的参数乱序排列
  • sort()有两个版本,第一个接受两个迭代器使用元素的<运算符为元素升序排序;第二个版本接受三个参数,前两个是迭代器第三个是指向函数的指针;函数的作用是充当<运算符(将结果反置就能实现降序排序)

另外我们前面还提到过另一种for循环的写法:

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

这种写法是可以修改参数中的值的,而函数for_each()是没有办法修改值的;

4.泛型编程

STL是一种泛型编程;对象编程关注的是数据方面,而泛型编程关注的是算法;它们之间的共同点是抽象和创建可重用的代码,但两者的理念是绝然不同的;泛型编程旨在编写独立于数据类型的代码,在C++中完成的工具是模板;

迭代器的作用是提供一个遍历某种数据结构的工具,虽然多种数据结构在实现上是不同的,但通过提供迭代器这一接口,我们可以编写一套通用的算法,而该算法可以接受不同数据结构的迭代器作为参数;

一个迭代器要有这些特点:

  • 能够被执行解引用操作,也就是说要对*解引用运算符进行重载
  • 能够将一个迭代器赋值给另一个,也就是说要重载赋值运算符
  • 相同数据结构的迭代器能够使用关系运算符,也就是说要对==和!=进行定义
  • 要能够遍历所有元素,至少要对++进行重载(注意++有前缀和后缀两种形式)

比如我们可以这样针对一个链表数据结构编写迭代器类:

struct Node
{
    double item;//本单元存储的数据
    Node * p_next;//指向下一个单元
};

class iterator
{
private:
    Node * pt;
public:
    iterator() : pt(0) {}
    iterator (Node * pn) : pt(pn) {}
    double operator*() {return pt->item;}
    iterator & operator++()//++运算符的前缀版本
    {
        pt = pt->p_next;
        return *this;
    }
    iterator & operator++(int)//++运算符的后缀版本
    {
        iterator tmp = *this;
        pt = pt->p_next;
        return tmp;
    }
    ...//继续重载其它运算符
};

另外,迭代器还应该增加一个超尾元素,这样可以通过和超尾元素进行比较来判断迭代是否已经进行到底;这是一种应用:

list<double>::iterator pr;
for (pr = scores.begin(); pr != scores.end(); pr++)
    cout << *pr << endl;

//使用自动类型推断
for (auto pr = scores.begin(); pr != scores.end(); pr++)
    cout << *pr << endl;

作为一种编程风格,最好避免直接使用迭代器,而尽可能使用STL函数;也可以使用C++11新增的基于范围的for循环:

for (auto x : scores) cout << x << endl;

不同的算法对迭代器有不同的要求,我们设计的目的是尽量让算法使用低级的迭代器,比如:

  • 查找算法需要定义++运算符,以便迭代器能够遍历整个容器,它要求读数据却不要求写数据;
  • 排序算法要求能够随机访问,以便交换两个不相邻的元素;如果iter是一个迭代器,则可以通过定义+运算符来实现随机访问,另外还要求能够写数据;

STL定义了5种迭代器,分别是输入迭代器(可以读取容器但是不能写入数据),输出迭代器(可以向容器写入数据但是不能读取),正向迭代器(可以从前往后访问,可以写但是只能从前往后访问),双向迭代器(可以读写并且可以向前向后访问),随机访问迭代器(可以读写,可以随机访问);

它们的层次结构是:

功能更强大
功能更强大
功能更强大
功能更强大
输入迭代器
输出迭代器
正向迭代器
双向迭代器
随机访问迭代器

实际上指针就可以用作迭代器,并且像浮点整型这样的数据本身有自己的<运算符,就可以使用sort()来排序,就像这样:

sort(Receipts, Receipts + SIZE);

另外函数copy()也是可以通用的,它的作用是复制数据;接受三个参数,前两个是迭代器指定要复制的内容,第三个迭代器是接受内容的目的地;

引出一个新概念:适配器;适配器的作用是,将一些其它接口转换为STL使用的接口;可以通过包含头文件<iterator>来实现:

#include <iterator>
...
ostream_iterator<int, char> out_iter(cout, " ");//输出的是int数据类型,输出形式是char字符;输入之间使用空格相隔;
//我们定义了一个接口,任何试图向这个接口写数据的操作都会导致数据显示在显示器上
//实际上对于自己定义的数据类型,可以使用自己定义的输出函数

解引用后,这个迭代器就相当于是cout;比如可以这样使用这个迭代器:

*out_iter++ = 15;
//相当于cout << 15;

有了这个接口,我们可以这样将内容输出到显示器:

copy(dice.begin(), dice.end(), out_iter);//使用out_iter接收数据写入
copy(dice.begin(), dice.end(), ostream_iterator<int, char>(cout, " "))//也可以使用匿名适配器接受数据写入

另外还可以定义istream_iterator适配器:

copy(istream_iterator<int, char>(cin),istream_iterator<int, char>(), dice.begin());
//从cin获得数字输入,转换为字符串形式后将输入写入dice.begin()迭代器中

在头文件<iteration>中有这几个迭代器:reverse_iterator,back_insert_iterator,front_insert_iteratorinsert_iterator;

如果我们需要反向打印容器的内容,可以使用反向迭代器:

copy(dice.rbegin(), dice.rend(), out_iter);
//反向迭代器的rbegin()指向超尾,并且执行++操作时是向左访问的
//同理,rend()迭代器指向开头

注意:rbegin()虽然和end()指向的是同一个位置,但它们表示的值是不同的;另外还有一点,rbegin()指向的是超尾,因此实际上是不能直接解引用的,应当先递减再解引用

现在具体看看另外三种插入迭代器:

  • back_insert_iteration迭代器将元素插入到容器的尾部,不覆盖原来的数据并且自动进行内存分配
  • front_insert_iteration将元素插入容器的最前端
  • insert_iteration将元素查到insert_iteration构造函数的参数指定的位置前面

上面三个迭代器都是输出容器概念的模型,它们的构造函数都以实际的容器标识符为参数;也就是说比如可以这样做:

back_insert_iterator<vector<int>> back_insert(dice);

必须声明容器的原因是,迭代器必须使用合适的容器方法;比如back_insert_iteration的构造函数就假设传递给它的类型有一个push_back()方法;

另外对于insert_iteration迭代器,参数列表中除了传入实际类型标识符以外还要传入一个指示插入位置的参数:

insert_iteratior<vector<int>> insert_iter(dice, dice.begin());

容器是有很多种类的,我们现在看看容器们都有哪些共同的特征;下面的表格中使用X表示容器类型,使用T表示存储在容器中的对象类型,a和b表示类型为X的值;r表示类型为X&的值,u表示类型为X的标识符:

表达式返回类型说明复杂度
X::iterator指向T的迭代器类型满足正向迭代器要求的任何迭代器编译时间
X::value_typeTT的类型编译时间
X u;创建一个名为u的空容器固定
X();创建一个匿名的空容器固定
X u(a);调用复制构造函数后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可转换为bool两个容器进行不等性比较,返回布尔值线性

我们注意到表格中有三种复杂度,分别是编译时间,固定和线性;具体而言,它们的时间成本是这样的:

编译时间
固定
线性

另外C++11新增了一些容器功能,下面使用rv表示类型为X的非常量右值,另外该表还要求X::iterator是正向迭代器或更高;这是表格本体:

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

可以通过添加要求来改进基本容器的概念,比如序列就是这样一种容器;这些都是序列:deque,forward_list,list,queue,priority_queue,stack,vector这些是所有序列容器都满足的标准功能:

表达式返回类型说明
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)voida.insert(a.begin(), t)vector,deque
a.push_back(t)voida.insert(a.end(), t)vector,list,deque
a.pop_front(t)voida.erase(a.begin())vector,deque
a.pop_back(t)voida.erase(a.--end())vector,list,deque
a[n]T&*(a.begin()+n)vector,deque
a.at(t)T&*(a.begin()+n)vector,deque

下面我们详细看看这几种容器:

  • vector:最简单的序列类型,朴实无华没有特点;是数组的一种类表示,有自动内存管理功能;在尾部添加删除元素是时间固定的,在头部添加删除元素是时间线性的;另外它还是可反转容器,支持rbegin()和rend()迭代器;
  • deque:表示双端队列,全名是double-end queue;支持随机访问,从两端插入和取出数据消耗的时间都是固定的
  • list:双向链表,可以双向遍历;在任何位置插入或删除数据的时间成本都是固定的,但是不支持随机访问;迭代器是按照“从前往后数/从后往前数n个元素”来记录所指向的位置的,因此向链表插入或删除数据时可能导致在不修改迭代器本身的情况下使迭代器解引用的值发生变化;

下面的表格是list链表容器特有的函数:

函数说明
void merge(list<T, Alloc>&x)将链表x和调用链表合并;两个链表必须已经排序;合并后的经过排序的链表保存在调用链表中,将x清空;复杂度为线性时间;
void remove(const T & val)从链表中删除val的所有实例;函数复杂度为线性时间;
void sort()使用<运算符对元素进行排序;N个元素的复杂度为 NlgN
void splice(iterator pos, list<T, Alloc>x)将链表x的内容插入到pos的前面,x将为空;时间复杂度为固定时间;
void unique()将连续相同的元素压缩为单个元素,复杂度为线性时间;
  • forward_list:单向链表,只需要正向迭代器,是不可反转的容器;
  • queue:适配器类;典型的队列,不允许随机访问队列元素,不允许遍历队列,能做的操作真的就是一个队列;可以将元素添加到对位,可以从队首删除元素,可以查看队尾和队首的值,检查元素数目和检查队列是否为空;

这是队列queue特殊支持的操作:

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

注意:pop()是删除数据的方法,不是读取并删除数据的方法;要注意先使用T & front()检索之后再用pop()删除;

  • priority_queue:这也是一个适配器类,但是最大元素被移动到队首;不过“哪个元素最大”的比较标准是可以指定的,方法提供一个可选的构造函数参数:
priority_queue<int> pq1;
priority_queue<int> pq2(great<int>);
//上面的great<>()我们后面会讨论,它是一个预定义的函数对象
  • stack:翻译成中文就是”“,也是一个适配器类,给底层提供了栈接口;同样具有很多限制(这是“栈”的身份决定的);几乎所有的操作都在为栈本身的性质服务,操作包括将元素压入栈顶,从栈顶弹出元素,查看栈顶的值,检查元素数目和测试栈是否为空;下面表格给出了这些操作:
方法说明
bool empty()const如果栈为空,返回true;否则返回false
size_type size()const返回栈中元素的数目
T & top()返回指向栈顶元素的引用
void push(const T & x)在栈顶部插入x
void pop()删除栈顶元素

注意:和queue相似,方法pop()只有弹出元素的功能,并没有读取元素的功能;如果有读取需求,应当先使用方法T & top()读取栈顶元素,再使用方法void pop()弹出栈顶的元素;

  • array: 这个类实际上不是容器类,在C++11中加入;在头文件<array>中定义,没有调整容器大小的操作;但是定义了对它来说有意义的成员函数,比如operator[]()at();可将很多标准TL算法用于array对象,比如copy()for_each();

另外还有一种容器,称为关联容器;它将值和键对应在一起;对于一般容器X,表达式X::value_type表达了存储在容器中的值类型,对于关联容器来说,表达式X::key_type表达了键的类型;STL提供了四种关联容器,分别是set,multiset,map,multimap;

最简单的关联容器是set,中文翻译是集合;值的类型和键的类型是相同的,并且键是唯一的(毕竟集合中的元素也是唯一的);实际上对set来说,值就是键;multimset类似于set,只是可能有多个值的键相同(换句话说,这个集合没有元素的唯一性,同一个元素可能多次出现);在map中,值与键的类型不同,键是唯一的,每个键只对应一个值;multimap和map相似,只不过同一个键可能和多个值相对应;

创建一个set类型容器:

set<string>A;
set<string, less<string>>A;//第二个参数是可选的,指示用来对键进行排序的比较函数或对象

下面的代码使用set完成了一个很小的功能:

const int N = 6;
string s1[N] = {"buffoon","thinkers","for","heavy", "can", "for"};
set<string> A(s1, s1 + N);//将集合容器A初始化为[s1,s1 + N)之间的数据,指针本身就是一个全功能迭代器
ostream_iterator<string, char> out(cout, " ");//输出流适配器,将针对迭代器的写入操作输出到屏幕上
copy(A.begin(), A.end(), out);//使用复制函数向out迭代器写入数据

另外,C++还为像set这种集合内置了一些很好用的算法,比如可以对集合求并集:

set_union(A.begin(), A.end(), B.begin(), B.end(), ostream_iterator<string, char> out(cout, " "));
//五个参数中的前两对划定了要求交集的两个集合(可能是某个集合的子集);最后一个参数是接受结果的迭代器

set_union(A.begin(), A.end(), B.begin(), B.end(), C.begin());
//将求并集的结果写入到C.begin()这个迭代器
//但是这样带来的问题是:新数据会覆盖旧数据,并且因为不自动进行内存管理,要求C必须提前拥有足够的空间
//下面这个版本是更完美的

set_union(A.begin(), A.end(), B.begin(), B.end(), insert_iteratior<set<string>>(C, C.begin()));

函数set_intersection()set_difference()分别求集合的交集和差;另外,方法lower_bound()将键作为参数并返回一个迭代器,该迭代器指向集合中第一个小于键参数的成员;方法upper_bound()将键作为参数并返回第一个大于键参数的成员;

集合是天生具有自动排序性质的,因此插入元素时不指定插入到位置:

string s("tennis");
A.insert(s);
B.insert(A.begin(), A.end());

和set相似,multimap也是可反转,经过排序的关联容器;但键和值的类型不同,并且同一个键可能和多个值关联;下面我们正式声明一个multimap容器:

multimnap<int, string> codes;//键的类型为int,值的类型为string
//实际上还有第三个参数,可选;提供一个用于排序的算法
//排序算法默认使用less<>模板

另外,这种“键值对”的数据结构实际上是一种名为pair<const T, V>构成的,所以我们可以这样操作一个集合:

pair<const int, string> item(213, "Los Angles");
codes.insert(item);
//算法的原理是:先创建一个键值对,再插入到codes这个multipan容器中

//或者创建匿名对象:
codes.insert(pair<const int, string> (213, "Los Angles"));

对于pair对象,可以分别使用函数first()second()来分别访问键和值;另外函数count()接受键作为参数,返回muptimap中和键匹配的值的数目;成员函数lower_bound()upper_bount()和set中函数的工作表征是相同的;成员函数equal_range()用键做参数,返回两个迭代器并封装在一个pair容器中;

无序关联容器这里不做详细的讨论,因为它真的很复杂;

5.函数对象

函数对象中一个很重要的概念是函数符,它的意思是函数中标识函数名称的那个关键字;实际上,类也可以被当作一个函数使用,只要我们重载了()运算符:

template<class T>
class TooBig
{
private:
    T cutoff;
public:
    ToobBig(const T & t) : cutoff(t) {}//构造函数
    bool operator()(const T & t) {return v > cutoff;}//对运算符()进行重载
};

这样我们就能把类作为函数来使用了;对于for_each()函数,它的定义原型是这样的:

template<class InputIterator, class Function> Function for_each(InputIterator first, InputIterator last, Function f);
//注意最后一个参数,for_each()函数要求最后一个变量是一个可以使用的函数,实际上我们可以直接传入一个类
//然后将某些参数作为类成员包含在内,并重载()运算符让它可以像一个函数那样工作

STL也定义了函数的概念:

  • 生成器是不用参数就可以调用的函数符
  • 一元函数是用一个参数就可以调用的函数符
  • 二元函数是用两个参数就可以调用的函数符
  • 返回bool值的一元函数是谓词
  • 返回bool值的二元函数是二元谓词

如果需要某个函数需要调用由用户传入的函数,实际实现并不是很复杂,本质上讲只要传入函数符:

bool WorseThan(const Review & r1, const Review & r2);
...
sort(books.begin(), books.end(), WorseThan);

在STL中,有一些将函数符作为参数的算法;比如sransform()函数,它的第一个和第二个参数是迭代器指定一个区间,第三个参数是接受处理结果用的迭代器,第四个函数是处理逻辑,具体使用是这样的:

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

另外,第二个版本是:

tramsform(gr8.begin(), gr8.end(), m8.begin(), out, mean);
/*第一和第二个参数是两个迭代器,表示一个区段;第三个参数也表示一个迭代器,是第二个区段的开始位置;第四个参数是用来接受结果的迭代器,第五个参数是用来进行数据处理的函数符;第五个参数应当为能够接受两个参数的函数;*/

缺点是,每进行一种运算都要重新定义一个函数,可以直接使用STL的模板函数:

#include<functional>
...
plus<double> add;
double y = add(2.2, 3.4);

//上面的第二种用法可以改写为:
transform(gr8.begin(), br8.end(), m8.begin(), out, plus<double>());

像这样的函数有很多,下面的表格总结了符号和对应函数符:

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

注意:在C++更老的版本中,不是使用multiplies而是使用times

要注意上面的函数都是自适应的,实际上STL有5个相关概念:自适应生成器,自适应一元函数,自适应二元函数,自适应谓词,自适应二元谓词;函数有携带了表示参数类型和返回类型的typedef成员;比如:result_type,first_argument_type,second_argument_type;

自适应的意义是:函数适配器可以使用函数对象,并认为存在这些typedef成员;STL提供了使用这些工具的函数适配器类,比如当我们需要将一个二元函数当作一元函数使用(其中一个参数可能是个常量)时:

binder1st(f2, val) f1;
//f2本身是二元函数的函数符,以它为基础生成的f1对象,其默认第一个参数是val

binder2nd(f2, val) f1;
//功能是相似的,f1默认第二个参数的值是val;

6.算法

这一部分书中讲的一点都不详细,实在是没整理出成体系的知识;后面会重新研究这一小节的内容,尝试再次整理;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值