在下面的代碼中,如果某個數組元素的構造/銷燬會拋出什麼?new []/delete []並在C++中拋出構造函數/析構函數
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道內存泄漏被防止,但額外的:
廣告(1)中,先前構造的元件破壞?如果是的話,如果析構函數在這種情況下拋出會發生什麼?
廣告(2),是否還未被破壞的元素被破壞?如果是的話,如果析構函數再次拋出會發生什麼?
在下面的代碼中,如果某個數組元素的構造/銷燬會拋出什麼?new []/delete []並在C++中拋出構造函數/析構函數
X* x = new X[10]; // (1)
delete[] x; // (2)
我知道內存泄漏被防止,但額外的:
廣告(1)中,先前構造的元件破壞?如果是的話,如果析構函數在這種情況下拋出會發生什麼?
廣告(2),是否還未被破壞的元素被破壞?如果是的話,如果析構函數再次拋出會發生什麼?
是,如果x[5]
構造拋出,然後x[0]..x[4]
已經成功地構建了五個數組元素將被正確地破壞。
std::terminate
。這是爲什麼析構函數不應該拋出。這裏有兩個相互排斥的選項:
如果達到標籤(2)
,構造沒有拋出。也就是說,如果成功創建了x
,則所有十個元素都已成功構建。在這種情況下,是的,它們全部被刪除。不,你的析構函數仍然不應該拋出。
如果構造函數拋出部分的方式,通過一步(1)
,則數組x
根本不存在。該語言試圖爲你創建,失敗,並拋出一個例外 - 所以你根本沒有達到(2)
。
最關鍵的事情要明白的是,x
要麼存在 - 在一個健全的和可預見的狀態 - 要麼沒有。
如果構造函數失敗,語言不會給你一些不可用的半初始化的東西,因爲無論如何你都無法做任何事情。 (你甚至不能安全地刪除它,因爲沒有辦法跟蹤哪些元素被構建,哪些只是隨機垃圾)。
這可能有助於將數組視爲具有10個數據成員的對象。如果你正在構造這樣一個類的實例,並且拋出了基類或成員構造函數之一,那麼所有先前構造的基類和成員將以完全相同的方式銷燬,並且對象永遠不會存在。
我們可以用下面的代碼進行測試:
#include <iostream>
//`Basic` was borrowed from some general-purpose code I use for testing various issues
//relating to object construction/assignment
struct Basic {
Basic() {
std::cout << "Default-Constructor" << std::endl;
static int val = 0;
if(val++ == 5) throw std::runtime_error("Oops!");
}
Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};
int main() {
Basic * ptrs = new Basic[10];
delete[] ptrs;
return 0;
}
此代碼會產生以下輸出崩潰之前:
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
[std::runtime_error thrown and uncaught here]
注意,在任何時候被稱爲析構函數。這不一定非常重要,因爲未捕獲的異常無論如何都會導致程序崩潰。但是,如果我們捕獲錯誤,我們看到一些令人欣慰:
int main() {
try {
Basic * ptrs = new Basic[10];
delete[] ptrs;
} catch (std::runtime_error const& e) {std::cerr << e.what() << std::endl;}
return 0;
}
輸出更改爲:
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Default-Constructor
Destructor
Destructor
Destructor
Destructor
Destructor
Oops!
所以析構函數都會被自動調用的完全構造的對象,即使沒有明確的delete[]
電話,因爲new[]
調用有處理機制來處理這個問題。
但是你不必擔心第六個對象:在我們的例子中,因爲Basic
沒有做任何資源管理(如果一個精心設計的程序不會有Basic
做資源管理,如果它的構造函數可以像這樣拋出),我們不必擔心。但是,我們可能會擔心,如果我們的代碼看起來像這個:
#include <iostream>
struct Basic {
Basic() { std::cout << "Default-Constructor" << std::endl; }
Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};
class Wrapper {
Basic * ptr;
public:
Wrapper() : ptr(new Basic) {
std::cout << "WRDefault-Constructor" << std::endl;
static int val = 0;
if(val++ == 5) throw std::runtime_error("Oops!");
}
Wrapper(Wrapper const&) = delete; //Disabling Copy/Move for simplicity
~Wrapper() noexcept { delete ptr; std::cout << "WRDestructor" << std::endl; }
};
int main() {
try {
Wrapper * ptrs = new Wrapper[10];
delete[] ptrs;
} catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
return 0;
}
在這裏,我們得到如下的輸出:
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Oops!
Wrapper
對象的大塊不會泄漏內存,但是第六Wrapper
對象會因爲未正確清理而泄漏Basic
對象!
幸運的是,通常任何的資源管理方案的情況下,所有這些問題消失,如果你使用智能指針:
#include <iostream>
#include<memory>
struct Basic {
Basic() { std::cout << "Default-Constructor" << std::endl; }
Basic(Basic const&) { std::cout << "Copy-Constructor" << std::endl; }
Basic(Basic &&) { std::cout << "Move-Constructor" << std::endl; }
Basic & operator=(Basic const&) { std::cout << "Copy-Assignment" << std::endl; return *this; }
Basic & operator=(Basic &&) { std::cout << "Move-Assignment" << std::endl; return *this; }
~Basic() noexcept { std::cout << "Destructor" << std::endl; }
};
class Wrapper {
std::unique_ptr<Basic> ptr;
public:
Wrapper() : ptr(new Basic) {
std::cout << "WRDefault-Constructor" << std::endl;
static int val = 0;
if(val++ == 5) throw std::runtime_error("Oops!");
}
//Wrapper(Wrapper const&) = delete; //Copy disabled by default, move enabled by default
~Wrapper() noexcept { std::cout << "WRDestructor" << std::endl; }
};
int main() {
try {
std::unique_ptr<Wrapper[]> ptrs{new Wrapper[10]}; //Or std::make_unique
} catch (std::runtime_error const& e) {std::cout << e.what() << std::endl;}
return 0;
}
和輸出:
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Default-Constructor
WRDefault-Constructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
WRDestructor
Destructor
Oops!
請注意,撥打Destructor
的電話數量現在與撥打Default-Constructor
的電話數量相匹配,這表明Basic
對象現在正在正確獲取清理乾淨。並且因爲Wrapper
正在執行的資源管理已被委託給unique_ptr
對象,所以第六個對象沒有其調用的刪除器的事實不再是問題。
現在,很多都涉及到草擬代碼:即使通過使用智能指針使其變得「安全」,沒有合理的程序員也不會有沒有適當處理代碼的資源管理器throw
。但是有些程序員只是不合理,即使他們是,也可能會遇到一個奇怪的,異乎尋常的場景,你必須編寫這樣的代碼。就我而言,教訓是始終使用智能指針和其他STL對象來管理動態內存。不要試圖推出自己的。在嘗試調試時,它會像這樣爲您節省頭痛。