Skip to content

Files

Latest commit

c70125a · Jan 8, 2022

History

History
473 lines (353 loc) · 27.7 KB

06.md

File metadata and controls

473 lines (353 loc) · 27.7 KB

六、资源获取即初始化

什么是 RAII?

RAII 代表“资源获取就是初始化。”RAII 是一种使用 C++ 代码来消除资源泄漏的设计模式。当您的程序获得的资源随后没有被释放时,就会发生资源泄漏。最常见的例子是内存泄漏。由于 C++ 不像 C# 那样有 GC,所以您需要小心确保动态分配的内存被释放。否则,你会泄露记忆。资源泄漏还会导致无法打开文件,因为文件系统认为它已经打开,无法在多线程程序中获得锁,或者无法释放 COM 对象。

【RAII 是如何工作的?

RAII 工作是因为三个基本事实。

  1. 当自动存储持续时间对象超出范围时,其析构器运行。
  2. 当异常发生时,自上次尝试块开始以来已经完全构造的所有自动持续时间对象都将按照与调用任何 catch 处理程序之前创建的顺序相反的顺序被销毁。
  3. 如果嵌套了 try-block,而内部 try-block 的 catch 处理程序都不处理该类型的异常,则该异常会传播到外部 try-block。然后,在调用任何 catch 处理程序之前,在该外部 try 块中完全构造的所有自动持续时间对象都将按照相反的创建顺序被销毁,以此类推,直到有东西捕获到异常或者您的程序崩溃。

RAII 通过简单地使用包含资源的自动存储持续时间对象来帮助确保您释放资源,而不会发生异常。类似于 C# 中的System.IDisposable界面和using语句的组合。一旦执行离开当前块,无论是通过成功执行还是异常,资源都会被释放。

说到异常,需要记住的一个关键部分是,只有完全构造的对象才会被销毁。如果您在构造器中收到一个异常,并且最后一个try块在该构造器之外开始,由于对象没有被完全构造,它的析构器将不会运行。

这并不意味着其成员变量(即对象)不会被销毁。异常发生前在构造器中完全构造的任何成员变量对象都是完全构造的自动持续时间对象。因此,这些成员对象将像任何其他完全构造的对象一样被销毁。

这就是为什么你应该总是把动态分配放在std::unique_ptr或者std::shared_ptr里面。当分配成功时,这些类型的实例将成为完全构造的对象。即使您正在创建的对象的构造器进一步失败,std::unique_ptr资源将被其析构器释放,std::shared_ptr资源的引用计数将减少,如果计数变为零,将被释放。

当然,RAII 不仅仅是关于shared_ptrunique_ptr的。它也适用于其他资源类型,例如文件对象,其中获取是打开文件,析构器确保文件被正确关闭。这是一个特别好的例子,因为您只需要在编写类时正确地创建代码一次,而不是一次又一次地创建,如果您在打开文件的每个地方都编写 close 逻辑,这就是您需要做的。

如何使用 RAII?

RAII 的用途用它的名字来描述:获取一个动态资源应该完成一个对象的初始化。如果您遵循这种每个对象一个资源的模式,那么就不可能导致资源泄漏。要么你将成功获取资源,在这种情况下封装它的对象将完成构建并被销毁,要么获取尝试将失败,在这种情况下你没有获取资源;因此,没有资源可以释放。

封装资源的对象的析构器必须释放该资源。这是析构器永远不应该抛出异常的重要原因之一,除了那些它们自己捕获和处理的异常。

如果析构器抛出了一个未被捕获的异常,那么,引用比雅尼·斯特劳斯特鲁普“各种不好的事情都有可能发生,因为会违反标准库的基本规则和语言本身。不要这样做。”

正如他所说,不要做。确保您知道析构器中调用的所有东西可能引发什么异常(如果有的话),这样您就可以确保正确处理它们。

现在你可能会想,如果你遵循这种模式,你最终会写出大量的课程。你偶尔会到处写一个额外的类,但是由于智能指针,你不太可能写太多。智能指针也是对象。大多数类型的动态资源可以放入至少一个现有的智能指针类中。当您将资源获取放入合适的智能指针中时,如果获取成功,那么该智能指针对象将被完全构造。如果发生异常,将调用智能指针对象的析构器,并释放资源。

