序列式容器
序列式容器包括:靜態數組 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);