前言:这一章已经相当靠近全书的后面部分了;这一章我们会深入探讨一些技术上的细节,比如string的具体构造函数,比如适用于string类的几个函数,比如我们还会介绍一下标准模板库STL的一些细节;后面还会涉及智能指针的具体实现方式,总之这一章将会对细节有相当程度的深入;
这个类是头文件string支持的,实际上是模板basic_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 |
将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 |
将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寄存器,检测到输入错误时设置failbit寄存器,出现无法识别的故障时设置badbit寄存器,一切顺利时设置goodbit寄存器;
下面的程序是一个从文件中读取数据的例子:
#include
#include
#include
#include
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对象中还内置了查找的方法,大小的判断依据是:
可以对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的查找函数并不只这么些,比如:
另外还有两个特殊的函数:
为了详细阐明这两个函数的作用,我们需要写一段测试用的代码:
#include
#include
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;
智能指针是行为和指针类似的类对象,它存在的意义是防止开发者忘记将申请到的内存释放,因此可以避免内存泄漏;智能指针类共有三种,分别是auto_ptr
,unique_ptr
和shared_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[]的;
标准模板库STL提供了一组关于容器,迭代器,函数对象和算法的模板;容器和数组类似,存储的都是同质的数据;迭代器能够用来遍历数组对象,与能够遍历数组的指针类似,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针;
STL引出的是一种新的编程方式:泛型编程;
模板类vector是类似于数组的类,具体而言是这样使用的:
#include
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 {...}
模板的特征是通用行,这些函数每种模板都具有:
迭代器是个广义的指针,支持的操作有:
每个容器都有它们自己的迭代器类型变量,这样声明一个迭代器变量:
vector<double>::iterator pd;
vector<double> scores;
pd = scores.begin();
*pd = 22.3;
++pd;
另外实际上我们也可以使用自动推断变量类型的特性:
vector<double>::iterator = pd;
auto pd = scores.begin();//使用自动推断
我们前面提到过“超过结尾迭代器”,它的意思是这个迭代器是指向最后一个元素的;方法容器.end()
可以获得这样一个迭代器;
关于容器,还有一些方法我们之前没有提到:
下面这个程序演示了size()
,begin()
,end()
,push_back()
,erase()
和insert()
的用法:
#include
#include
#include
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循环的写法:
double prices[5] = {4.99,10.99,6.87,7.99,8.49};
for (double x : prices)
cout << x << std::endl;
这种写法是可以修改参数中的值的,而函数for_each()
是没有办法修改值的;
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;
不同的算法对迭代器有不同的要求,我们设计的目的是尽量让算法使用低级的迭代器,比如:
STL定义了5种迭代器,分别是输入迭代器(可以读取容器但是不能写入数据),输出迭代器(可以向容器写入数据但是不能读取),正向迭代器(可以从前往后访问,可以写但是只能从前往后访问),双向迭代器(可以读写并且可以向前向后访问),随机访问迭代器(可以读写,可以随机访问);
它们的层次结构是:
实际上指针就可以用作迭代器,并且像浮点整型这样的数据本身有自己的<运算符,就可以使用sort()
来排序,就像这样:
sort(Receipts, Receipts + SIZE);
另外函数copy()
也是可以通用的,它的作用是复制数据;接受三个参数,前两个是迭代器指定要复制的内容,第三个迭代器是接受内容的目的地;
引出一个新概念:适配器;适配器的作用是,将一些其它接口转换为STL使用的接口;可以通过包含头文件
#include
...
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()迭代器中
在头文件reverse_iterator
,back_insert_iterator
,front_insert_iterator
和insert_iterator
;
如果我们需要反向打印容器的内容,可以使用反向迭代器:
copy(dice.rbegin(), dice.rend(), out_iter);
//反向迭代器的rbegin()指向超尾,并且执行++操作时是向左访问的
//同理,rend()迭代器指向开头
注意:rbegin()虽然和end()指向的是同一个位置,但它们表示的值是不同的;另外还有一点,rbegin()指向的是超尾,因此实际上是不能直接解引用的,应当先递减再解引用
现在具体看看另外三种插入迭代器:
上面三个迭代器都是输出容器概念的模型,它们的构造函数都以实际的容器标识符为参数;也就是说比如可以这样做:
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_type |
T | T的类型 | 编译时间 |
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) |
void | a.insert(a.begin(), t) |
vector,deque |
a.push_back(t) |
void | a.insert(a.end(), t) |
vector,list,deque |
a.pop_front(t) |
void | a.erase(a.begin()) |
vector,deque |
a.pop_back(t) |
void | a.erase(a.--end()) |
vector,list,deque |
a[n] |
T& | *(a.begin()+n) |
vector,deque |
a.at(t) |
T& | *(a.begin()+n) |
vector,deque |
下面我们详细看看这几种容器:
下面的表格是list链表容器特有的函数:
函数 | 说明 |
---|---|
void merge(list |
将链表x和调用链表合并;两个链表必须已经排序;合并后的经过排序的链表保存在调用链表中,将x清空;复杂度为线性时间; |
void remove(const T & val) |
从链表中删除val的所有实例;函数复杂度为线性时间; |
void sort() |
使用<运算符对元素进行排序;N个元素的复杂度为 NlgN |
void splice(iterator pos, list |
将链表x的内容插入到pos的前面,x将为空;时间复杂度为固定时间; |
void unique() |
将连续相同的元素压缩为单个元素,复杂度为线性时间; |
这是队列queue特殊支持的操作:
方法 | 说明 |
---|---|
bool empty()const |
如果队列为空,则返回true,否则返回false |
size_type size()const |
a返回队列中的元素数目 |
T & front() |
返回指向队首的元素的引用 |
T & back() |
返回指向队尾的元素的引用 |
void push(const T& x) |
在队尾插入x |
void pop() |
删除队首元素 |
注意:
pop()
是删除数据的方法,不是读取并删除数据的方法;要注意先使用T & front()
检索之后再用pop()
删除;
priority_queue<int> pq1;
priority_queue<int> pq2(great<int>);
//上面的great<>()我们后面会讨论,它是一个预定义的函数对象
方法 | 说明 |
---|---|
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()
弹出栈顶的元素;
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
构成的,所以我们可以这样操作一个集合:
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容器中;
无序关联容器这里不做详细的讨论,因为它真的很复杂;
函数对象中一个很重要的概念是函数符,它的意思是函数中标识函数名称的那个关键字;实际上,类也可以被当作一个函数使用,只要我们重载了()运算符:
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 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
...
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;
这一部分书中讲的一点都不详细,实在是没整理出成体系的知识;后面会重新研究这一小节的内容,尝试再次整理;