2017-07-17 139 views
0

在下面的代码中,如果某个数组元素的构造/销毁会抛出什么?new []/delete []并在C++中抛出构造函数/析构函数

X* x = new X[10]; // (1) 
delete[] x;  // (2) 

我知道内存泄漏被防止,但额外的:

  1. 广告(1)中,先前构造的元件破坏?如果是的话,如果析构函数在这种情况下抛出会发生什么?

  2. 广告(2),是否还未被破坏的元素被破坏?如果是的话,如果析构函数再次抛出会发生什么?

回答

5
  1. 是,如果x[5]构造抛出,然后x[0]..x[4]已经成功地构建了五个数组元素将被正确地破坏。

    • 破坏者不应该扔。如果一个析构函数确实抛出,那么在前一个(构造函数)异常仍在处理的时候会发生这种情况。由于不支持嵌套异常,因此立即调用std::terminate。这是为什么析构函数不应该抛出。
  2. 这里有两个相互排斥的选项:

    1. 如果达到标签(2),构造没有抛出。也就是说,如果成功创建了x,则所有十个元素都已成功构建。在这种情况下,是的,它们全部被删除。不,你的析构函数仍然不应该抛出。

    2. 如果构造函数抛出部分的方式,通过一步(1),则数组x根本不存在。该语言试图为你创建,失败,并抛出一个例外 - 所以你根本没有达到(2)

最关键的事情要明白的是,x要么存在 - 在一个健全的和可预见的状态 - 要么没有。

如果构造函数失败,语言不会给你一些不可用的半初始化的东西,因为无论如何你都无法做任何事情。 (你甚至不能安全地删除它,因为没有办法跟踪哪些元素被构建,哪些只是随机垃圾)。

这可能有助于将数组视为具有10个数据成员的对象。如果你正在构造这样一个类的实例,并且抛出了基类或成员构造函数之一,那么所有先前构造的基类和成员将以完全相同的方式销毁,并且对象永远不会存在。

2

我们可以用下面的代码进行测试:

#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对象来管理动态内存。不要试图推出自己的。在尝试调试时,它会像这样为您节省头痛。

相关问题