本文适合对于c++有一些基础的人阅读,指出一些容易被忽略的点。
本文包括顺序容器(array/vector/string/deque/list/forward_list)和他们的迭代器大部分的使用方法,注意事项,顺序容器的限制;对指针、数组、和数组指针的解释;stack和queue容器适配器。
3.1 顺序容器
顺序容器理解
元素是由存储位置来排序的(关联容器是根据元素值来排序的)。
顺序容器种类
内存连续型:array/string/vector,特点:随机访问快,尾部插入快,中间插入慢。
内存不连续性型:list/forward_list,特点:无法随机访问,插入快。
中间类型:deque,特点,是vector和list的结合,随机访问和插入速度都较快。
顺序容器的限制
应该有默认构造函数:因为有resize等功能,需要调用默认构造函数。
vector必须有拷贝构造函数:因为动态分配空间时需要拷贝对象。
vector最好有移动构造函数:会增加拷贝时的效率。
注意:容器应该放对象,尽量不要放指针,但是不放引用。
案例1:
deque<int&> dq; //无法编译通过
for (int i; i <= 10; ++i) {
dq.emplace_back(i);
}
for (auto i : dq) {
cout << i << endl;
}
解释:引用的使命周期一定要小于等于被引用的对象,但是放在容器中会出现被引用的对象已经无效,但他的引用还在。
案例2:
deque<int*> dq;
for (int i; i <= 10; ++i) {
dq.emplace_back(&i);
}
for (auto i : dq) {
cout << i << endl;
}
解释:指针可以放在容器中,但是我们要维护指针所指向的内存有效性,这样就出像了指针悬挂(野指针)的问题,在容器应用时应慎重存放指针。
3.2 顺序容器的基本操作
通过迭代器操作容器
为什么要引入迭代器?
vector<int> v{1,2,3,4,5,6,7,8,9,10};
for (auto itr = v.begin(); itr != v.end(); ++itr) {
++(*itr);
}
deque<int> dq{1,2,3,4,5,6,7,8,9,10};
for (auto itr = dq.begin(); itr != dq.end(); ++itr) {
++(*itr);
}
list<int> l{1,2,3,4,5,6,7,8,9,10};
for (auto itr = l.begin(); itr != l.end(); ++itr) {
++(*itr);
}
迭代器可以完全不用担心你使用的是什么容器。
迭代器有四种:
正向iterator,只读正向const_iterator。
反向reverse_iterator,只读反向const_reverse_iterator。
迭代器的主要用途为:查看和修改元素。
注意:增加和删除元素,有可能会造成迭代器的失效,需要慎重使用。
顺序容器的迭代器的主要操作:
所有迭代器都支持向前迭代器
的操作:自增++,比较==,取元素*/->。
非forward_list容器都支持双向迭代器
的操作:自减–。
内存连续和deque都支持 随机访问迭代器
的操作:移动迭代器±(int)/±(int)/itr[int];迭代器距离(itr)±(itr);迭代器位置比较>/</>=/<=。
其他封装的函数:advance,distance,next,prev。(向前迭代器慎用)
注意:如果迭代器有溢出操作(在begin()前或者在end()后),程序不由抛出异常也不会core,是很危险的行为。
初始化
//顺序容器都可以如下初始化,以vector为例,array除外。
vector<int> v1; //无参构造
vector<int> v2(10, 100); //按个数构造,10个100
vector<int> v3{1,2,3,4,5}; //列表初始化
vector<int> v4(v2); //拷贝构造
vector<int> v5(v2.begin(), v2.end()); //迭代器构造
//{}先调用列表初始化,如果无法转化类型,在退化成()的作用。
vector<string> v6{10}; //只有一个元素10
vector<bool> v7(10); //有十个元素
cout << v6.size() << endl; //10
cout << v7.size() << endl; //10
//array要特殊一些
array<int, 10> a1;
array<int, 10> a2{1,2,3};
除了array以外的顺序容器都可以动态分配大小。{}列表初始化可以退化成()。
修改操作
交换:swap。(除array元素外,不会引发元素的删除、拷贝等操作,效率很高)。
修改:assgin。(array除外)
注意:会导致迭代器失效。
插入操作
forward_list和array比较特殊,不予讨论。
插入:insert。
尾部插入:push_back,emplace_back。
头部插入:push_front,emplace_front。(vector和string无)
注意:push插入对象是会拷贝一份到容器中,不会对原有的对象有影响。
emplace则会直接调用构造函数到容器中。
插入操作会使迭代器失效。
访问操作
首元素/尾元素:front,back。应防止容器为空。back不适用于forward_list。
下标查找:operator[],at。at会检查溢出,溢出会抛出out_ot_range异常。只有内存连续的容器支持此操作。
删除操作
此操作会改变容器的大小,所以array不支持删除。
删除头尾:pop_front,pop_back。避免容器为空,返回void,forward_list不支持pop_back,内存连续的容器不支持pop_back。
删除一部分:eraser。
删除所有:clear。
注意:迭代器会失效。顺序容器的size会改变,但是capacity不变。
从容器中删除一个元素,会自动调用他的析构函数。
容其大小管理操作
容器中元素个数:size。
重置容器元素个数:resize。
容器以申请空间(容积):capacity,只适用于vector和string。
为容器预留空间:reserve,只适用于vector和string。
调整容器容积到适当大小:shrink_to_fit,值适用于vector,string和deque。
注意:reserve后capacity至少为shrink_to_fit后的值。reserve一般用于预留空间,不会小于当前capacity,如果想缩小容积需要用shrink_to_fit。resize>size时会调用默认构造函数,或拷贝传入的第二个参数,小于时会调用析构函数。
内存连续容器
string和vectro内存增长机制:
案例:
#include <iostream>
#include <vector>
using namespace std;
class Test {
public:
int x;
Test() : x(0) {cout << "Test() this = " << this << endl;}
Test(int x) : x(x) {cout << "Test(int) this = " << this << endl;}
Test(const Test& another) : x(another.x) {cout << "Test(const Test&) this = " << this << ", &another = " << &another << endl;}
Test(const Test&& another) noexcept : x(another.x) {cout << "Test(const Test&&) this = " << this << ", &another = " << &another << endl;}
Test& operator= (const Test& another) {
cout << "operator=" << endl;
if (this == &another)
return *this;
x = another.x;
return *this;
}
};
int main()
{
vector<Test> v;
for (int i = 0; i < 6; ++i) {
v.emplace_back(i);
cout << "size = " << v.size() << endl;
cout << "capacity = " << v.capacity() << endl;
cout << "==============" << endl;
}
return 0;
}
输出:
Test(int) this = 0x1041770
size = 1
capacity = 1
==============
Test(int) this = 0x1041794
Test(const Test&&) this = 0x1041790, &another = 0x1041770
size = 2
capacity = 2
==============
Test(int) this = 0x1041778
Test(const Test&&) this = 0x1041770, &another = 0x1041790
Test(const Test&&) this = 0x1041774, &another = 0x1041794
size = 3
capacity = 4
==============
Test(int) this = 0x104177c
size = 4
capacity = 4
==============
Test(int) this = 0x10417a0
Test(const Test&&) this = 0x1041790, &another = 0x1041770
Test(const Test&&) this = 0x1041794, &another = 0x1041774
Test(const Test&&) this = 0x1041798, &another = 0x1041778
Test(const Test&&) this = 0x104179c, &another = 0x104177c
size = 5
capacity = 8
==============
解释:体积增长是以2的倍数增加。
如果size == capacity时,再插入一个会引发内存扩展。
扩展时默认调用移动构造函数,如果没有移动构造函数,会调用拷贝构造函数。
移动构造函数的出现,是将将亡值(可能是匿名变量也可能是被move修饰过的变量)
拷贝给普通变量,存在大量内存是可以利用浅拷贝的方式,但是要注意double free。
3.3 数组
c语言中指针、数组、数组指针的区别
案例:
int array[10]; //数组
int *ptr = array; //指针
int (*ptr_array)[10] = &array; //数组指针
cout << "大小: 数组为 " << sizeof(array)
<< ", 指针为 " << sizeof(ptr)
<< ", 数组指针为 " << sizeof(ptr_array)
<< endl;
cout << "指向: 数组为 " << array
<< ", 指针为 " << ptr
<< ", 数组指针为 " << ptr_array
<< endl;
cout << "步长: 数组为 " << (uint64_t)(array + 1) - (uint64_t)(array)
<< ", 指针为 " << (uint64_t)(ptr + 1) - (uint64_t)ptr
<< ", 数组指针为 " << (uint64_t)(ptr_array + 1) - (uint64_t)(ptr_array)
<< endl;
输出:
大小: 数组为 40, 指针为 8, 数组指针为 8
指向: 数组为 0x61fde0, 指针为 0x61fde0, 数组指针为 0x61fde0
步长: 数组为 4, 指针为 4, 数组指针为 40
解释:
c语言类型区别在于大小(sizeof),和步长(地址+1)。
数组和指向首元素的指针(ptr)区别在于大小(sizeof)不同。
数组和指向数组的指针(ptr_array)区别在于步长(地址+1)不同。
上面array是数组,ptr和&array[0]是指向数组首元素的指针,ptr_array和&array是数组指针(指向数组的指针)。
注意:指针数组和数组指针并没什么关系,前者是放指针的数组,后者是指向数组的指针。
int *a[4]是指针数组,int (*a)[4]是数组指针,语法上带括号的是数组指针。
c++11后array只是在数组和容器中间做了适配。
3.4 string
插入
尾部插入多个:append/operator+=。
查看
查看单独元素:operator[]越界返回’\0’。
c类型字符串:data和c_str,c_str为只读。(容器操作)
查找元素:find/find_first_of/find_last_of等。未找到返回string::npos。
修改
直接全部覆盖:operator=;
与数字相互转化
字符串到正数:stoi/stol/stoll/stoul/stoull,可以选择进制,会抛出out_of_range和invalid_argument。
字符串到浮点数:stof/stod/stold,会抛出out_of_range和invalid_argument。
数字到字符串:to_string。
其他操作
比较:operator比较操作符。
字符串拼接:operator+。
字符串IO流:operator >> <<。
子字符串:substr。
字符串替换:replace。
3.5 forward_list
单向链表有自己的接口
头节点的迭代器:before_begin()/cbefore_begin()。
插入:insert_after/emplace_list。
删除:eraser。
3.6 容器的两个适配器
模板第二个参数可以改变实现他们的容器。
栈(stack)
默认用deque实现。
push/emplace/push/top
队列(queue)/优先级队列(priority_queue)
queue默认用deque实现,priority_queue默认用vector实现。
push/emplace/pop/front/back