有几种重要的智能指针类型。让我们看看他们。

std::unique_ptr

唯一指针std::unique_ptr被设计为保存指向动态分配对象的指针。只有当您希望存在一个指向对象的指针时,才应该使用此类型。它是一个模板类,接受一个强制的和一个可选的模板参数。强制参数是它将保存的指针的类型。例如auto result = std::unique_ptr<int>(new int());将创建一个包含int*的唯一指针。可选参数是 deleter 类型。我们将在接下来的示例中看到如何编写删除程序。通常,您可以避免指定删除程序,因为default_deleter在没有指定删除程序的情况下为您提供,几乎涵盖了您能想象的所有情况。

std::unique_ptr作为成员变量的类不能有默认的复制构造器。std::unique_ptr的复制语义被禁用。如果希望类中的复制构造器具有唯一的指针,则必须编写它。还应该为复制操作符编写一个重载。通常情况下,在这种情况下,你需要std::shared_ptr

但是,您可能有类似数据数组的东西。您可能还希望该类的任何副本创建当时存在的数据的副本。在这种情况下,带有自定义复制构造器的唯一指针可能是正确的选择。

std::unique_ptr<memory>头文件中定义。

std::unique_ptr有四个感兴趣的成员函数。

get成员函数返回存储的指针。如果需要调用一个需要将包含的指针传递到的函数,使用get检索指针的副本。

release成员函数也返回存储的指针,但是release通过用空指针替换存储的指针来使进程中的 unique_ptr 无效。如果你有一个函数,你想创建一个动态对象,然后返回它,同时仍然保持异常安全,使用std:unique_ptr存储动态创建的对象,然后返回调用release的结果。这为您提供了异常安全性,同时允许您返回动态对象,而无需在控件结束时返回释放的指针值时使用std::unique_ptr的析构器销毁它。

swap成员函数允许两个唯一的指针交换它们存储的指针,所以如果A持有指向X的指针,B持有指向Y的指针,调用A::swap(B);的结果是A现在持有指向Y,的指针,B持有指向X的指针。每个的删除器也将被交换,所以如果你有一个自定义的删除器用于一个或两个唯一的指针,请确保每个都将保留其相关的删除器。

reset成员函数导致存储指针指向的对象(如果有)在大多数情况下被销毁。如果当前存储的指针为空,则不会破坏任何内容。如果您传入一个指向当前存储的指针所指向的对象的指针,则不会破坏任何内容。您可以选择传入新指针nullptr,或者调用不带参数的函数。如果你传入一个新的指针,那么这个新的对象就会被存储。如果您传入nullptr,则唯一指针将存储空值。调用不带参数的函数和调用nullptr是一样的。

std::shared_ptr

共享指针std::shared_ptr设计用于保存指向动态分配对象的指针,并为其保留引用计数。这不是魔法;如果创建两个共享指针,并分别向它们传递一个指向同一个对象的指针,那么最终会得到两个共享指针——每个指针的引用计数都是 1,而不是 2。第一个被破坏的将释放底层资源,当你试图使用另一个或者当另一个被破坏并试图释放已经释放的底层资源时,会产生灾难性的结果。

若要正确使用共享指针,请使用对象指针创建一个实例,然后从该对象的现有有效共享指针为该对象创建所有其他共享指针。这确保了公共引用计数,因此资源将具有适当的生存期。让我们看一个快速的例子,看看创建shared_ptr对象的正确和错误方法。

