1.什么是容器
容器就是一些特定类型对象的集合。
2.容器的分类
容器可以分为顺序容器和关联容器。
- 顺序容器:为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。也就是说,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
- 关联容器:关联容器是按关键字来保存和访问元素。
3.容器操作的利器—迭代器
3.1 什么是迭代器
迭代器是一种更通用的用于访问容器中各个元素的机制。它是泛型编程的产物。它没有统一的实现方式,有些容器的迭代器可能是指针,而有些容器的迭代器则可能是对象。但是不管是何种实现,它们都提供所需的操作,如*和++等操作。
3.2 迭代器的类型
一般来说,我们不需要知道迭代器的精确类型(比如插入迭代器insert_iterator、流迭代器istream_iterator、反向迭代器、随机迭代器等)。而在标准库的容器中,使用iterator和const_iterator来表示迭代器的类型。如:
std::vector<int>::iterator iter;
std::vector<int>::const_iterator citer;
其中,const_iterator和常量指针差不多,能读取大不能修改它所指的元素值。而iterator的对象可读也可写。
3.4 使用迭代器
3.4.1 获取容器的迭代器
每种标准库容器都提供了begin()和end()这两个方法来获取指向容器中的元素的迭代器对象。其中,begin()返回的是指向容器中第一个元素的迭代器对象,而end()返回的是指向容器中最后一个元素的下一个位置的迭代器对象。也就是说,end()返回的其实是一个指向空位置的迭代器对象。
另外,begin()和end()返回的迭代器类型由容器对象是否是常量决定。如果容器对象是常量,begin()和end()返回const_iterator;否则返回iterator。如果确定要使用const_iterator,C++11新标准中引入了cbegin()和cend()这两个函数。
3.4.2 通过迭代器访问元素成员
对迭代器进行解引用操作,就可以获得迭代器器所指的对象。如:
int main(int argc, char **argv)
{
std::vector<int> intVec;
intVec.push_back(1);
intVec.push_back(2);
auto iter = intVec.begin();
std::cout << *iter << std::endl;
std::cin.get();
return 0;
}
如果迭代器所指的对象的类型是个类,就可能需要通过迭代器来访问这个对象的成员。访问方法是先对迭代器解引用,然后使用.运算符,也可以直接使用->运算符。如:
int main(int argc, char **argv)
{
std::vector<std::string> strVec;
strVec.push_back("hello");
strVec.push_back("world");
auto strIter = strVec.begin();
if (!(*strIter).empty())
{
std::cout << *strIter << std::endl;
}
strIter++;
if (!strIter->empty())
{
std::cout << *strIter << std::endl;
}
std::cin.get();
return 0;
}
4.标准库中的容器
4.1 顺序容器
模板类 | 类型 | 能力/特性 |
---|---|---|
vector | 可变大小的动态数组 | * 以动态数组的方式来管理元素; * 支持随机访问,只要知道位置,就可以在常量时间内访问任何一个元素。 提供随机访问迭代器,所以适用于任何STL算法; * 在尾部插入或删除元素较快,在其他位置则较慢; * 支持主动改变容器的容量(即重新分配内存); * vector的元素分布于连续的空间中,因此可以将vector当成C风格的数组使用。使用data()函数可以获取这个数组的首地址。其作用类似于&a[0]。(a是一个vector对象) * 元素可重复,且不会对元素进行自动排序。 |
deque | 双端队列 | * 以动态数组的方式来管理元素 ; * 支持随机访问,只要知道位置,就可以在常量时间内访问任何一个元素。 提供随机访问迭代器,所以适用于任何STL算法。并有着与vector几乎一摸一样的接口; * 在头尾两端都可以进行快速安插和删除,而在其他位置则相对较慢; * 元素的访问和迭代器的动作比vector要稍稍慢一些; * 不支持主动改变容器的容量(即不支持对容量和内存重新分配时机的控制),所以deque没有capacity()这样与容量相关的成员函数; * 除头尾两端外,在其他任何位置安插或删除元素,都会导致指向deque元素的reference、pointer和iterator失效。在头尾两端插入时,只有iterator会受影响; * 重新分配内存时不需要复制所有元素,所以在内存重新分配方面,效率优于vector; * deque会释放不再使用的内存区块,因此deque的容量是可缩减,但具体实现取决于平台; * 元素可重复,且不会对元素进行自动排序 |
list | 双向链表 | * 使用双向串连列表来管理元素; * 不提供随机访问。因此没有at()函数,也不支持操作符[]。访问除头尾两端以外的元素会比较慢; * 提供双向迭代器,所以所有用来操作元素顺序的算法(比如排序算法,list有自己的sort()函数)都不能用于list; * 在任何位置上安插和删除元素都非常快,始终都是常量时间内完成; * 安插和删除动作并不会造成指向其他元素的各个pointer、reference和iterator失效; * 没有容量和空间重新分配等操作函数,每个元素都有自己的内存,在元素被删除前一直有效; * 使用list自己的用于移动和移除元素的函数要比通用算法提供的函数更快速; * 元素可重复,且不会对元素进行自动排序 |
stack | 栈 | * 是一个后进先出的特殊容器; * 默认情况下使用deque来作为stack内部存放元素的实际容器。可以通过指定stack的第二个模板参数来修改它,前提是这个类型的容器提供back()、push_back()和pop_back()函数。 * 元素可重复。 |
queue | 队列 | * 是一个先进先出的特殊容器; * 默认情况下使用deque来作为queue内部存放元素的实际容器。可以通过指定queue的第二个模板参数来修改它,前提是这个类型的容器提供front()、back()、push_back()和pop_front()函数。 * 元素可重复。 |
4.2 关联容器
模板类 | 类型 | 能力/特性 |
---|---|---|
set/multiset | 键和值相同的集合 | * 会根据特定的排序准则自动将元素排序,排序准则可通过第二个模板参数指定,默认是less; * multiset允许元素重复而set不允许; * 通常以平衡二叉树来实现,查找元素时拥有良好效能; * 不能直接改变元素值,要改变元素值,必须先删除旧元素,再插入新元素; * 提供双向迭代器,所以所有用来操作元素顺序的算法都不能用于set和multiset; * 所有元素都被视为常量,所以也不能对set和multiset调用任何更易型算法(如remove()),要对其执行更易型操作,只能使用其自身提供的成员函数; |
map/multimap | 键和值不相同的映射 | * 将键-值对当作元素进行管理; * 根据key的排序准则自动为元素排序,排序准则可通过第三个模板参数指定,默认是less; * multimap允许键重复,map不允许键重复,如果有重复的键安插,则会安插不成功; * 键和值的类型都必须是可复制的或者可移动的; * 通常以平衡二叉树来实现,通过key来查找元素时拥有良好效能,而通过value来查找元素时,效率比较差; * 不可以直接改变元素的key,要修改元素的key,必须先移除拥有该key的元素,然后插入拥有新key-value的元素; * 元素的value可以直接修改,前提是value是非const类型; * 提供双向迭代器,所以所有用来操作元素顺序的算法都不能用于map和multimap; * 所有元素的key都被视为常量,因此元素的实质类型是pair<const key, T>。所以不能对map和multimap调用任何更易型算法; * 对于非const的map,可以通过[key]来直接访问容器中这个key所对应的value的reference。如果容器中不存在这个key,则返回值类型对应的默认值。还可以通过m[key]=value的形式来直接向容器m中插入元素。另外无论是非const的map还是const的map都可以通过at(key)函数来访问实参key所对应的value的reference。 |
4.3 无序关联容器
无序关联容器通常以hash table为基础,其内的元素没有清晰明确的次序。如果拥有良好的hash策略和实现,在安插、删除和查找元素时,无序关联容器能够获得摊还常量时间。
无序关联容器有以下特性:
- 不提供<、>、<=和>=操作符。从C++11开始提供了==和!=;
- C++标准只保证无序关联容器的迭代器至少是个前向迭代器,所以无序关联容器不提供rbegin()、rend()等函数;
- 不可以直接改动元素(如unordered_set)或元素的key(如unordered_map);
4.4 注意点
-
所有STL容器提供的都是“value语义”而非“reference”语义。容器进行元素的安插动作时,内部实施的是copy和/或move动作,而不是管理元素的reference。也就是说,容器中存放的是一个个真实的对象(实体或指针),而不能是引用。因为引用不是对象,它只是一个别名而已。而在访问容器中的元素时,只要容器不是const的,访问元素的函数返回的都是元素的引用。如at()函数返回的就是元素的引用。
-
capacity()、size()和max_size()这三个函数的区别。
函数 含义 size() 当前容器中已放入的元素数量 capacity() 容器在当前所分配的内存下,最大能够存放的元素数量 max_size() 容器所能够存放的最大元素数量。这与是否重新分配的内存无关。其具体值与平台与容器中元素的类型相关。即同一种容器,其元素是int和string时,max_size()返回的结果是不一样的。 -
reserve()和resize()的区别。
函数 含义 reserve() 只改变容器的容量,即影响的是capacity()函数的返回值 resize() 改变容器中现有元素的数量。如果容器中现有元素数量大于其实参,则会删除多出的元素,但不影响容器的容量;如果容器中现有元素数量小于其实参,则会追加新的元素,新追加的元素使用默认值进行初始化,同时容器的容量也会被改变。如果元素类型没有default构造函数,则不能使用resize()函数。 -
在使用vector时,需要注意容量管理。vector是通过分配比“需要容纳的元素”更多的内存来获取效率上的提升的。所以一旦内存不足,需要重新分配时,与vector元素相关的所有reference、pointer、iterator都会失效。而且内存重新分配很耗时间。如果需要为vector指定内存空间大小,可以使用下面两种方式:
// 方法一 std::vector<int> v; v.reserve(10); // 这里的10表示是10个元素,而不是指字节 // 方法二 std::vector<int> v(10);
推荐使用方法一。方法二一是需要元素类型必须提供一个默认构造函数,二是对于复杂的元素类型,即使提供了默认构造函数,初始化动作也很耗时。因为系统会调用元素的default构造函数来一一制造新元素。方法一和方法二的使用对比示例:
class A { public: explicit A(int) { }; }; int main(int argc, char **argv) { // 使用方法一 std::vector<A> va; std::cout << "va count = " << va.capacity() << std::