顺序容器
概述
顺序容器类型
vector | 可变大小数组,支持快速随机访问,尾部之外位置插入删除元素会很慢。 |
deque | 双端队列,支持快速随机访问,在头尾插入删除速度都很快(list和forward_list相当)。 |
list | 双向链表,只支持双向顺序访问不支持随机访问,在任何位置插入删除速度都很快。 |
forward_list | 双向链表,只支持双向顺序访问不支持随机访问,在任何位置插入删除速度都很快。 |
array | 固定大小数组,支持快速随机访问,不能添加删除元素。 |
string | 与vector类似,专门用于保存字符。随机访问快,尾部插入删除快。 |
迭代器
标准容器迭代器运算符 | |
---|---|
*iter | 返回迭代器iter所指向元素的引用。 |
iter->men | 解引用iter并获取该元素的mem成员,等价于(*iter).mem。 |
++iter | 令iter指向容器中下一元素。 |
- -iter | 令iter指向容器中上一元素。 |
iter1 == iter2 iter1 != iter2 | 判断两迭代器是否相等,若指向同一元素或同一容器尾后迭代器,则相等,否则不等。 |
string s("some string.");
//将字符串转换成大写,遇到空格或结尾停止
for(auto it = s.begin(); it != s.end() && !isspace(*it); ++it){ //begin(),end()为容器首元素迭代器和尾后迭代器,*it为解引用,也就是it指向的元素。
*it = toupper(*it);
}
C++中,只有string和vector等标准库类型有下标运算符,并非全部类型都有。所有的容器的迭代器都定义了==和!=运算符,除了无序关联容器外都定义了关系运算符(>, >=, <, <=),所以最好养成迭代器和!=一起使用的习惯。
容器的相等运算实际是调用元素的==运算符实现的。
而其他关系运算符是使用元素的< 运算符。如果元素类型不支持该运算符,那就不能进行关系运算。
迭代器范围
迭代器的范围为左闭右开区间[begin,end),其中begin是容器首元素,end是最后一个元素的后一个位置。
string s(10, ' ');//创建10个空格的string
for(auto it = s.begin(); it != s.end(); ++ it){
*it = 'c'; //为迭代器指向的元素赋值
// ++it; //指向下一元素
}
容器类型别名
通过类型别名,可以在不了解容器元素类型的情况下使用它。若需要确定元素类型,可以使用容器的value_type,如果需要元素类型的一个引用,可以使用reference或者const_reference。
list<string>::iterator iter; //通过list<string>定义的一个迭代器类型
vector<int>::defference_type count; //通过vector<int>定义的difference_type类型
顺序容器定义及初始化
每个容器类型都定义了一个默认构造函数。除array之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接收指定容器大小和初值的参数。
vector<int> v; //默认构造函数,v是空的vector
vector<int>v(10); //定义大小为10int的空vector,除arraystring外都支持
vector<int>v(10,1); //创建10个元素为1的vector,除array外其他顺序容器都支持这种创建方式
array<int, 10> arr; //10个默认初始化的int
array<int, 10> arr1 = {0, 1, 2, 3}; //列表初始化
array<int, 10> arr2 = arr1; //拷贝赋值,类型大小必须都一样, 内置数组不支持这样操作
定义和初始化方式 | |
---|---|
C c; | 默认构造函数,构建一个空的顺序容器,除array外,array会按默认方式进行初始化。 |
C c1(c2); C c1 = c2; | c1初始化为c2的拷贝,c1c2类型必须相同,对于array,大小还必须相同。 |
C c{a,b,c,…}; C c={a,b,c,…} | c初始化为列表中元素,元素类型必须相同,对于array,列表元素数目必须小于等于array大小 |
C c(b,e) | c初始化为迭代器be之间的元素,且元素类型必须相容(array不适用) |
只有顺序容器(array除外)的构造函数才能接收大小参数 | |
C s(n); | 创建的c大小为n,进行了值初始化,且此构造函数时显式的。(string不适用) |
C s(n, t) | 创建n个初值为t的顺序容器。 |
赋值和swap
c1 = c2; //将容器c1的内容替换为c2中的拷贝
c1 = {a, b, c}; //赋值后,c的大小为3
容器为array的情况:
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {0}; //所有元素初始化为0
a1 = a2; //正确,array支持赋值
a2 = {0}; //错误,不能将花括号列表赋予数组。此外,array也不支持assign。
容器赋值运算 | |
---|---|
c1 = c2 | 将c2拷贝赋值给c1,必须具有相同类型 |
c = {a,b,…} | 列表初始化,array不支持 |
c1.swap(c2); swap(c1, c2); | 交换c1c2中的元素,必须具有相同类型。通常比拷贝快很多。 |
assign不支持关联容器和array | |
seq.assign(b, e); | 将seq中的元素替换为迭代器b,e范围内的元素。b,e不能指向seq。会替换所有元素 |
seq.assign(il); | 将seq中的元素替换为初始化列表中的元素 |
seq.assign(n, t); | 将seq中的元素替换为n个值为t的元素。 |
赋值运算会导致指向左边容器内部的迭代器、引用、指针失效,而swap则不会。(array和string除外。)
对于除array以外的容器,其大小不一样也可以swap,如:
vector<string> v1 = {"hello"};
vector<string> v2 = {"hi", "jack"};
swap(v1, v2);
除array外,swap只是交换两个容器内部的数据结构,并没有进行元素拷贝移动或者插入,所以速度很快。对于string,swap会导致其迭代器、引用、指针失效。
小练习:编写程序,将一个list中的char*指针元素赋值给一个vector中的string
list<char*> c = {"abc"};
vector<string> v;
v.assign(c.cbegin(), c.cend());
容器大小操作
forward_list支持max_size,empty。
其余容器都支持size,max_size,empty三种操作。
顺序容器添加元素
forward_list有自己专属的insert和emplace
forward_list不支持push_back和emplace_back
vector和string不支持push_front和emplace_front
向一个vector、string和deque插入元素会使指向其的迭代器、引用和指针失效。
向一个vector、string和deque的任何位置插入元素都是合法的,但是会很慢。
vector<string> v;
list<string> l;
//等价于调用l.push_front("Hello!");
l.insert(l.begin(), "Hello!");
//vector不支持push_front,但可以插入到begin之前,会很慢
v.insert(v.begin(), "Hello!");
将vector中的字符串赋值给list。
vector<string> v = {"tom", "jack", "rose"};
list<string> l = {"downey"};
l.insert(l.begin(), v.begin(), v.end()); //在l开头插入
l.insert(l.end(), v.begin(), v.end()); //在l末尾插入
//对于insert,返回指向其插入的第一个元素的迭代器
emplace
新标准引入emplace_front,emplace,emplace_back对应push_front,insert,push_back。
不同之处是,调用push或insert成员函数时,是将元素类型的对象传递给他们,这些对象被拷贝到容器中。而调用emplace成员函数时,是将参数传递给元素类型的构造函数,在内存空间中直接构造元素。
也就是说传递给emplace的参数必须与元素类型的构造函数参数像匹配。
访问元素
所有顺序容器都有front成员,除forward_list外都有back成员函数。
顺序容器访问元素 | |
---|---|
c.back(); | 返回c中尾元素引用,若c为空,函数行为未定义。 |
c.front(); | 返回c中首元素引用,若c为空,函数行为未定义。 |
c.at(k); | 返回下标为n的元素的引用,如越界,抛出out_of_range异常。 |
c[n]; | 返回下标n的元素的引用,若n>=c.size(),函数行为未定义。 |
对于顺序容器,at和下标操作只适用于string,vector,deque,array。back不适用于forward_list。
容器中访问元素的成员函数(front,back,下标和at)都返回元素的引用。如容器是const对象,则返回的也是const引用。
提供快速随机访问的容器(string,vector,array,deque)都提供下标运算。
删除元素
顺序容器删除操作 | |
---|---|
c.pop_back(); | 删除尾元素,返回void。若c为空,函数行为未定义。 |
c.pop_front(); | 删除首元素,返回void。若c为空,函数行为未定义。 |
c.erase§; | 删除迭代器所指向的元素,返回被删除元素之后一个元素的迭代器,若p是尾后迭代器,函数行为围挡一。 |
c.erase(b,e); | 删除迭代器be范围内的元素。指向最后一个被删除元素的后一个元素的迭代器。若e本身就是尾后迭代器,则也返回尾后迭代器。 |
c.clear(); | 删除c中所有元素,返回void |
删除deque中首尾元素之外的任何元素都会使迭代器、引用、指针失效。指向vector、string中删除点之后的位置的迭代器、引用、指针也都会失效。
特殊的forward_list插入删除操作
单向链表,无法获取一个元素的前驱节点,所以只能改变一个元素的后一个元素来实现插入删除。
forward_list中插入或删除元素 | |
---|---|
lst.before_begin();lst.cbefore_begin() | 返回指向链表首元素之前不存在的元素的迭代器,不能解引用。 |
lst.insert_after(p, t); lst.insert_after(p,n,t); lst.insert_after(p,b,e); lst.insert_after(p, il); | 在迭代器p之后插入元素。t是一个元素,n是数量。be表示一对迭代器。il是花括号列表,返回一个指向最后一个插入元素的迭代器。若范围空,则返回p。若p尾后迭代器则函数行为未定义。 |
lst.emplace_after(p.args); | 使用参数在p元素后创建一个新元素。返回指向这个新元素的迭代器。若p为尾后迭代器,函数行为未定义。 |
lst.erase_after§; lst.erase_after(b,e); | 删除p指向元素之后的元素。或者删除b到e前1个元素范围内的元素。若不存在这样的元素,返回尾后迭代器。若p指向lst的尾元素或尾后迭代器,则函数行为未定义。 |
改变容器大小
resize扩大或缩小容器。若当前大小>要求的大小,则容器后面的元素会被删除,如果当前大小<要求大小,会将新元素添加到容器后面。
list<int> ilist(10, 42); //10个int,每个都是42
ilist.resize(15); //5个值为0的元素添加到容器尾部。
ilist.resize(25, -1); //将10个值为-1的元素添加到ilist尾部
ilist.resize(5); //删除后面20个元素,保留前5个元素,分别都是42
如果缩小容器,则指被删除元素的迭代器指针引用都会失效。对vector,string,deque进行resize可能导致迭代器、指针、引用失效。
不要保存end返回的迭代器
因为在删除或插入vector、string或deque之外任意位置的元素后,end迭代器总会失效。
管理容量的成员函数 | |
---|---|
c.shrink_to_fit() | 将capacity()减小为size()大小。只适用于vector、string、deque。 |
c.capacity() | 不重新分配内存空间的情况下,c可以保存多少元素。只适用于vector和string |
c.recerve(n) | 分配至少能容纳n个元素的空间(通常会更大) |
调用resize永远不会减少容器占用空间,resize只改变容器中元素个数,不改变容器容量。如果要减小,用shrink_to_fit()(不保证一定退回)。
vector<int> ivec;
//size为0,capacity依赖于具体实现
cout<< "ivec size:" << ivec.size() << endl;
cout<< "ivec capacity:" <<ivec.capacity() <<endl;
//添加24个元素
for (vector<int>::size_type ix = 0; ix != 24; ++ix){
ivec.push_back(ix);
}
//size=24,capacity=32
cout<< "ivec size:" << ivec.size() << endl;
cout<< "ivec capacity:" <<ivec.capacity() <<endl;
vector的一个内存分配策略是,每次需要分配新的内存空间时,将当前容量翻倍。(不同实现不一样)
string的特殊操作
string的特殊的构造方法 | |
---|---|
string s(cp, n) | s是cp数组的前n个元素的拷贝。cp至少有n个元素。 |
string s(s2, pos) | s是s2的pos下标开始的字符的拷贝。若pos>s2.size(),构造函数未定义。 |
string s(s2, pos2, len) | s是s2从下标pos2开始的len个字符的拷贝,不管len是多少,之多拷贝s2.size()-pos2个字符。 |
const char* cp = "Hello World!!";//以空字符结束的数组
char noNull[] = {'H', 'i'}; //不是以空字符结束
string s(cp); //拷贝cp中的字符,直到遇到空字符结尾。
string s1(noNull); //未定义,noNull不是以空字符结尾
改变string的其他方法
s.substr(pos, n)
返回一个string,包含从pos开始的n个字符的拷贝。如果开始位置pos超出string大小,则抛出out_of_range异常。
string除了接收迭代器版本的insert和erase之外,还提供下标版本。
s.insert(s.size(), 5, '!'); //s末尾插入5个感叹号
s.erase(s.size() - 5, 5); //从s删除最后5个字符
还有C风格字符串数组的insert和assign。
const char* cp = "Stately, plump Buck";
s.assign(cp, 7); //赋予s指针cp开始的7个字符
s.insert(s.size(), cp + 1); //s="Stately, plump Buck"
此外还有两个额外的成员函数,append和replace。
append在字符串末尾添加字符串。replace在指定位置替换字符串。
string s("hello world!");
s.append("good."); //s="hello world!good."
a.repalce(6, 5, "jack"); //s="hello jack!good." 也就是说替换字符串的长度与被替换字符串的长度不需要一样。
string的搜索操作
每个搜索操作都返回一个string::size_type值,表示匹配位置的下标。如果搜索失败,则返回名为string::npos的static成员。npos是一个unsigned类型的。
string的搜索操作 | |
---|---|
s.find(args) | 查找s中args第一次出现的位置 |
s.rfind(args) | 查找s中 args最后依次出现的位置 |
s.find_first_of(args) | 在s中查找args中任意一个字符第一次出现的位置 |
s.find_last_of(args) | 在s中查找args中任意一个字符第一次出现的位置 |
s.find_first_not_of(args) | 在s中查找第一个不在args中的字符 |
s.find_last_not_of(args) | 在s中查找最后一个不在args中的字符 |
args必须是以下形式之一 | |
c, pos | 从s中位置pos开始查找字符c。 |
s2, pos | 从s中位置pos开始查找字符串s2。 |
cp, pos | 在s中pos开始查找cp指向的以空字符结尾的C风格字符串 |
cp, pos, n | 在s中pos开始查找cp指向的以空字符结尾的C风格字符串的前n个字符 |
字符串比较函数
s.compare()的集中参数形式 | |
---|---|
s2 | 比较s与s2 |
pos1, n1, s2 | 将s中从pos1开始的n1个字符与s2比较 |
pos1, n1n s2, pos2, n2 | 将s中从pos1开始的n1个字符与s2中从pos2开始的n2个字符进行比较 |
cp | 比较s与cp指向的字符串数组 |
pos1, n1, cp | 将s中从pos1开始的n1个字符与cp指向的字符串数组比较 |
pos1, n1, cp, n2 | 将s中pos1开始的n1个字符与指针指向的地址开始的n2个字符串进行比较 |
string与数值之间的转换
int i = 50;
string s = to_string(i); //将数值型转换成string
string与数值之间的转换 | |
---|---|
stoi(s, p, b),stol(s, p, b), stoul(s, p, b),stoll(s, p, b), stoull(s, p, b) | 返回s的起始字串的数值返回值类型分别是int,long,unsigned long, long long,unsigned long long,b表示转换所用基数,默认10进制。p是size_t指针,用来保存第一个非数值字符下标,默认为0 |
stof(s, p), stod(s, p), stold(s, p) | 返回s的起始字串(表示浮点数内容的)数值。 |
容器适配器
一个容器适配器接受一种已有类型,使其行为看起来像另一种不同的类型。
stack, queue, priorty_queue
所有容器适配器都支持的操作和类型 | |
---|---|
size_type | 一种类型,足以保存当前类型的最大对象大小 |
value_type | 元素类型 |
container_type | 实现该适配器的底层容器类型 |
A a; | 创建一个空的适配器 |
A a©; | 创建一个适配器a,带有容器c的一个拷贝。 |
==、!=、<、<=、>、>= | 每个适配器都支持所有关系运算符 |
a.empty() | 返回是否为空 |
a.size() | 返回a中元素数目 |
swap(a, b),a.swap() | 交换a,b内容,必须具有相同类型且底层容器类型也必须相同 |
deque<int>deq = {1, 2, 3};
stack<int> s(dep);//用容器deque对象来初始化stack对象。
默认情况下,stack和queue是基于deque实现的,priorty_queue是基于vector实现的。
stack只要求push_back,pop_back,back操作,所以可以使用除array和forward_list之外的任意顺序容器实现。queue要求back,push_back,front,push_front,因此可以基于deque和list,但不能基于vector。priority_queue除了要求front,back,push_back之外还要求随机访问,所以可以基于vector和deuqe,但不能基于list。
这是默认情况,我们也可以在创建一个适配器时,指定一个命名的顺序容器,来重载默认的容器类型。
//vector上实现栈
stack<int, vector<int>> int_stk;
deque<int> dq = { 1,2,3,4,5,5 }; //默认的stack是基于deque的
stack<int> s0(dq);
cout << s0.size() << endl; //size=6
vector<int> v = { 1, 2, 3, 4 };
stack<int, vector<int>> s2(v); //将stack重载为基于vector的
cout << s2.size() << endl; //size=4
可以看到,stack s0和s2是基于不同容器构造的。
stack特有的操作 | |
---|---|
s.pop() | 删除栈顶元素,但不返回元素。 |
s.push(item) | 压栈 |
s.emplace(args) | 将args压栈 |
s.top() | 返回栈顶元素,但不将元素弹出栈。 |
每个容器适配器都定义了自己的特殊操作,只可以使用适配器操而不能使用底层容器的操作。
队列适配器特有操作 | |
---|---|
q.pop() | 弹出queue的首元素或者priority_queue的最高优先级元素,但不返回此元素。 |
q.front() | 返回首元素或者尾元素,但不删除元素 |
q.back() | 只适用于queue |
q.top() | 返回最高优先级元素,但不删除元素 |
q.push(item); q.emplace(args) | 在queue的末尾或者priority_queue的适当位置创建一个元素,其值为item,或者为args |