STL容器內存淺析(2)-減少vector內存佔用

版權聲明:原創文章,歡迎轉載,但請註明出處,謝謝。
https://blog.csdn.net/qiuguolu1108/article/details/107146466


vector之所以會發生大量的拷貝,是因爲其內存分配策略造成的。每當vector的內存不夠用時,vector都會重新申請兩倍的空間,並將之前的元素搬移到新空間。這纔是發生拷貝的根源,既然這樣,我們能不能預先給vector申請一定的空間,避免因空間不夠而發生元素搬移。

1、使用reverse()函數預申請空間

reverse()函數可以給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;

在這裏插入圖片描述
使用reverse()給va預先分配了1024個空間,所以再往va推入1024個元素的時候,並沒有發生多餘的拷貝構造。
在這裏插入圖片描述
通過reverse()給vector預分配空間,確實可以減少元素的拷貝構造,但我們在使用vector時,有時很難確定容器元素的個數。

在使用reverse()時需要自己去平衡,如果reverse()過大,會造成空間的浪費,如果過小還是會發生拷貝構造。

現在又帶來一個問題,如何將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);

/*刪除0~29元素*/
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()函數用於交換兩個vector

swap()函數可以用於交換兩個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;

在這裏插入圖片描述
從示例中可以看出,vector的data()發生了交換,但vi0和vi1所在的地址並沒有發生變化。swap()函數還交換了其他內部成員數據,但我們弄清了我們關心的一點,swap並沒有發生空間的大量拷貝,交換的僅僅是兩個空間地址。
在這裏插入圖片描述
補充一點:swap()函數,不僅僅交換兩個容器的內容,同時它們的迭代器、指針和引用也被交換。在swap發生後,原先指向容器中元素的迭代器、指針和引用依然有效,並指向同樣的元素----但是,這些元素已經在另一個容器中了。

5、vector的拷貝構造

使用一個vector去構造器另外一個vector,在構造新的vector時,僅會根據vector實際元素個數去構造新的vector。

vector<int> vi0;
vi0.reserve(100);

//插入5個元素
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;

在這裏插入圖片描述
雖然vi0的內存空間可以存放100個int,但實際有效元素只有5個int。通過vi0拷貝構造vi1的時候,並不會像vi0那樣佔用100個int空間,而是根據實際元素的個數申請空間,並不會有多餘的空間。

6、使用swap技巧移除多餘的容量

鋪墊了這麼多,回到我們的主題上,如何減少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);
    }//tmp在此處就會調用vector的析構器,將其自己銷燬。
    
    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 清空vi

clear()僅會清空容器中的元素,並不能真正的釋放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;

在這裏插入圖片描述
和上面類似,但新元素的生成,調用了拷貝構造器。但這裏多發生了一次拷貝構造,根據調用情況,猜測resize()先把元素拷貝到其內部,所有新元素的拷貝都是基於resize()內部的副本。

9.3 resize()參數大於capacity()

重新申請空間,將原數據拷貝過來,在新的位置調用默認構造器生成新的元素。

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源碼剖析》 : https://book.douban.com/subject/1110934/

​ 《Effective STL》:https://book.douban.com/subject/24534868/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章