1. 程式人生 > >vector用erase刪除元素時,為什麼被刪元素的解構函式會被呼叫更多次?

vector用erase刪除元素時,為什麼被刪元素的解構函式會被呼叫更多次?

 

vector::erase的原型如下:

iterator erase(iterator position);

iterator erase(iterator first, iterator last);

對應的相關說明如下:

"

...

This effectively reduces the vector size by the number of elements removed, calling each element's destructor before.

...

"

上面的說明中,有下劃線的那句的含義是:

這實際上就是減少的容器的大小,減少的數目就是被刪除元素的數目,在刪除元素之前,將會呼叫被刪元素的解構函式

在該博文後續的一些程式碼說明中,當呼叫vector的erase函式時,發現vector中的元素物件的解構函式被呼叫了多次。按照通常的理解,解構函式被呼叫一次就可以銷燬物件,那為什麼vector容器在用erase刪除其元素物件時,其被刪除的元素物件的解構函式要被多次呼叫呢?

一、 到底發生了什麼事情?

如在上面提及的博文中一樣,假定定義了一個類如下:

#include <iostream>

#include <algorithm>

#include <string>

#include <vector>

using namespace std;

class Student

{

public:

         Student(const string name = "Andrew", const int age = 7) : name(name), age(age)

         {}

         ~Student()

         {

                   cout << name << "\tdeleted." << endl;

         }

         const string get_name() const

         {

                   return name;

         }

         const int get_age() const

         {

                   return age;

         }

private:

         string name;

         int age;

};

那麼當在程式中,類似下面的new表示式(new expression)和delete表示式(delete expression),即

Student *stu = new Student("Bob", 7);

delete stu;

背後到底發生了什麼事情?

在上面的new表示式,實際上做了3步工作:

第一步: 呼叫庫函式operator new,分配可以容納一個Student型別物件的原始的、無型別的記憶體;

第二步: 根據給定的實參,呼叫Student類的建構函式,以構造一個物件;

第三步: 返回在第二步中被構造物件的地址。

對應地,上面的delete表示式,也實際上做了2步工作:

第一步: 呼叫Student類的解構函式,以銷燬物件。這一步完成後,物件已經被銷燬,但該物件佔用的記憶體此時仍然沒有

返還給系統;

第二步: 呼叫庫函式operator delete,將已經被刪除物件所佔用的記憶體交回給系統。

需要特別注意的是,new表示式和庫函式operator new不是一回事情,事實上,new表示式總是會呼叫更底層的庫函式operator new。delete表示式和庫函式operator delete之間的關係也與此類似。

二、 allocator類

在一中說明了一些最基本的概念。在這裡,我們將簡要討論一下allocator類,因為在STL中,所有的容器關於記憶體的動態分配,都是通過allocator來完成的。

allocator是一個模板類,它將記憶體分配和物件構造,物件析構和記憶體回收分別分開執行。主要地,它包含了3個大的功能:

1.       針對特定型別,進行記憶體分配;

2.       物件構造;

3.       物件析構。

具體一點就是提供了下表所列的一些功能:

成員函式

描述

allocator<T> a;

定義了一個allocator物件a,a可以用來分配記憶體或者構造T型別的物件

a.allocate(n)

為n個T型別的物件,分配原始的、未構建的(unconstructed)記憶體。

注意:此處的a就是上面所定義的a。

a.deallocate(p, n)

將從p開始的n個T型別的物件所佔用的記憶體交回給系統。在呼叫deallocate之前,程式設計師必須先將有關的物件用destroy予以銷燬。

注意:p的型別是T*,其宣告如: T* p;

a.construct(p, t)

在p所指向的記憶體中,構造一個新的T型別物件。在這個過程中將呼叫T的拷貝建構函式,將T型別的物件t的一個拷貝作為無名的,臨時的T型別物件複製到p所指定的記憶體中。

注意:t是一個T型別的物件,其宣告如:T t;

a.destroy(p)

呼叫p所指向物件的解構函式,以銷燬該物件。注意,此函式呼叫後,物件已經被銷燬,但其所佔用的記憶體並未交還給系統

uninitialized_copy(b, e, b2)

將有迭代器b和e所規定範圍內的元素複製到由迭代器b2開始的原始的、未構建的記憶體中。本函式是目標記憶體中建構函式,而不是將元素賦值到目標記憶體。因為,將一個物件賦值到一個未構建的記憶體中這種行為時未定義的。

uninitialized_fill(b, e, t)

用t的拷貝初始化由迭代器b和e指定範圍的物件。該範圍是原始的、未構建的記憶體,其中的物件構造是通過其拷貝建構函式來完成的。