sample:sharedptrsaaexample \ sharedptrsaaexample . CPP

    #include <memory>
    #include <iostream>
    #include <ostream>
    #include "../pchar.h"

    using namespace std;

    struct TwoInts
    {
          TwoInts(void) : A(), B() { }
          TwoInts(int a, int b) : A(a), B(b) { }
          int A;
          int B;
    };

    wostream& operator<<(wostream& stream, TwoInts* v)
    {
          stream << v->A << L" " << v->B;
          return stream;
    }

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          //// Bad: results in double free.
          //try
          //{
          //    TwoInts* p_i = new TwoInts(10, 20);

          //    auto sp1 = shared_ptr<TwoInts>(p_i);
          //    auto sp2 = shared_ptr<TwoInts>(p_i);
          //    p_i = nullptr;

          //    wcout << L"sp1 count is " << sp1.use_count() << L"." << endl <<
          //          L"sp2 count is " << sp2.use_count() << L"." << endl;
          //}
          //catch(exception& e)
          //{
          //    wcout << L"There was an exception." << endl;
          //    wcout << e.what() << endl << endl;
          //}
          //catch(...)
          //{
          //    wcout << L"There was an exception due to a double free " <<
          //          L"because we tried freeing p_i twice!" << endl;
          //}

          // This is one right way to create shared_ptrs.
          {
                auto sp1 = shared_ptr<TwoInts>(new TwoInts(10, 20));
                auto sp2 = shared_ptr<TwoInts>(sp1);

                wcout << L"sp1 count is " << sp1.use_count() << L"." << endl <<
                      L"sp2 count is " << sp2.use_count() << L"." << endl;

                wcout << L"sp1 value is " << sp1 << L"." << endl <<
                      L"sp2 value is " << sp2 << L"." << endl;
          }

          // This is another right way. The std::make_shared function takes the
          // type as its template argument, and then the argument value(s) to the
          // constructor you want as its parameters, and it automatically
          // constructs the object for you. This is usually more memory-
          // efficient, as the reference count can be stored with the
          // shared_ptr's pointed-to object at the time of the object's creation.
          {
                auto sp1 = make_shared<TwoInts>(10, 20);
                auto sp2 = shared_ptr<TwoInts>(sp1);

                wcout << L"sp1 count is " << sp1.use_count() << L"." << endl <<
                      L"sp2 count is " << sp2.use_count() << L"." << endl;

                wcout << L"sp1 value is " << sp1 << L"." << endl <<
                      L"sp2 value is " << sp2 << L"." << endl;
          }

          return 0;
    }

std::shared_ptr在<内存>头文件中定义。

std::shared_ptr有五个感兴趣的成员函数。

get成员函数的工作原理与std::unique_ptr::get成员函数相同。

use_count成员函数返回一个long,告诉你目标对象的当前参考计数是多少。这不包括弱引用。

unique成员函数返回一个bool,通知您这个特定的共享指针是否是目标对象的唯一所有者。

swap成员函数的工作方式与std::unique_ptr::swap成员函数相同,只是资源的引用计数保持不变。

reset成员函数递减底层资源的引用计数,如果资源计数变为零,则销毁该引用计数。如果一个指向对象的指针被传入,共享指针将存储它,并为该指针开始一个新的引用计数。如果传入nullptr,或者没有传入参数,那么共享指针将存储空值。

std::make_shared

std::make_shared模板函数是构造初始std::shared_ptr的一种便捷方式。正如我们之前在 SharedPtrSample 中看到的,您将类型作为模板参数传递,然后简单地传递所需构造器的参数(如果有的话)。std::make_shared 将构造一个模板参数对象类型的堆实例,并使其成为一个std::shared_ptr。然后,您可以将该std::shared_ptr作为参数传递给std::shared_ptr构造器,以创建对该共享对象的更多引用。

ComPtr在 WRL 推出地铁式应用

Windows 运行时模板库(WRL)在Microsoft::WRL命名空间内提供了一个名为 ComPtr 的智能指针,用于 Windows 8 Metro 风格应用程序中的 COM 对象。指针位于< wrl/client.h >头中,作为 Windows SDK(最低版本 8.0)的一部分。

大多数可以在 Metro 风格的应用程序中使用的操作系统功能都是由 Windows 运行时(“WinRT”)公开的。WinRT 对象为对象创建和销毁提供了自己的自动引用计数功能。有些系统功能,比如 Direct3D,需要你通过经典的 COM 直接使用和操作。ComPtr为你处理 COM 的IUnknown基引用计数。它还为QueryInterface提供了方便的包装器,并包含了对智能指针有用的其他功能。

您通常使用的两个成员函数是As为底层 COM 对象获取不同的接口,以及Get获取指向 ComPtr 持有的底层 COM 对象的接口指针(这相当于std::unique_ptr::get)。

