首页 > 代码库 > C++基础之容器
C++基础之容器
顺序容器简介:
顺序容器类型 | 描述 |
---|---|
vector | 可变大小数组,支持快速访问,在尾部之外的地方插入或删除时可能很慢 |
deque | 双端队列。支持快速访问,在头尾插入删除会很快。 |
list | 双向列表。只支持双向顺序访问。插入删除很快 |
forward_list | 单向列表。只支持单向顺序访问,在任何位置插入或删除都很快 |
array | 固定大小数组,支持快速随机访问,不能添加删除元素 |
string | 与vector类似,专门用于保存字符。随机访问快,在尾部添加删除快 |
其中array和forward_list是新C++标准增加的类型。与内置数组相比,array是一种更安全更容易使用的数组类型。而 forward_list设计目标是大道与最好的手写单向链表相当的性能,因此它没有size操作,而对其他容器,size是快速的常量时间操作。
选用容器的基本原则:
- 除非有很好的原因,否则应该使用vector;
- 如果程序有很多很小的元素,且空间的额外开销很重要,则不要使用list和forward_list;
- 如果要求随机访问元素,则使用vector或deque;
- 如果需要在容器中间插入删除,应该使用list或forward_list;
- 如果需要在容器头尾插入删除,不需要在中间插入删除,则使用deque;
- 如果只有在读入数据时,需要在容器中间插入元素,随后要随机访问元素,则首先要确定是真的需要在中间插入,能否在尾部插入然后重排来实现,如果还是必须在中间插入,则先用list接受数据在拷贝到vector中;
注意:较旧的编译器需要在vector<vector<string> >的两个尖括号之间加上空格。
所有容器的通用操作:
相关类型总结:
size_type 无符号整型,足以存储此容器类型的最大可能容器长度
iterator 此容器类型的迭代器类型
const_iterator 元素的只读迭代器类型
reverse_iterator 按逆序寻址元素的迭代器(不支持forward_lsit)
const_reverse_iterator 元素的只读(不能写)逆序迭代器(不支持forward_lsit)
difference_type 足够存储两个迭代器差值的有符号整型,可为负数
value_type 元素类型
reference 元素的左值类型,是 value_type& 的同义词
const_reference 元素的常量左值类型,等效于 const value_type&
构造函数总结
C<T> c |
创建一个名为c 的空容器,C 是容器类型名,如vector ,T 是元素类型,如int ,string 。适用于所有容器 |
C c(c2) |
创建容器c2 的副本c ;c2 和c 必须具有相同的容器类型,并存放相同类型的元素。适用于所有容器 |
C c={a,b,c...} |
c初始化为初始化列表中元素的拷贝。列表中元素类型必须与c的元素类型相容,对于array类型,列表中元素数目必须等于 或小于array的大小,任何遗漏的元素都进行值初始化。 |
C c(n) |
创建有n 个初始化元素的容器c 。只适用顺序容器 |
C c(n, t) |
使用n 个为t 的元素创建容器c ,其中值t 必须是容器类型C 的元素类型的值,或者是可以转换为该类型的值。只适用顺序容器 |
C c(b, e) |
创建容器c ,其中元素是迭代器b 和e 标示的范围内元素的副本。适用于所有容器 |
将一个容器创建为另一个容器的拷贝的两种方法:
- 直接拷贝整个容器;两个容器的类型和元素类型必须匹配;
- 拷贝迭代器指定的元素范围;不要求容器类型相同,元素类型也可以不同只要能够相互转换。
如果元素是内置类型或有默认构造函数的类类型,怎可以只提供容器大小,如果元素没有默认构造函数就必须显示指定初始值。
array对类类型初始化时,需要该类类型由默认构造函数,array类型可以拷贝或赋值,但要求初始值类型和容器类型相同,且元素类型和大小一样。
赋值操作
c1 = c2; c2赋给c1,如果两容器大小不同,赋值后,两者大小与右边容器的大小相同。array允许赋值,但是左右两边的对象必须有相同类型。
c1 = {a,b,c}; c1中的元素替换为列表中的元素,array不支持assign和值列表进行赋值(因为两边大小可能不一样)。
赋值相关运算会导致左边容器内部迭代器、引用和指针失效,除了array和string外,swap不会导致容器的迭代器、引用和指针失效。
顺序容器可以用assign来从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。
seq.assign(b,e);//seq替换为b到e之间的元素
seq.assign(li);//seq替换为初始化列表中的元素
seq.assign(n,t);//seq替换为n个值为t的元素
swap函数:
a.swap(b); a和b交换
swap(a,b); a和b交换
除了array外,swap交换两个容器内容操作很快,因为他并没有交换元素本身,而是交换了容器内部的数据结构。所以,可以保证在常数时间内完成。
因此,除了string外指向容器的迭代器、引用和指针在swap后也不会失效,只是它们所属的容器变化了。
注意。早期版本只有成员函数版本的swap,新版本两个都有,但是非成员函数的swap对泛型编程很重要,统一使用非成员版本的swap是个好习惯。
其他函数
c.size() 返回容器 c 中的元素个数。返回类型为 c::size_type;(不支持forward_list)
c.max_size() 返回容器 c 可容纳的最多元素个数,返回类型为 c::size_type
c.empty() 返回标记容器大小是否为 0 的布尔值
c.erase(args) 删除args指定的元素
c.clear() 删除容器 c 内的所有元素。返回 void
c.insert(args) 将args中的元素拷贝到c中
c.begin() 返回一个迭代器,它指向容器 c 的第一个元素
c.end() 返回一个迭代器,它指向容器 c 的最后一个元素的下一位置
c.rbegin() 返回一个逆序迭代器,它指向容器 c 的最后一个元素
c.rend() 返回一个逆序迭代器,它指向容器 c 的第一个元素前面的位置
c.crbegin() 返回一个const逆序迭代器,它指向容器 c 的最后一个元素
c.crend() 返回一个const逆序迭代器,它指向容器 c 的第一个元素前面的位置
以c开头的函数是C++新标准引入的用以支持auto与begin和end结合使用。auto与begin和end结合使用时,获取的迭代器类型依赖于容器类型,而c开头的获得的是const_iterator。
每个容器都支持!=和==,除了无序容器外的所有容器都支持关系运算符(>、>=、<、<=)。因此比较容器时,尽量使用!=和==。
添加元素(array不支持这些操作):
push_back(t);//适用于所有顺序容器
push_front(t);//仅适用于list和deque
insert(p,t);//在迭代器p前面插入一个元素,返回新添加元素的迭代器
insert(p,n,t)//在迭代器p前面插入n个t元素,返回void
insert(p,b,e)//在迭代器p前面插入迭代器b和e之间的元素
还可以插入初始化列表insert(p,li);在迭代器p前面插入li初始化列表。
新标准增加的方法
emplace();//对应insert()的功能
emplace_back(t);//对应push_back()的功能
emplace_front(t);//对应push_front()的功能
emplace不是拷贝元素,而是调用参数传递给元素类型的构造函数直接在容器的内存中构造元素。
forward_list有自己专有版本的insert和emplace;forward_list不支持push_back和emplace_back;
容器中访问元素的函数back()、front()、at()和下标操作返回的都是引用。
如果想下标是合法的可以使用at(),它会检查界限。越界抛出out_of_range异常。
删除元素(array不支持这些操作):
C.pop_back();//forward_list不支持该方法,返回void
C.pop_front();//仅适用于vector和deque容器,返回void
C.erase(p);//删除p所指的元素,返回下一个位置
C.erase(b,e);//删除迭代器b和e之间的元素,返回下一个位置
C.clear();//删除所有元素,返回void
删除前请确保元素存在。
特殊的forward_list操作
forward_list是单向链表,所以在它上面添加或删除元素的操作是通过改变给定元素之后的元素来完成的。
c.push_front() |
在开头插入元素 |
c.pop_front() |
删掉开头元素 |
c.insert_after(pos, elem) |
在pos之后插入元素, 返回新元素的位置 |
c.insert_after(pos, n, elem) |
在pos之后插入n个元素elem, 返回第一个新元素的位置 |
c.insert_after(pos, begin, end) |
在pos之后插入元素[begin, end), 返回第一个新元素的位置 |
c.insert_after(pos, initiallist) |
在pos之后插入initiallist, 返回第一个新元素的位置 |
c.emplace_after(pos, args...) |
在pos之后插入args…元素,返回第一个新元素的位置 |
c.emplace_front(args...) |
在开头插入元素args…, 无返回值 |
c.erase_after(pos) |
删掉pos后面那个元素 |
c.erase_after(begin, end) |
删掉(begin, end)之间的元素 |
before_begin返回一个首前(off-the-beginning)迭代器。这个迭代器允许我们在链表受元素之前不存在的元素之后添加或删除元素。
大小操作
C.size();//返回容器内元素的个数
C.max_size(0;//返回容器可容纳的最大元素个数
C.empty();
C.resize(n);//调整容器的长度,使其能容纳n个元素,如果n<C.size(),删除过多的,else 添加采用值初始化的新元素;类类型需要提供默认构造函数
C.resize(n,t);//调整容器的长度,使其能容纳n个元素,新添加的元素初始化为t
总结1:容器操作可能是迭代器失效:
向容器添加元素时,
如果容器是vector和string,且存储空间被重新分配,则指向容器的迭代器。指针和引用都会失效,如果为重新分配存储空间,指向插入位置之前的元素的迭代器、指针和引用有效,之后的失效;
如果是deque,插入收尾之外的位置都会使迭代器、指针和引用失效,插入首尾时会使迭代器失效;
如果是list和forward_list,都有效。
向容器删除元素时,
如果是list和forward_list,指向其他地方的迭代器、指针和引用都有效;
如果是deque,删除收尾之外的位置都会使迭代器、指针和引用失效;删除首尾时,其他地方的迭代器、指针和引用都有效,但是删除为元素时,尾后迭代器会失效;
如果容器是vector和string,指向被删除元素之前的元素的迭代器、指针和引用仍有效,尾后迭代器会失效;
因此,不要保存尾后迭代器,添加和删除元素常常会使尾后迭代器失效;
下面是迭代器失效的例子和解决方法:
1 #include <iostream> 2 #include <map> 3 using namespace std; 4 5 typedef map<int, int> Map; 6 typedef map<int, int>::iterator MapIt; 7 8 void print(Map &m) 9 { 10 MapIt it; 11 for(it = m.begin(); it != m.end(); it++) 12 { 13 cout << it->second << " "; 14 } 15 16 cout << endl; 17 } 18 19 void deleteValueFromMap(Map &m, int n = 5) 20 { 21 MapIt it; 22 for(it = m.begin(); it != m.end(); it++) 23 { 24 if(0 == it->second % n) 25 { 26 m.erase(it); 27 } 28 } 29 } 30 31 int main() 32 { 33 Map m; 34 int i = 0; 35 for(i = 0; i < 21; i++) 36 { 37 m[i] = i; 38 } 39 40 print(m); 41 42 deleteValueFromMap(m); // 程序崩溃 43 44 return 0; 45 }
运行上述程序, 结果程序崩溃,什么原因呢? 原来, 对于关联的容器map来说, m.erase(it);后, it就失效了, 而for循环中有it++, 自然而然会出问题啊。 那怎么办呢? 且看:
1 #include <iostream> 2 #include <map> 3 using namespace std; 4 5 typedef map<int, int> Map; 6 typedef map<int, int>::iterator MapIt; 7 8 void print(Map &m) 9 { 10 MapIt it; 11 for(it = m.begin(); it != m.end(); it++) 12 { 13 cout << it->second << " "; 14 } 15 16 cout << endl; 17 } 18 19 void deleteValueFromMap(Map &m, int n = 5) 20 { 21 MapIt it; 22 MapIt tmp; 23 for(it = m.begin(); it != m.end(); /*不能再自增了*/) 24 { 25 if(0 == it->second % n) 26 { 27 tmp = it++; 28 m.erase(tmp); 29 } 30 else 31 { 32 it++; 33 } 34 } 35 } 36 37 int main() 38 { 39 Map m; 40 int i = 0; 41 for(i = 0; i < 21; i++) 42 { 43 m[i] = i; 44 } 45 46 print(m); 47 48 deleteValueFromMap(m); // 程序ok 49 print(m); 50 51 return 0; 52 } 53 结果为: 54 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 55 1 2 3 4 6 7 8 9 11 12 13 14 16 17 18 19 56 57 58 59 60 61 当然, 上述程序也可以继续简化为: 62 63 64 65 #include <iostream> 66 #include <map> 67 using namespace std; 68 69 typedef map<int, int> Map; 70 typedef map<int, int>::iterator MapIt; 71 72 void print(Map &m) 73 { 74 MapIt it; 75 for(it = m.begin(); it != m.end(); it++) 76 { 77 cout << it->second << " "; 78 } 79 80 cout << endl; 81 } 82 83 void deleteValueFromMap(Map &m, int n = 5) 84 { 85 MapIt it; 86 for(it = m.begin(); it != m.end(); /*不能再自增了*/) 87 { 88 if(0 == it->second % n) 89 { 90 m.erase(it++); 91 } 92 else 93 { 94 it++; 95 } 96 } 97 } 98 99 int main() 100 { 101 Map m; 102 int i = 0; 103 for(i = 0; i < 21; i++) 104 { 105 m[i] = i; 106 } 107 108 print(m); 109 110 deleteValueFromMap(m); // 程序ok 111 print(m); 112 113 return 0; 114 } 115 结果ok.
注意上面第90行m.erase(it++);这句话就不会出现迭代器失效的情况。因为it++;会返回加一之前的迭代器。
vector的增长
vector和string每次分配空间会分配比需求更大的空间
capacity() 可以告诉我们容器在不扩张内存的情况下可以容纳多少各元素
reserve() 可以让我们通知容器它应该准备保存多少个元素的空间
容器大小管理操作:
shrink_to_fit 只能用于vector,string,deque
capacity,reserve 只能用于vector,string
c.shrink_to_fit() 将capacity()减少为size()相同的大小,实际实现中可以忽略该函数,所以实际上不一定会返回空间
c.capacity() 不重新分配内存空间的话,c可以保存多少元素个数
c.reserve(n) 分配至少能容纳n个元素的内存空间,n如果<=capacity(),那么reserve什么也不做;n大于当前容量时,才会分配空间。
c.size() 容器中元素的个数,与capacity是不一样的;
大部分vector采用的分配策略:就是在每次需要分配内存空间时,将当前的容量capacity翻倍;
这也是不确定的,应该具体问题具体分析。
通过在一个初始为空的vector上调用push_back来创建一个n个元素的vector,所花费的时间不能超过n的常数倍。
C++基础之容器