uninitialized_fill_n(b, e, t, n)

最多用t的拷貝初始化由迭代器b和e指定範圍的n個物件。b和e之間的範圍至少是n。該範圍是原始的、未構建的記憶體,其中的物件構造是通過其拷貝建構函式來完成的。

由上表可見,拷貝建構函式在allocator中的使用非常頻繁。

三、 定位new表示式(placement new expression)

定位new表示式的寫法:

new (place_address) type;

new (place_address) type(initializer_list);

下面是一個例子:

int main(void)

{

         allocator<Student> alloc;

         Student *stu = alloc.allocate(3);

         Student stu01;

         Student stu02("Bob", 7);

         new (stu) Student(stu01);                             // 定位new表示式:使用預設拷貝建構函式

         new (stu + 1) Student(stu02);                      // 定位new表示式:使用預設拷貝建構函式

         new (stu + 2) Student("Chris", 7);               // 定位new表示式:使用普通建構函式

         cout << stu->get_name() << endl;

         cout << (stu + 1)->get_name() << endl;

         cout << (stu + 2)->get_name() << endl;

         alloc.destroy(stu + 2);

         // 絕對不能將上面的語句寫成: delete (stu + 2);

         // 因為,用allocator分配記憶體的物件,必須由對應的destroy來銷燬

         return 0;

}

上面程式的輸出結果:

Andrew

Bob

Chris

Chris         deleted.

Bob           deleted.

Andrew     deleted.

可見物件的構建和析構的順序剛好是反過來的。

前面我們曾經提到construct只能用拷貝建構函式構建物件,從上面程式碼中,我們可以看到定位new表示式,不僅可以是用拷貝建構函式來構造物件,也可以使用普通的建構函式,這表明:定位new表示式要比construct更加靈活。而且,有些時候,比如當拷貝建構函式是私有的時候,就只能使用定位new表示式了。

從效率的角度來看,根據Stanley B. Lippman的觀點,定位new表示式和construct沒有太大的差別。

還有一個必須注意到的現象: 使用普通建構函式構造的物件,在其作用域結束時,並不會自動析構,而必須由destroy來完成這項工作。其他通過拷貝建構函式構造的物件,在其作用域結束時,均會被自動析構。

如果使用construct函式,下面是一個例子(可將其拷貝到上面的main函式中):

         Student stu04("Dudley", 8);

         alloc.construct(stu + 3, stu04);

         cout << (stu + 3)->get_name() << endl;

由於construct只能使用拷貝建構函式,因此,stu04在其作用域結束時,也會被自動析構,程式執行結果也應證了這一點。

結論:

如果一個物件是通過拷貝建構函式構造的物件,那麼在其作用域結束時,該物件會自動析構;而通過非拷貝建構函式構建的物件,則不會自動析構,必須通過destroy函式對其析構。

四、 vector的模擬實現

為了說明本文開始處提出的問題,我們自然需要對vector的實現機制有所瞭解。而要了解vector的實現機制,就必須瞭解allocator及其相關的內容,正如前面所提及的,在STL中,所有的容器都使用allocator來對記憶體進行管理,這就 是為什麼我講了一、二和三的原因。

在這部分內容中,我們將模擬實現一個vector。在後續的內容中,我們將使用這個vector的模擬實現,看看會不會出現本文開始處所提出的現象。

好了,既然我們的準備工作已經完成,那麼現在開始模擬實現一個vector,不妨將其命名為Vector。

下圖就是Vector記憶體分配策略。其中,elements指向陣列中的第一個元素,first_free指向最後有效元素後面緊接著的位置,end指向陣列末尾後面緊接著的位置。

根據這樣的假定,很容易可以推知:

Vector中包含元素的數目 = first_free – elements,Vector的容量 = end – elements,Vector剩餘可用空間 = end – first_free。

下面是Vector的實現程式碼:

注意:為方便起見,以下各個類以及測試程式碼,都處於同一個cpp檔案中,該cpp檔案包含以下標頭檔案:

#include <iostream>

#include <string>

#include <memory>

#include <cstddef>

#include <stdexcept>

using namespace std;

// vector的模擬實現 – Vector

template<typename T>

class Vector

{

private:

         T *elements;              // 指向第一個元素的指標

         T *first_free;               // 指向最後有效元素後面緊接著位置的指標

         T *end;                        // 指向陣列末尾後面緊接著位置的指標

private:     

         // 用於獲取為構造記憶體(unconstructed memory)的物件。在此,它必須是static的,因為:

