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/ 中提到的關於解構函式被多次呼叫的問題。
最後再次強調一下:顯式呼叫解構函式,可以銷燬物件,但不會釋放物件所佔的記憶體。