C++ STL :Vector記憶體分配與釋放
技術標籤:c++/CLI
vector 可以容納許多型別的資料,如若干個整數,所以稱其為容器。
vector 是C++ STL的一個重要成員,使用它時需要包含標頭檔案:#include<vector>。
關於vector的使用,雖然可以動態的分配記憶體,但是稍不注意,就會落入記憶體陷阱中,無形中增大了程式的記憶體開銷,導致程式崩潰。基於此,有必要梳理一下C++ STL中的vector的記憶體分配與釋放機制。
文章從“定義”、“新增”、“清空”三個部分來探究vector的記憶體分配和釋放機制。
導讀:
1. vector記憶體相關介紹
- size(),capacity(),push_back(),reserve(),resize(),clear(),swap()
- vector記憶體增長特點
2. vector定義與記憶體分配
- 類中定義 & 類外定義(包括全域性、主函式、類成員函式、自定義函式等)
- 一維vector & 二維vector
- 空vector & 指定行vector & 指定行列vector
3. vector定義方式建議
4. vector清空元素與記憶體釋放
- clear()
- swap()
以下是正文,測試程式碼會放在最後。
1. vector記憶體相關介紹
1.1 相關函式
(1)b.size():容器當前擁有的元素個數。
(2)b.capacity():容器在必須分配新儲存空間之前可以儲存的元素總數。
區別:建立完空vector後,其size和capacity都為0,但是向vector插入元素後,會發生變化,通常capacity大於等於size,這是vector記憶體增長機制決定的。
(3)b.push_back():在向量最後插入一個元素。在呼叫push_back時,若當前容量已經不能夠放入新的元素(capacity=size),那麼vector會重新申請一塊記憶體,把之前的記憶體裡的元素拷貝到新的記憶體當中,然後把push_back的元素拷貝到新的記憶體中,最後要析構原有的vector並釋放原有的記憶體。所以說這個過程的效率是極低的,為了避免頻繁的分配記憶體,C++每次申請記憶體都會成倍的增長。
(4)b.reserve(a):容器預留空間a,但在空間內不真正建立元素物件(capacity=a,無size)。所以在沒有新增新的物件之前,不能引用容器內的元素。加入元素時,要呼叫push_back()/insert()函式,值得一提的是,若新增元素沒有超出預留,那麼是不會對記憶體進行重新分配的
(5)b.resize(a,0):改變容器的大小,且建立物件(指定或預設0,初始capacity = size = a)。因此,呼叫這個函式之後,可以引用容器內的物件了,因此當加入新的元素時,用operator[]操作符,或者用迭代器來引用元素物件。此時再呼叫push_back()函式,是加在這個新的空間後面的。同樣也會觸發push_back()的空間預留機制。
容器型別提供resize操作來改變容器所包含的元素個數:如果當前的容器長度大於新的長度值,則該容器後部的元素會被刪除;如果當前的容器長度小於新的長度值,則系統會在該容器後部新增新元素。
需要注意的是:resize操作可能會使迭代器失效,對於所有的容器型別,如果resize操作壓縮了容器,則指向已刪除的元素的迭代器失效。
區別:
- reserve函式只有一個引數,即需要預留的容器的空間;resize函式可以有兩個引數,第一個引數是容器新的大小, 第二個引數是要加入容器中的新元素,如果這個引數被省略,那麼就呼叫元素物件的預設建構函式。
- 當不超過預留空間時,reserve()不涉及記憶體的重新分配,resize()會涉及記憶體的重新分配。但是如果是對空容器操作,那麼二者看不出內在的區別。
- reserve()只修改capacity大小,不修改size大小,
- resize()既修改capacity大小,也修改size大小。
(6)b.clear():清空向量中的元素。但是即使clear(),vector所佔用的記憶體空間依然如故,無法保證記憶體的回收。一維vector.clear(),清空一維的元素,但是仍舊保留著列的capacity;二維vector.clear(),清空各行的列,並且回收列記憶體capacity,但是保留行的capacity。
(7)a.swap(b):將a中的元素和b中的元素進行整體性交換。除此之外,①可以利用swap()方法去除vector多餘的容量:vector<T>(x).swap(x);其中,x是當前要操作的容器,T是容器的型別。②利用swap()方法清空vector容器:當 swap() 成員方法用於清空 vector 容器時,可以套用如下的語法格式:vector<T>().swap(x)。
1.2 vector記憶體增長策略和特點
(1)vector的容器的大小隻可以增加,不可以減少。當我們使用push_back() , insert() , emplace()等成員方法的時候,有可能會增加容量,但是我們使用 pop_back()、erase()、clear() 等方式的時候,並不會減少實際 的記憶體容量。只是可以刪除容器裡面的內容。
(2)vector 有一個機制是這樣的,如果新加入一個元素,比如通過push_back(),但是size 大於了capacity,那麼vector 就會重新找一塊更大的地方再把資料放進去。重新分配的過程:申請一塊新的記憶體 > 拷貝資料 > 釋放原記憶體。
所以,使用vector容器的時候可以預先空間,把capacity定得夠大,這樣可以儘量避免重新分配vector 的記憶體,不增加程式的負擔。即:reserve()預留空間+push_back()壓入元素+size()控制讀取的策略,但是還是不推薦這麼用,原因後面解釋。
(3)綜合來看,vector為了支援快速的隨機訪問,vector容器的元素以連續方式存放,每一個元素都緊挨著前一個元素儲存。設想一下,當vector新增一個元素時,為了滿足連續存放這個特性,都需要重新分配空間、拷貝元素、撤銷舊空間,這樣效能難以接受。因此STL實現者在對vector進行記憶體分配時,其實際分配的容量要比當前所需的空間多一些。就是說,vector容器預留了一些額外的儲存區,用於存放新新增的元素,這樣就不必為每個新元素重新分配整個容器的記憶體空間。
在呼叫push_back時,每次執行push_back操作,相當於底層的陣列實現要重新分配大小;這種實現體現到vector實現就是每當push_back一個元素,都要重新分配一個大一個元素的儲存,然後將原來的元素拷貝到新的儲存,之後在拷貝push_back的元素,最後要析構原有的vector並釋放原有的記憶體。
2. vector定義與記憶體分配
2.1 類中定義 & 類外定義
vector在類中私有/公有成員中的定義方式居然和全域性、函式內的定義方式還有所區別,具體原因也沒了解到(和預設建構函式有關),總之用這玩意兒到處都是坑,吐。
(1)類外定義(圓括號)
一維vector
vector<int>a(10);
二維vector
vector<vector<int>>c(5, vector<int>(10));
以上定義都是有初始值的,所以可以直接用下標訪問。舉例的是已知一些行列資訊的定義方式,其餘的定義方式在下面說,差不多。
(2)類中定義{花括號}
不能使用以上類外的兩種定義方式。
一維vector:無法指定列長,可以用花括號{ }初始化列元素。
vector<int>a1{ 1,2,3 };
二維vector:可以用花括號{ }指定行數,無法指定列長,但可以用花括號{ }初始化列元素。
vector<vector<int>>c{ N };
vector<vector<int>>d1{ N ,vector<int> {1,2,3} };
只要vector中沒有定義列長,那麼就是個空的容器。具體細節見下程式碼:
class Vector_define
{
private:
int i, j;
/*一維vector*/
//vector<int>a(N); /*在自定義類中無法對vector實現圓括號()初始化長度的定義*/
vector<int>a{ N }; /*此種花括號{}的初始化可以定義。作用:只是輸入一個元素,而非定義長度*/
vector<int>a1{ 1,2,3 }; /*支援直接輸入元素*/
vector<int>b; /*正常定義,空的,無長度*/
/*二維vector*/
//vector<vector<int>>c(N, vector<int>(T)); /*該種定義同一維,無效定義*/
vector<vector<int>>c{ N }; /*二維vector該種花括號{}定義有效。作用:佔行,而非輸入元素,這和一維不一樣*/
//vector<vector<int>>c{ 1,2,3 }; /*無效定義*/
vector<vector<int>>d{ N ,vector<int> {T} }; /*該種定義,支援行定義,不支援列定義,但支援行新增*/
vector<vector<int>>d1{ N ,vector<int> {1,2,3} }; /*支援行直接輸入元素,且是為每一行都插入*/
vector<vector<int>>e; /*正常定義,空的,無長度*/
}
3. vector定義方式建議
經過以上的一些探索,在這對不同vector定義方式的記憶體變化再做出一些分析和建議。
(1)一維vector
①一開始知道列長。
vector<int>a(10);
②一開始不知道列長,resize()策略。
vector<int>c;
...
c.resize(25);
優點:按需分配,有初始值,可以用下標直接改值。
③一開始不知道列長,reserve()+push_back()策略。
vector<int>a;
...
a.reserve(25);
for (i = 0; i < 25; i++)
{
a.push_back(i + 1);
}
缺點:不建議用,雖然也是按需分配,但是由於push_back()可能帶來記憶體溢位。
(2)二維vector(以類中定義為例)
①一開始知道行、列資訊的,resize()策略。
vector<vector<int>>h(5);
for (i = 0; i < h.size(); i++)
{
h[i].resize(10);
}
當對空vector採取resize(),capacity=size,但如果是對一個已有元素的vector進行resize(),或者是又對resize後的vector,push_back了一些元素,那麼就會觸發記憶體預留機制,所以如果要想去除多餘的空間,可以採用如下方法:
vector<int>(h).swap(h)
優點:按需分配,有初始值,可以用下標直接改值。
②一開始知道行、列資訊的,reserve()+push_back()策略。
不推薦!!!原因如下:
1)因為但凡用到push_back(),如果忘記清空,稍不注意,在某些迴圈中,迴圈一次就壓入一堆元素,不光影響資料的使用,而且還會不停擴張記憶體,更嚴重的是當用完預留的記憶體後,更是成倍的開闢記憶體空間,直逼程式崩潰。
2)即便是記得使用清空的策略去除無關的元素,也容易錯誤的使用清空方式。比如:
vector<vector<int>> b{ N };
for (i = 0; i < N; i++)
{
b[i].reserve(T);
}
該二維vector在定義時,已經指定了行數,所以“b.clear()”的行為是錯誤的,而“b[0],clear()”才是正確的,也即只能清空列。
又或者你想這樣定義(錯誤!):
vector<vector<int>> e;
e.reserve(10);
for (i = 0; i < N; i++)
{
b[i].reserve(10);
}
看著沒問題,實際是個錯誤的定義。若一開始沒有定義行,那麼是無法通過reserve()固定行的;只能通過resize()的方式。
總結起來就是,這種策略的定義方式,特別容易出錯!
③其他的情況暫且不論,總之能用resize()就用,能不用push_back()就不用,一旦用了,根據情況,需要清空的一定要正確及時的清空,避免記憶體損耗和溢位!
4. vector清空元素與記憶體釋放
由於vector的記憶體佔用空間只增不減,比如你首先分配了10,000個位元組,然後erase掉後面9,999個,留下一個有效元素,但是記憶體佔用仍為10,000個。所有記憶體空間是在vector析構時候才能被系統回收。empty()用來檢測容器是否為空的,clear()可以清空所有元素。但是即使clear(),vector所佔用的記憶體空間依然如故,無法保證記憶體的回收。
就像前面所說的,vector的記憶體空間是隻增加不減少的,我們常用的操作clear()和erase(),實際上只是減少了size(),清除了資料,並不會減少capacity,所以記憶體空間沒有減少。那麼如何釋放記憶體空間呢,正確的做法是swap()操作。
swap交換技巧實現記憶體釋放思想:vector()使用vector的預設建構函式建立臨時vector物件,再在該臨時物件上呼叫swap成員,swap呼叫之後原來vector佔用的空間就等於一個預設構造的物件的大小,臨時物件就具有原來物件v的大小,而該臨時物件隨即就會被析構,從而其佔用的空間也被釋放。
當 swap() 成員方法用於清空 vector 容器時,可以套用如下的語法格式:
vector<T>().swap(x);
這裡沒有為 vector<T>() 表示式傳遞任何引數。這意味著,此表示式將呼叫 vector 模板類的預設建構函式,而不再是複製建構函式。也就是說,此格式會先生成一個空的 vector 容器,再借助 swap() 方法將空容器交換給 x,從而達到清空 x的目的。
完整測試程式碼:
#include<iostream>
using namespace std;
#include<vector>
#define N 20
#define T 10
/*類Vector_define:探究在類中對vector進行定義的特點*/
class Vector_define
{
private:
int i, j;
/*一維vector*/
//vector<int>a(N); /*在自定義類中無法對vector實現圓括號()初始化長度的定義*/
vector<int>a{ N }; /*此種花括號{}的初始化可以定義。作用:只是輸入一個元素,而非定義長度*/
vector<int>a1{ 1,2,3 }; /*支援直接輸入元素*/
vector<int>b; /*正常定義,空的,無長度*/
/*二維vector*/
//vector<vector<int>>c(N, vector<int>(T)); /*該種定義同一維,無效定義*/
vector<vector<int>>c{ N }; /*二維vector該種花括號{}定義有效。作用:佔行,而非輸入元素,這和一維不一樣*/
//vector<vector<int>>c{ 1,2,3 }; /*無效定義*/
vector<vector<int>>d{ N ,vector<int> {T} }; /*該種定義,支援行定義,不支援列定義,但支援行新增*/
vector<vector<int>>d1{ N ,vector<int> {1,2,3} }; /*支援行直接輸入元素,且是為每一行都插入*/
vector<vector<int>>e; /*正常定義,空的,無長度*/
public:
void test1()
{
/*teat a*/
cout << "a.size()=" << a.size() << endl; /* a.size()=1 ,可知類中花括號只是添加了元素而已*/
for (i = 0; i < a.size(); i++)
{
cout << a[i] << " "; /* 只輸出 20 ,也即只有一個元素*/
}
cout << endl << endl;
/*teat b*/
cout << "b.size()=" << b.size() << endl; /* b.size()=0 ,沒有元素,正常*/
/*for (i = 0; i < a.size(); i++)
{
cout << b[i] << " "; // 報錯:out of range! 空的容器,沒有長度,沒有東西輸出!
}
cout << endl ;*/
cout << endl;
/*teat c*/
cout << "c.size()=" << c.size() << endl; /* c.size()=20,可知二維vector支援花括號{}定義行數 */
for (i = 0; i < c.size(); i++)
{
for (j = 0; j < c[i].size(); j++)
{
cout << c[i][j] << " "; /* 不報錯,有行沒列,輸出不了什麼, */
}
cout << endl;
}
cout << endl;
/*teat d*/
cout << "d.size()=" << d.size() << endl; /* d.size()=20,花括號{}行定義仍舊有效 */
for (i = 0; i < d.size(); i++)
{
cout << "d[" << i << "].size()=" << d[i].size() << " ";
} /* d[i].size()=1,可知花括號{}列長定義無效 */
cout << endl;
for (i = 0; i < d.size(); i++)
{
for (j = 0; j < d[i].size(); j++)
{
cout << d[i][j] << " "; /* d[i][j]=T,可知花括號{}列長定義無效,只是添加了一個元素 */
}
cout << endl;
}
cout << endl;
/*teat e*/
cout << "e.size()=" << e.size() << endl; /* e.size()=0,行0,列空 */
for (i = 0; i < e.size(); i++)
{
cout << "e[" << i << "].size()=" << e[i].size() << " ";
} /* 列空 */
cout << endl;
for (i = 0; i < e.size(); i++)
{
for (j = 0; j < e[i].size(); j++)
{
cout << e[i][j] << " "; /* 容器空 */
}
cout << endl;
}
cout << endl;
}
};
/*類Vector_memory:探究在類中定義的vector的記憶體特點*/
class Vector_memory
{
private:
int i, j;
/*一維*/
vector<int>a;
vector<int>b{ 1,2,3 };
vector<int>c;
vector<int>d;
/*二維*/
vector<vector<int>>e;
vector<vector<int>>f{ N };
vector<vector<int>>g{ N,vector<int>{1,2,3} };
public:
void test2()
{
/*一維記憶體*/
cout << "a.capacity()=" << a.capacity() << endl; /* a.capacity()=0 */
cout << "a.size()=" << a.size() << endl << endl; /* a.capacity()=0 */
cout << "b.capacity()=" << b.capacity() << endl; /* a.capacity()=3,不會預留空間*/
cout << "b.size()=" << b.size() << endl << endl; /* a.capacity()=3 */
//①往 a 中新增元素:研究 pushback() 對記憶體的改變
for (i = 0; i < N; i++)
{
a.push_back(i + 1);
}
cout << "a.capacity()=" << a.capacity() << endl; /* a.capacity()=28,可知pushback()會預留部分空間*/
cout << "a.size()=" << a.size() << endl << endl; /* a.size()=20,實際空間等於元素個數*/
//②給 a 重設長度:研究 resize() 對記憶體的改變
a.resize(30);
cout << "a.capacity()=" << a.capacity() << endl; /* a.capacity()=42,可知resize()會對快取區進行重新分配和空間預留*/
cout << "a.size()=" << a.size() << endl << endl; /* a.capacity()=30,可知resize()會產生初始值*/
//③給 c 重設長度:研究 resize() 對記憶體的改變
c.resize(25);
cout << "c.capacity()=" << c.capacity() << endl; /* c.capacity()=25,可以發現:當對空容器resize(),不會預留空間*/
cout << "c.size()=" << c.size() << endl << endl; /* c.capacity()=25*/
//④給 a 預設長度:研究 reserve() 對記憶體的改變
a.reserve(50);
cout << "a.capacity()=" << a.capacity() << endl; /* a.capacity()=50,可知reserve()不會對快取區進行重新分配和空間預留*/
cout << "a.size()=" << a.size() << endl << endl; /* a.size()=30,可知reserve()不會產生初始值*/
//⑤給 a 新增超出它預設capcity的元素:研究 capcity 的變化
for (i = 0; i < 25; i++)
{
a.push_back(i + 1);
}
cout << "a.capacity()=" << a.capacity() << endl; /* a.capacity()=75,可知當元素超出預設的capcity,就會觸發空間預留*/
cout << "a.size()=" << a.size() << endl << endl; /* a.size()=55*/
/*二維記憶體*/
cout << "e.capacity()=" << e.capacity() << endl; /* e.capacity()=0,列的capcity報錯 */
cout << "f.capacity()=" << f.capacity() << endl; /* f.capacity()=20 */
cout << "f[0].capacity()=" << f[0].capacity() << endl; /* f[0].capacity()=0,指定行則有列的capcity */
cout << "g.capacity()=" << g.capacity() << endl; /* g.capacity()=20 */
cout << "g[2].capacity()=" << g[2].capacity() << endl; /* g[2].capacity()=3,可知每行都被插入了相同的元素 */
/*經過一維vector的探究,選出一些適合二維的節省資源的定義方式!*/
//①對於可求得固定行、列資訊的二維vector,定義如下:
vector<vector<int>>h{ N };
for (i = 0; i < h.size(); i++)
{
h[i].reserve(T);
}
cout << "h.capacity()=" << h.capacity() << endl; /* h.capacity()=20 */
cout << "h[0].capacity()=" << h[0].capacity() << endl; /* h[0].capacity()=10 */
cout << endl;
//②對於可求得固定行資訊,但是列長未知的二維vector,定義如下:
vector<vector<int>>p{ N };
/*通過push_back壓入元素,會有預留空間*/
for (i = 0; i < p.size(); i++)
{
for (j = 0; j < i + 1; j++)
{
p[i].push_back(j + 1);
}
}
/*看看預留空間:發現確實有預留現象!*/
for (i = 0; i < p.size(); i++)
{
cout << "p[" << i << "].size()=" << p[i].size() << endl;
cout << "p[" << i << "].capacity()=" << p[i].capacity() << endl;
}
cout << endl;
/*有沒有辦法切去多餘的空間?——利用swap()方法去除vector多餘的容量*/
vector<vector<int>>(p).swap(p);
for (i = 0; i < p.size(); i++)
{
cout << "p[" << i << "].size()=" << p[i].size() << endl;
cout << "p[" << i << "].capacity()=" << p[i].capacity() << endl;
}
cout << endl;
//③對於無任何資訊的二維vector,直接定義空的就行。
vector<vector<int>>t;
/*注意:若一開始沒有定義行,那麼是無法通過reserve()固定行的;
只能通過resize()的方式。
*/
}
};
/*類Vector_release:探究如何徹底釋放在類中定義的vector的記憶體*/
class Vector_release
{
private:
int i, j;
public:
vector<vector<int>> a;
vector<vector<int>> b{ N };
vector<vector<int>> c;
vector<int>d{ 1,2,3 };
void define()
{
//①經過以上測試的探索,我們可以有新的定義方式:用 行、列resize()+swap()
a.resize(N);
for (i = 0; i < a.size(); i++)
{
a[i].resize(i + 1);
}
vector<vector<int>>(a).swap(a); /*切除多餘空間*/
//輸出
for (i = 0; i < a.size(); i++)
{
for (j = 0; j < a[i].size(); j++)
{
cout << a[i][j] << " ";
}
cout << endl;
}
cout << endl;
/*cout << "a.size()" << a.size() << endl;
for (i = 0; i < a.size(); i++)
{
cout << "a[" << i << "].capacity()=" << a[i].capacity() << endl;
}
cout << endl;*/
//②用 指定行、列reserve()+push_back()定義
for (i = 0; i < N; i++)
{
b[i].reserve(T);
}
for (i = 0; i < N; i++)
{
for (j = 0; j < T; j++)
{
b[i].push_back(1);
}
}
cout << "bbbbb" << endl;
for (i = 0; i < b.size(); i++)
{
for (j = 0; j < b[i].size(); j++)
{
cout << b[i][j] << " ";
}
cout << endl;
}
cout << endl;
//③copy一個
c = a;
}
void clean()
{
//b.clear(); /*報錯!:因為b在定義時,指定了行數,所以不能clear行*/
//for (i = 0; i < b.size(); i++)
//{
// b[i].clear();
//}/*可以清空列元素,但是列的capacity還在。但至少可以保證迴圈時的push_back()不會溢位記憶體*/
for (i = 0; i < b.size(); i++)
{
vector<int>().swap(b[i]);
}/*可以清空列元素以及列的capacity。*/
//c.clear();/*二維vector.clear(),可以徹底清除列的capacity,但是保留著行的capacity*/
vector<vector<int>>().swap(c);/*利用swap()徹底清除capacity*/
d.clear();/*一維vector.clear(),保留著列的capacity*/
}
};
int main()
{
Vector_define v1;
v1.test1();
Vector_memory v2;
v2.test2();
Vector_release v3;
for (int it = 0; it < 4; it++)
{
cout << "第" << it << "次:" << endl;
v3.define();
v3.clean();
for (int i = 0; i < v3.a.size(); i++)
{
cout << "a[i].capacity[" << i << "]=" << v3.a[i].capacity() << " ";
}
cout << endl << endl;
for (int i = 0; i < v3.b.size(); i++)
{
cout << "b[i].capacity[" << i << "]=" << v3.b[i].capacity() << " ";
}
cout << endl << endl;
cout << "c.capacity()=" << v3.c.capacity() << endl;
for (int i = 0; i < v3.c.size(); i++)
{
cout << "c[i].capacity[" << i << "]=" << v3.c[i].capacity() << " ";
}
cout << endl;
cout << "d.capacity()=" << v3.d.capacity() << endl;
}
return 0;
system("pause");
}
參考文章:
1.reserve和resize_lusic01的專欄-CSDN部落格
2.C++ vector中的resize,reserve,size和capacity函式講解
4.vector記憶體機制和效能分析_https://github.com/JelinYao-CSDN部落格_vector 記憶體
6.STL中vector的記憶體分配與正確釋放_System Architect-CSDN部落格_vector指標記憶體釋放
7.c++ vector 初始化_C++語言之vector記憶體分配技術