2:c++标准库中的顺序容器,所有顺序容器都提供了快速顺序访问元素的能力。
但是在两方面还是有性能上的折中,一是向容器中添加或从容器中删除元素的代价,二是非顺序访问容器中元素的代价。
类型 | 含义 | 缺点 |
---|---|---|
vector | 可变大小数组。支持快速随机访问 | 在尾部之外的位置插入或者删除可能比较慢 |
deque | 双端队列支持快速访问 | |
list | 双向链表,只支持双向顺序访问,在list中任何位置插入删除都非常快 | |
forward_list | 单向链表,只支持单项顺序访问,在任何位置进行插入删除操作位置都非常快 | |
array | 普通数组,固定大小 | 大小不可变 |
string | 与vector相似,专门用于保存字符,随机访问较快,在尾部插入/删除很快 |
除了固定大小的array外,其他容器都提供高效、灵活的内存管理。
1:string和vector相似,会将元素存储在连续的内存空间内,因此随机访问的时候利用下标来计算地址是非常迅速的,但是如果插入或者删除元素,就需要将插入或者删除的位置之后的元素统一后移,这样是非常低效率的。
2:list和forward_list这两个链表的设计目的是让容器的任何位置插入和删除元素变的简单。前提肯定是我们找到了这个位置,然后在它的前面或者后面插入或者删除元素,要么malloc
一块空间,要么free
一块空间。但是我们也无法向a[5]
这样去访问一个链表的5号元素,只能设置好移动大小,然后再移动过去。
3:deque叫双端队列,在随机访问上面,deque和vector及string是一样支持的,同样,在deque中间位置插入或者删除元素的代价非常大,但是在deque两端删除或者插入元素的效率是比较高的。
3:与容器一样,迭代器也有公共的接口。如果一个迭代器实现某种操作,那么所有提供这种操作的迭代器对这个操作的实现方式都是相同的。
通用:标准容器迭代器支持的运算符如下:
运算符 | 含义 |
---|---|
*iter | 返回迭代器iter所指元素的引用 |
iter->item | 解引用iter并获取该元素的名为item的成员,等价于(*iter).item |
++iter | 令iter指向容器中下一个元素 |
–iter | 令iter指向容器中上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等,相等条件:它们指向同一个迭代器或者是同一个容器的尾后迭代器 |
但是应该注意两个特殊的点:
1:forward_list不支持--
运算符。
2:对于+、-、+=、-=、>、>=、<、<=
等算术运算符不能在链表上使用(list和forward_list)。
4:begin()和end()的关系:[begin,end)
- 如果begin和end相等,那么范围为空
- 如果begin不等于end,那么范围中至少有一个元素
- 我们可以对begin递增若干次得到end
5:下面两个不能同时auto,这是为什么呢?
bool find(auto begin,auto end,int a) //正确
auto find(std::vector::iterator begin,std::vectoriterator end,int a) //正确
auto find(auto begin,auto end,int a) //错误!!
6:对于begin()和end()与auto的配合使用
选择原则:如果需要使用const和reverse就将
std::list<std::string> a = {"beijing","xian","tangshan"};
auto it1 = a.begin(); //std::list<std::string>::iterator
auto it2 = a.rbegin(); //std::list<std::string>::reverse_iterator
auto it3 = a.cbegin(); //std::list<std::string>::const_iterator
auto it4 = a.rcbegin(); //std::list<std::string>::const\_reverse\_iterator
7:有关容器的定义和初始化
1:将一个容器拷贝给另一个容器的初始化:
(1):如果是直接使用另一个容器,那么就必须保证容器类型
和元素类型一致
。
(2):如果是使用迭代器拷贝,则只要元素类型可以转换,那么不需要保证上面说的类型一致。
2:与顺序容器大小相关的构造函数:
std::vector<int> ivec(10,5); //10个int类型的元素,每个元素都初始化成5。
3:array在使用的时候必须同时指定元素类型和大小。并且array不向内置数组类型,它是支持拷贝的,但是要注意array要求初始值的类型必须与创建的容器类型相同。
int a[3] = {1,2,3};
int b[3] = a; //错误:内置数组不允许拷贝
std::array<int,3> c = {1,2,3};
std::array<int,3> d = c; //d和c是完全一样的。
8:赋值和交换
1:array不能使用列表赋值
运算。但是当两个array是相同类型的时候,可以拷贝。
2:使用assgin:
list<string> names;
vector<const char *> old;
names = old; //错误
names.assign(old.cbegin(),old.cend()); //用法一:将old拷贝
names.assign(10,"nihao"); //用法二:用nihao指定初始化assign
3:使用swap:使用于交换两个相同类型的容器
9:容器大小的操作有三个:size()和empty()以及max_size(),但是单链表forward_list没有size()操作。
10:关系运算符:适用于顺序容器。每个容器类型都支持相等运算符。他们之间比较的原则如下
- 如果两个容器大小相同,对应元素也相同,则他们是相同的容器。
- 如果大小不同,但较小容器是较大容器”前子缀”,则较小容器小于较大容器。
- 如果都不是子序列,则比较结果取决于第一个不相等的元素。
注意:容器之所以能使用相应的运算符,是因为元素
定义了相应的运算符。容器中的元素要是没有定义相应的运算符,那么容器也是无法使用相应运算符来比较大小的。
11:顺序容器的相关操作。
(1):添加元素:这样会改变容器原来的大小,因此array
不支持这样的操作,forward_list
不支持push_back
和emplace_front
等操作
vector
和string
不支持push_front
和emplace_front
。一般我们使用下面的操作添加元素:
操作 | 含义 |
---|---|
push_back(t)和emplace_back(args) | 在尾部创建一个值为t或者由args创建的元素。返回void |
push_front(t)和emplace_front(args) | 在头部创建一个值为t或者由args创建的元素。返回void |
insert() | 可以在指定元素后面插入也可以在相应迭代器指定的范围插入 |
注意:
1:向一个vector
和string
以及deque
中添加元素会使得所有指向容器的迭代器失效。
2:使用push_back()时候或者初始化容器的时候我们用的实际上是对象的拷贝,容器中的元素与原来对象是没有关系的。
3:我们不能在使用insert()函数确定范围的时候使用指向容器本身的迭代器。
4:insert()函数添加成功返回指向第一个新加入元素的迭代器,要是范围为空,即不添加任何元素,则insert操作会将第一个参数返回。
std::list<int> lst;
std::string word;
auto iter = lst.begin();
while(cin >> word) {
iter = lst.insert(iter,word);
}
/*此循环的目的在于实现了push_front,因为insert的返回值的原因
因此每次添加之后返回的都是指向新元素的迭代器,我们用此迭代器再次
添加,即实现了push_front。*/
5:有关emplace()函数的操作
emplace_front()
和emplace()
以及emplace_back()
分别对应于push_front()
和insert()
和push_back()
之所以引入emplace()操作的原因是因为emplace()成员使用参数在容器管理的内存空间中直接构造元素。
Sales_data c;
c.emplace_back("000-0000-00000",25,12.99); //正确,利用和构造函数一样的参数构造
c.push_back("000-0000-00000",25,12.99); //错误,没有接受三个参数的push_back版本。
12:访问元素
操作 | 含义 |
---|---|
c.back() | 返回c中尾元素的引用,若c为空,函数行为未定义 |
c.front() | 返回c中首元素的引用,若c为空,函数行为未定义 |
c[n] | 返回c中下标为n的元素的引用 |
c.at(n) | 返回下标为n的元素的引用 |
注意:
1:每一个顺序容器都有一个front()成员函数,除过forward_list之外的所有顺序容器都有一个back()成员函数。这两个操作分别返回首元素和尾元素的引用。
2:注意判空操作,对于没有元素的空容器调用front和back,就像使用一个越界的下标一样,是存在未知风险的。
3:front()和back()函数返回的是引用,我们如果想使用此引用来修改容器中的值,必须在声明的时候将变量声明成&
类型的。
{
std::vector<int> c = {1,2,3,4,5};
if(!c.empty()) {
c.front() = 42; //可以直接修改
auto v = c.back(); //v的类型不是引用,因此下面的修改是没有效果的。
v = 1024;
auto &v = c.back();
v = 1024; //v被声明成了引用,所以修改将会成功。
}
}
4:为了不发生下标溢出的错误,我们可以使用at()函数,如果发现下标越界,会报出out_of_range的错误。
13:删除元素
操作 | 含义 |
---|---|
pop_back() | 删除c中尾元素 |
pop_front() | 删除c中首元素 |
c.erase(p) | 删除迭代器p指向的元素,返回指向删除元素下一个元素的迭代器 |
c.erase(b,e) | 删除迭代器b和e所指定范围内的元素。若e本身是尾后迭代器,则返回尾后迭代器 |
c.clear() | 返回void |
注意:
1:forward_list()不支持pop_back,vector和string不支持pop_front。
2:注意删除之后迭代器,引用和指针都可能失效。
14:forward_list的特殊操作
我们知道forward_list是单链表,对于单链表中删除某个元素是要改变前一个元素的指向来指向后一个元素的,那我们就不能使用原来的删除方法直接删除,首先应该找到元素的前面位置。forward_list定义了一些操作如下:
操作 | 含义 |
---|---|
lst.before_begin() | 返回指向链表首元素之前不存在的元素的迭代器 |
lst.cbefore_begin() | 返回const_iterator |
lst.begin() | 返回首元素 |
lst.insert_after(p,t) | 在迭代器p之后的位置插入元素,t是一个对象 |
lst.emplace_after(p,args) | 使用args在p指定的位置之后创建一个元素 |
lst.erase_after(p) | 删除p指向位置之后的元素 |
下面的代码删除了list1中的奇数元素
#include<iostream>
#include<list>
#include<forward_list>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
std::forward_list<int> list1 = {1,2,3,4,5,6,7,8,9,10};
auto b_begin = list1.before_begin(); //定义指向首元素之前位置的迭代器
auto begin = list1.begin(); //指向首元素
while(begin != list1.end()) {
if(*begin%2) {
begin = list1.erase_after(b_begin); //注意:这一步比较关键,它实际上删除的是b_begin指向元素的下一个元素
} else { //否则我们将两个都后移。
b_begin = begin;
++begin;
}
}
for(auto u : list1) cout << u << " ";cout << endl;
return 0;
}
15:改变容器的大小
list<int> ilist(10,42); //10个int,每个值都是42
ilist.resize(15); //将5个为0的元素加在后面
ilist.resize(25,-1); //将10个值为-1的元素加在ilist的末尾
ilist.resize(5); //从ilist的末尾删除20个元素。
16:避免迭代器失效的两个方法
1:尽量避免保存end返回的迭代器,因为vector,string以及deque在添加或者删除元素之后end迭代器一定会失效。
2:编写改变容器的循环程序,我们需要注意时时更新迭代器,应用或者指针。
17:vector对象对内存的分配管理
操作 | 含义 |
---|---|
c.shrink_to_fit() | 请将capacity()减少为与size()相同的大小 |
c.capacity() | 不重新分配内存的话,c可以保存多少个元素。 |
c.reserve() | 分配至少能容纳n个元素的内存空间,并不会改变容器的元素个数,只影响预留空间的大小 |
注意:
1:capacity和size的区别:size是指目前已经存入容器的对象个数,但是capacity是指不重新分配内存的话,目前容器最多存多少值。
2:c.shrink_to_fit()表示只是我们发出请求操作系统收回容器没有利用的空间,但是至于时候收回是不一定的。
#include<iostream>
#include<vector>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
std::vector<int> v1 = {1,2,3,4,5};
cout << "size : " << v1.size() << endl;
cout << "capacity : " << v1.capacity() << endl;
for(int i = 0;i != 10;++i) {
v1.push_back(i);
}
cout << "size : " << v1.size() << endl;
cout << "capacity : " << v1.capacity() << endl;
v1.reserve(50);
cout << "size : " << v1.size() << endl;
cout << "capacity : " << v1.capacity() << endl;
while(v1.size() != v1.capacity()) {
v1.push_back(0);
}
cout << "size : " << v1.size() << endl;
cout << "capacity : " << v1.capacity() << endl;
v1.push_back(0);
cout << "size : " << v1.size() << endl;
cout << "capacity : " << v1.capacity() << endl;
return 0;
}
18:有关构造string的新的方法以及一些特殊方法
方法 | 含义 |
---|---|
string s(cp,n) | s是cp指向的数组中前n个字符的拷贝 |
string s(s2,pos2) | s是string s2中的pos2位置后面的拷贝 |
string s(s2,pos2,len2) | s是string s2中从pos2开始之后的len2长度的字符的拷贝 |
1: insert和erase以及append和replace
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
std::string str = "hello";
str.insert(str.size(),5,'!');
cout << str << endl;
str.erase(str.size()-5,5);
cout << str << endl;
const char *cp = "hello,my name is YangBodong";
str.assign(cp,5);
cout << str << endl; //str是"hello"
str.insert(str.size(),cp+5); // 从cp之后5个元素开始全部复制过来
cout << str << endl;
std::string str2 = str;
//下面的insert和append实际上是一样的
str.insert(str.size()," how are you!");
str2.append(" how are you!");
cout << "str : " << str << endl;
cout << "str2 : " << str2 << endl;
//replace是调用erase和insert的一种简写形式,下面将最后一个"!"换成"?"
str.erase(str.size()-1,1);
str.insert(str.size(),1,'?');
cout << "str : " << str << endl;
cout << "str2 : " << str2 << endl;
//等价于上面的str的操作
str2.replace(str2.size()-1,1,"?");
cout << "str : " << str << endl;
cout << "str2 : " << str2 << endl;
return 0;
}
2:string的搜索操作
#include<iostream>
#include<string>
using std::cout;
using std::cin;
using std::endl;
int main(int argc,char *argv[])
{
std::string numbers("1234567890");
std::string s("ab3cafad34fdas");
std::string alphabet("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
//从s的pos位置开始查找numbers中的任何一个字符第一次出现的位置,找到就返回.
//直到找到s的末尾最后pos变成std::string::npos返回
for(int pos = 0;(pos = s.find_first_of(numbers,pos)) != std::string::npos;++pos) {
cout << s[pos] << " ";
}
cout << endl;
for(int pos = 0;(pos = s.find_first_of(alphabet,pos)) != std::string::npos; ++pos ) {
cout << s[pos] << " ";
}
cout << endl;
return 0;
}
3:string的compare函数类似于strcmp
实际上比较的是不一样的字母的ASCII码值。
4:数值转换
方法 | 含义 |
---|---|
to_string(val) | 将val转换成字符串 |
stoi(s,p,b) | b表示转换所用的基数,默认值为10,p是size_t指针,默认为0 |
stol(),stoul(),stoll(),stoull() | 分别转换成long,unsigned long,long long,unsigned long long |
stof(),stod(),stold() | 分别转换成float,double,long double |
19:容器适配器
1:给定一个适配器,使用那些容器是有限制的。
- 适配器要求能够添加和删除元素,因此不能构造在array之上。
- vector和string以及deque可以被stack(栈)用来构造。
- list或者deque可以被queue(队列)用来构造。
- priority_queue不能用list构造。
2:栈适配器
操作 | 含义 |
---|---|
s.pop() | 删除栈顶元素 |
s.push(item) | 将item拷贝之后入栈 |
s.emplace(args) | 由args构造一个栈 |
s.top() | 获取栈顶 |
3:队列适配器
操作 | 含义 |
---|---|
q.pop() | 返回queue的首元素或者priority_queue的最高优先级的元素 |
q.front() | 返回首元素或者尾元素,但是不删除 |
q.back() | 只适用于queue |
q.top() | 返回最高优先级的元素,适用于priority_queue |
q.push(item) | 在queue的末尾创建一个元素 |
q.emplace(args) | 由args构造 |