         // 建立物件之前,必須要為其提供記憶體。如果非static,那麼alloc物件必須是在Vector

         // 物件建立之後才可以使用,而alloc的初衷卻是為即將要建立的物件分配記憶體。因此非

         // static是斷然不行的。

         // static成員是類級別的,而非類之物件級別的。也就是說,static成員早於物件存在,

         // 因此,下面的alloc可以為即將要建立的物件分配記憶體。

         static std::allocator<T> alloc;

         // 當元素數量超過容量時,該函式用來分配更多的記憶體,並複製已有元素到新空間。

         void reallocate();

public:

         Vector() : elements(0), first_free(0), end(0)        // 全部初始化為空指標

         {}

         void push_back(const T&);                          // 增加一個元素

         void reserve(const size_t);                           // 保留記憶體大小

         void resize(const size_t);                              // 調整Vector大小

         T& operator[](const size_t);                          // 下標操作符

         size_t size();                                                    // 獲取Vector中元素的個數

         size_t capacity();                                             // 獲取Vector的容量

         T& erase(const size_t);                                 // 刪除指定元素

};

// 初始化靜態變數。注意,即使是私有成員,靜態變數也可以用如下方式初始化

template<typename T>

allocator<T> Vector<T>::alloc;

template<typename T>

void Vector<T>::reallocate()

{

         // 計算現有元素數量

         ptrdiff_t size = first_free - elements;

         // 分配現有元素大小兩倍的空間

         ptrdiff_t new_capacity = 2 * max(size, 1);  //(size == 0) ? 2 : 2 * size;

         T *new_elements = alloc.allocate(new_capacity);

         // 在新空間中構造現有元素的副本

         uninitialized_copy(elements, first_free, new_elements);

         // 逆序銷燬原有元素

         for(T *p = first_free; p != elements; )

         {

                   alloc.destroy(--p);

         }

         // 釋放原有元素所佔記憶體

         if(elements)

         {

                   alloc.deallocate(elements, end - elements);

         }

         // 更新個重要的資料成員

         elements = new_elements;

         first_free = elements + size;

         end = elements + new_capacity;

}

template<typename T>

void Vector<T>::push_back(const T &t)

{

         if(first_free == end)                      // 如果沒有剩餘的空間

         {

                   reallocate();                         // 分配更多空間,並複製已有元素

         }

         alloc.construct(first_free, t);       // 將t複製到first_free指定的位置

         first_free++;                                  // 將first_free加

}

template<typename T>

void Vector<T>::reserve(const size_t n)

{

         // 計算當前Vector的大小

         size_t size = first_free - elements;

         // 如果新分配空間小於當前Vector的大小

         if(n < size)

         {

                   throw custom_exception("所保留的空間不應少於容器中原有元素的個數");

         }

         // 分配可以儲存n個T型別元素的空間

         T *newelements = alloc.allocate(n);

         // 在新分配的空間中,構造現有元素的副本

         uninitialized_copy(elements, first_free, newelements);

         // 逆序銷燬原有元素,但此時並未將原有元素佔用的空間交還給系統

         for(T *p = first_free; p != elements;)

         {

                   alloc.destroy(--p);

         }

         // 釋放原有元素所佔用的記憶體

         if(elements)

         {

                   alloc.deallocate(elements, end - elements);

         }

         // 更新個重要的資料成員

         elements = newelements;

         first_free = elements + size;

         end = first_free + n;

}

template<typename T>

void Vector<T>::resize(const size_t n)                          // 調整Vector大小

{

         // 計算當前Vector大小以及容量

         size_t size = first_free - elements;

         size_t capacity = end - elements;

         if(n > capacity)  // 如果新空間的大小大於原來的容量

         {

                   reallocate();

                   T temp;

                   uninitialized_fill(elements + size, elements + n, temp);

                   end = elements + n;

         }

         else if(n > size)          // 如果新空間的大小大於原來Vector的大小

         {

                   uninitialized_fill(elements + size, elements + n, temp);

         }

         else // 如果新空間的大小小於或等於原來Vector的大小

         {

                   // 逆序銷燬多餘元素

                   for(T *p = first_free; p != elements + n;)

                   {

                            alloc.destroy(--p);

                   }

         }

         // 更新相關資料成員

         // elements沒有改變,無需更新

         first_free = elements + n;

         // end在上面n > capacity時,已經被更改

}

template<typename T>

T& Vector<T>::operator[](const size_t index)               // 下標操作符