有时您会使用Detach,它的工作方式与std::unique_ptr::release相同,但名称不同,因为在 COM 中释放意味着减少引用计数,Detach不会这样做。

您可以使用ReleaseAndGetAddressOf来处理这样的情况:您有一个现有的ComPtr可以容纳一个 COM 对象,并且您想要用一个相同类型的新 COM 对象来替换它。ReleaseAndGetAddressOfGetAddressOf成员函数做同样的事情,但是it首先发布它的底层接口,如果有的话。

c++ 中的异常

不像。NET,其中所有异常都是从System.Exception派生的,并且有保证的方法和属性,C++ 异常不需要从任何东西派生;甚至不要求它们是类类型。在 C++ 中,throw L"Hello World!";throw 5;一样完全可以被编译器接受。基本上,例外可以是任何东西。

也就是说,许多 C++ 程序员会不高兴看到一个不是从std::exception派生的异常(在<exception>头中找到)。从std::exception导出所有异常提供了一种方法来捕获未知类型的异常,并在重新抛出它们之前通过what成员函数从它们那里检索信息。std::exception::what不接受任何参数,并返回一个const char*字符串,您可以查看或记录该字符串,以便了解导致异常的原因。

除了 C++ 异常,没有栈跟踪(不包括调试器提供的栈跟踪功能)。因为捕获异常的 try-block 范围内的自动持续时间对象会在激活适当的 catch 处理程序(如果有)之前自动销毁,所以您没有时间检查可能导致异常的数据。您最初需要处理的只是来自what成员函数的消息。

如果很容易重新创建导致异常的条件,您可以设置一个断点并重新运行程序,允许您逐步执行故障区域,并可能发现问题。因为这并不总是可能的,所以对错误消息尽可能精确是很重要的。

当从std::exception派生时,您应该确保覆盖what成员函数,以提供有用的错误消息,帮助您和其他开发人员诊断错误。

一些程序员使用一个规则的变体,声明你应该总是抛出std::exception派生的异常。记住入口点(mainwmain)返回一个整数,当他们的代码可以恢复时,这些程序员会抛出std::exception派生的异常,但是如果故障不可恢复,他们只会抛出一个定义良好的整数值。入口点代码将被包装在一个试块中,这个试块有一个int的捕捉器。捕捉处理器将返回捕捉到的int值。在大多数系统中,程序的返回值 0 意味着成功。任何其他价值都意味着失败。

如果出现灾难性的失败,那么抛出一个定义明确的整数值而不是 0 可以帮助提供一些意义。除非您正在处理一个首选风格的项目,否则您应该坚持使用std::exception派生的异常,因为它们允许程序使用简单的日志系统来记录来自未处理的异常的消息来处理异常,并且它们执行任何安全的清理。抛出一些不是从std::exception派生的东西会干扰这些错误记录机制。

最后要注意的一点是,C# 的finally构造在 C++ 中没有等价的。RAII 的习惯用法,当被正确实现时,就变得没有必要了,因为一切都已经被清理了。

C++ 标准库异常

我们已经讨论过std::exception,但是有比标准库中更多的类型,并且还有额外的功能需要探索。我们先来看看<exception>头文件的功能。

std::terminate功能,默认情况下,让你崩溃的任何应用程序。应该谨慎使用它,因为调用它而不是引发异常将绕过所有正常的异常处理机制。如果您愿意,您可以编写一个没有参数和返回值的自定义终止函数。这方面的一个例子将在即将到来的例外样本中看到。

要设置自定义终止,您需要调用std::set_terminate并将函数的地址传递给它。您可以随时更改自定义终止处理程序;最后一个函数集是在调用std::terminate或出现未处理异常时将调用的函数。默认处理程序从头文件调用中止函数。

头为异常提供了一个基本框架。它定义了两个继承自std::exception的类。这两个类充当其他几个类的父类。

std::runtime_error类是运行时引发的异常或由于 C++ 标准库函数中的错误引发的异常的父类。它的孩子是std::overflow_error班、std::range_error班和std::underflow_error班。

std::logic_error类是由于程序员错误引发异常的父类。它的孩子是std::domain_error班、std::invalid_argument班、std::length_error班和std::out_of_range班。

