特性
vector
是表示可變大小數組的序列容器。- 就像數組一樣,
vector
也採用的連續存儲空間來存儲元素,說明可以採用下標對vector
的元素進行訪問,和數組一樣高效。但是又不像數組,它的大小是可以動態改變的,而且它的大小會被容器自動處理。 - 本質上
vector
使用動態分配數組來存儲它的元素。當新元素插入時候,這個數組需要被重新分配大小爲了增加存儲空間。其做法是,分配一個新的數組,然後將全部元素移到這個數組。就時間而言,這是一個相對代價高的任務,因爲每當一個新的元素加入到容器的時候,vector
並不會每次都重新分配大小。 vector
分配空間策略:vector
會分配一些額外的空間以適應可能的增長,因爲存儲空間比實際需要的存儲空間更大。不同的庫採用不同的策略權衡空間的使用和重新分配。但是無論如何,重新分配都應該是對數增長的間隔大小,以至於在末尾插入一個元素的時候是在常數時間的複雜度完成的。- 因此,
vector
佔用了更多的存儲空間,爲了獲得管理存儲空間的能力,並且以一種有效的方式動態增長。 - 與其它動態序列容器相比(
deques
,list
和forward_lists
),vector
在訪問元素的時候更加高效,在末尾添加和刪除元素相對高效。對於其它不在末尾的刪除和插入操作,效率更低。比起list
和forward_list
統一的迭代器和引用更好。
迭代器失效問題
vector
容器的迭代器:
- 那麼什麼是
vector
容器的失效問題? - 是指對
vector
進行插入 / 刪除(insert
/erase
)導致的迭代器指針非法現象。
代碼演示:
int main(){
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 刪除pos位置的數據,導致pos迭代器失效。
v.erase(pos); //現在的pos位置是一個失效位置
cout << *pos << endl; // 此處會導致非法訪問
// 在pos位置插入數據,導致pos迭代器失效。
// insert會導致迭代器失效,是因爲insert可能會導致增容,增容後pos還指向原來的空間,而原來的空間已經釋放了。
pos = find(v.begin(), v.end(), 3);
v.insert(pos, 30);
cout << *pos << endl; // 此處會導致非法訪問
return 0;
}
常見的迭代器失效的場景(刪除vector
中全部偶數):
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main(){
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 實現刪除v中的所有偶數
// 下面的程序會崩潰掉,如果是偶數,erase導致it失效
// 對失效的迭代器進行++it,會導致程序崩潰
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0)
v.erase(it);
++it;
}
// 以上程序要改成下面這樣,erase會返回刪除位置的下一個位置
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0)
it = v.erase(it); //使用迭代器 重新接收~
else
++it;
}
return 0;
}
源碼剖析
其實vector
底層維護了三個成員參數,類型都是迭代器(原生指針):
start
:開始位置finish
:實際結束位置end_of_storage
:容量結束位置
end_of_storage
與finish
的差值就是備用位置。
差值如果爲0
了,說明二者相等,就要進行擴容操作了,這會涉及重新配置空間、數據拷貝、釋放舊空間等一系列動作,將會付出額外代價進行數據搬運。
模擬實現
實現一個Vector
類,模擬vector
容器的功能與特性。
#include <assert.h>
template<class T>
class Vector{
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
//拷貝構造
// 老式寫法:
//Vector(const Vector<T>& v)
//{
// _start = new T[v.Capacity()];
// memcpy(_start, v._start, v.Size()*sizeof(T));
// _finish = _start + v.Size();
// _endofstorage = _start + v.Capacity();
//}
// 新式寫法:
Vector(const Vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
Reserve(v.Size());
for (size_t i = 0; i < v.Size(); ++i){
this->PushBack(v[i]); //逐個丟進去~
}
}
// v1 = v2
Vector<T>& operator=(Vector<T> v){
this->Swap(v);
return *this;
}
void Swap(Vector<T>& v){ //只交換三個成員變量,代價遠低於使用算法交換對象
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
~Vector(){
if (_start){
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
iterator begin(){
return _start;
}
iterator end(){
return _finish;
}
const_iterator begin() const{
return _start;
}
const_iterator end() const{
return _finish;
}
void Reserve(size_t n){
if (n > Capacity()){
size_t size = Size();
// 開新空間
T* newarray = new T[n];
if (_start)
memcpy(newarray, _start, sizeof(T)*Size());
// T的 operator=
/* for (size_t i = 0; i < size; ++i){
newarray[i] = _start[i];
}*/
// 釋放舊空間
delete[] _start;
// 賦值
_start = newarray;
_finish = _start + size;
_endofstorage = _start + n;
}
}
void Resize(size_t n, const T& val = T()){ //缺省參數,匿名對象
if (n <= Size()){
_finish = _start + n;
}
else{
Reserve(n);
while (_finish != _start + n){
*_finish = val;
++_finish;
}
}
}
void PushBack(const T& x){
if (_finish == _endofstorage){
//我們選擇2倍增長,其實不是硬性規定
//VS下是1.5倍增長,CentOS下是2倍增長
size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
Reserve(newcapacity);
}
*_finish = x;
++_finish;
}
void PopBack(){
assert(_finish > _start);
--_finish; //標記爲無效數據即可~
}
void Insert(iterator pos, const T& x){
assert(pos < _finish);
if (_finish == _endofstorage){
size_t n = pos - _start;
size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
Reserve(newcapacity);
pos = _start + n;
}
iterator end = _finish-1;
while (end >= pos){
*(end + 1) = *end; //數據搬運,從後往前
--end;
}
*pos = x;
++_finish;
}
iterator Erase(iterator pos){
assert(pos < _finish);
iterator it = pos;
while (it < _finish - 1){
*it = *(it + 1); //數據搬運:覆蓋
++it;
}
--_finish;
return pos;
}
size_t Size() const{
return _finish - _start;
}
size_t Capacity() const{
return _endofstorage - _start;
}
T& operator[](size_t pos){
assert(pos < Size());
return _start[pos];
}
const T& operator[](size_t pos) const{
assert(pos < Size());
return _start[pos];
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
vector與list的對比
vector | list | |
---|---|---|
底層結構 | 動態順序表,一段連續空間 | 帶頭結點的雙向循環鏈表 |
隨機訪問 | 支持隨機訪問,訪問某個元素效率O(1) |
不支持隨機訪問,訪問某個元素效率O(N) |
插入和刪除 | 任意位置插入和刪除效率低,需要搬移元素,時間複雜度爲O(N) ,插入時有可能需要增容,(增容:開闢新空間,拷貝元素,釋放舊空間,導致效率更低) |
任意位置插入和刪除效率高,不需要搬移元素,時間複雜度爲O(1) |
空間利用率 | 底層爲連續空間,不容易造成內存碎片,空間利用率高,緩存利用率高 | 底層節點動態開闢,小節點容易造成內存碎片,空間利用率低,緩存利用率低 |
迭代器 | 原生態指針 | 對原生態指針(節點指針)進行封裝 |
迭代器失效 | 在插入元素時,要給所有的迭代器重新賦值,因爲插入元素有可能會導致重新擴容,致使原來迭代器失效,刪除時,當前迭代器需要重新賦值否則會失效 | 插入元素不會導致迭代器失效,刪除元素時,只會導致當前迭代器失效,其他迭代器不受影響 |
使用場景 | 需要高效存儲,支持隨機訪問,不關心插入刪除效率 | 大量插入和刪除操作,不關心隨機訪問 |
在這裏給出list
篇的博客鏈接,感興趣的看官請移步至:【https://blog.csdn.net/qq_42351880/article/details/100111223】