序列式容器
序列式容器包括:静态数组 array 、动态数组 vector 、双端队列 deque 、单链表 forward_ list 、双链表 list 。这五个容器中,我们需要讲解三个 vector 、 deque 、 list 的使 用,包括:初始化、遍历、尾部插入与删除、头部插入与删除、任意位置进行插入与 删除、元素的清空、获取元素的个数与容量的大小、元素的交换、获取头部与尾部元素等。
头文件
#include <vector>
template<class T,class Allocator = std::allocator<T>
> class vector;#include <deque>
template<class T,class Allocator = std::allocator<T>
> class deque;#include <list>
template<class T,class Allocator = std::allocator<T>
> class list;
初始化容器对象
对于序列式容器而言,初始化的方式一般会有五种。
初始为空
vector<int> number;
deque<int> number;
list<int> number;
初始为多个相同的值
vector<int> number(10, 1);
deque<int> number(10, 1);
list<int> number(10, 1);
使用迭代器范围
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> number(arr, arr + 10);//左闭右开区间
//vector可以直接替换为deque与list
使用大括号
vector<int> number = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//vector可以直接替换为deque与list
拷贝构造或者移动构造
vector<int> number1 = {1, 2, 4, 6};
vector<int> number2(number1);
vector<int> number3(std::move(number1));
//vector可以直接替换为deque与list
遍历容器中的元素
就是访问容器中的每个元素,一般可以采用下标或者迭代器的方式进行遍历。
//1、使用下标进行遍历(要求容器必须是支持下标访问的,list不支持下标,所以就不适用)
for(size_t idx = 0; idx != number.size(); ++idx)
{cout << number[idx] << " ";
}//2、使用未初始化的迭代器进行遍历
vector<int>::iterator it;
for(it = numbers.begin(); it != numbers.end(); ++it)
{cout << *it << " ";
}//3、使用初始化迭代器进行遍历
vector<int>::iterator it = number.begin();
for(; it != number.end(); ++it)
{cout << *it << " ";
}//4、使用for加上auto进行遍历
for(auto &elem : number)
{cout << elem << " ";
}
在容器的尾部插入与删除
可以向容器的尾部插入一个元素或者将容器的最后一个元素删除。
//尾部插入一个元素(注意:是拷贝一份副本到尾部)
void push_back( const T& value);
void push_back( T&& value);
//尾部删除
void pop_back();
在容器的头部插入与删除
可以向容器的头部插入一个元素或者将容器的第一个元素删除。
//头部插入
void push_front( const T& value);
void push_front( T&& value);
//头部删除
void pop_front();
对于 deque 与 list 而言,是支持这两个操作的, 但是对于vector没有提供这两个操作。 vector 不支持在头部进行插入元素与删除元素。 这是从效率方面进行的考虑。
vector的原理
vector 头部是固定的,不能进行插入与删除,只提供了在尾部进行插入与删除的操作,所以如果真的要在头部插入或者删除,那么其他的元素会发生移动,这样操作就比较复杂。
探讨 vector 的 底层实现 ,三个指针:
_ M _ start :指向第一个元素的位置;
_ M _ finish :指向最后一个元素的下一个位置;
_ M _ end _ of _ storage :指向当前分配空间的最后一个位置的下一个位置;
那么这三个指针是怎么来的呢,我们可以从其源码中获取答案(这里可以阅读 vector的源码)

void test()
{
vector<int> number = {1, 2, 3, 4};
&number;//error,只是获取对象栈上的地址,也就是_M_start的地址
&number[0];//ok
&*number.begin();//ok
int *pdata = number.data();//ok
cout << "pdata = " << pdata << endl;//使用printf,思考一下printf与cout打印地址的区别
}
deque的原理
探索 deque 的 底层实现 :
deque 是由多个片段组成的,片段内部是连续的,但是片段之间不连续的、分散的,多个片段被一个称为中控器的结构控制,所以说deque 是在物理上是不连续的,但是逻辑上是连续的。
我们依旧可以从其源码中获取答案(这里可以阅读 deque 的源码)

从继承图中可以看到,中控器其实是一个二级指针,由 _M_map 表示,还有一个表示中控器数组大小的 _M_map_size , deque 的迭代器也不是一个简单类型的指针,其迭代器是一个类类型,deque 有两个迭代器指针,一个指向第一个小片段,一个指向最后一个小片段。其结构图如下:

