(給CPP開發者加星標,提升C/C++技能)
https://blog.csdn.net/qiuguolu1108/article/details/107146466【導讀】:接上一篇《C++ vector內存分配策略淺析》,其中定義了用於測試的類A,本文中也有用到,大家可以點擊連結跳轉至上一篇,了解下類A的定義。本片文章從使用角度講解如何減少vector的內存佔用,內容較多,建議先碼後看。
以下是正文
vector之所以會發生大量的拷貝,是因為其內存分配策略造成的。每當vector的內存不夠用時,vector都會重新申請兩倍的空間,並將之前的元素搬移到新空間。這才是發生拷貝的根源,既然這樣,我們能不能預先給vector申請一定的空間,避免因空間不夠而發生元素搬移。
1、使用reserve()函數預申請空間reserve()函數可以給vector預先分配指定大小的空間。
vector<A> va;
va.reserve(1024);
cout<<"va size = "<<va.size()<<endl;cout<<"va capacity = "<<va.capacity()<<endl<<endl;
A a;
for(int i=0;i<1024;i++){ va.push_back(a);}
A::dis_copy_construct_count();cout<<endl;
cout<<"va size = "<<va.size()<<endl;cout<<"va capacity = "<<va.capacity()<<endl;使用reserve()給va預先分配了1024個空間,所以再往va推入1024個元素的時候,並沒有發生多餘的拷貝構造。
通過reserve()給vector預分配空間,確實可以減少元素的拷貝構造,但我們在使用vector時,有時很難確定容器元素的個數。在使用reserve()時需要自己去平衡,如果reserve()過大,會造成空間的浪費,如果過小還是會發生拷貝構造。
現在又帶來一個問題,如何將vector過多的沒有存放元素的空間還給系統。
2、erase()函數和clear()會減少vector的內存佔用嗎?上篇文章中,我們說過vector佔用空間的大小,可以通過capacity()函數來查看。
通過一個示例,來驗證一下erase()、clear()會減少vector內存佔用嗎?
vector<int> vi;
for(int i=0;i<65;i++){ vi.push_back(i);}
cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
auto itr = find(vi.begin(),vi.end(),30);
vi.erase(vi.begin(),itr);cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
vi.clear();cout<<"vi size = "<<vi.size()<<endl;cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
通過測試我們發現,erase()和clear()只能將vector空間的元素給析構掉,並不能減少vector內存的佔用。
這是侯捷老師《STL源碼剖析》對vector的erase()、clear()函數實現介紹。這也進一步證實了erase()、clear()並不能釋放vector的空間。
3、data()函數可以返回vector元素存放的位置
#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi;
for(int i=0;i<10;i++) { vi.push_back(i); }
int * p = vi.data();
for(int i=0;i<10;i++) { cout<<*p++<<endl; }
return 0;}
data()這個函數不用多說了,通過示例就可以看出這個函數好強大,直接殺入了vector的老巢。
4、swap()函數用於交換兩個vectorswap()函數可以用於交換兩個vector,但是交換了vector的哪些東西?
vector<int> vi0;for(int i=0;i<5;i++){ vi0.push_back(i);}cout<<"&vi0 = "<<&vi0<<endl;cout<<"vi0.data() = "<<vi0.data()<<endl<<endl;
vector<int> vi1;for(int i=0;i<5;i++){ vi1.push_back(i*100);}cout<<"&vi1 = "<<&vi1<<endl;cout<<"vi1.data() = "<<vi1.data()<<endl;
cout<<endl<<"====================="<<endl<<endl;
vi0.swap(vi1);
cout<<"&vi0 = "<<&vi0<<endl;cout<<"vi0.data() = "<<vi0.data()<<endl<<endl;
cout<<"&vi1 = "<<&vi1<<endl;cout<<"vi1.data() = "<<vi1.data()<<endl;5、vector的拷貝構造
從示例中可以看出,vector的data()發生了交換,但vi0和vi1所在的地址並沒有發生變化。swap()函數還交換了其他內部成員數據,但我們弄清了我們關心的一點,swap並沒有發生空間的大量拷貝,交換的僅僅是兩個空間地址。
補充一點:swap()函數,不僅僅交換兩個容器的內容,同時它們的迭代器、指針和引用也被交換。在swap發生後,原先指向容器中元素的迭代器、指針和引用依然有效,並指向同樣的元素----但是,這些元素已經在另一個容器中了。使用一個vector去構造器另外一個vector,在構造新的vector時,僅會根據vector實際元素個數去構造新的vector。
vector<int> vi0;vi0.reserve(100);
for(int i=0;i<5;i++){ vi0.push_back(i);}
cout<<"vi0 size = "<<vi0.size()<<endl;cout<<"vi0 capacity = "<<vi0.capacity()<<endl<<endl;
vector<int> vi1(vi0);cout<<"vi1 size = "<<vi1.size()<<endl;cout<<"vi1 capacity = "<<vi1.capacity()<<endl;6、使用swap技巧移除多餘的容量
雖然vi0的內存空間可以存放100個int,但實際有效元素只有5個int。通過vi0拷貝構造vi1的時候,並不會像vi0那樣佔用100個int空間,而是根據實際元素的個數申請空間,並不會有多餘的空間。鋪墊了這麼多,回到我們的主題上,如何減少vector的容量?
vector的構造器在構建新的容器時,會自動的去掉多餘的空間,我們可以利用這個特性,結合swap函數去掉vector中多餘的容量。
6.1 方法一:通過定義一個新的vector#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
vector<int> tmp(vi); cout<<"tmp size = "<<tmp.size()<<endl; cout<<"tmp capacity = "<<tmp.capacity()<<endl<<endl;
cout<<"=================="<<endl<<endl;
tmp.swap(vi);
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
cout<<"tmp size = "<<tmp.size()<<endl; cout<<"tmp capacity = "<<tmp.capacity()<<endl<<endl;
return 0;}
vi有多餘的容量,通過vi構造一個新的容器tmp,tmp沒有多餘的容量,通過swap函數將tmp和vi交換,則tmp變成了有多餘容量的容器。tmp是一個局部變量,在離開其作用域時,會調用vector的析構器,將其自己釋放。#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); }
{ vector<int> tmp(vi); tmp.swap(vi); } return 0;}這樣可以更加快速的消除多餘容量。之前示例,tmp需要在main函數的最後才被銷毀,此處的示例,tmp在swap之後就立即被銷毀。
6.2 方法二:使用臨時變量上面的方法確實可以消除vector多餘的容量,但不夠優雅,略顯囉嗦。使用臨時變量可以更加簡潔。
#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
{ vector<int>(vi).swap(vi); }
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl;
return 0;}
沒有定義多餘的變量,一行代碼搞定,真的很優雅了。簡單的說一下:vector<int>(vi)定義一個臨時對象,這個臨時對象通過拷貝構造器構造。臨時對象調用swap成員函數將其自己和vi交換。臨時對象在離開其作用域時被銷毀。
6.3 清空viclear()僅會清空容器中的元素,並不能真正的釋放vector佔用的內存。使用swap()可以釋放vector內存。
#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100); for(int i=0;i<5;i++) { vi.push_back(i); } cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
{ vector<int>().swap(vi); }
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl;
return 0;}
vector<int>()會產生一個臨時對象,這個對象沒有名字,其size和capacity皆為零。
6.4 總結:去除vi多餘的容量:vector<int>(vi).swap(vi)
將vi的空間清空:vector<int>().swap(vi)
7、使用C++11中shrink_to_fit()函數去除多餘容量看看官方介紹
#include <iostream>#include <vector>
int main(){ std::vector<int> v; std::cout << "Default-constructed capacity is " << v.capacity() << '\n'; v.resize(100); std::cout << "Capacity of a 100-element vector is " << v.capacity() << '\n'; v.clear(); std::cout << "Capacity after clear() is " << v.capacity() << '\n'; v.shrink_to_fit(); std::cout << "Capacity after shrink_to_fit() is " << v.capacity() << '\n';}
官方給的示例,很容易理解。#include <iostream>#include <vector>
using namespace std;
int main(){ vector<int> vi; vi.reserve(100);
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
for(int i=0;i<10;i++) { vi.push_back(i); }
cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
vi.shrink_to_fit(); cout<<"vi size = "<<vi.size()<<endl; cout<<"vi capacity = "<<vi.capacity()<<endl<<endl;
return 0;}vi的容量是100,向其推入10個元素,則有90個多餘的空間,調用shrink_to_fit()後,其容量變為10,釋放了多餘的空間。
8、減少vector容量,必要的拷貝依然存在。不管是swap()函數還是shrink_to_fit()函數,在去除vector多餘空間的時候,還是會發生必要的元素拷貝。
8.1 swap方法vector<A> va;va.reserve(10);
A a;
for(int i=0;i<3;i++){ va.push_back(a);}
cout<<endl<<"======================"<<endl<<endl;
vector<A>(va).swap(va);
cout<<endl<<"======================"<<endl<<endl;
這裡的拷貝構造主要是在構造臨時對象產生的。
8.2 shrink_to_fit方法
vector<A> va;va.reserve(10);
A a;
for(int i=0;i<3;i++){ va.push_back(a);}
cout<<endl<<"======================"<<endl<<endl;
va.shrink_to_fit();
cout<<endl<<"======================"<<endl<<endl;結果都是一樣的,發生了拷貝構造。
9、resize()函數順便說一下resize()函數,這個函數使用也有一定的迷惑性。現在通過幾個例子說明一下其背後都做了哪些事情。
現在vector有兩個大小,一個是size(),vector實際元素的個數;另一個是capacity(),vector的容量。
size是小於等於capacity的。
9.1 resize()參數小於size()將多餘的元素析構掉
#include <iostream>#include <vector>
using namespace std;
class A{public: A(int data = 100) :data_(data) { cout<<"constructor : "<<this<<endl; }
A(const A& a) { data_ = a.data_; cout<<this<<" : copy constructor form : "<<&a<<endl; }
void display() { cout<<data_<<" "; }
~A() { cout<<"deconstructor : "<<this<<endl; }
private: int data_;};
int main(){ vector<A> va; va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl; va.push_back(a); va.push_back(b); va.push_back(c); va.push_back(d);
cout<<endl<<"========================"<<endl; for(auto & i : va) { i.display(); } cout<<endl;
va.resize(2);
for(auto & i : va) { i.display(); } cout<<endl;
return 0;}
9.2 resize()參數大於size(),小於capacity()。9.2.1 情況一:使用默認構造構造新元素vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
va.resize(6);
for(auto & i : va){ i.display();}cout<<endl;
需要添加兩個新的元素,沒有指定添加的元素,則調用類A的默認構造生成新元素。A(int data = 100) :data_(data){ cout<<"constructor : "<<this<<endl;}這是類A的默認構造器,使用這個構造器生成的對象,其默認值為100。
9.2.2 情況二:使用指定的元素構造新元素vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
A aa(99);
va.resize(6,aa);
for(auto & i : va){ i.display();}cout<<endl;9.3 resize()參數大於capacity()
和上面類似,但新元素的生成,調用了拷貝構造器。但這裡多發生了一次拷貝構造,根據調用情況,猜測resize()先把元素拷貝到其內部,所有新元素的拷貝都是基於resize()內部的副本。重新申請空間,將原數據拷貝過來,在新的位置調用默認構造器生成新的元素。
vector<A> va;va.reserve(8);
A a(0),b(1),c(2),d(3);
cout<<endl<<"========================"<<endl;va.push_back(a);va.push_back(b);va.push_back(c);va.push_back(d);
cout<<endl<<"========================"<<endl;for(auto & i : va){ i.display();}cout<<endl;
va.resize(10);
for(auto & i : va){ i.display();}cout<<endl;
cout<<va.capacity()<<endl;
10、總結本文介紹了如何去除vector多餘的容量。shrink_to_fit()是C++11提供的新方法,在C++98中可以使用swap()函數實現去除多餘的容量。
參考:
《STL源碼剖析》
《Effective STL》
- EOF -
關於如何減少vector的內存佔用,歡迎在評論中和我探討。覺得文章不錯,請點讚和在看支持我繼續分享好文。謝謝!
關注『CPP開發者』
看精選C++技術文章 . 加C++開發者專屬圈子
↓↓↓
點讚和在看就是最大的支持❤️