您可以从这些类派生或者创建自己的异常类。想出一个好的异常层次结构是一项困难的任务。一方面,您希望异常足够具体,以便能够在构建时根据您的知识处理所有异常。另一方面,您不希望每个可能发生的错误都有一个异常类。您的代码最终会变得臃肿和笨拙,更不用说浪费时间为每个异常类编写 catch 处理程序了。

花点时间在白板上,或者用笔和纸,或者你想要的任何方式思考你的应用程序应该有什么样的异常树。

以下示例包含一个名为InvalidArgumentExceptionBase的类,该类用作名为InvalidArgumentException的模板类的父类。可以用一个异常处理程序捕获的基类和允许我们根据参数类型定制输出诊断的模板类的组合,是在专门化和代码膨胀之间取得平衡的一种选择。

模板类现在可能看起来很混乱;我们将在即将到来的一章中讨论模板,在这一点上,任何目前不清楚的事情都应该澄清。

范例:异常范例\ invalidargumentexception . h

    #pragma once
    #include <exception>
    #include <stdexcept>
    #include <string>
    #include <sstream>

    namespace CppForCsExceptions
    {
          class InvalidArgumentExceptionBase :
                public std::invalid_argument
          {
          public:
                InvalidArgumentExceptionBase(void) :
                      std::invalid_argument("") { }

                virtual ~InvalidArgumentExceptionBase(void) throw() { }

                virtual const char* what(void) const throw() override = 0;
          };

          template <class T>
          class InvalidArgumentException :
                public InvalidArgumentExceptionBase
          {
          public:
                inline InvalidArgumentException(
                      const char* className,
                      const char* functionSignature,
                      const char* parameterName,
                      T parameterValue
                      );

                inline virtual ~InvalidArgumentException(void) throw();

                inline virtual const char* what(void) const throw() override;

          private:
                std::string m_whatMessage;
          };

          template<class T>
          InvalidArgumentException<T>::InvalidArgumentException(
                const char* className,
                const char* functionSignature,
                const char* parameterName,
                T parameterValue) : InvalidArgumentExceptionBase(),
                m_whatMessage()
          {
                std::stringstream msg;
                msg << className << "::" << functionSignature <<
                      " - parameter '" << parameterName << "' had invalid value '" <<
                      parameterValue << "'.";
                m_whatMessage = std::string(msg.str());
          }

          template<class T>
          InvalidArgumentException<T>::~InvalidArgumentException(void) throw()
          { }

          template<class T>
          const char* InvalidArgumentException<T>::what(void) const throw()
          {
                return m_whatMessage.c_str();
          }
    }