list的原理
list是双向链表,其实现如下:
在容器的任意位置插入
三种序列式容器在任意位置进行插入的操作是insert函数,函数接口如下
//1、在容器的某个位置前面插入一个元素
iterator insert( iterator pos, const T& value );
iterator insert( const_iterator pos, const T& value );
number.insert(it, 22);//2、在容器的某个位置前面插入count个相同元素
void insert(iterator pos, size_type count, const T& value);
iterator insert(const_iterator pos, size_type count, const T& value);
number.insert(it1, 4, 44);//3、在容器的某个位置前面插入迭代器范围的元素
template<class InputIt>
void insert(iterator pos, InputIt first, InputIt last);
template<class InputIt>
iterator insert(const_iterator pos, InputIt first, InputIt last);
vector<int> vec{51, 52, 53, 54, 55, 56, 57, 58, 59};
number.insert(it, vec.begin(), vec.end());//4、在容器的某个位置前面插入大括号范围的元素
iterator insert(const_iterator pos, std::initializer_list<T> ilist);
number.insert(it, std::initialiser_list<int>{1, 2, 3});
三种序列式容器的插入示例如下:
//此处list可以换成vector或者deque
list<int> number = {1, 4, 6, 8, 9};
++it;
auto it = number.begin();//1、在容器的某个位置前面插入一个元素
number.insert(it, 22);//2、在容器的某个位置前面插入count个相同元素
number.insert(it, 3, 100);//3、在容器的某个位置前面插入迭代器范围的元素
vector<int> vec{51, 52, 53, 54, 55, 56, 57, 58, 59};
number.insert(it, vec.begin(), vec.end());//4、在容器的某个位置前面插入大括号范围的元素
number.insert(it, {1, 2, 3});
insert 在任意位置进行插入, list 使用起来很好,没有任何问题,但是 deque 与 vector 使用起来可能会出现问题,因为 vector 是物理上连续的 ,所以在中间插入元素会导致插入元素后面的所有元素向后移动,deque 也有类似情况, 可能因为插入而引起底层容量 不够而扩容,从而使得迭代器失效 ( 申请了新的空间,但是迭代器还指向老的空间 ) ,即使没有扩容,插入之后的迭代器也失效了( 不再指向之前的元素了 ) 。
vector的迭代器失效
以 vector 为例,如果使用 insert 插入元素,而每次插入元素的个数不确定,可能剩余空间不足以存放插入元素的个数,那么insert 在插入的时候 底层就可能导致扩容,从而导 致迭代器还指向老的空间,继续使用该迭代器会出现迭代器失效的问题 。
void test()
{
vector<int> number = {1, 2, 3, 4, 5, 6, 7, 8, 9};
display(number);
cout << "number.size() = " << numbers.size() << endl;//9
cout << "number.capacity() = " << numbers.capacity() << endl;//9cout << endl << "在容器尾部进行插入: " << endl;
number.push_back(10);
number.push_back(11);
display(number);
cout << "number.size() = " << number.size() << endl;//11
cout << "number.capacity() = " << number.capacity() << endl;//18cout << endl << "在容器vector中间进行插入: " << endl;
auto it = number.begin();
++it;
++it;
number.insert(it, 22);
display(number);
cout << "*it = " << *it << endl;
cout << "number.size() = " << number.size() << endl;//12
cout << "number.capacity() = " << number.capacity() << endl;//18numbers.insert(it, 7, 100);//因为插入个数不确定,有可能底层已经发生了扩容
display(numbers);
cout << "*it = " << *it << endl;
cout << "numbers.size() = " << numbers.size() << endl;//19
cout << "numbers.capacity() = " << numbers.capacity() <<endl;//24//正确办法是重置迭代器的位置
vector<int> vec{51, 52, 53, 56, 57, 59};
numbers.insert(it, vec.begin(), vec.end());//继续使用该迭代器就会出现问题(内存错误)
display(numbers);
cout << "*it = " << *it << endl;
cout << "numbers.size() = " << numbers.size() << endl;
cout << "numbers.capacity() = " << numbers.capacity() << endl;//解决方案:每次在插入元素的时候,可以将迭代器的位置进行重置更新,避免因为底层扩容,迭代器还指向老
//的空间而出现问题
vector<int> vec{51, 52, 53, 56, 57, 59};
it = number.begin();//重新置位
++it;
++it;
numbers.insert(it, vec.begin(), vec.end());//继续使用该迭代器就会出现问题(内存错误)
display(numbers);
cout << "*it = " << *it << endl;
cout << "numbers.size() = " << numbers.size() << endl;
cout << "numbers.capacity() = " << numbers.capacity() << endl;
}
因为 vector 的 push _ back 操作每次只会插入一个元素,所以可以按照统一的形式 2 * capacity() ,但是 insert 的时候,插入的元素个数是不定的,所以就不能一概而论。这里可以分别讨论一下,我们设置capacity () = n , size () = m , insert 插入的元素个数为 t个:
如果 t < n - m ,新插入元素的个数比剩余空间小,这个时候就无需扩容,所以直接插入;
如果 n - m < t < m ,就按照 m 的 2 倍去进行扩容,新的空间就是 2 * m ;如果n - m < t < n 且 t > m , 就按照 t + m 去进行扩容;
如果 t > n 时,依旧按照 t + m 去进行扩容 ;
这就是vector 进行 insert 扩容的原理(这个原理可以了解一下,主要是为了告诉大家不
是两倍扩容)。
在容器的任意位置删除元素
三种序列式容器的删除操作是erase函数,函数接口如下
//删除指定迭代器位置的元素
iterator erase(iterator position);
//删除一个迭代器范围的元素
iterator erase(iterator first, iterator last);
对于 vector 而言,会导致删除迭代器之后的所有元素前移,从而导致删除元素之后的所有迭代器失效(迭代器的位置没有改变,但是因为元素的移动,导致迭代器指向的不是删除之前的元素,所以失效);deque 比 vector 复杂,要看 pos 前后的元素个数来决定,deque 的 erase 函数可以看 STL 源码,需要看删除位置与 size () 的一半的大小,然后看是挪动前一半还是后一半,尽量减少挪动的次数;list 会删除指向的元素,从而导致指向删除元素的迭代器失效。
这里以 vector 的 erase 为例,看看其删除元素的操作与删除后的效果。
//题意:删除vector中所有值为4的元素。
vector<int> vec = {1, 3, 5, 4, 4, 4, 4, 7, 8,4, 9};
for (vector<int>::iterator it = vec.begin(); it != vec.end(); ++it)
{if(4 == *it){vec.erase(it);}
}//发现删除后有些4没有删除掉,可以推测出是什么原因吗?是那些4没有删除呢?
//正确解法:
for (auto it = vec.begin(); it != vec.end();)
{ if (4 == *it){vec.erase(it);//此处可以使用it接收erase的结果,更通用一些,即:it = vec.erase(it);}else{++it;}
}
其他操作
//1、清除容器中的所有元素(三个序列式容器都有)
void clear();//2、获取元素个数(三个序列式容器都有)
size_type size() const;//3、获取容量大小(只有vector有)
size_type capacity() const;//4、回收多余的空间,使得元素的个数与容量大小对应,不存在没有使用的空间(vector与deque有这个函数)
void shrink_to_fit();//5、交换两个相同容器中的元素(三个序列式容器都有)
void swap( vector& other);
vector<int> number1 = {1, 2, 3};
vector<int> number2 = {10, 20, 30};
number1.swap(number2);//之后number1中的内容与number2中的内容做了交换//6、更改容器中元素个数(三个序列式容器都有)
//以vector为例,执行resize时候,如果count < size(),就将多余的元素删除;如果count > size(),就在//之前的元素后面执行insert添加元素(没有指定就添加默认值),元素的个数在改变的同时,容量也在发生改变 //(上一次的两倍或者本次元素个数)
void resize( size_type count, T value = T() );
void resize( size_type count);
void resize( size_type count, const value_type& value);//7、获取第一个元素(三个序列式容器都有)
reference front();
const_reference front() const;//8、获取最后一个元素(三个序列式容器都有)
reference back();
const_reference back() const;//9、C++11增加的可变参数模板的几个函数
//在容器的尾部就地构造一个元素
template< class... Args >
void emplace_back( Args&&... args);
vector<Point> vec;
vec.emplace_back(1, 2);//就地将(1, 2)构建为一个对象存放在vector的尾部,减少拷贝或者移动
list的特殊操作
排序函数sort
void sort();//默认以升序进行排序,其实也就是,使用operator<进行排序
template< class Compare >
void sort(Compare comp);//其实也就是传入一个具有比较的类型,即函数对象
template <typename T1, typename T2>
struct Compare
{bool operator()(const T1 &a, const T2 &b) const{return a < b;}
};
移除重复元素u n i q u e
void unique();
size_type unique();
注意使用 unique 的时候,要保证元素 list 是已经排好顺序的,否则使用 unique 是没有用的。
逆置链表中的元素r e v e r s e
void reverse();
void reverse() noexcept;
将链表中的元素逆置。
合并链表的函数m e r g e
//合并两个链表(other既可以是左值也可以是右值)
void merge( list& other );
void merge( list&& other );
template <class Compare>
void merge( list& other, Compare comp );
template <class Compare>
void merge( list&& other, Compare comp );
合并的链表必须是有序的,如果没有顺序,合并没有效果。两个链表合并之后,并且另一个链表就为空了。
从一个链表转移元素到另一个链表s p l i c e
//移动other链表到另一个链表的某个指定位置前面
void splice(const_iterator pos, list& other);
void splice(const_iterator pos, list&& other);//移动other链表中的某个元素到另一个链表的某个指定位置前面
void splice(const_iterator pos, list& other, const_iterator it);
void splice(const_iterator pos, list&& other, const_iterator it);//移动other链表的一对迭代器范围元素到另一个链表的某个指定位置前面
void splice(const_iterator pos,list& other, const_iterator first, const_iterator last);
void splice(const_iterator pos,list&& other,const_iterator first, const_iterator last);