{

         size_t size = first_free - elements;

         // 如果接受的引數不在有效的範圍內,則丟擲異常

         if(index < 0 || index > size)

         {

                   throw custom_exception("給定的索引引數錯誤");

         }

         return elements[index];

}

template<typename T>

size_t Vector<T>::size()                                                   // 獲取Vector中元素的個數

{

         size_t temp = first_free - elements;

         return temp;

}

template<typename T>

size_t Vector<T>::capacity()                                            // 獲取Vector的容量

{

         size_t temp = end - elements;

         return temp;

}

在Vector中用到的自定義異常類以及Student類分別定義如下:

// 自定義異常類,從std::runtime_error繼承而來

// 注意,可以在此基礎上,增加更復雜的內容。本例為了方便,使用了最簡單的形式。

class custom_exception : public runtime_error

{

public:

         // 定義一個explicit的建構函式,並將引數傳遞給基類

         explicit custom_exception(const string& s) : runtime_error(s)

         {

         }

         // An empty specification list says that the function does not throw any exception

         // 解構函式不丟擲任何異常

         virtual ~custom_exception() throw()

         {

         }

};

// 這個類將作為Vector中元素的型別

class Student

{

public:

         Student(const string name = "Andrew", const int age = 7) : name(name), age(age)

         {}

         ~Student()

         {

                   cout << name << "\tdeleted." << endl;

         }

         const string get_name() const

         {

                   return name;

         }

         const int get_age() const

         {

                   return age;

         }

private:

         string name;

         int age;

};

下面是測試程式碼:

// 測試程式碼

int main(void)

{

         Vector<Student> svec;

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         //svec.reserve(32);                                                            (1)

         //構造9個Student物件

         Student stu01;

         Student stu02("Bob", 6);

         Student stu03("Chris", 5);

         Student stu04("Dudley", 8);

         Student stu05("Ely", 7);

         Student stu06("Fiona", 3);

         Student stu07("Greg", 2);

         Student stu08("Howard", 9);

         Student stu09("Iris", 6);

         // 向svec增加一個元素。以下對應各句,與此相同。

         svec.push_back(stu01);

         // 輸出svec的大小和容量。以下對應各句,與此相同。

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu02);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu03);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu04);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu05);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu06);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu07);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu08);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         svec.push_back(stu09);

         cout << "size:" << svec.size() << "\t\tcapacity:" << svec.capacity() << endl;

         return 0;

}

上面程式輸出的結果為:

size:0              capacity:0

size:1              capacity:2

size:2              capacity:2

Bob           deleted.

Andrew     deleted.

size:3              capacity:4

size:4              capacity:4

Dudleydeleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size:5              capacity:8

size:6              capacity:8

size:7              capacity:8

size:8              capacity:8

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudleydeleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

size:9              capacity:16

Iris             deleted.

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudleydeleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

從上面程式碼中我們看到,解構函式在同一個物件上被呼叫了很多次,原因其實很簡單:

當元素超過Vector的容量後,Vector會自動增加容量(為原來大小的2倍),然後Vector會呼叫元素的拷貝建構函式(本例中為預設的拷貝建構函式,因為Student類中指標成員變數,所以無需自定義一個過載拷貝建構函式),將已有的元素複製到新分配的記憶體,然後呼叫destroy銷燬原來的元素物件,destroy會顯式呼叫物件的解構函式。在本例中,Vector容量改變了4次,每次容量的改變都會通過destroy顯式呼叫元素物件的解構函式以銷燬物件,這就是為什麼解構函式被多次呼叫的真正原因。

在上面的測試程式碼中,如果我們把(1)的註釋去掉,即一開始就為Vector物件svec分配可以儲存32個Student物件的空間,那麼執行結果將是:

size:0              capacity:0

size:1              capacity:32

size:2              capacity:32

size:3              capacity:32

size:4              capacity:32

size:5              capacity:32

size:6              capacity:32

size:7              capacity:32

size:8              capacity:32

size:9              capacity:32

Iris             deleted.

Howard    deleted.

Greg                   deleted.

Fiona        deleted.

Ely             deleted.

Dudleydeleted.

Chris         deleted.

Bob           deleted.

Andrew     deleted.

在Vector中,並沒有實現erase,這是因為erase要用到Iterator,限於篇幅就不在此列出相關程式碼了,因為其原理和上面的程式碼是一樣的,而上面的程式碼已經足以解釋博文http://patmusing.blog.163.com/blog/static/13583496020101831514657/ 中提到的關於解構函式被多次呼叫的問題。

最後再次強調一下:顯式呼叫解構函式,可以銷燬物件,但不會釋放物件所佔的記憶體。