目錄
?1. insert的實現
2. 迭代器失效
2.1 迭代器失效的兩種情況
指向已釋放的內存(物理失效)
元素移動導致迭代器指向錯誤(邏輯失效)
2.2 修改代碼
3. erase的實現
?編輯修改代碼
?4. resize的實現
5. 構造函數
5.1 默認構造函數
5.2 使用迭代器范圍構造
5.3 使用n個val構造
?6. 拷貝構造函數
7. 賦值運算符重載
8. 關于vector對象的淺拷貝問題
?1. insert的實現
reserve(n)函數的核心邏輯是:當容器當前容量不足以容納新元素時,會重新申請一塊更大的內存空間,將原來元素復制到新空間中,然后釋放舊的內存空間。
在下面這段代碼中進行插入操作時,擴容后pos仍然指向舊的內存位置,而舊的內存已經被釋放,循環條件end>=pos地址比較毫無意義,對*pos賦值時會導致程序崩潰。
//錯誤示例
void insert(iterator pos, const T& x)
{assert(pos >= _start);assert(pos <= _finish);//擴容 若觸發擴容,舊內存被釋放,pos失效if (_finish ==_end_of_storage){ reserve(capacity() == 0 ? 4 : capacity() * 2); }iterator end = _finish - 1;//向后挪動數據while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;
}
正確做法:先記錄pos相對于起始位置的偏移量,擴容后再根據偏移量來更新pos。
//right!
void insert(iterator pos, const T& x)
{ assert(pos >= _start);assert(pos <= _finish); //擴容 if (_finish ==_end_of_storage){//記錄pos相對于起始地址的偏移量size_t len = pos - _start;//擴容(重新申請內存,舊內存釋放)reserve(capacity() == 0 ? 4 : capacity() * 2);//擴容后,更新pos到新內存的對應位置pos = _start + len;}iterator end = _finish - 1;//挪動數據while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;
}
2. 迭代器失效
2.1 迭代器失效的兩種情況
指向已釋放的內存(物理失效)
//插入元素
void insert(iterator pos, const T& x)
{assert(pos >= _start);assert(pos <= _finish);//擴容if (_finish == _end_of_storage){//記錄pos相對于起始地址的偏移量size_t len = pos - _start;//擴容(重新申請內存,舊內存釋放)reserve(capacity() == 0 ? 4 : capacity() * 2);//擴容后,更新pos到新內存的對應位置pos = _start + len;}iterator end = _finish - 1;//挪動數據while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;
}
……
//測試調用void test_vector2(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);//再插入元素則需要擴容print_vector(v);int x;cin >> x;auto p = find(v.begin(), v.end(), x);if (p != v.end()){v.insert(p, 300); //傳入p的拷貝給insert的形參pos 形參的改變不影響實參 //(*p) *= 10; //錯誤!進行擴容時insert內部更新迭代器pos,但不影響實參p,p仍指向舊位置,所以外部p已失效,訪問可能會出錯。} print_vector(v);}
元素移動導致迭代器指向錯誤(邏輯失效)
自定義insert函數中,即使不擴容,迭代器也會失效。
原因:插入時,pos及之后的元素會向后挪動一位,假設迭代器p指向位置 i ,插入后原位置i的元素被移動到 i+1,新元素被插入到 i 位置,p仍指向物理地址 i ,但該地址的元素已經不是原來的了,p指向的“語義”已經改變,屬于邏輯失效。
在VS環境下,標準庫vector的迭代器失效檢查非常嚴格,即使不發生擴容,insert后使用原迭代器也會觸發調試錯誤。
總結:迭代器的 “失效” 不僅指 “指向已釋放內存”(物理失效),還包括 “容器結構改變導致指向邏輯錯誤”(邏輯失效)。這也是為什么標準庫文檔強調:對容器執行修改操作(
insert
/erase
?等)后,所有可能受影響的迭代器都應視為失效,不應再使用。
2.2 修改代碼
插入操作(無論是否擴容)都會導致原迭代器可能失效:
- 若發生擴容:舊內存被釋放,原迭代器指向無效地址(物理失效)。
- 若未擴容:元素后移導致原迭代器指向的 “語義位置” 改變(邏輯失效,比如原迭代器本應指向元素 A,插入后指向的是新元素)。
讓 insert 返回更新后的 pos 迭代器,這也是STL標準庫中 insert 的原型,STL中vector::insert 的原型正是:iterator? insert(iteartor? pos ,const? T&? value);
iterator insert(iterator pos, const T& x)
{assert(pos >= _start);assert(pos <= _finish);//擴容if (_finish == _end_of_storage){//記錄pos相對于起始地址的偏移量size_t len = pos - _start;//擴容(重新申請內存,舊內存釋放)reserve(capacity() == 0 ? 4 : capacity() * 2);//擴容后,更新pos到新內存的對應位置pos = _start + len;}iterator end = _finish - 1;//挪動數據while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;return pos; //返回更新后的pos迭代器! ! ! !
}……
//安全使用
void test_vector2(){vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);print_vector(v);int x;cin >> x;auto p = find(v.begin(), v.end(), x);if (p != v.end()){p = v.insert(p, 99);//用insert的返回值更新p,獲取新的有效迭代器p現在指向新插入的99(*p) *= 10; //安全操作:此時p有效,修改后新元素變為990} print_vector(v);}
3. erase的實現
//錯誤示例
//刪除偶數元素
void erase(iterator pos)
{assert(pos >= _start);assert(pos < _finish);iterator it = pos + 1;while (it != end()){*(it-1) = *it;++it;}--_finish;
}//測試調用 刪除所有的偶數
auto it = v.begin();
while (it != v.end())
{if (*it % 2 == 0){v.erase(it); //刪除后it已失效}++it; //錯誤:無論是否刪除都對it自增
}print_vector(v);
對于上面erase的實現大有問題:
- 如果v=[1,2,4,5]含有連續的偶數,按當前邏輯執行,最終會導致v=[1,4,5],偶數4被漏刪,邏輯錯誤。
- 如果v=[1,2,3,4],那么當it超出有效范圍[_start,_finish]后,*it屬于未定義行為,程序可能讀取到任意值,也可能直接崩潰,如果碰巧讀取到一個偶數,那么就會發生斷言錯誤。
修改代碼
對于上面的邏輯錯誤,我們只需要添加else分支,將++it封裝到else里面即可解決問題。
//刪除元素 void erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);iterator it = pos + 1;while (it != end()){*(it-1) = *it;++it;}--_finish; }//……//刪除所有的偶數 auto it = v.begin(); while (it != v.end()) {if (*it % 2 == 0){v.erase(it); }else{++it;} }
這種寫法在數組式容器中能正常工作,是因為當刪除it指向的元素后,it并沒有失效(仍指向原來的內存地址),但該地址的值已經變成了“原下一個元素”。此時不執行++it,下一輪循環會直接檢查這個值,相當于間接實現了“迭代器后移”的效果。
但這種寫法有兩個隱藏的風險:
1.但是在erase函數里面有擴容邏輯時 ,it指向已釋放的內存,可能導致程序崩潰。
2.?依賴容器底層實現,只有當容器是連續內存存儲時,邏輯才成立。如果換成鏈表式容器,鏈表的底層是離散的節點,每個節點存“數據”和“指向下一節點的指針”,節點間通過指針連接,內存不連續。刪除節點后 it 會指向“已釋放的內存”,下一次循環解引用*it時可能直接崩潰,或讀取到垃圾數據。
如果想讓代碼更健壯、更符合常規設計,建議讓erase函數返回“下一個有效迭代器”,并使用標準寫法:it=v.erase(it),這樣無論擴不擴容,無論容器底層是數組還是鏈表,邏輯都能保持一致且安全。
正確代碼:
//刪除元素 iterator erase(iterator pos) {assert(pos >= _start);assert(pos < _finish);iterator it = pos + 1;while (it != end()){*(it-1) = *it;++it;}--_finish;return pos; //返回下一個有效迭代器 }//……//刪除所有的偶數 auto it = v.begin(); while (it != v.end()) {if (*it % 2 == 0){it = v.erase(it); //用返回值更新 it(指向刪除后的下一個元素)}else{++it;} }
這一設計符合 STL 規范,能確保迭代器操作的安全性,邏輯與 STL 標準庫的vector::erase完全一致。
?4. resize的實現
resize的作用是調整容器的大小,當容器需要擴展大小時,用
val
來初始化所有新增的元素;如果不傳遞這個參數,就使用元素類型T
的默認構造值(比如內置類型是 0,自定義類型調用默認構造)。
函數原型:void resize( size_t n, const T& val= T() )?,這樣寫的意思就是調用T類型的默認構造函數生成匿名對象(臨時對象),引用綁定到臨時對象上,T是int時,T()就是0;T是自定義類時,調用其默認構造函數創建對象。使用常量引用,傳遞參數時可以避免拷貝,同時保證了val不會被修改。
也可以這樣寫:void resize( size_t n, T val = T() ) ,這里 = T()表示調用T類型的默認構造函數生成的臨時對象,再調用拷貝構造函數構造val,編譯器可能會優化為直接構造val。
void resize(size_t n, const T& val= T()) //第二個參數也可以寫作 T val = T()
{if (n < size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;++_finish;}}
}
5. 構造函數
5.1 默認構造函數
//構造函數vector() //自定義空構造:_start / _finish / _end_of_storage 默認初始化(垃圾值)
{}//或 vector() = default; C++11 引入,編譯器生成的默認構造函數同樣讓三個指針默認初始化(垃圾值)
對于第一種寫法,用戶自定義的空構造函數,編譯器不再生成默認構造。函數體為空,且沒有成員初始化列表,此時成員變量會執行默認初始化。
默認初始化規則:
- 若成員是內置類型(如int、指針):默認初始化是“不初始化”,值為隨機垃圾值。
- 若成員是類類型(如string、自定義類):默認初始化是“調用該類的默認構造函數”。
對于第二種寫法,是c++11及以后的標準中合法且推薦的寫法,用于顯式要求編譯器生成“默認的構造函數”,編譯器生成的默認構造函數同樣會對成員變量進行默認初始化,兩種寫法效果類似,但=default? 表意更清晰,是更推薦的寫法。
5.2 使用迭代器范圍構造
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last)//使用!= 是考慮容器是鏈表的情況{push_back(*first);++first;}
}
它的作用是通過一段迭代器范圍[first,last)來初始化vector對象,這個構造函數的特點在于,它本身也是一個函數模版。為什么需要把它設計成為一個函數模版呢?
當使用一段迭代器范圍來初始化vector時,這段范圍可能來自一個數組、鏈表,為了兼容這些不同類型的迭代器,這個構造函數必須自身成為一個函數模版。如果固定使用類內部的iterator,就無法使用其它容器初始化vector。
如果構造函數寫成固定使用類內部的iterator會導致下面問題:
template <classT>class vector { public: typedef T* iterator; typedef const T* const_iterator; // 僅接受自身迭代器的構造函數(局限性很大)vector(iterator first, iterator last) {while (first != last) {push_back(*first);++first;}}// ... };
//無法用數組初始化vector int arr[] = {1,2,3,4}; vector<int> v(arr, arr+4); // 編譯失敗!arr是int*,不是vector<int>::iterator//也無法用list的迭代器初始化vector std::list<int> lt = {1,2,3}; vector<int> v(lt.begin(), lt.end()); // 編譯失敗!list的迭代器不是vector的iterator
它的實際用法:
// 1. 從數組初始化vector
int arr[] = {1, 2, 3, 4, 5};
vector<int> v1(arr, arr + 5);// 2. 從另一個容器(list)的迭代器區間初始化vector
std::list<double> lst = {1.1, 2.2, 3.3};
vector<double> v2(lst.begin(), lst.end()); // 3. 從同類型vector的迭代器區間初始化vector
vector<string> v3 = {"a", "b", "c"};
vector<string> v4(v3.begin() + 1, v3.end());
這個函數是vector類模版的模版構造函數,屬于帶參的構造函數的一種,這樣設計實現了“從任意序列范圍初始化vector的功能”,極大提升了容器的通用性,這也是C++標準庫std::vector的標準構造函數之一。
5.3 使用n個val構造
vector(size_t n,const T& val=T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}
但是,這樣寫會出現下面的情況:
//模版構造函數
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last)//使用!= 是考慮容器是鏈表的情況{push_back(*first);++first;}
}vector(size_t n,const T& val=T())
{reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}
}//……vector<string> v6(10, "11111111");
print_container(v6);vector<int> v7(10);
print_container(v7);vector<int> v8(10, 1); //這里報錯!
print_container(v8);
為什么v6、v7都能正常運行,而v8卻出錯了呢?
C++函數重載決議規定,當有多個構造函數可以匹配時,編譯器會選擇最匹配的那個。問題出在類型匹配上,v7的初始化只提供了一個參數10,不會匹配到模版構造函數,核心原因是參數數量不匹配,此構造函數的第一個參數是size_t類型,需要將int轉換為size_t類型。
v8的初始化提供了兩個參數,(10是int,1是int)
對于模版構造函數,可以直接推導出 InputIterator = int,,此時兩個參數類型完全匹配,不需要任何轉換。對于第二個構造函數,第一個參數需要走隱式類型轉換將int類型的10轉換為siez_t類型。
所以編譯器認為模板構造函數是更匹配的,從而選擇了它。但是在模版構造函數中使用push_back(*first); first是int類型,解引用int是不合法的,所以會報錯。
解決方法:讓第二個構造函數的參數匹配度更高,將size_t 改成 int,構造函數對(int , int)類型的調用實現 “完全匹配”。
當非模板函數和模板函數都能提供 “同樣好的匹配” 時,非模板函數的優先級更高,此時v8會調用第二個構造函數。
vector(int n, const T& val = T()) {reserve(n);for (size_t i = 0; i < n; i++){push_back(val);} }
?6. 拷貝構造函數
//拷貝構造函數
vector(const vector<T>& v)
{reserve(v.size());for (auto& e : v){push_back(e);}
}
當通過拷貝構造函數創建新對象時,新對象的三個指針作為內置類型會先執行默認初始化,值為隨機值,后續通過?reserve(v.size()); 一次性分配足夠內存,并將這三個指針顯式賦值,從而完成它們的 “有效初始化”,通過后續操作(push_back)來規范它們的值。
7. 賦值運算符重載
傳統寫法:
//賦值 v1 = v3
vector<T>& operator=(const vector<T>& v)
{if (this != &v) //避免自己給自己賦值{clear();//清理數據,但保留空間reserve(v.size());//一次擴容到位,避免頻繁擴容for (auto& e : v){push_back(e);}}return *this;
}
現代寫法:
//交換函數
void swap(vector<T> v)
{std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);
}//賦值 拷貝并交換 v1 = v3
vector<T>& operator=(vector<T> v) //這里是值傳遞 也可以這樣寫:vector& operator=(vector v)
{swap(v);return *this;
}
當函數參數是值傳遞時,會觸發拷貝構造函數,將v3拷貝一份到參數v中(v是v3的副本),swap(v)交換的是v1和v3的副本,交換后當前對象*this有了v3的資源,完成了賦值,參數v擁有了當前對象原來的舊資源,函數結束時,v會被銷毀(觸發析構函數),避免了內存泄漏。這種寫法天然處理了自賦值,不會出現資源被提前釋放的問題。
在這個模板類的內部,編譯器已經知道當前處于的vector<T>的作用域中,所以在模板類的內部成員函數定義中,可以直接用類名vector 代替完整的vector<T>,這是 C++ 的語法規則允許的簡寫,核心原因是模板類的作用域內會自動關聯模板參數。
8. 關于vector<string>對象的淺拷貝問題
vector<string> v;
v.push_back("1111111111111111111");
v.push_back("1111111111111111111");
v.push_back("1111111111111111111");
v.push_back("1111111111111111111");
print_vector(v);v.push_back("1111111111111111111"); //擴容
print_vector(v);
運行結果:
為什么會出現亂碼呢?
核心原因是:memcpy是淺拷貝,而std::string是帶動態內存管理的類,淺拷貝會導致多個string對象共享同一塊堆內存,析構時重復釋放或非法訪問已釋放內存。
std::string的內部結構(簡化理解):
class string {
private:char* _data; // 指向堆上的字符緩沖區size_t _size;size_t _capacity;// ... 析構函數會釋放 _data 指向的堆內存
};
void reserve(size_t n) { if (n > capacity()){size_t old_size = size(); T* tmp = new T[n];memcpy(tmp, _start, size() * sizeof(T));delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;} }
memcpy(tmp, _start, size() * sizeof(T));?,當使用memcpy拷貝string對象時,只是按字節賦值string對象的成員變量,沒有復制_data指向的堆上的字符內容。這會導致舊內存中的string對象和新內存的string對象,_data指針指向同一塊堆內存。舊內存執行delete[] _start; 時,底層調用n次string的析構函數完成n個對象中資源的清理,再調用operator delete[ ]函數釋放空間,新內存中的string對象的_data指針變成野指針(指向已釋放的內存)。所以,后續訪問這些新的string對象(如打印、再次析構)時,就會出現未定義行為(亂碼、崩潰等)。
解決思路:修改reserve函數,把memcpy換成循環賦值:
void reserve(size_t n) { if (n > capacity()){size_t old_size = size(); T* tmp = new T[n];//memcpy(tmp, _start, old_size * sizeof(T)); 只適合內置類型,對于帶動態內存管理的類(如string)memcpy執行淺拷貝會出錯 for (size_t i = 0; i < old_size; ++i){tmp[i] = _start[i]; //tmp[i]和_start[i]都是string對象,對它們的賦值會觸發string類的operator= 重載函數 ,相當于a.operator=(b) string的賦值運算符會深拷貝堆上的字符}delete[] _start;_start = tmp;_finish = tmp + old_size;_end_of_storage = tmp + n;} }
總結一下,不能用memcpy來拷貝自定義類型(尤其是帶資源管理的類型,如string、vector自己等),必須用深拷貝(調用拷貝構造或賦值運算符),否則會導致多個對象共享同一份資源,引發資源管理錯誤(重復釋放、野指針訪問等),從而出現亂碼等問題。
push_back("11111111")參數傳遞過程
void push_back(const T& x)
push_back的參數是const string& x,字符串字面量(類型為const char[N],會自動“衰減”為const char*)。因為std::string類提供了非explicit的構造函數string(const char*) 允許隱式類型轉換,因此編譯器會自動用這個構造函數把const char*隱式轉換成string臨時對象,然后將臨時對象綁定到const string&參數上(const引用會延長臨時對象生命周期,直到函數調用結束)。