6. STL容器(page143)
C++标准程序库还提供了一些特殊容器类别——即 容器配接器(container adapter,包括stack、queue、priority queue),以及bitset和valarray。
这些容器都有特殊接口,不满足STL容器的一般要求。
6.1 容器的共通性
8.1.1 容器的共通能力(page144)
所有STL容器三个核心能力:
1.所有容器提供的是value语意。
进行插入操作时,内部是拷贝操作,故STL容器元素须能被拷贝。
若要存放的对象没有public copy构造函数,
或要存放的对象不是副本(eg是被多个容器共同容纳的元素),
那容器元素只能是指针(指向对象)。
2.总体上所有元素形成一个次序,即可按相同次序一次或多次遍历每个元素。
所有容器都提供“可返回迭代器”的函数,运用这些迭代器可遍历元素。
3.一般各项操作并非绝对安全。调用者须保证传递的函数参数符合要求。
6.1.2 容器的共同操作(page145)
以下操作为所有容器共有,但前提是满足上述核心能力:
- 初始化(initialization)
每个容器类别都有一个default构造函数、一个copy构造函数和一个析构函数。
可用某已知区间的内容作为容器初值,
负责此工作的构造函数从另一容器或数组或标准输入装置得到元素并构造处容器。
这些构造函数是mumber template,故若提供了从“源端”到“目标端”的元素型别自动装换,
那容器类别和元素型别都可不同。
eg1: 以另一容器元素为初值完成初始化操作
std::list I; // I is a linked list of ints
// ...
// copy all elements of the list as floats into a vector
std::vector c(I.begin(), I.end());
eg2:以某数组元素为初值完成初始化操作
int array[] = { 2, 3, 17, 33, 45, 77 };
// ...
// copy all elements of the array into a set
std::set c(array, array + sizeof(array)/sizeof(array[0] );
eg3:以标准输入完成初始化操作
// read all integer elements of the deque from standard input
std::deque c( ( std::istream_iterator(std::cin) ),
( std::istream_iterator() ) );
note: eg3不能遗漏了“初始化参数”的那对多余的括号,否则该语句就会变成一个函数声明:
若eg3不写括号:
std::deque c( std::istream_iterator(std::cin),
std::istream_iterator() );
会是一个返回值为deque的函数,其第一参数型别为istream_iterator参数名cin,
其第二参数是型别为一个不接受参数、返回值型别为istream_iterator的函数。
据C++规则其被视为声明。
- 与大小相关的函数操作(size operation)
1. size(): 返回当前容器的元素数量。
2. empty(): 即size()==0表达式的快捷形式。
但empty()的实现可能比size()==0效率更高。
3. max_size(): 返回容器所能容纳的最大元素数量。
其值因具体实现版本而不同。
如vector通常是一个内存块包含全部元素,故在PC上可能有相关限定。
max_size()通常返回索引型别的最大值。
- 比较(comparison)
含常用比较操作符==、!=、<、<=、>、>=。它们的定义依据以下三条规则:
1. 比较操作的两端(两容器)须型别相同。
2. 若两容器所有元素对应位置相等,则这两容器相等。
3. 用字典式(lexicographical)顺序比较原则来判断某容器是否小于另一容器。(见page360)
- assignment and swap
对容器赋值元素,源容器所有元素被拷贝到目标容器,后者所有元素被移除,故容器赋值操作代价高。
若两容器类别相同,且拷贝后源容器不再使用,则可使用一个优化方法:
swap()。其时间复杂度为常数。
因为swap()事实上只交换某些内部的指针(指向实际数据如元素、配置器、排序准则)。
6.2 vector(page148)
vector是一个动态数组。
因此,其本身是”将元素置于动态数组中加以管理“的一个抽象概念。
使用vector,须包含头文件:#include
其中型别vector是一个定义于namespace std内的template:
namespace std
{
template >
class vector;
}
vector的元素可为任意型别T,但须能assignment和copyable两个性质。
第二个参数可有可无,用于定义内存模型,缺省的内存模型是C++标准程序库提供的allocator。
6.2.1 vector能力(page149)
vector支持随机存取;vector迭代器是随机存取迭代器。
末端增删元素,性能好。但首部或中部增删元素性能低,
由于增删点后每个元素需移动,而每次移动要调用assignment操作符。
大小(size)和容量(capacity)
vector优异性能秘诀之一:配置比其所容纳元素所需更多的内存。
capacity():返回实际能容纳的元素数量。
若超过这个数量,vector就须重新配置内部存储器。
vector容量的重要性,原因如下:
1. 内存重新配置会导致和vector元素有关的所有reference、pointer、iterator都失效。
2. 内存重新配置时耗很高。
内存重配(配置)方法:
1. reserve()保留适当容量,以避免一再重配内存。
如此只要保留的容量有余,就不会使reference失效。eg:
std::vector v; // create an empty vector
v.reserve(80); // reserve memory for 80 elements
2. 初始化期间向构造函数传递附加参数,以构造足够空间。
若该附加参数是数值,该数则为vector的起始大小。
std::vector v(5); // create a vector and initializes it with five values
// calls five times the default constructor of type T
note:若用初始化方法配置内存,前提是容器的元素型别须有default构造函数。
若型别很复杂,就算提供了default ctor,初始化操作也很耗时。若只是为了保留足够内存,不如使用reserve()。
但是,vector不能使用reserve()来缩减内存容量:
vector容量概念上和string类似,vector不能用reserve()来缩减容量,这和string不同。
若调用reserve()所给的参数比当前vector容量小,不会有任何反应。
如何达到时间和空间的最佳效率,由具体实现版本决定。
且vector内存容量不会缩减,但有个间接缩减vector容量的方法:两个vector交换内容后,两者的容量也会互换。eg:
template
void shrinkCapacity (std::vector& v)
{
std::vector tmp(v); // copy elements into a new vector
v.swap(tmp); // swap internal vector data
}
或者使用如下语句直接缩减容量:
// shrink capacity of vector v for type T
std::vector(v).swap(v);
swap()操作后原有reference、pointer、iterator都换了指向对象,即会失效。
6.2.2 vector的操作函数(page150)
-
构造、拷贝和析构
-
非变动性操作(Nonmodifying Operation)
reserve()确实会modify vector,会使reference、pointer、iterator失效,但从逻辑内容上容器没变化,所以还是列在此处。
-
赋值(Assignment)
所有赋值操作都可能会调用元素型别的default ctor、copy ctor、assignment操作符以及可能析构函数,视元素数量变化而定,eg:
std::list l;
std::vector col1;
// ...
// make col1 be a copy of the contents of l
col1.assign(l.begin(), l.end() );
-
元素存取(Element Access)(page152)
若发生越界,没有范围检查会引发未定义行为。
std::vector col1; // empty
col1[5] = elem; // RUNTIME ERROR --> undefined behavior
std::cout << col1.front(); // RUNTIME ERROR --> undefined behavior
故调用operator[]时,确保索引有效:
std::vector col1; // empty
if(col1.size() >5)
{
col1[5] = elem; // OK
}
if (!col1.empty() )
{
cout << col1.front(); // OK
}
col1.at(5) = elem; // throws out_of_range exception
- 迭代器相关函数(Iterator Function)
迭代器的确切型别有实现版本决定。对vector而言通常是一般指针。一般指针就是随机存取迭代器。
vector迭代器持续有效,除非发生如下两种情况:
1. 在一个较小索引位置增删元素。
2. 由于容量变化而致的内存重配。
- 插入(insert)和删除(remove)元素(page153)
性能方面,以下情况稍好:
1. 容器尾部增删元素
2. 容器容量起初就足够大
3. 插入多个元素时,”调用一次“比”调用多次“要快
vector未提供函数用于直接移除”与某值相等“的所有元素。则应使用算法了,如下语句可将值val的所有元素移除:
std::vector col1;
// ...
// remove all elements with value val
col1.erase( remove(col1.begin(), col1.end(), val), col1.end() );
详解可见page111
若只是移除”与某值相等“的第一个元素:
std::vector col1;
// ...
// remove first element with value val
std::vector::iterator pos;
pos = find(col1.begin(), col1.end(), val );
if (pos != col1.end() )
{
col1.erase(pos);
}
6.2.3 将vector当做一般array使用
若需要一个动态数组,即可用vector,但须确保vector大小能容纳所有数据。
eg:使用C-String,记住最后有个'\0'。
即若需一个元素型别为T的数组,就vector,然后传递第一元素的额地址给它。
note:不能把迭代器当做第一元素的地址来传递。vector迭代器由实现版本定义的,或许不是一个指针。
6.2.4 异常处理(page155)
使用vector调用的函数抛出异常,C++标准程序库保证:
1.push_back()时发生异常,该函数不起作用。
2.若元素拷贝(包括copy ctor和assignment操作符)不抛出异常,要么insert()成功,要么不生效。
3.pop_back()不会抛出异常。
4.若元素拷贝(包括copy ctor和assignment操作符)不抛出异常,则erase()和clear()不抛出异常。
5.swap()不抛出异常。
6.若元素拷贝(包括copy ctor和assignment操作符)不抛出异常,则所有操作要么成功,要么无效。
这类元素称为POD(plain old data)。
POD泛指那些无C++特性的型别(如 C structure)。
所有上述保证基于一个条件:析构函数不抛出异常。
8.2.5 vector example
// 展示vector的简单用法
// cont/vector1.cpp
#include
#include
#include
#include
#include
using namespace std;
int main()
{
// create empty vector fro strings
vector sentence;
// reserve memory for five elements to avoid reallocation
sentence.reserve(5);
// append some elements
sentence.push_back("Hello");
sentence.push_back("how");
sentence.push_back("are");
sentence.push_back("you");
sentence.push_back("?");
// print elements separated with spaces
copy (sentence.begin(), sentence.end(), ostream_iterator(cout, " "));
cout << endl;
// print "technical data"
cout << "max_size(): " << sentence.max_size() << endl;
cout << "size(): " << sentence.size() << endl;
cout << "capacity(): " << sentence.capacity() << endl;
// swap second and fourth element
swap (sentence[1], sentence[3]);
// insert element "always" before element "?"
sentence.insert(find(sentence.begin(), sentence.end(), "?"), "always");
// assign "!" to the last element
sentence.back() = "!";
// print elements separated with space
copy (sentence.begin(), sentence.end(), ostream_iterator(cout, " "));
cout << endl;
// print "technical data" again
cout << " max_size(): " << sentence.max_size() << endl;
cout << " size(): " << sentence.size() << endl;
cout << " capacity(): " << sentence.capacity() << endl;
}
输出可能是:
(因为max_size()和capacity()结果有具体实现版本决定)
6.2.6 class vector(page158)
C++标准程序库对元素型别为bool的vector定制了特殊版本,以获取一个优化的vector:
其耗用空间远小于一般vector实现的bool vector。
一般vector的实现会为每个元素至少分配一个byte空间,
而vector
由于vector大小可动态改变,故可当成动态大小的bitfield(位域)。
如此可增删bit。
若需要静态大小的bitfield,应使用bitset。
vector
namespace std
{
class vector
{
public:
// auxiliary type for subscript operator
class reference
{
public:
// automatic type conversion to bool
operator bool() const;
// assignments
reference& operator= (const bool);
reference& operator= (const reference&);
// bit complement
void flip();
};
// ...
// operations for element access
// - return types is reference instead of bool
reference operator[] (size_type n);
reference at(size_type n);
reference front();
reference back();
// ...
};
}
6.3 deque(page160)
与vector类似,采用动态数组管理元素,可随机存取,并与vector有类似的接口。
但deque动态数组头尾两端都可增删元素。
同vector,deque型别定义与命名空间std内的一个class template:
namespace std
{
template >
class deque;
}
与vector同,第一个template参数用于表明元素型别(只要assignable和copyable)。
6.3.1 deque能力
- 与vector相比,deque不同之处:
1. 两端都可增删元素。
2. 存取元素时,deque内部会多一个间接过程,故元素存取及迭代器动作稍慢。
3. 迭代器需在不同区块间跳转,故须是特殊的智能型指针。
4. 在对内存块有限制的系统中(如PC系统),deque可内含更多元素,
因为它使用不止一块内存,故其max_size()可能更大。
5.不支持对容量和内存重配的时机的控制。
除头尾的其它位置增删会导致deque的pointer、reference、iterator失效。
但 deque的内存重配优于vector,
因为deque在内存重配时不必复制所有元素。
6.deque内存区块不再被使用时会被释放。
deque的内存大小可缩减(不同版本实现方式不同)。
- 与vector相比,deque相似之处:
1. 中部增删元素很慢,因为所有元素都需移动。
2. 迭代器是random access iterator。
总之,以下情形优选deque:
1. 需在两端增删元素。
2. 无需引用容器内的元素。
3. 要求容器释放不再使用的元素。
6.3.2 deque操作(page162)
deque各项操作只有以下几点与vector不同:
1. deque不提供容量操作(capacity()和reserve())。
2. deque直接提供函数用于头部元素增删(push_front()和pop_front())。
6.3.3 异常处理&&example(page164)
原则上deque提供的异常处理和vector一样,因此C++标准程序库保证:
1. 若push_back()或push_front()发生异常时,该操作不带来任何影响。
2. pop_back()和pop_front()不会抛出异常。
// 展示deque的简单运用
// cont/deque1.cpp
#include
#include
#include
#include
#include
using namespace std;
int main()
{
// create empty deque of strings
deque col1;
// insert several elements
col1.assign(3, string("string"));
col1.push_back("last string");
col1.push_front("first string");
// print elements separated by newlines
copy(col1.begin(), col1.end(), ostream_iterator(cout, "\n"));
cout << endl;
// remove first and last elements
col1.pop_front();
col1.pop_back();
// insert "another" into every element but the first
for (int i = 1; i < col1.size(); ++i)
{
col1[i] = "another " + col1[i];
}
// change size to four elements
col1.resize (4, "resized string");
// print elements separated by newlines
copy (col1.begin(), col1.end(), ostream_iterator(cout, "\n"));
cout << endl;
}
输出:
6.4 list(page166)
list是double linked list(双向链表)。
list型别定义于namespace std中,是class template:
namespace std
{
template >
class list;
}
6.4.1 list能力
list由于内部结构和vectror/deque不同,故特性不同:
1. 不支持随机存取。只能顺序遍历。
2. 任何位置增删很快,无需移动元素。
且增删元素不会使pointer、reference、iterator失效。
3. list对于异常处理方式:操作要么成功,要么无效。
list提供的成员函数反映它和vector/deque的不同:
1. 由于不支持随机存取,故不提供下标操作符和at()。
2. 各元素都有自己的内存,被删除前一直有效,故无容量、内存重配操作。
3. 提供了一些特殊成员函数以移动元素(调整指针)。
6.4.2 list 操作(page167)
只有运用迭代器才能存取list中各个元素。
为移除元素,list配备了remove()的特殊版本,该成员函数比remove()算法要快(只有指针操作,无需顾及元素)。
故移除元素应用成员函数remove()而不是像vector/deque调用STL算法的remove()。
-
splice 函数
6.4.3 异常处理&&example(page172)
// 展示list特殊成员函数的用法
// cont/list1.cpp
#include
#include
#include
#include
using namespace std;
void printLists (const list& l1, const list&l2)
{
cout << "list: ";
copy (l1.begin(), l1.end(), ostream_iterator(cout, " "));
cout << endl << "list2: ";
copy (l2.begin(), l2.end(), ostream_iterator(cout," "));
cout << endl << endl;
}
int main()
{
// create two empty lists
list list1, list2;
// fill both lists with elements
for (int i = 0; i < 6; ++i)
{
list1.push_back(i);
list2.push_front(i);
}
printLists(list1, list2);
// insert all elements of list1 before the first element with value 3 of list2
// - find() returns an iterator to the first element with value 3
list2.splice(find(list2.begin(), list2.end(), 3), // destination positon
list1); // source list
printLists(list1, list2);
// move first element to the end
list2.splice(list2.end(), // destination position
list2, // source list
list2.end()); // source position
printLists(list1, list2);
// sort second list, assign to list1 and remove duplicates
list2.sort();
list1 = list2;
list2.unique();
printLists(list1, list2);
// merge both sorted lists into the first list
list1.merge(list2);
printLists(list1, list2);
}
输出:
6.5 set and multiset(page175)
6.5.1 set/multiset能力
set和multiset会根据特定排序准则自动对元素排序,set不允许重复值而multiset允许。二者定于于namespace std中的class template:
namespace std
{
template ,
class Allocator = allocator >
class set;
template ,
class Allocator = allocator >
class set;
}
满足assignable、copyable、comparable(根据特定排序准则)的型别T,都可称为set/multiset的元素型别。
仿函数less以operator<比较元素。
6.5.2 set和multiset操作(page177)
与所有标准关联式容器类似,set/multiset通常以平衡二叉树实现。
自动排序优势在于二叉树查找的良好性能。
但自动排序对set/multiset的限制:不能直接改变元素值(否则会打乱原本正确顺序),要易值须先删除旧元素,再插入新元素。提供的接口反映此行为:
set/multiset不提供直接存取元素的操作。
通过迭代器存取元素,有限制:从迭代器角度看,元素值是常数。
容器元素的比较只限于相同型别的容器之间,即容器元素型别和排序准则须有相同的型别。
比较操作以“字典顺序”比较(即第一元素具有高优先级)。若比较不同型别的容器元素,须用STL算法。
equal_range()返回一个pair,eg:
// 展示使用 lower_bound()、upper_bound()、equal_range()
// cont/set2.cpp
#include
#include
#include
using namespace std;
int main()
{
set c;
c.insert(1);
c.insert(2);
c.insert(4);
c.insert(5);
c.insert(6);
cout << "lower_bound(3): " << *c.lower_bound(3) << endl;
cout << "upper_bound(3): " << *c.upper_bound(3) << endl;
cout << "equal_range(3): " << *c.equal_range(3).first << " "
<< *c.equal_range(3).second << endl;
cout << endl;
cout << "lower_bound(5): " << *c.lower_bound(5) << endl;
cout << "upper_bound(5): " << *c.upper_bound(5) << endl;
cout << "equal_range(5): " << *c.equal_range(5).first << " "
<< *c.equal_range(5).second << endl;
}
输出:
若用multiset,输出相同。
赋值操作的容器须具有相同型别。
与所有关联式容器类似,这里返回的迭代器是双向迭代器(因为实现是基于二叉树)。故对于只适用于随机迭代器的STL算法,associate container不适用。(page182)
且,对迭代器操作,所有元素都被视为常数,以确保不会修改元素值而打乱原定顺序,故无法对associate container调用modifying algorithm。
note:set和multiset的insert()成员函数返回型别不一定相同:
set:
pair insert(const value_type& elem);
iterator insert(iterator pos_hint, const value_type& elem);
multiset:
iterator insert(const value_type& elem);
iterator insert(iterator pos_hint, const value_type& elem);
返回型别不同原因:multiset允许元素重复而set不允许,若insert一个元素到set中,而set内已存在该元素,则插入操作失败,所以set返回pair型别,其second成员表插入成功否,first成员表新元素位置或现存同值元素位置。
注:所有拥有“位置提示参数”的插入函数,其返回型别都一样即一个迭代器。
还用一个返回型别不一致的情况:作用于序列式容器和关联式容器的erase()函数:
1.序列式容器erase()成员函数:
iterator erase(iterator pos);
iterator erase(iterator beg, iterator end);
2. 关联式容器erase()成员函数:
void erase(iterator pos);
void erase(iterator beg, iterator end);
6.5.3 exception handling
set/multiset是“以节点为基础”的容器。若节点构造失败,则容器保持原样。
另,由于析构函数通常不抛出异常,所以节点的移除不可能失败。
详见page139 5.11.2和p248 6.10.10的“异常出现时的特殊保证”。
6.5.4 example(page186)
example of set:
// 展示set用法
// cont/set1.cpp
#include
#include
#include
using namespace std;
int main()
{
/* type of the collection:sets:
* - no duplicates
* - elements are integral values
* - descending order
*/
typedef set > IntSet;
IntSet coll1; // empty set container
// insert elements in random order
coll1.insert(4);
coll1.insert(3);
coll1.insert(5);
coll1.insert(1);
coll1.insert(6);
coll1.insert(2);
coll1.insert(5);
// iterator over all elements and print them
IntSet::iterator pos;
for(pos = coll1.begin(); pos != coll1.end(); ++pos)
{
cout << *pos << " ";
}
cout << endl;
// insert 4 again and process return value
pair status = coll1.insert(4);
if (status.second)
{
cout << " 4 inserted as element "
<< distance(coll1.begin(), status.first) + 1
<< endl;
}
else
{
cout << " 4 already exist " << endl;
}
// assign elements to another set with ascending order
set coll2(coll1.begin(), coll1.end());
// print all elements of the copy
copy (coll2.begin(), coll2.end(), ostream_iterator(cout, " "));
cout << endl;
// remove all elements up to elements with value 3
coll2.erase(coll2.begin(), coll2.find(3));
// remove all elements with value 5
int num;
num = coll2.erase(5);
cout << num << " element(s) removed " << endl;
// print all elements
copy (coll2.begin(), coll2.end(), ostream_iterator(cout, " "));
cout << endl;
}
output:
example of multiset:
// 相比较于set1.cpp,要做写改变并得到不同结果
// cont/mset1.cpp
#include
#include
#include
using namespace std;
int main()
{
/* type of collection: sets
* - duplicates allowed
* - elements are integral values
* - descending order
*/
typedef multiset > IntSet;
IntSet coll1; // empty multiset container
// insert elements in random order
coll1.insert(4);
coll1.insert(3);
coll1.insert(5);
coll1.insert(1);
coll1.insert(6);
coll1.insert(2);
coll1.insert(5);
// iterate over all elements and print them
IntSet::iterator pos;
for (pos = coll1.begin(); pos != coll1.end(); ++pos)
{
cout << *pos << " ";
}
cout << endl;
// insert 4 again and process return value
IntSet::iterator ipos = coll1.insert(4);
cout << " 4 inserted as element "
<< distance(coll1.begin(), ipos) + 1
<< endl;
// assign elements to another multiset with ascending order
multiset coll2(coll1.begin(), coll1.end());
// print all elements of the copy
copy (coll2.begin(), coll2.end(), ostream_iterator(cout," "));
cout << endl;
// remove all elements up to element with value 3
coll2.erase(coll2.begin(), coll2.find(3));
// remove all element with value 5
int num;
num = coll2.erase(5);
cout << num << " element(s) removed " << endl;
// print all elements
copy(coll2.begin(), coll2.end(), ostream_iterator(cout, " "));
cout << endl;
}
output:
6.5.5 运行时指定排序准则(page191)
有时需要在运行时处理排序准则,就需要一个“表示排序准则”的特殊型别,使能在运行时传递某准则,eg:
// 运行时处理排序准则
// cont/setcmp.cpp
#include
#include
#include
#include "../stl/print.hpp"
using namespace std;
// type for sorting criterion
template
class RuntimeCmp
{
public:
enum cmp_mode{normal, reverse};
private:
cmp_mode mode;
public:
// constructor for sorting criterion
// - default criterion uses value normal
RuntimeCmp (cmp_mode m = normal) : mode(m){}
// comparision of elements
bool operator() (const T& t1, const T& t2) const
{
return mode == normal ? t1 < t2 : t2 < t1;
}
// comparision of sorting criteria
bool operator== (const RuntimeCmp& rc)
{
return mode == rc.mode;
}
};
// type of a set that uses this sorting criterion
typedef set > IntSet;
// forward declaration
void fill (IntSet& set);
int main()
{
// create, fill , and print set with normal element order
// - uses default sorting criterion
IntSet coll1;
fill(coll1);
PRINT_ELEMENTS(coll1,"coll1: ");
// create sorting criterion with reverse element order
RuntimeCmp reverse_order(RuntimeCmp::reverse);
// create , fill , and print set with reverse element order
IntSet coll2(reverse_order);
fill(coll2);
PRINT_ELEMENTS(coll2, "coll2: ");
// assign elements and sorting criterion
coll1 = coll2;
coll1.insert(3);
PRINT_ELEMENTS(coll1, "coll1: ");
// just to make sure ...
if (coll1.value_comp() == coll2.value_comp())
{
cout << "coll1 and coll2 have some sorting criterion" << endl;
}
else
{
cout << "coll1 and coll2 have different sorting criterion" << endl;
}
}
void fill (IntSet& set)
{
// fill insert elements in random order
set.insert(4);
set.insert(7);
set.insert(5);
set.insert(1);
set.insert(6);
set.insert(2);
set.insert(5);
}
output:
在此程序中,RuntimeCmp<>是个简单的template,提供“运行时对任意型别定义的一个排序准则”的泛化能力;assignment操作符不仅赋值了元素,还赋值了排序准则。(当排序准则是变量时可赋值)