STL主要由容器container、算法algorithm和迭代器iterator组成。容器指的是常用的数据结构;算法是操作这些容器的泛型算法;迭代器是算法实现处理不同类型容器的途径。STL是C++标准库的重要组成部分。
一、迭代器
像string、vector等都支持对元素的随机访问,也就是说支持下标运算符[],但是C++标准库里还有许多其他的容器类型不支持随机访问。为了统一,C++引入了迭代器,它是访问容器中数据元素的最普通方式。 迭代器其实就是一种泛型的指针,每一种容器都有一个与之关联的迭代器。可以通过容器成员begin和end获取第一个元素和尾后元素的迭代器。
vector<int> vi = {0, 1, 2, 3};
vector<int>::iterator itb = vi.begin(); //itb指向vi的第一个元素,auto itb = vi.begin();
vector<int>::iterator ite = vi.end(); //ite指向vi的尾后元素, auto ite = vi.end();
/*使用迭代器遍历容器中的元素示例*/
for (auto it = vi.begin(); it != vi.end(); ++it) { //it类型为vector <int>::iterator
cout << *it << endl;
}
迭代器将STL中的算法和容器关联起来,STL提供的算法几乎都是通过迭代器实现对容器中元素的操作。这些算法都接受表示一个范围的二个迭代器begin和end,这个范围包含在范围[begin, end)之间的所有元素。
迭代器的分类如下,一个迭代器的类型取决于与其关联的容器类型,比如一个执行vector类型的迭代器的类型为随机访问迭代器,可以随机访问容器中的任意一个位置,具有自增、自减运算、随机移动、关系>、关系<等功能。
输入(input)迭代器 | 只能单步向前迭代(自增运算符),不允许修改由该类迭代器引用的元素 |
输出(output)迭代器 | 和输入迭代器类似,也只能单步向前迭代,不同的是该类迭代器对引用的元素只能执行写操作。 |
前向(forward)迭代器 | 该类迭代器可以在一个正确的区间中进行读写操作,它拥有输入和输出迭代器的特性,仅支持自增运算。 |
双向(bidirectional)迭代器 | 该类迭代器在前向迭代器的基础上提供了单步向后迭代的功能,支持自增和自减运算。 |
随机访问(random access)迭代器 | 该类迭代器具有上面所有迭代器的功能,并且能直接访问容器中的任意一个元素,支持iter+n, iter-n, iter+=n, iter-=n, iter1-iner2 |
二、容器
1)容器概述
STL中的容器主要有两大类,顺序容器和关联容器,每种容器都被定义为类模板。另外STL还提供了被称为容器适配器的类,它们是顺序容器和关联容器的变种,包含的功能有限,用于满足特定的需求。
cbegin和cend
C++11为每一种容器提供了cbegin和cend成员,分别返回第一个元素和尾后元素的const迭代器,不允许对指向的元素指向写操作。
/*对于begin成员,只有当容器是const类型时才返回const类型迭代器;否则返回非const迭代器。
为了避免产生不必要的修改错误,C++11为容器新增加了cbegin和cend成员,无论容器是否为const,
都返回const迭代器*/
vector<int> vec = {0, 1, 2, 3, 4};
auto it1 = vec.begin(); //返回第一个元素的迭代器
auto it2 = vec.cbegin(); //返回第一个元素的const迭代器
*it1 = 4; //正确,修改第一个元素的值
*it2 = 5; //错误,it2为const迭代器,不允许修改指向的对象
创建容器
除了array容器外,其他容器都支持构造一个空的容器。比如:
vector<string> vs; //创建一个存放string类型元素的vector类型对象vs, vs初始化为空
插入和删除元素
除了array容器外,其他容器都是可变长的,即支持插入/删除(insert/erase)数据;另外,C++11还为可变长容器新增了emplace成员,它通过参数包(C++11允许使用数目可变的模板参数,可变数目的参数称为参数包,用省略号表示,可包含从0到任意个模板参数)接受的参数来构造一个元素,并将该元素插入容器中。这是普通的插入数据成员做不到的。
/*通过如下代码说明insert、push_back和emplace成员的区别:*/
Struct Foo {
Foo(const string &name, int id):n_name(name), m_id(id) {}
string m_name;
int m_id;
}
vector<Foo> vf;
/*与emplace_back成员相比,push_back和insert成员只能将已构造的元素移动或复制到容器中相应的
位置,不能在指定位置直接构造一个元素*/
vf.push_back(Foo("Lisha", 12)); //将临时对象移到容器的末尾
vf.insert(vf.begin(), Foo("Mandy", 13)); //将临时对象移到容器的开始位置
/*emplace_back函数调用把两个实参传递给Foo类的构造函数,并在vf的末尾利用这二个参数值构造一个
新元素。类似地,最后一个emplace函数调用在vf的首部插入一个新元素*/
vf.emplace_back("Kevin", 11); //在容器的末尾新增一个元素
vf.emplace(vf.begin(), "Rosieta", 10); //在容器的首部插入一个元素
//上述前两个插入操作通过右值引用将临时对象移动到指定位置。
swap操作
vector<int> v1 = {0, 1}; //2个元素的vector
vector<int> v2 = {0, 1, 2, 3}; //4个元素的vector
/*这里,swap操作交换两个相同类型容器的数据,swap调用完成后,v1包含4个元素,v2包含2个元素*/
swap(v1, v2);
除了array外,swap函数不会执行任何数据复制、插入或删除操作,因此该函数可以保证在常数时间内完成两个容器之间的数据交换。swap执行过程中不会移动数据元素意味着原来与容器绑定的迭代器、指针和引用都是有效的,只不过swap之后属于另一个容器了。对于array来说,swap操作会真正交换相同位置的元素。因此对于array执行完swap后,与容器绑定的指针、引用和迭代器不会改变指向,但所指向的元素的内容发生了改变。
2)常用容器
array
C++11标准提供了定长数组容器array,与普通数组的行为类似,array是定长数组,不支持插入、删除等改变容器大小的操作。创建array对象时需要指明元素的类型和数组的大小。与普通数组相比,array对象支持赋值和复制操作,还能通过size成员获取数组的大小。
array<int, 4>arr = {1, 2, 3, 4}; //创建一个存放4个int类型元素的array对象
for(auto it = arr.begin(); it != arr.end(); ++it)
cout << *it << endl;
array<int, 4>arr2 = arr; //array对象允许复制
arr2.fill(0); //所有元素赋值为0
vector
定义和初始化vector对象的方法如下:
vector <T> v1; //定义一个存放T类型元素的空对象v1
vector <T> v2(v1); //复制v1里的所有元素到v2
vector <T> v3(n); //指定初始元素为n个
vector <T> v4(n, valute); //指定n个值为value的元素
vector <T> v5 = {a, b, c,......}; //采用初始化列表,v5的元素个数为列表里面值的个数
vector <T> v6 {a, b, c,......}; //等价于v5 = {a, b, c,......}
int arr[] = {1, 2, 4, 10, 5, 4, 1, 8, 20, 30, 15};
vector<int> vi;
vi.assign(arr, arr+7); //这里采用区间成员,并利用assign算法完成数据的复制
deque
deque是一种双端队列容器,与vector类似,支持随机访问。与vector不同的是,deque可以在首位两端进行快速插入和删除操作。
deque<int>dq = {1, 2, 3}
dq.push_back(4); //在尾部插入一个元素
dq.push_front(0); //在首部插入一个元素
cout << dq[3] << endl; //随机访问
forward_list
forward_list实现了一个快速插入和删除元素的单向链表。它无法访问到给定位置的前驱,所以仅提供了在给定位置之后的插入或删除操作。
forward_list<int> flst = {2, 3}; //初始化一个含有两个元素的单链表
flst.push_front(1); //在flst首部插入数据
/*before_begin返回第一个元素的前驱的迭代器,类似于end成员返回的迭代器;before_begin成员返
回的迭代器也是不能解引用的,主要在emplace_after、insert_after、erase_atter或splice_after
等成员中使用,用来指明插入或删除序列(元素)的开始位置。
insert_after在迭代器指定的位置后面插入一个元素。*/
flst.insert_after(flst.before_begin(), 0); //在flst首部插入数据
for(auto it = flst.begin(); it != flst.end(); ++it)
cout << *it << endl; //打印输出:0 1 2 3
/*考虑到性能因素,forward_list有意放弃成员size函数。如果要获取元素的数目,则可以使用distance
函数,例如:*/
cout <<"size:"<<distance(flst.begin(), flst.end()) <<endl; //size: 4
与vector、array、deque相比,forward_list不支持随机访问,不能通过下标运算符访问某个位置元素,只能从开始位置依次检索;但是对于元素的插入、删除、移动等操作,它的性能要好于前三者。
list
list也是一种不支持随机访问但能在任意位置快速插入或删除数据的线性容器。和forward_list相比,list中的每个元素不但能直接访问它的后继,也能访问它的前驱(即list的迭代器支持自减操作)。list是一个双向链表。
除了使用insert、push_back、push_front、emplace等成员执行元素插入外,还可以使用splice成员将一个list中的元素转移到另一个list中;另外splice函数还支持将一个列表中某一范围内的元素移动到另一个列表的指定位置。
list<int>lst1 = {2, 3}, lst2 = {1}, list<int> lst3 = {6, 7, 8};
lst1.push_back(5); //在lst1的尾部插入元素5
lst2.push_front(0); //在lst2的首部插入元素0
auto pos = find(lst1.begin(), lst1.end(), 5); //找到指定元素5的迭代器
lst1.insert(pos, 4); //在此位置插入元素5
lst1.splice(lst1.begin(), lst2); //将lst2插入到lst1中第1个元素位置,splice后lst2变空
for(auto it = lst1.begin(); it != lst1.end(); ++it)
cout << *it << " "; //打印输出:0 1 2 3 4 5
/*下面splice调用结束后lst1尾部新增两个元素,lst3剩余一个元素*/
auto it = lst3.begin();
advance(it, 2); //将it后移两个位置
lst1.splice(lst1.end(), lst3, lst3.begin(), it); //将lst3中前两个元素移动到lst1尾部
set和map
set中每个元素只包含一个关键字,与数学上的集合类似,set不包含重复的元素,且它们都是有序的。因此,set支持高效查找和访问。
当向set插入一个元素时,如果set中已有此元素,则将其抛弃;否则按序将其插入容器中。
//统计输入的一组数字中不同数字的个数,并将它们的排序输出。
//输入:1 8 4 2 0 1 4 3 5 4
//输出:0 1 2 3 4 5 8
set<int> counter; //创建一个关键字类型为int的set对象,初始化为空
int number;
while(cin >> number) //输入数字
counter.insert(number); //将输入的数字插入set中
cout << "不同的数字的个数" <<counter.size() << endl; //获取元素个数
for(auto &i : counter)
cout << i << " "; //输出每个元素
/*可以利用成员find函数查找元素,如果该元素存在,则返回该元素的迭代器;否则返回尾后迭
代器(end)。如果要删除某个元素或某一范围的元素,则可以使用成员erase。*/
vector<int> v = {1, 8, 4, 2, 0, 1, 4, 3, 5, 4, 7};
set<int> s(v.begin(), v.end()); //利用指向vector的迭代器范围创建set
auto it = s.find(0); //查找关键字为0的元素
s.erase(s.find(0)); //删除关键字为0的元素
s.erase(s.find(3), s.find(7)); //删除[3, 7)范围内的元素
for(auto &i : s)
cout << i << " "; //打印输出:1 2 7 8
map和set类似,都是有序容器。不同之处在于map中的元素是pair类型,而set中的元素类型为关键字本身。
/*pair是一种标准库类型模板,它定义在头文件utility中。一个pair包含两个数据成员,在创建
一个pair对象时,必须显式指明这两个数据的类型,例如:*/
pair<int, int>p1; //创建一个保存两个int类型数据的pair对象,两个成员的值均为0。
pair<string, int>p2 = {"Hello", 0}; //列表初始化两个成员
auto p3 = make_pair("Hello", 1); //make_pair函数返回一个pair对象
/*与其他标准库类型不同,pair的两个数据成员是共有的,它们的名字分别为first和second。因此,
可以直接访问一个pair对象的两个数据成员,例如:*/
cout << p2.first << p2.second << endl;
..................................................................................
/*map中每个pair元素的第一个成员为关键字,用来索引元素,第二个成员为与关键字相关的
值。示例如下:*/
map<int, int> counter; //创建一个存放pair<int, int>类型的map对象
int number;
while(cin >> number)
/*map提供了下标运算符,用来获取与关键字关联的值,即每个pair元素的第二个成员。执行下面一行
代码时可能出现两种情况:
1) 如果找到关键字为number的元素,则此元素的关键字的值自增;
2) 如果没有找到关键字为number的元素,则以此关键字生成一个新的元素,并将其插入到map中。新
元素的值执行值初始化(本例中默认值为0)。然后,新元素的关键字的值自增。*/
++counter[number];
for(auto &i : counter) //遍历map中的每个元素
cout << i.first << ":" << i.second << endl; //打印每个元素的关键字和值
注意,由于下标运算符可能会插入新的元素。因此,它只能作用于非const的map对象。可以使用insert成员向map添加新的元素(对于map来说,其成员insert和下标运算符有着不同的功能,使用下标运算符意味着可能插入新的元素或覆盖已有元素的值,而insert专用于插入,不会覆盖已有元素。如果只想访问元素,则可以使用C++11提供的at成员)。
counter.insert({3, 1}); //使用C++11提供的初始化列表来创建一个pair
counter.insert(make_pair(3, 1))
/*insert函数返回一个pair对象,该对象的第一个成员为一个指向map中给定关键字的迭代器,第二个
成员是一个bool值。如果给定关键字已存在,则其值为false; 否则为true*/
auto res = counter.insert(pair<int, int>(2, 1));
if(!res.second) //关键字2已经存在
++res.first->second; //关键字为2的元素的值自增
multiset和multimap
set中不允许有重复元素,multiset中允许有重复元素; map单映射中,key和value是一一对应的关系,multimap多映射中,key和value可以是一对多的关系。
unordered_set和unordered_map和unordered_multiset和unordered_multimap
这里需要注意有些STL也实现了hash_set和hash_map和hash_multiset和hash_map,它们与支持的unordered_*类似,但是它们不是C++标准的一部分,不是所有的平台都支持。
3)容器适配器
stack
以LIFO的方式存储元素
queue
以FIFO的方式存储元素
priority_queue
以特定顺序存储元素,优先级最高的元素总是位于队列开头
三、STL字符串类
STL提供了一个专门操作字符串而设计的模板类:std::basic_string<T>,该模板类的两个常用具体化如下所示:
std::string:基于char的std::basic_string具体化,用于操纵简单字符串
std::wstring:基于wchar_t的std::basic_string具体化,用于操纵宽字符串,通常用于存储支持各种
语言中符号的Unicode字符。
四、泛型算法
查找、排序和反转等都是标准的编程需求,不应该让程序员反复重复实现这样的功能,因此STL以函数模板的形式提供这些功能,通过结合使用这些函数和迭代器,可以让我们对容器执行一些常见的操作。最常见的STL算法如下所示:
find
find_if
reverse
remove_if
transform
1)向算法传递函数
使用函数
谓词(predicate)是一个可以调用的表达式,其返回的结果能用于条件测试。标准算法库使用的谓词有一元谓词(unary predicate)和二元谓词(binary predicate,有二个参数)。
/*示例:如下所示,如果直接调用sort算法对vp中的元素进行排序,它将按照指针的大小进行排序,而
不是对指针指向的对象进行排序。因此,需要使用自己定义的比较方式。这也意味着需要向sort算法传
递自定义的比较方式。sort算法的第二个版本包含第三个参数,用来接受一个二元谓词,这个参数可以
解决这个问题。*/
struct LargeData {
LargeDta(int id):m_id(id) { }
int m_id;
int m_arr[1000];
}
vector<LargeData*> vp; //为了提高程序的效率使用指针容器代替对象容器vector<LargeData>vo
/*定义一个函数,该函数接受两个LargeData类型指针,比较的是指针指向的对象id。*/
bool Less(const LargeDate *a, const LargeData *b) {
return a->m_id < b->m_id;
}
/*上面的Less函数满足一个谓词的定义,可以传递给第二个版本sort算法的第三个参数。如下代码执行
完成后,vp中的元素将会按照id升序排列。当sort算法需要比较两个元素时便会调用Less函数。*/
sort(vp.begin(), vp.end(), Less);
使用函数对象
除了向算法传递普通的函数外,还可以传递一个函数对象。
/*示例: 为Compare类定义一个函数调用运算符*/
struct Compare {
bool operator() (const LargeDate*a, const LargeData *b) {
return a->m_id < m_id;
}
};
/*Compare类的函数调用运算符的形参和函数体与上面的Less函数一样,也是比较与两个形参指针绑定的
LargeData对象的id。以下sort函数调用中的第三个实参为一个函数对象。
通过Compare默认构造函数创建一个函数对象作为sort函数调用的第三个实参。当sort算法需要比较两个
元素时,便会使用这个函数对象*/
sort(vp.begin, vp.end(), Compare());
函数对象可以保存调用时的状态。相比于普通函数,函数对象更加灵活,能够完成函数不能完成的任务。
/*示例:下面通过Checker类和find_if算法来查找容器中第n个元素*/
struct Checker {
/*m_cnt用来计数,m_nth用来保存设定值*/
int m_cnt = 0, m_nth;
Checker(int n): n_nth(n) { } //初始化设定值
/*每一次调用Checker对象,该对象的计数器就会自增,当计数器增加到设定值时,该调用返回真*/
bool operator(int) {return ++m_cnt == m_nth;}
}
vector<int> v = {3, 7, 3, 11, 3, 3, 2};
/*find_if的第三个参数接受一个Checker类对象,其成员m_nth设置为4。find_if每次使用该对象,该
对象的计数器就会自增。同时,指向容器v的迭代器也会自增。当这个对象第四次被使用的时候,它便返回
一个真值。此时find_if返回指向当前元素的迭代器*/
auto i = find_if(v.begin(), v.end(), Checker(4)); //返回第四个元素的迭代器
使用lambda表达式
一个lamba表达式为一个可调用的代码单元,因此可以向算法传递一个lambda表达式。和上面两种方法相比,使用lambda表达式会更加简洁和方便。它不需要额外定义一个函数或者一个函数对象类,而且还可以利用捕获列表访问外围对象。如果传递的可调用对象的操作比较简单且只在局部使用,则lambda表达式是最佳选择。
/*下面第三个参数的lambda表达式的捕获列表为空,函数形参为两个指针类型,函数体与Less函数和
Compare函数调用运算符一样,都是比较两个指针指向的对象的id。当sort算法需要比较两个元素时,
便会使用这个lambda表达式*/
sort(vp.begin(), vp.end(),
[](const LargeDate *a, const LargeData *b)) {return a->m_id < b->m_id;})
2)参数绑定
有的标准库算法只接受一个包含一个参数的调用对象,但有的时候需要传递给算法的函数包含两个参数。因此这样的函数不能传递给算法。例如,定义一个fliter函数,用来将容器中小于一个给定值的元素设置为0:
void filter(int &a, int n) {
a = a < n ? 0 : a;
}
可以用for_each算法来遍历每个元素,但问题是该算法的第三个参数接受只包含一个参数的可调用对象,而filter函数有两个参数。这时可以使用lambda表达式来实现,通过lambda捕获列表,可以很容易解决该问题。
vector<int> vi = {3, 7, 1, 11, 3, 3, 2};
int n = 3;
for_each(vi.begin(), vi,end(), [n](int &i){i = (i < n ? 0 : i);});
bind函数
但如果依然坚持使用filter函数代替lambda表达式,则可以使用标准库中的bind函数。bind函数接受一个可调用对象,然后生成一个新的可调用对象来仿造原调用对象的参数列表。使用bind函数需要包含functional头文件。
/*bind函数的使用格式如下,其中fun是一个已定义的调用对象,newFun是fun的仿造者,arg_list是
fun的参数列表。arg_list可能包含一些名为_n的参数,其中n是一个正整数。这些参数称为“占位符”,
它们是newFun的参数,n的值表示在newFun参数列表中的位置。比如, _1为newFun的第一个参数,_2
为newFun的第二个参数,依次类推。当调用newFun时,newFun会调用fun, 并把arg_list中的参数传递
给fun。*/
auto newFun = bind(fun, arg_list);
下面根据filter函数仿造一个新的调用对象uf:
/*仿函数uf包含一个参数_1(形如_n的名字都会定义在一个名为placeholders的命名空间中,而此命名空间
又属于std)。调用uf的时候,它会调用filter函数来完成实际的工作,并把参数_1和参数n传递给filter
函数*/
atuo uf = bind(filter, std::placeholders::_1, n);
/*调用for_each算法时,它会将给定的元素传递给uf,而uf将这个元素和n传递给filter,最终完成filter
函数的调用*/
for_each(vi.begin(), vi.end(), uf)
不仅可以利用bind函数来仿造已定义函数,而且还能重新安排原函数的参数在参数列表中的位置。
/*例如对于vector<LargeData * >对象vp:
利用bind将产生的仿函数的两个参数交换位置,分别传递给Less函数。当执行完sort算法之后,vp中的
元素将根据id降序排列*/
sort(vp.begin(), vp.end(), bind(Less, std::placeholders::_2, std::palceholders::_1));
引用绑定
默认情况下,bind函数中不是占位符的参数将以复制的方式传递给可调用对象。当要传递一个引用给可调用对象时,可以使用C++11提供的函数ref。
/*例如定义如下函数, 功能是把第一个参数的值累加到与形参s绑定的实参中*/
void sun(int a, int &s) {
s += a;
}
int s = 0; //保存累加和
/*ref函数返回一个对象,该对象包含s的引用,它保证了for_each算法正确地将容器vi中的所有元素的
值累加到s中*/
for_each(vi.begin(), vi.end(), bind(sum, std::placeholders::_1, ref(s)));
除了ref函数,标准库提供了cref函数。cref函数返回一个包含const引用类型的对象。使用它们时需要包含functional头文件。
3)使用function类模板
C++语言中可调用对象包括:函数、函数指针、函数对象、lambda表达式、bind函数创建的对象。对于上述用于LargeData对象进行比较的调用对象,尽管它们具有不同的使用方式,但它们都具有相同的调用形式:
/*下述代码是一个函数类型,它接受两个LargeData指针类型形参,返回类型为bool*/
bool(LargeData* ,LargeData*)
可以使用C++11新标准提供的function类模板将下面不同表现形式的调用对象统一起来:
/*function是个模板,使用的时候需要提供一个调用方式的类型信息。例如,下面使用一个类型
为CallType的调用形式,其中CallType为bool(LargeData* ,LargeData*)类型。*/
using CallType = bool(LargeData*, LargeData* );
function<CallType> f1 = Less; //函数
function<CallType> f2 = Compare(); //函数对象
function<CallType> f3 =
[](const LargeData *a, const LargeData *b){return a->m_id < b->m_id}; //lambda
function<CallType> f4 =
bind(Less, std::placeholders::_2, std::palceholders::_1); //bind函数
/*接下来便可以使用新建创建的function对象对上面4种不同的调用对象实现统一的使用方式:*/
LargeData a(0), b(1);
if(f1(&1, &b)) {/*...*/}
if(f2(&1, &b)) {/*...*/}
if(f3(&1, &b)) {/*...*/}
if(f4(&1, &b)) {/*...*/}