示例:异常示例\异常示例. cpp

    #include <iostream>
    #include <ostream>
    #include <memory>
    #include <exception>
    #include <stdexcept>
    #include <typeinfo>
    #include <algorithm>
    #include <cstdlib>
    #include "InvalidArgumentException.h"
    #include "../pchar.h"

    using namespace CppForCsExceptions;
    using namespace std;

    class ThrowClass
    {
    public:
          ThrowClass(void)
                : m_shouldThrow(false)
          {
                wcout << L"Constructing ThrowClass." << endl;
          }

          explicit ThrowClass(bool shouldThrow)
                : m_shouldThrow(shouldThrow)
          {
                wcout << L"Constructing ThrowClass. shouldThrow = " <<
                      (shouldThrow ? L"true." : L"false.") << endl;
                if (shouldThrow)
                {
                      throw InvalidArgumentException<const char*>(
                            "ThrowClass",
                            "ThrowClass(bool shouldThrow)",
                            "shouldThrow",
                            "true"
                            );
                }
          }

          ~ThrowClass(void)
          {
                wcout << L"Destroying ThrowClass." << endl;
          }

          const wchar_t* GetShouldThrow(void) const
          {
                return (m_shouldThrow ? L"True" : L"False");
          }

    private:
          bool        m_shouldThrow;
    };

    class RegularClass
    {
    public:
          RegularClass(void)
          {
                wcout << L"Constructing RegularClass." << endl;
          }
          ~RegularClass(void)
          {
                wcout << L"Destroying RegularClass." << endl;
          }
    };

    class ContainStuffClass
    {
    public:
          ContainStuffClass(void) :
                m_regularClass(new RegularClass()),
                m_throwClass(new ThrowClass())
          {
                wcout << L"Constructing ContainStuffClass." << endl;
          }

          ContainStuffClass(const ContainStuffClass& other) :
                m_regularClass(new RegularClass(*other.m_regularClass)),
                m_throwClass(other.m_throwClass)
          {
                wcout << L"Copy constructing ContainStuffClass." << endl;
          }

          ~ContainStuffClass(void)
          {
                wcout << L"Destroying ContainStuffClass." << endl;
          }

          const wchar_t* GetString(void) const
          {
                return L"I'm a ContainStuffClass.";
          }

    private:

          unique_ptr<RegularClass>      m_regularClass;
          shared_ptr<ThrowClass>        m_throwClass;
    };

    void TerminateHandler(void)
    {
          wcout << L"Terminating due to unhandled exception." << endl;

          // If you call abort (from <cstdlib>), the program will exit
          // abnormally. It will also exit abnormally if you do not call
          // anything to cause it to exit from this method.
          abort();

          //// If you were instead to call exit(0) (also from <cstdlib>),
          //// then your program would exit as though nothing had
          //// gone wrong. This is bad because something did go wrong.
          //// I present this so you know that it is possible for
          //// a program to throw an uncaught exception and still
          //// exit in a way that isn't interpreted as a crash, since
          //// you may need to find out why a program keeps abruptly
          //// exiting yet isn't crashing. This would be one such cause
          //// for that.
          //exit(0);
    }

    int _pmain(int /*argc*/, _pchar* /*argv*/[])
    {
          // Set a custom handler for std::terminate. Note that this handler
          // won't run unless you run it from a command prompt. The debugger
          // will intercept the unhandled exception and will present you with
          // debugging options when you run it from Visual Studio.
          set_terminate(&TerminateHandler);

          try
          {
                ContainStuffClass cSC;
                wcout << cSC.GetString() << endl;

                ThrowClass tC(false);
                wcout << L"tC should throw? " << tC.GetShouldThrow() << endl;

                tC = ThrowClass(true);
                wcout << L"tC should throw? " << tC.GetShouldThrow() << endl;
          }
          // One downside to using templates for exceptions is that you need a
          // catch handler for each specialization, unless you have a base
          // class they all inherit from, that is. To avoid catching
          // other std::invalid_argument exceptions, we created an abstract
          // class called InvalidArgumentExceptionBase, which serves solely to
          // act as the base class for all specializations of
          // InvalidArgumentException<T>. Now we can catch them all, if desired,
          // without needing a catch handler for each. If you wanted to, however,
          // you could still have a handler for a particular specialization.
          catch (InvalidArgumentExceptionBase& e)
          {
                wcout << L"Caught '" << typeid(e).name() << L"'." << endl <<
                      L"Message: " << e.what() << endl;
          }
          // Catch anything derived from std::exception that doesn’t already
          // have a specialized handler. Since you don't know what this is, you
          // should catch it, log it, and re-throw it.
          catch (std::exception& e)
          {
                wcout << L"Caught '" << typeid(e).name() << L"'." << endl <<
                      L"Message: " << e.what() << endl;
                // Just a plain throw statement like this is a re-throw.
                throw;
          }
          // This next catch catches everything, regardless of type. Like
          // catching System.Exception, you should only catch this to
          // re-throw it.
          catch (...)
          {
                wcout << L"Caught unknown exception type." << endl;
                throw;
          }

          // This will cause our custom terminate handler to run.
          wcout << L"tC should throw? " <<
                ThrowClass(true).GetShouldThrow() << endl;

          return 0;
    }

虽然我在评论中提到了它,但我只想再次指出,除非您从命令提示符运行这个示例,否则您不会看到自定义终止函数运行。如果您在 Visual Studio 中运行它,调试器将截取程序,并在给您一个检查状态的机会,看看您是否可以确定哪里出错后,编排它自己的终止。此外,请注意,此程序将始终崩溃。这是设计好的,因为它允许您看到终止处理程序在运行。