Skip to content

Latest commit

 

History

History
1154 lines (845 loc) · 80.5 KB

File metadata and controls

1154 lines (845 loc) · 80.5 KB

十、诊断和调试

软件是复杂的;然而,无论是在代码开发的正常测试阶段,还是在发布错误报告时,您在设计代码时,有时都必须对其进行调试。 谨慎的做法是设计代码,使测试和调试尽可能简单。 这意味着添加跟踪和报告代码,确定不变量以及前置条件和后置条件,以便有一个起点来测试代码,并编写具有可理解且有意义的错误代码的函数。

准备代码

C++ 和 C 标准库具有广泛的函数,允许您应用跟踪和报告函数,以便您可以测试代码是否以预期的方式处理数据。 这些工具大多使用条件编译,因此报告只在调试版本中出现,但如果您为跟踪提供有意义的消息,它们将成为代码文档的一部分。 在您可以报告代码的行为之前,您首先必须知道它会带来什么。

不变量和条件

类不变式是条件,即对象状态,您知道它保持为真。 在方法调用期间,对象状态将更改,可能更改为使对象无效的状态,但一旦公共方法完成,对象状态必须保持一致状态。 不能保证用户调用类上的方法的顺序,甚至根本不能保证它们调用方法,所以无论用户调用什么方法,对象都必须是可用的。 对象的不变方面适用于方法调用级别:在方法调用之间,对象必须一致且可用。

例如,假设您有一个表示日期的类:它包含一个介于 1 和 31 之间的日期数字,一个介于 1 和 12 之间的月份数字,以及一个年份数字。 类不变式是,无论您对 Date 类的对象执行什么操作,它都将始终保存有效日期。 这意味着用户可以安全地使用 Date 类的对象。 这还意味着类上的其他方法(例如,确定两个日期之间有多少天的方法operator-)可以假定 Date 对象中的值是有效的,因此这些方法不必检查它们操作的数据的有效性。

但是,有效日期大于范围 1 到 31(天数)和 1 到 12(月数),因为不是每个月都有 31 天。 因此,如果您有一个有效的日期,比如 1997 年 4 月 5 日,并且您调用set_day方法将天数设置为 31,那么就违反了类不变条件,因为 4 月 31 日不是一个有效的日期。 如果要更改 Date 对象中的值,唯一安全的方法是同时更改所有值:日、月和年,因为这是保持类不变性的唯一方法。

一种方法是在调试构建中定义私有方法,该方法测试类的不变条件,并使用断言(参见后面)确保维护不变条件。 您可以在可公开访问的方法离开之前调用此类方法,以确保对象保持一致状态。 方法还应该定义前置条件和后置条件。 前置条件是在调用方法之前强制为真的条件,后置条件是在方法完成后保证为真的条件。 对于类上的方法,类不变量是前提条件(因为在调用方法之前对象的状态应该是一致的),不变量也是后置条件(因为在方法完成之后,对象状态应该是一致的)。

还有一些前提条件是方法的调用方负责的。 前提条件是调用者确保的有文档记录的责任。 例如,Date 类将有一个前提条件,即日期数字介于 1 和 31 之间。 这简化了类代码,因为接受天数的方法可以假设传递的值永远不会超出范围(尽管,因为有些月份的天数少于 31 天,所以值可能仍然无效)。 同样,在调试版本中,您可以使用断言来检查这些前提条件是否为真,并且断言中的测试将在发布版本中编译掉。 在方法的末尾将有后置条件,也就是说,将维护类不变量(并且对象的状态将是有效的),并且返回值将是有效的。

条件编译

正如第 1 章、*从 C++*开始的解释,当编译 C++ 程序时,有一个预编译步骤,将 C++ 源文件中包含的所有文件整理成单个文件,然后编译该文件。 预处理器还展开宏,并根据符号的值包括一些代码和排除其他代码。

在其最简单的形式中,条件编译用#ifdef#endif括起代码(也可以选择使用#else),以便只有在定义了指定符号的情况下才编译这些指令之间的代码。

    #ifdef TEST 
       cout << "TEST defined" << endl;     
    #else 
       cout << "TEST not defined" << endl; 
    #endif

您可以保证只编译其中的一行,并且保证至少编译其中的一行。 如果定义了符号TEST,则将编译第一行,而对于编译器而言,第二行不存在。 如果未定义符号TEST,则将编译第二行。 如果您想以相反的顺序键入这些行,可以使用#ifndef指令。 通过条件编译提供的文本可以是 C++ 代码,也可以使用当前翻译单元中的其他符号(使用#define)或未定义的现有符号(使用#undef)来定义。

#ifdef指令只是确定符号是否存在:它不测试它的值。 #if指令允许您测试表达式。 您可以将符号设置为具有值,并根据该值编译特定代码。 表达式必须是整型的,因此单个#if块可以使用#if和多个#elif指令以及(最多)一个#else指令测试多个值:

    #if TEST < 0 
       cout << "negative" << endl; 
    #elif TEST > 0 
       cout << "positive" << endl; 
    #else 
       cout << "zero or undefined" << endl; 
    #endif

如果未定义符号,则#if指令将该符号视为具有值0;如果要区分这些情况,可以使用defined运算符来测试是否定义了符号。 最多只编译#if/#endif块中的一个部分,如果值不匹配,则不会编译任何代码。 表达式可以是宏,在这种情况下,宏将在测试条件之前展开。

定义符号有三种方法。 第一种方式不受您的控制:编译器将定义一些符号(通常带有___前缀),为您提供有关编译器和编译过程的信息。 这些符号中的一些将在后面的部分中描述。 其他两种方式完全由您控制--您可以使用#define在源文件(或头文件)中定义符号,也可以使用/D开关在命令行中定义它们:

    cl /EHsc prog.cpp /DTEST=1

这将编译源代码,并将符号TEST设置为值1

您通常会使用条件编译来提供不应在生产代码中使用的代码,例如,在调试模式或测试代码时使用额外的跟踪代码。 例如,假设您有从数据库返回数据的库代码,但您怀疑库函数中的 SQL 语句有问题,返回的值太多。 在这里,您可以决定测试,添加代码以记录返回的值的数量:

    vector<int> data = get_data(); 
    #if TRACE_LEVEL > 0 
    cout << "number of data items returned: " << data.size() << endl; 
    #endif

这样的跟踪消息会污染您的用户界面,您会希望在生产代码中避免它们。 但是,在调试过程中,它们在确定哪里发生问题方面可能是无价的。

在调试模式下调用的任何代码,条件代码都应该是const方法(这里是vector::size),也就是说,它们不应该影响任何对象或应用数据的状态。 您必须确保代码的逻辑在调试模式下与在发布模式下的逻辑完全相同。

使用语用

Pragma 是特定于编译器的,通常关注目标文件中代码段的技术细节。 有几个 Visual C++ 编译指示在调试代码时很有用。

通常,您会希望在编译代码时尽可能少地出现警告。 Visual C++ 编译器的默认警告是/W1,这意味着只列出最严重的警告。 将该值增加到 2、3 或最大值 4 会逐渐增加编译期间给出的警告数。 使用/Wall将发出 4 级警告和默认禁用的警告。 最后一个选项,即使对于最简单的代码,也会产生一个充满警告的屏幕。 当您有数百个警告时,有用的错误消息将隐藏在大量不重要的警告之间。 由于 C++ 标准库很复杂,并且使用了一些几十年前的代码,因此编译器会警告您一些构造。 为防止这些警告污染生成的输出,已禁用选择性文件中的特定警告。

如果您支持较旧的库代码,您可能会发现代码编译时会出现警告。 您可能会尝试使用编译器/W开关来降低警告级别,但这将抑制所有高于您启用的警告的警告,并且它同样适用于您的代码,就像您可能包含在项目中的库代码一样。 warning杂注为您提供了更大的灵活性。 有两种方式可以调用它--可以重置警告级别以覆盖编译器/W开关,还可以更改特定警告的警告级别或完全禁用警告报告。

例如,在<iostream>标题的顶部是一行:

    #pragma warning(push,3)

这表示存储当前警告级别,对于此文件的其余部分(或直到其更改),将警告级别设置为 3。文件的底部为一行:

    #pragma warning(pop)

这会将警告级别恢复到先前存储的级别。

您还可以更改报告一个或多个警告的方式。 例如,在<istream>的顶部是:

    #pragma warning(disable: 4189)

pragma的第一部分是说明符disable,它指示禁用了警告类型(在本例中为 4189)的报告。 如果选择,可以使用警告级别(1234)作为说明符来更改警告的警告级别。 此功能的一个用途是只降低您正在处理的一段代码的警告级别,然后在该代码之后将其返回到其默认级别。 例如:

    #pragma warning(2: 4333) 
    unsigned shift8(unsigned char c)  
    { 
        return c >> 8;  
    } 
    #pragma warning(default: 4333)

此函数将字符右移 8 位,这将生成 1 级警告 4333(右移过大,数据丢失)。 这是一个问题,需要修复,但目前,您希望编译代码时不使用此代码中的警告,因此警告级别更改为级别 2。使用默认警告级别(/W1)将不会显示警告。 但是,如果使用更敏感的警告级别(例如,/W2)进行编译,则会报告此警告。 警告级别的这种更改只是暂时的,因为最后一行将警告级别重置回其默认值(即 1)。 在这种情况下,警告级别会增加,这意味着您只能在编译器上看到更敏感的警告级别。 您还可以降低警告级别,这意味着报告警告的可能性更大。 您甚至可以将警告级别更改为error,这样当代码中存在此类型的警告时,代码将不会编译。

添加信息性消息

在测试和调试代码时,您将不可避免地遇到一些地方,在这些地方您可以看到潜在的问题,但与您正在处理的内容相比,这些问题的优先级较低。 重要的是要记下这个问题,以便您可以在以后的阶段解决该问题。 在 Visual C++ 中,有两种方法可以实现这一点,一种是良性的,另一种是会产生错误的。

第一种方式是添加TODO:注释,如下图所示:

    // TODO: potential data loss, review use of shift8 function 
    unsigned shift8(unsigned char c)  
    { 
        return c >> 8;  
    }

Visual Studio 编辑器有一个名为任务列表的工具窗口。 这将列出项目中以某个预定任务开始的注释(缺省值为TODOHACKUNDONE)。

如果任务列表窗口不可见,请通过视图菜单将其启用。 Visual Studio 2015 中的默认设置是启用 C++ 中的任务。 早期版本不是这种情况,但可以通过工具菜单、选项对话框,然后通过文本编辑器、C/C++、格式、查看方式将枚举注释任务设置为是来启用它。 任务标签列表可以在选项对话框中的环境、任务列表项下找到。

任务列表列出了带有文件和行号的任务,您可以通过双击条目打开文件并找到注释。

识别需要注意的代码的第二种方法是message杂注。 顾名思义,这只允许您在代码中放置一条信息性消息。 当编译器遇到这个杂注时,它只是将消息放在输出流上。 请考虑以下代码:

    #pragma message("review use of shift8 function") 
    unsigned shift8(unsigned char c)  
    { 
        return c >> 8;  
    }

如果使用此代码和/W1(默认)警告级别编译test.cpp命令文件,则输出将如下所示:

 Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

test.cpp
review the use of shift8 function
test.cpp(8): warning C4333: '>>': right shift by too large amount, data loss

正如您所看到的,字符串就像编译器看到的那样打印出来,与警告消息形成对比的是,没有文件或行号的指示。 有一些方法可以使用编译器符号来解决这个问题。

如果条件很重要,则需要发出一个错误,其中一种方法是使用#error指令。 当编译器达到此指令时,它将发出一个错误。 这是一个严重的操作,因此只有在有其他选择时才会使用它。 您很可能希望将其与条件编译一起使用。 通常用于只能用 C++ 编译器编译的代码:

    #ifndef __cplusplus 
    #error C++ compiler required. 
    #endif

如果使用/Tc开关编译包含此代码的文件,将代码编译为 C,则不会定义__cplusplus预处理器符号,并将生成错误。

C++ 11 添加了一个名为static_assert的新指令。 它的调用方式类似于函数(调用以分号结尾),但它不是函数,因为它只在编译时使用。 此外,该指令可以在不使用函数调用的地方使用。 该指令有两个参数:表达式和字符串文字。 如果表达式为false,则字符串文字将在编译时与源文件和行号一起输出,并将生成错误。 在最简单的级别上,您可以使用下面的代码来发出一条消息:

    #ifndef __cplusplus 
    static_assert(false, "Compile with /TP"); 
    #endif 
    #include <iostream> // needs the C++ compiler

由于第一个参数是false,指令将在编译期间发出错误消息。 同样的事情也可以通过#error指令来实现。 <type_traits>库有各种用于测试类型属性的谓词。 例如,is_class模板类有一个简单的模板参数,该参数是一种类型,如果类型是class,则将static成员value设置为true。 如果您有一个只应为类实例化的模板化函数,则可以添加以下内容static_assert

    #include <type_traits> 

    template <class T> 
    void func(T& value) 
    { 
        static_assert(std::is_class<T>::value, "T must be a class"); 
        // other code 
    }

在编译时,编译器将尝试实例化函数,并使用value在该类型上实例化is_class,以确定编译是否应该继续。 例如,以下代码:

    func(string("hello")); 
    func("hello");

第一行将正确编译,因为编译器将实例化一个函数func<string>,,而参数是class。 但是,第二行不会编译,因为实例化的函数是func<const char*>,而const char*不是class。 输出为:

Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

test.cpp
test.cpp(25): error C2338: T must be a class
test.cpp(39): note: see reference to function template instantiation 

'void func<const char*>(T)' being compiled
with
[
 T=const char *
]

static_assert行 25上,因此会产生T must be a class的错误。 第 39 行是对func<const char*>的第一次调用,并给出了错误的上下文。

用于调试的编译器开关

要允许您使用调试器单步执行程序,您必须提供信息以允许调试器将机器码与源代码相关联。 这至少意味着关闭所有优化,因为在尝试优化代码时,C++ 编译器将重新排列代码。 优化在默认情况下是关闭的(因此使用/Od开关是多余的),但显然,为了能够调试进程和单步执行 C++ 代码,您需要删除所有的/O优化开关。

由于 C++ 标准库使用 C 运行库,因此您需要编译代码才能使用后者的调试版本。 您使用的开关取决于您是在构建进程还是动态链接库(DLL),以及是静态链接 C 运行库还是通过 DLL 访问它。 如果您正在编译一个进程,您可以使用/MDd在 DLL 中获取 C 运行时的调试版本,如果您使用/MTd,您将获得静态链接的 C 运行时的调试版本。 如果您正在编写动态链接库,则除了 C 运行时开关之一(/MTd是默认开关)之外,还必须使用/LDd。 这些开关将定义一个称为_DEBUG的预处理器符号。

调试器需要知道调试器符号信息--变量的名称和类型,以及与代码相关的函数名称和行号。 可以接受的方法是通过一个名为程序数据库的文件,扩展名为pdb。 您可以使用其中一个/Z开关生成pdb文件:/Zi/ZI开关将创建两个文件,一个文件的名称以VC(例如VC140.pdb)开头,包含所有obj文件的调试信息;另一个文件的名称为包含进程调试的项目名称。 如果编译时没有链接(/c),则只创建第一个文件。 默认情况下,Visual C++ 项目向导将使用/Od /MDd /ZI进行调试生成。 /ZI开关表示程序数据库的创建格式允许 Visual C++ 调试器执行EditContinue,也就是说,您可以更改一些代码并继续单步执行代码,而无需重新编译。 当您为发布版本进行编译时,向导将使用/O2 /MD /Zi开关,这意味着代码已针对速度进行了优化,但仍将创建程序数据库(没有EditContinue支持)。 代码不需要程序数据库来运行(实际上,您不应该将其与代码一起分发),但如果您有崩溃报告并且需要在调试器下运行发布构建代码,则它很有用。

这些/Z编译器开关假定链接器与/debug开关一起运行(如果编译器调用链接器,它将通过此开关)。 链接器将根据VC程序数据库文件中的调试信息创建项目程序数据库。

这就提出了为什么发布构建文件需要程序数据库的问题。 如果在调试器下运行程序并查看调用堆栈,您通常会在操作系统文件中看到一长串堆栈帧。 这些名称通常是由 DLL 名称和一些数字和字符组成的相当无意义的名称。 可以安装 Windows 的符号(pdb文件),如果没有安装,则指示 Visual C++ 调试器从网络上称为符号服务器的计算机下载正在使用的库的符号。 这些符号不是库的源代码,但它们确实为您提供了函数名称和参数类型,这为您提供了有关单步执行时调用堆栈状态的附加信息。

预处理器符号

要访问代码中的跟踪、断言和报告功能,必须启用调试运行时库,这可以通过使用/MDd/MTd/LDd编译器开关来完成,这些开关将定义_DEBUG预处理器符号。 _DEBUG预处理器符号启用了很多功能,相反,不定义该符号将有助于优化代码。

    #ifdef _DEBUG 
       cout << "debug build" << endl; 
    #else 
       cout << "release built" << endl; 
    #endif

C++ 编译器还将通过一些标准的预处理器符号提供信息。 其中大多数只对库编写者有用,但也有一些您可能想要使用。

ANSI 标准规定,当编译器将代码编译为 C++(而不是 C)时,应定义__cplusplus符号,并指定__FILE__符号应包含文件名,而__LINE__符号将包含您访问它的点的行号。 __func__符号将具有当前函数名称。 这意味着您可以创建如下跟踪代码:

    #ifdef _DEBUG 
    #define TRACE cout << __func__ << " (" << __LINE__ << ")" << endl; 
    #else 
    #define TRACE 
    #endif

如果此代码是为调试而编译的(例如,/MTd),则每当使用TRACE时,cout行将被内联;如果代码不是为调试而编译的,则TRACE将不执行任何操作。 __func__符号只是函数名,它没有限定,所以如果您在类方法中使用它,它将不会提供有关类的信息。

Visual C++ 还定义了特定于 Microsoft 的符号。 __FUNCSIG__符号提供完整的签名,包括类名(和任何namespace名称)、返回类型和参数。 如果您只需要完全限定名称,则可以使用__FUNCTION__符号。 您将在 Windows 头文件中经常看到的一个符号是_MSC_VER。 它的编号是当前 C++ 编译器的版本,它与条件编译一起使用,因此新的语言功能只能用支持它们的编译器编译。

Visual C++ 项目页面定义了名为$(ProjectDir)$(Configuration)生成宏。 它们仅由 MSBuild 工具使用,因此它们在编译期间不会自动出现在源文件中,但是,如果将预处理器符号设置为生成宏的值,则可以在编译时通过该符号获得该值。 系统环境变量也可以作为构建宏使用,因此可以使用它们来影响构建。 例如,在 Windows 上,系统环境变量USERNAME具有当前登录用户的名称,因此您可以使用它设置一个符号,然后在编译时访问该符号。

在 Visual C++ 项目页中,您可以在名为的 C/C++ 预处理器项目页上添加预处理器定义

    DEVELOPER="$(USERNAME)"

然后,可以在代码中使用以下符号添加一行:

    cout << "Compiled by " << DEVELOPER << endl;

如果您使用的是 make 文件,或者只是从命令行调用cl,则可以添加一个开关来定义符号,如下所示:

    /DDEVELOPER="$(USERNAME)"

在这里转义双引号很重要,因为如果没有双引号,编译器就会吃掉双引号。

前面,您看到了如何使用#pragma message#error指令将消息放入编译器的输出流中。 在 Visual Studio 中编译代码时,编译器和链接器输出将出现在“输出”窗口中。 如果消息的格式为:

    path_to_source_file(line) message

其中path_to_source_file是文件的完整路径,line是出现message的行号。 然后,当您在输出窗口中双击此行时,文件将被加载(如果尚未加载),并将插入点放置在该行上。

__FILE____LINE__符号为您提供了使#pragma message#error指令更加有用所需的信息。 输出__FILE__很简单,因为它是一个字符串,并且 C++ 将连接字符串文字:

    #define AT_FILE(msg) __FILE__ " " msg 

    #pragma message(AT_FILE("this is a message"))

宏作为编译指示的一部分被调用,以正确格式化消息;但是,您不能从宏调用编译指示,因为#有特殊用途(稍后会用到)。 此代码的结果如下所示:

    c:\Beginning_C++ Chapter_10test.cpp this is a message

通过宏输出__LINE__需要更多的工作,因为它包含一个数字。 这个问题在 C 中很常见,所以有一个使用两个宏和字符串运算符#的标准解决方案。

    #define STRING2(x) #x 
    #define STRING(x) STRING2(x) 
    #define AT_FILE(msg) __FILE__ "(" STRING(__LINE__) ") " msg

STRING宏用来将__LINE__符号扩展为一个数字,而STRING2宏用来将数字串化。 AT_FILE宏用正确的格式设置整个字符串的格式。

生成诊断消息

诊断消息的有效使用是一个广泛的主题,因此本节将只向您介绍基本知识。 在设计代码时,应该使编写诊断消息变得容易,例如,提供转储对象内容的机制,并提供对测试类不变式以及前置条件和后置条件的代码的访问。 您还应该分析代码以确保记录适当的消息。 例如,在循环中发出诊断消息通常会填满日志文件,从而使读取日志文件中的其他消息变得困难。 但是,循环中的某项操作持续失败这一事实本身可能是一个重要的诊断信息,尝试执行失败操作的次数也可能是一个重要的诊断信息,因此您可能想要记录下来。

对诊断消息使用cout具有将这些消息与您的用户输出集成在一起的优势,这样您就可以看到中间结果的最终效果。 缺点是诊断消息与用户输出集成在一起,由于通常有大量的诊断消息,这些消息将完全淹没程序的用户输出。

C++ 有两个流对象,您可以使用它们来代替coutclogcerr流对象会将字符数据写入标准错误流(C 流指针stderr),这通常会在控制台上显示为您正在使用cout(输出到标准输出流,即 C 流指针stdout),但您可以将其重定向到其他地方。 clogcerr之间的区别在于clog使用缓冲输出,这可能比未缓冲的cerr性能更好。 但是,如果应用在未刷新缓冲区的情况下意外停止,则可能会丢失数据。

由于clogcerr流对象在发布版本和调试版本中都可用,因此您应该只将它们用于您希望最终用户看到的消息。 这使得它们不适合用于跟踪消息(稍后将介绍)。 相反,您应该将它们用于用户将能够处理的诊断消息(可能是找不到文件,或者进程没有执行操作的安全访问权限)。

    ofstream file; 
    if (!file.open(argv[1], ios::out)) 
    { 
        clog << "cannot open " << argv[1] << endl; 
        return 1; 
    }

此代码分两步打开文件(而不是使用构造函数),如果文件无法打开,open方法将返回false。 代码检查打开文件是否成功,如果失败,它将通过clog对象告诉用户,然后从包含该代码的任何函数返回,因为file对象现在是无效的,不能使用。 缓冲了clog对象,但在本例中,我们希望立即通知用户,这是由endl操纵器执行的,它在流中插入换行符,然后刷新流。

默认情况下,clogcerr流对象将输出到标准错误流,这意味着对于控制台应用,您可以通过重定向这些流来分离输出流和错误流。 在命令行上,可以使用stdin的值为 0、stdout,的值为 1、stderr的值为 2 和重定向操作符>来重定向标准流。 例如,应用app.exe可以在main函数中包含以下代码:

    clog << "clog" << endl; 
    cerr << "cerrn"; 
    cout << "cout" << endl;

cerr对象没有缓冲,所以换行符是使用n还是endl都无关紧要。 当您在命令行上运行此命令时,您将看到类似以下内容:

C:\Beginning_C++ \Chapter_10>app
clog
cerr
cout

要将流重定向到文件,请将流句柄(1 表示stdout,2 表示stderr)重定向到该文件;控制台将打开该文件并将该流写入该文件:

C:\Beginning_C++ \Chapter_10>app 2>log.txt
cout

C:\Beginning_C++ \Chapter_10>type log.txt
clog
cerr

正如上一章所展示的,C++ 流对象是分层的,因此将数据插入到流中的调用将根据流的类型将数据写入底层流对象,无论是否使用缓冲。 此流缓冲区对象是使用rdbuf方法获取和替换的。 如果希望应用将clog对象重定向到文件,可以编写如下代码:

    extern void run_code(); 

    int main() 
    { 
        ofstream log_file; 
        if (log_file.open("log.txt")) clog.rdbuf(log_file.rdbuf()); 

        run_code(); 

        clog.flush(); 
        log_file.close(); 
        clog.rdbuf(nullptr); 
        return 0; 
    }

在这段代码中,应用代码将在run_code函数中,其余代码设置clog对象以重定向到文件。

请注意,当run_code函数返回(应用已完成)时,文件将显式关闭;这并不完全是因为ofstream析构函数将关闭文件,在本例中,这将在main函数返回时发生。 最后一行很重要。 标准流对象是在调用main函数之前创建的,它们将在main函数返回之后的某个时间被销毁,也就是说,在文件对象被销毁之后很久。 为了防止clog对象访问被销毁的文件对象,调用rdbuf方法传递nullptr,以指示没有缓冲区。

使用 C 运行时跟踪消息

通常,您会希望通过实时运行应用来测试代码,并输出跟踪消息来测试算法是否工作。 有时,您可能希望测试调用函数的顺序(例如,正确的分支出现在switch语句或if语句中),而在其他情况下,您可能希望测试中间值,以查看输入数据是否正确以及对该数据的计算是否正确。

跟踪消息可能会生成大量数据,因此将这些数据发送到控制台是不明智的。 非常重要的一点是,跟踪消息仅在调试版本中生成。 如果在产品代码中留下跟踪消息,可能会严重影响应用的性能(稍后将对此进行解释)。 此外,跟踪消息不太可能本地化,也不会检查它们是否包含可用于对算法进行反向工程的信息。 发布版本中跟踪消息的最后一个问题是,您的客户会认为您向他们提供的是未经完全测试的代码。 因此,重要的是,只有在定义了_DEBUG符号时,才会在调试版本中生成跟踪消息。

C 运行时提供了一系列名称以_RPT开头的宏,当定义了_DEBUG时,这些宏可用于跟踪消息。 这些宏有char和宽字符版本,有些版本只报告跟踪消息,其他版本报告消息和消息的位置(源文件和行号)。 最终,这些宏将调用名为_CrtDbgReport的函数,该函数将使用在别处确定的设置生成消息。

_RPTn宏(其中n012345)将接受一个格式字符串和 0 到 5 个参数,这些参数将在报告之前放入字符串中。 宏的第一个参数指示要报告的消息类型:_CRT_WARN_CRT_ERROR_CRT_ASSERT。 这两个类别中的最后两个是相同的,它们指的是断言,这将在后面的小节中介绍。 报告宏的第二个参数是格式字符串,其后将跟随所需数量的参数。 _RPTFn宏格式相同,但将报告源文件和行号以及格式化消息。

默认操作是_CRT_WARN消息不会产生任何输出,_CRT_ERROR_CRT_ASSERT消息将生成一个弹出窗口,允许您中止或调试应用。 您可以通过调用_CrtSetReportMode函数并提供类别和指示要采取的操作的值来更改对任何这些消息类别的响应。 如果使用_CRTDBG_MODE_DEBUG,则消息将写入调试器输出窗口。 如果使用_CRTDBG_MODE_FILE,则消息将写入一个文件,您可以打开该文件并将句柄传递给_CrtSetReportFile函数。 (您还可以使用_CRTDBG_FILE_STDERR_CRTDBG_FILE_STDOUT作为文件句柄,将消息发送到标准输出或错误输出。)。 如果使用_CRTDBG_MODE_WNDW作为报告模式,则将使用中止/重试/忽略对话框显示该消息。 由于这将暂停当前执行线程,因此它应该仅用于断言消息(默认操作):

    include <crtdbg.h> 

    extern void run_code(); 

    int main() 
    { 
        _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); 
        _RPTF0(_CRT_WARN, "Application startedn"); 

        run_code(); 

        _RPTF0(_CRT_WARN, "Application endedn"); 
        return 0; 
    }

如果您没有在消息中提供n,那么下一条消息将被附加到您的消息的末尾,在大多数情况下,这不是您想要的(尽管您可以证明对_RPTn宏的一系列调用是合理的,其中最后一条以n结束)。

Visual Studio Output 窗口在编译项目时显示(要在调试时显示它,请选择 View 菜单中的 Show Output 选项),顶部是一个标记为 Show Output From 的组合框,通常设置为 Build。 如果将其设置为 Debug,那么您将看到调试会话期间生成的调试消息。 其中包括有关加载调试符号的消息,以及从_RPTn宏重定向到“输出”窗口的消息。

如果您希望将消息定向到文件,则需要使用 Win32CreateFile函数打开该文件,并在调用_CrtSetReportFile函数时使用该函数中的句柄。 为此,您需要包括 Windows 头文件:

    #define WIN32_LEAN_AND_MEAN 
    #include <Windows.h> 
    #include <crtdbg.h>

WIN32_LEAN_AND_MEAN宏将减小包含的 Windows 文件的大小。

    HANDLE file =  
       CreateFileA("log.txt", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); 
    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); 
    _CrtSetReportFile(_CRT_WARN, file); 
    _RPTF0(_CRT_WARN, "Application startedn"); 

    run_code(); 

    _RPTF0(_CRT_WARN, "Application endedn"); 
    CloseHandle(file);

此代码将把警告消息定向到文本文件log.txt,每次运行应用时都会创建新的文本文件log.txt

使用 Windows 跟踪邮件

函数的作用是:向调试器发送消息。 该函数通过名为DBWIN_BUFFER共享内存节来完成此操作。 共享内存意味着任何进程都可以访问该内存,因此 Windows 提供了两个名为DBWIN_BUFFER_READYDBWIN_DATA_READY事件对象来控制对该内存的读写访问。 这些事件对象在进程之间共享,可以处于有信号或无信号状态。 调试器将通过发信号通知DBWIN_BUFFER_READY事件来指示它不再使用共享内存,此时OutputDebugString函数可以将数据写入共享内存。 调试器将等待DBWIN_DATA_READY事件,当OutputDebugString函数完成对存储器的写入并且可以安全地读取缓冲区时,该事件将由OutputDebugString函数发出信号。 写入内存节的数据将是调用OutputDebugString函数的进程的进程 ID,后跟最多 4KB 的数据字符串。

问题是,当您调用OutputDebugString函数时,它将等待DBWIN_BUFFER_READY事件,这意味着当您使用此函数时,您会将应用的性能耦合到另一个进程的性能,该进程通常是调试器(但可能不是)。 编写一个进程来访问DBWIN_BUFFER共享内存节并访问相关的事件对象非常容易,因此您的生产代码可能会在运行此类应用的计算机上运行。 因此,使用条件编译非常重要,这样OutputDebugString函数只在调试版本中使用--这些代码永远不会发布给您的客户:

    extern void run_code(); 

    int main() 
    { 
        #ifdef _DEBUG 
            OutputDebugStringA("Application startedn"); 
        #endif 

        run_code(); 

        #ifdef _DEBUG 
           OutputDebugStringA("Application endedn"); 
        #endif 
        return 0; 
    }

您需要包括windows.h头文件来编译此代码。 对于_RPT示例,您必须在调试器下运行此代码才能查看输出,或者运行像DebugView这样的应用(可从 Microsoft 的 TechNet 网站获得)。

Windows 提供了DBWinMutex互斥对象来充当访问此共享内存和事件对象的总。 顾名思义,当您拥有互斥体的句柄时,您将拥有对资源的互斥访问权限。 问题在于,进程不必拥有该互斥锁的句柄就可以使用这些资源,因此,如果您的应用认为它拥有独占访问权,那么您无法保证它是否真的拥有独占访问权。

使用断言

断言检查条件是否为真。 断言的意思就是:如果条件不为真,程序就不应该继续。 显然,发布代码中不应该调用断言,因此必须使用条件编译。 断言应该用来检查不应该发生的情况:永远不会发生事件。 由于这些条件不会发生,因此在发布版本中应该不需要断言。

C 运行时提供可通过<cassert>头文件使用的assert宏。 除非定义了NDEBUG符号,否则宏以及作为其唯一参数传递的表达式中调用的任何函数都将被调用。 也就是说,您不必定义_DEBUG符号来使用断言,并且您应该采取额外的操作来显式阻止调用assert

这一点值得再重复一遍。 即使没有定义_DEBUG,也会定义assert宏,因此可以在发布代码中调用断言。 为了防止这种情况发生,您必须在发布版本中定义NDEBUG符号。 相反,您可以在调试版本中定义NDEBUG符号,以便可以使用跟踪,但不必使用断言。

通常,您将在调试版本中使用断言来检查函数中是否满足前置条件和后置条件,以及是否满足类不变条件。 例如,您可能有一个二进制缓冲区,它在第十个字节位置有一个特定值,因此编写了一个函数来提取该字节:

    const int MAGIC=9; 

    char get_data(char *p, size_t size) 
    { 
        assert((p != nullptr)); 
        assert((size >= MAGIC)); 
        return p[MAGIC]; 
    }

在这里,对assert的调用用于检查指针是否不是nullptr以及缓冲区是否足够大。 如果这些断言为真,则意味着通过指针访问第十个字节是安全的。

虽然这在此代码中并不是严格必需的,但断言表达式放在圆括号中。 养成这样做的习惯是很好的,因为assert是宏,因此表达式中的逗号将被视为宏参数分隔符;圆括号不受此影响。

由于默认情况下将在发布版本中定义assert宏,因此您必须通过在编译器命令行的 make 文件中定义NDEBUG来禁用它们,或者您可能希望显式使用条件编译:

    #ifndef _DEBUG 
    #define NDEBUG 
    #endif

如果调用 Assert 但失败,则控制台会打印一条 Assert 消息以及源文件和行号信息,然后通过调用abort终止该进程。 如果该过程是使用发布版本标准库构建的,则过程abort很简单,但是,如果使用调试版本,则用户将看到标准的中止/重试/忽略消息框,其中的中止和忽略选项中止该过程。 重试选项将使用实时(JIT)调试将已注册的调试器附加到进程。

相反,只有在定义了_DEBUG时才定义_ASSERT_ASSERTE宏,因此这些宏在发布版本中将不可用。 这两个宏都接受一个表达式,并在表达式为false时生成一条断言消息。 _ASSERT宏的消息将包括源文件和行号,以及声明断言失败的消息。 _ASSERTE宏的消息类似,但包含失败的表达式。

    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); 
    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT); 

    int i = 99; 
    _ASSERTE((i > 100));

此代码设置报告模式,以便失败的断言将是在控制台上打印的消息(而不是默认的中止/重试/忽略对话框)。 由于变量明显小于 100,断言将失败,因此进程将终止,控制台上将打印以下消息:

    test.cpp(23) : Assertion failed: (i > 100)

中止/重试/忽略对话框为测试应用的人员提供了将调试器附加到进程的选项。 如果您认为断言的失败是令人发指的,则可以通过调用_CrtDbgBreak强制调试器附加到进程。

    int i = 99; 
    if (i <= 100) _CrtDbgBreak();

您不需要使用条件编译,因为在发布版本中,_CrtDbgBreak函数是无操作的。 在调试版本中,此代码将触发 JIT 调试,这为您提供了关闭应用或启动调试器的选项,如果选择后者,则将启动已注册的 JIT 调试器。

应用终止

main函数是应用的入口点。 但是,操作系统不会直接调用它,因为 C++ 将在调用main之前执行初始化。 这包括构造标准库全局对象(cincoutcerrclog,和宽字符版本),并且为支撑 C++ 库的 C 运行时库执行了一整套初始化。 此外,还有您的代码创建的全局和静态对象。 当main函数返回时,必须调用全局和静态对象的析构函数,并在 C 运行时执行清理。

有几种方法可以故意停止进程。 最简单的方法是从main函数返回,但这假设有一条简单的路径返回到main函数,从您的代码想要完成该过程的那一点开始。 当然,进程终止必须是有序的,您应该避免在代码中的任何位置正常停止进程的地方编写代码。 但是,如果您遇到数据已损坏且无法恢复的情况,并且任何其他操作可能会损坏更多数据,则您可能别无选择,只能终止应用。

<cstdlib>头文件提供对头文件的访问,以及允许您终止和处理应用终止的函数。 当 C++ 程序正常关闭时,C++ 基础结构将调用在main函数中创建的对象的析构函数(与其构造顺序相反)和static对象的析构函数(可能是在main函数以外的函数中创建的)。 atexit函数允许您注册在main函数完成并且调用了static对象析构函数之后调用的函数(没有参数和返回值)。 您可以通过多次调用此函数来注册多个函数,在终止时,这些函数的调用顺序将与它们的注册顺序相反。 调用用atexit函数注册的函数后,将调用任何全局对象的析构函数。

还有一个名为_onexit的 Microsoft 函数,它还允许您注册要在正常终止期间调用的函数。

exit_exit函数执行进程的正常退出,即,它们在关闭进程之前清理 C 运行时并刷新所有打开的文件。 exit函数通过调用任何已注册的终止函数来执行额外的工作;_exit函数不调用这些终止函数,快速退出也是如此。 这些函数不会调用临时或自动对象的析构函数,因此如果使用堆栈对象来管理资源,则必须在调用exit之前显式调用析构函数代码。 但是,将调用静态和全局对象的析构函数。

quick_exit函数导致正常关机,但它不调用任何析构函数,也不刷新任何流,因此不会进行资源清理。 向atexit注册的函数不会被调用,但您可以通过向at_quick_exit函数注册它们来注册调用终止函数。 在调用这些终止函数之后,quick_exit函数调用关闭进程的_Exit函数。

您还可以调用terminate函数来关闭进程,而不进行清理。 此过程将调用已向set_terminate函数注册的函数,然后调用abort函数。 如果程序中发生异常而未被捕获--并因此传播到main函数--C++ 基础结构将调用terminate函数。 abort函数是终止进程的最严格的机制。 此函数将退出进程,而不调用对象的析构函数或执行任何其他清理。 该函数引发SIGABORT信号,因此可以使用signal函数注册函数,该函数将在进程终止之前被调用。

误差值

有些函数旨在执行某个操作并根据该操作返回一个值,例如,sqrt将返回一个数字的平方根。 其他函数执行更复杂的操作,并使用返回值指示函数是否成功。 此类错误值没有通用约定,因此如果函数返回一个简单整数,则不能保证一个库使用的值与从另一个库中的函数返回的值具有相同的含义。 这意味着您必须仔细检查您使用的任何库代码的文档。

Windows 确实提供了公共错误值,可以在winerror.h头文件中找到,Windows软件开发工具包(SDK)中的函数仅返回此文件中的值。 如果您编写的库代码将仅在 Windows 应用中使用,请考虑使用此文件中的错误值,因为您可以使用 Win32FormatMessage函数来获取错误描述,如下节所述。

C 运行时库提供了一个名为errno的全局变量(实际上,它是一个宏,您可以将其视为变量)。 C 函数将返回一个值来指示它们已失败,您可以访问errno值来确定错误是什么。 <errno.h>头文件定义标准 POSIX 错误值。 errno变量不表示成功,它只表示错误,所以您应该只在函数指示有错误时才访问它。 strerror函数将返回一个 C 字符串,其中包含您作为参数传递的错误值的描述;这些消息根据通过调用setlocale函数设置的当前 C 语言环境进行本地化。

获取消息描述

要在运行时获取 Win32 错误代码的描述,请使用 Win32FormatMessage函数。 这将获得系统消息或自定义消息的描述(在下一节中介绍)。 如果要使用自定义消息,则必须加载绑定了消息资源的可执行文件(或 DLL),并将HMODULE句柄传递给FormatMessage函数。 如果您想要获取系统消息的描述,则不需要加载模块,因为 Windows 将为您执行此操作。 例如,如果调用 Win32CreateFile函数打开一个文件,但找不到该文件,则该函数将返回值INVALID_HANDLE_VALUE,,指示存在错误。 要获取错误的详细信息,可以调用GetLastError函数(它返回一个 32 位无符号值,有时称为DWORDHRESULT)。 然后可以将错误值传递给FormatMessage

    HANDLE file = CreateFileA( 
        "does_not_exist", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); 
    if (INVALID_HANDLE_VALUE == file) 
    { 
        DWORD err = GetLastError(); 
        char *str; 
        DWORD ret = FormatMessageA( 
            FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER, 
            0, err, LANG_USER_DEFAULT, reinterpret_cast<LPSTR>(&str),  
            0, 0); 
        cout << "Error: "<< str << endl; 
        LocalFree(str); 
    } 
    else 
    { 
        CloseHandle(file); 
    }

此代码尝试打开一个不存在的文件,并获取与故障相关的错误值(该值将为ERROR_FILE_NOT_FOUND)。 然后,代码调用FormatMessage函数来获取描述错误的字符串。 函数的第一个参数是指示函数应该如何工作的标志;在本例中,FORMAT_MESSAGE_FROM_SYSTEM标志表示错误是系统错误,FORMAT_MESSAGE_ALLOCATE_BUFFER标志表示函数应该分配足够大的缓冲区来使用 Win32LocalAlloc函数保存字符串。

If the error is a custom value that you have defined then you should use the FORMAT_MESSAGE_FROM_HMODULE flag, open the file with LoadLibrary and use the resulting HMODULE as the parameter passed in through the second parameter.

第三个参数是错误消息编号(来自GetLastError),第四个参数是指示要使用的语言 ID 的LANGID(在本例中为LANG_USER_DEFAULT,以获取当前登录用户的语言 ID)。 FormatMessage函数将为错误值生成格式化的,该字符串可能有替换参数。 格式化的字符串在缓冲区中返回,您有两种选择:您可以分配一个字符缓冲区并将指针作为第五个参数传入,将长度作为第六个参数传入,或者您可以使用本例中的LocalAlloc函数请求函数分配缓冲区。 要访问函数分配的缓冲区,可以通过第五个参数传递指针变量的地址

请注意,第五个参数用于获取指向用户分配的缓冲区的指针,或者返回系统分配的缓冲区的地址,这就是在本例中必须强制转换指向指针的指针的原因。

某些格式字符串可能有参数,如果有,这些值将通过第七个参数中的数组传入(在本例中,不传递任何数组)。 前面代码的结果是字符串:

    Error: The system cannot find the file specified.

使用消息编译器、资源文件和FormatMessage,您可以提供一种机制来从函数返回错误值,然后根据当前区域设置将这些值转换为本地化字符串。

使用消息编译器

上一个示例显示,您可以获取 Win32 错误的本地化字符串,但也可以创建自己的错误并提供作为资源绑定到进程或库的本地化字符串。 如果您打算向最终用户报告错误,则必须确保描述已本地化。 Windows 提供了一个名为 Message Compiler(mc.exe)的工具,它将获取包含各种语言的消息条目的文本文件,并将它们编译成可以绑定到模块的二进制资源。

例如:

    LanguageNames = (British = 0x0409:MSG00409) 
    LanguageNames = (French  = 0x040c:MSG0040C) 

    MessageId       = 1 
    SymbolicName    = IDS_GREETING 
    Language        = English 
    Hello 
    . 
    Language        = British 
    Good day 
    . 
    Language        = French 
    Salut 
    .

这为同一消息定义了三个本地化字符串。 这里的消息是简单的字符串,但是您可以使用可以在运行时提供的占位符来定义格式化消息。 中立的语言是美国英语,此外,我们还定义了英式英语和法语的字符串。 用于语言的名称在文件顶部的LanguageNames行中定义。 这些条目具有稍后将在文件中使用的名称、语言的代码页以及将包含消息资源的二进制资源的名称。

MessageIdFormatMessage函数将使用的标识符,SymbolicName是将在头文件中定义的预处理器符号,因此您可以在 C++ 代码中使用此消息,而不是数字。 该文件通过将其传递给命令行实用程序mc.exe进行编译,该实用程序将创建五个文件:一个带有符号定义的头文件、三个二进制源(MSG00001.bin,默认情况下为中立语言创建,以及MSG00409.binMSG0040C.bin,,它们是由于LanguageNames行创建的),以及一个资源编译器文件。 对于本例,资源编译器文件(扩展名为.rc)将包含:

    LANGUAGE 0xc,0x1 
    1 11 "MSG0040C.bin" 
    LANGUAGE 0x9,0x1 
    1 11 "MSG00001.bin" 
    LANGUAGE 0x9,0x1 
    1 11 "MSG00409.bin"

这是可由 Windows SDK 资源编译器(rc.exe)编译的标准资源文件,它会将消息资源编译为可绑定到可执行文件或 DLL 的.res文件。 绑定了类型为11的资源的进程或 DLL 可由FormatMessage函数用作描述性错误字符串源。

通常,您不会使用消息 ID 1,因为它不太可能是唯一的,并且您可能希望利用工具代码严重性代码(有关工具代码的详细信息,请查看winerror.h头文件)。 此外,要指示该消息不是 Windows,您可以在运行mc.exe时使用/c开关设置错误代码的客户位。 这意味着您的错误代码不会是像 1 这样的简单值,但这应该无关紧要,因为您的代码将使用头文件中定义的符号。

C++ 异常

顾名思义,例外是针对异常情况的。 这不是正常情况。 它们不是你想要发生的条件,而是可能发生的条件。 任何异常情况通常都意味着您的数据将处于不一致的状态,因此使用异常意味着您需要从事务的角度考虑问题,也就是说,要么操作成功,要么对象的状态应该保持与尝试操作之前的状态相同。 当代码块中发生异常时,代码块中发生的所有内容都将无效。 如果代码块是更大的代码块的一部分(比方说,一个函数是由另一个函数调用的一系列函数),则该另一个代码块中的工作将无效。 这意味着异常可能会向外传播到调用堆栈上方的其他代码块,从而使依赖于操作成功的对象无效。 在某一时刻,异常情况将是可恢复的,因此您需要防止异常进一步发展。

例外规范

异常规范在 C++ 11 中已弃用,但您可能会在早期代码中看到它们。 规范是通过应用于函数声明的throw表达式,给出可以从函数抛出的异常。 throw规范可以是省略号,这意味着函数可以抛出异常,但没有指定类型。 如果规范为空,则意味着函数不会抛出异常,这与在 C++ 11 中使用noexcept说明符相同。

noexcept说明符告诉编译器不需要异常处理,因此如果函数中确实发生了异常,则异常不会从函数中冒泡出来,而会立即调用terminate函数。 在这种情况下,不能保证调用自动对象的析构函数。

C++ 异常语法

在 C++ 中,异常情况是通过抛出异常对象来生成的。 该异常对象可以是您喜欢的任何对象:对象、指针或内置类型,但是因为异常可能由其他人编写的代码处理,所以最好对用于表示异常的对象进行标准化。 为此,标准库提供了exception类,它可以用作基类。

    double reciprocal(double d) 
    { 
        if (d == 0)  
        { 
            // throw 0; 
            // throw "divide by zero"; 
            // throw new exception("divide by zero"); 
            throw exception("divide by zero"); 
        } 
        return 1.0 / d; 
    }

此代码测试参数,如果参数为零,则抛出异常。 这里给出了四个示例,它们都是有效的 C++,但只有最后一个版本是可接受的,因为它使用了一个 Standard Library 类(或从 Standard Library 类派生的一个类),并且它遵循按值抛出异常的约定。

当抛出异常时,异常处理基础结构将接管。 执行将在当前代码块中停止,异常将向上传播到调用堆栈。 当异常通过代码块传播时,将销毁所有自动对象,但不会销毁在代码块中的堆上创建的对象。 这是一个称为堆栈展开的过程,,在异常移动到调用堆栈中它上面的堆栈帧之前,尽可能多地清理每个堆栈帧。 如果没有捕获到异常,它将向上传播到main函数,此时将调用terminate函数来处理该异常(因此它将终止该进程)。

您可以保护代码以处理传播的异常。 代码受try块保护,并受关联的catch块捕获:

    try  
    { 
        string s("this is an object"); 
        vector<int> v = { 1, 0, -1}; 
        reciprocal(v[0]); 
        reciprocal(v[1]); 
        reciprocal(v[2]); 
    } 
    catch(exception& e) 
    { 
        cout << e.what() << endl; 
    }

与 C++ 中的其他代码块不同,即使trycatch块包含单行代码,大括号也是必需的。 在前面的代码中,对reciprocal函数的第二次调用将引发异常。 异常将暂停块中任何更多代码的执行,因此不会发生对reciprocal函数的第三次调用。 相反,异常会传播出代码块。 try块是在大括号之间定义的对象的作用域,这意味着这些对象的析构函数将被调用(sv)。 然后将控制权传递给相关的catch块,在本例中,只有一个处理程序。 catch块是一个独立于try块的块,因此您不能访问try块中定义的任何变量。 这是有意义的,因为当生成异常时,整个代码块被污染,因此您不能信任在该块中创建的任何对象。 此代码使用公认的约定,即通过引用捕获异常,因此捕获的是实际的异常对象,而不是副本。

惯例是:抛出我的值,通过引用捕获。

标准库提供了一个名为uncaught_exception的函数,如果抛出异常但尚未处理,该函数将返回true。 能够对此进行测试似乎很奇怪,因为当发生异常时,除了异常基础设施之外不会调用任何代码(例如,catch处理程序),您应该将异常代码放在那里。 然而,当抛出异常时,还有个代码可以调用:在堆栈清除期间销毁的自动对象的析构函数。 应在析构函数中使用uncaught_exception函数来确定对象是否由于异常而被销毁,而不是由于对象超出范围或被删除而导致的正常对象销毁。 例如:

    class test 
    { 
        string str; 
    public: 
        test() : str("") {} 
        test(const string& s) : str(s) {} 
        ~test() 
        { 
            cout << boolalpha << str << " uncaught exception = " 
             << uncaught_exception() << endl; 
        } 
    };

这个简单的对象指示它是否因为异常堆栈展开而被销毁。 它可以像这样测试:

    void f(bool b) 
    { 
        test t("auto f"); 
        cout << (b ? "f throwing exception" : "f running fine")  
            << endl; 
        if (b) throw exception("f failed"); 
    } 

    int main() 
    { 
        test t1("auto main"); 
        try 
        { 
            test t2("in try in main"); 
            f(false); 
            f(true); 
            cout << "this will never be printed"; 
        } 
        catch (exception& e) 
        { 
            cout << e.what() << endl; 
        } 
        return 0; 
    }

仅当使用true值调用f函数时,它才会引发异常。 main函数调用了两次f,第一次使用值false(因此异常不会在f中抛出),第二次使用true。 输出为:

 f running fine
 auto f uncaught exception = false
 f throwing exception
 auto f uncaught exception = true
 in try in main uncaught exception = true
 f failed
 auto main uncaught exception = false

第一次调用f时,test对象被正常销毁,因此uncaught_exception将返回false。 第二次f被称为函数中的test对象在异常被捕获之前被销毁,因此uncaught_exception将返回true。 因为抛出异常,所以执行离开try块,因此try块中的test对象被销毁,uncaught_exception将返回true。 最后,当异常被处理并且控制在catch块之后返回到代码时,当main函数返回时,在main函数中的堆栈上创建的test对象将被销毁,因此uncaught_exception将返回false

标准异常类

exception类是 C 字符串的一个简单容器:该字符串作为构造函数参数传递,并可通过what访问器使用。 标准库在<exception>库中声明 Exception 类,并鼓励您从中派生自己的 Exception 类。 标准库提供以下派生类;大多数在<stdexcept>中定义。

| 类别 | 抛出 | | bad_alloc | 当new操作员无法分配内存时(在<new>中) | | bad_array_new_length | 当要求new运算符创建长度无效的数组时(在<new>中) | | bad_cast | 当引用类型的dynamic_cast失败时(在<typeinfo>中) | | bad_exception | 发生意外情况(在<exception>中) | | bad_function_call | 调用了空的function对象(在<functional>中) | | bad_typeid | 当typeid的参数为空时(在<typeinfo>中) | | bad_weak_ptr | 访问指向已销毁对象的弱指针时(在<memory>中) | | domain_error | 当尝试在定义操作的域外执行操作时 | | invalid_argument | 当参数使用无效值时 | | length_error | 尝试超出为对象定义的长度时 | | logic_error | 当存在逻辑错误时,例如,类不变量或前置条件 | | out_of_range | 尝试访问为对象定义的范围之外的元素时 | | overflow_error | 当计算结果的值大于目标类型时 | | range_error | 当计算结果的值超出该类型的范围时 | | runtime_error | 当错误发生在代码范围之外时 | | system_error | 包装操作系统错误的基类(在<system_error>中) | | underflow_error | 当计算导致下溢时 |

上表中提到的所有类都有一个接受const char*const string&参数的构造函数,而不是接受 C 字符串的exception类(因此,如果描述通过string对象传递,则使用c_str方法构造基类)。 没有宽字符版本,因此如果要从宽字符串构造异常描述,则必须对其进行转换。 还要注意,标准异常类只有一个构造函数参数,这可以通过继承的what访问器获得。

关于异常可以保存的数据没有绝对规则。 您可以从exception派生一个类,并使用您想要为异常处理程序提供的任何值来构造它。

按类型捕获异常

每个try块可以有多个catch块,这意味着您可以根据异常类型定制异常处理。 catch子句中的参数类型将按照声明的顺序对照异常类型进行测试。 异常将由第一个与异常类型匹配或为基类的处理程序处理。 这突出了通过引用捕获异常对象的约定。 如果将其作为基类对象捕获,则会创建一个副本,对派生类对象进行切片。 在许多情况下,代码将抛出从exception类派生的类型的对象,因此这意味着exception的 Catch 处理程序将捕获所有异常。

由于代码可以引发任何对象,因此异常可能会传播出处理程序。 C++ 允许您通过在catch子句中使用省略号来捕获所有内容。 显然,您应该从派生程度最高的到派生程度最低的顺序对catch处理程序进行排序,并且(如果您使用它)在末尾使用省略号处理程序:

    try  
    { 
        call_code(); 
    } 
    catch(invalid_argument& iva) 
    { 
        cout << "invalid argument: " << e.what() << endl; 
    } 
    catch(exception& exc) 
    { 
        cout << typeid(exc).name() << ": " << e.what() << endl; 
    } 
    catch(...) 
    { 
        cout << "some other C++ exception" << endl; 
    }

如果受保护的代码没有抛出异常,则不会执行catch块。

当您的处理程序检查异常时,它可能会决定不想取消该异常;这称为重新引发异常。 为此,您可以使用不带操作数的throw语句(只允许在catch处理程序中这样做),这将重新抛出捕获的实际异常对象,而不是副本。

异常是基于线程的,因此很难将异常传播到另一个线程。 exception_ptr类(在<exception>中)为任何类型的异常对象提供共享所有权语义。 您可以通过调用make_exception_ptr对象来获取异常对象的共享副本,甚至可以使用current_exception获取catch块中正在处理的异常的共享副本。 这两个函数都返回一个exception_ptr对象。 exception_ptr对象可以包含任何类型的异常,而不仅仅是派生自exception类的异常,因此从包装的异常中获取信息是特定于异常类型的。 exception_ptr对象对这些细节一无所知,因此您可以将其传递给要使用共享异常(另一个线程)的上下文中的rethrow_exception,然后捕获适当的异常对象。 在下面的代码中,有两个线程正在运行。 first_thread函数在一个线程上运行,second_thread函数在另一个线程上运行:

    exception_ptr eptr = nullptr; 

    void first_thread() 
    { 
        try  
        { 
            call_code(); 
        } 
        catch (...)  
        { 
            eptr = current_exception();  
        } 
        // some signalling mechanism ... 
    } 

    void second_thread() 
    { 
        // other code 

        // ... some signalling mechanism 
        if (eptr != nullptr)  
        { 
            try 
            { 
                rethrow_exception(eptr); 
            } 
            catch(my_exception& e) 
            { 
                // process this exception 
            } 
            eptr = nullptr; 
        } 
        // other code 
    }

前面的代码看起来像是使用exception_ptr作为指针。 实际上,eptr被创建为全局对象,对nullptr的赋值使用复制构造函数创建一个空对象(其中包装的异常是nullptr)。 类似地,与nullptr的比较实际上测试了包装的异常。

这本书不是关于 C++ 线程的,所以我们不会深入讨论两个线程之间的信令细节。 此代码显示异常的共享副本任何异常可以存储在一个上下文中,然后在另一个上下文中重新引发和处理。

函数 try 块

您可能决定使用try块来保护整个函数,在这种情况下,您可以编写如下代码:

    void test(double d) 
    { 
        try 
        { 
            cout << setw(10) << d << setw(10) << reciprocal(d) << endl; 
        } 

        catch (exception& e) 
        { 
            cout << "error: " << e.what() << endl; 
        } 
    }

这使用了前面定义的reciprocal函数,如果参数为零,该函数将抛出exception。 另一种替代语法是:

    void test(double d) 
    try 
    { 
        cout << setw(10) << d << setw(10) << reciprocal(d) << endl; 
    } 
    catch (exception& e) 
    { 
        cout << "error: " << e.what() << endl; 
    }

这看起来相当奇怪,因为函数原型后面紧跟着try... catch块,而且没有外部花括号。 函数体是try块中的代码;当此代码完成时,函数返回。 如果函数返回值,它必须在try块中返回值。 在大多数情况下,您会发现这种语法降低了代码的可读性,但有一种情况下它可能有用--对于构造函数中的初始值设定项列表。

    class inverse 
    { 
        double recip; 
    public: 
        inverse() = delete; 
        inverse(double d) recip(reciprocal(d)) {} 
        double get_recip() const { return recip; } 
    };

在这段代码中,我们包装了一个double值,它只是传递给构造函数的参数的倒数。 通过调用初始化器列表中的reciprocal函数来初始化数据成员。 由于这在构造函数主体之外,因此此处发生的异常将直接传递给调用构造函数的代码。 如果您想做一些额外的处理,那么可以在构造函数体内调用倒数函数:

    inverse::inverse(double d)  
    {  
        try { recip = reciprocal(d); } 
        catch(exception& e) { cout << "invalid value " << d << endl; } 
    }

需要注意的是,异常将被自动重新抛出,因为构造函数中的任何异常都意味着对象无效。 但是,如果需要,这确实允许您执行一些额外的处理。 此解决方案不适用于基对象构造函数中引发的异常,因为尽管您可以在派生构造函数体中调用基构造函数,但编译器将自动调用默认构造函数。 如果希望编译器调用默认构造函数以外的构造函数,则必须在初始值设定项列表中调用它。 在inverse构造函数中提供异常代码的另一种语法是使用函数try块:

    inverse::inverse(double d)  
    try 
        : recip (reciprocal(d)) {}  
    catch(exception& e) { cout << "invalid value " << d << endl; }

这看起来有点杂乱无章,但构造函数体仍然在初始化式列表之后,为recip数据成员提供初始值。 调用reciprocal的任何异常都将被捕获,并在处理后自动重新抛出。 初始值设定项列表可以包含对基类和任何数据成员的调用,所有这些都将受到try块的保护。

系统错误

<system_error>库定义了一系列类来封装系统错误。 error_category类提供了一种将数值错误值转换为本地化描述性字符串的机制。 通过<system_error>中的generic_categorysystem_category函数可以获得两个对象,<ios>有一个名为isostream_category的函数;所有这些函数都返回一个error_category对象。 error_category类有一个名为message的方法,它返回作为参数传递的错误号的字符串描述。 从generic_category函数返回的对象将返回 POSIX 错误的描述性字符串,因此您可以使用它来获取errno值的描述。 从system_category函数返回的对象将使用FORMAT_MESSAGE_FROM_SYSTEM参数通过 Win32FormatMessage函数返回错误描述,因此这可用于获取string对象中 Windows 错误消息的描述性消息。

Note that message has no extra parameters to pass in values for a Win32 error message that takes parameters. Consequently, in those situations you will get back a message that has formatting placeholders.

尽管名称不同,isostream_category对象本质上返回的描述与generic_category对象相同。 system_error异常是一个报告由error_category对象之一描述的值的类。 例如,这是早先针对FormatMessage使用的示例,但使用system_error重写:

    HANDLE file = CreateFileA( 
       "does_not_exist", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); 
    if (INVALID_HANDLE_VALUE == file) 
    { 
        throw system_error(GetLastError(), system_category()); 
    } 
    else 
    { 
        CloseHandle(file); 
    }

这里使用的system_error构造函数将错误值作为第一个参数(从 Win32 函数GetLastError返回的ulong)和一个system_category对象,该对象用于在调用system_error::what方法时将错误值转换为描述性字符串。

嵌套异常

catch块可以通过调用不带任何操作数的throw来重新抛出当前异常,并且在调用堆栈到达下一个try块之前将进行堆栈展开。 您还可以重新抛出嵌套在另一个异常中的当前异常*。 这是通过调用throw_with_nested函数(在<exception>中)并传递新异常来实现的。 该函数调用current_exception并将异常对象与参数一起包装在嵌套异常中,然后抛出该参数。 调用堆栈上方的try块可以捕获此异常,但它只能访问外部异常;它不能直接访问内部异常。 相反,内部异常可以通过调用rethrow_if_nested抛出。 例如,以下是用于打开文件的另一个版本的代码:*

    void open(const char *filename) 
    { 
        try  
        { 
            ifstream file(filename); 
            file.exceptions(ios_base::failbit); 
            // code if the file exists 
        } 
        catch (exception& e)  
        { 
            throw_with_nested( 
                system_error(ENOENT, system_category(), filename)); 
        } 
    }

代码打开一个文件,如果该文件不存在,则设置一个状态位(您可以稍后通过调用rdstat方法来测试这些位)。 下一行指示抛出异常的类应该处理的状态位的值,在本例中提供了ios_base::failbit。 如果构造函数无法打开文件,则将设置此位,因此exceptions方法将以抛出异常作为响应。 在本例中,异常被捕获并包装到嵌套异常中。 外部异常是一个system_error异常,它被初始化为错误值ENOENT(表示文件不存在)和一个error_category对象来解释它,并将文件名作为附加信息传递。

此函数可按如下方式调用:

    try 
    { 
        open("does_not_exist"); 
    } 
    catch (exception& e) 
    { 
        cout << e.what() << endl; 
    }

可以访问此处捕获的异常,但它只提供有关外部对象的信息:

 does_not_exist: The system cannot find the file specified.

此消息由system_error对象使用传递给其构造函数的附加信息和来自 CATEGORY 对象的描述来构造。 要在嵌套异常中获取内部对象,您必须通过调用rethrow_if_nested告诉系统抛出内部异常。 因此,您可以调用如下函数,而不是打印出外部异常:

    void print_exception(exception& outer) 
    { 
        cout << outer.what() << endl; 
        try { rethrow_if_nested(outer); } 
        catch (exception& inner) { print_exception(inner); } 
    }

这将打印外部异常的描述,然后调用rethrow_if_nested,,只有在异常是嵌套的情况下才会抛出该异常。 如果是,它抛出内部异常,然后捕获该异常并递归调用print_exception函数。 结果是:

    does_not_exist: The system cannot find the file specified. 
    ios_base::failbit set: iostream stream error

最后一行是调用ifstream::exception方法时抛出的内部异常。

结构化异常处理

Windows 中的本机异常是结构化异常处理(SEH),Visual C++ 有一个语言扩展来允许您捕获这些异常。 重要的是要理解,它们与 C++ 异常不同,C++ 异常被编译器认为是同步的,也就是说,编译器知道一个方法是否可能(或者明确地说,不会)抛出 C++ 异常,并且它在分析代码时使用该信息。 C++ 异常也按类型捕获。 SEH 不是一个 C++ 概念,因此编译器将结构化异常视为异步,这意味着它将受 SEH 保护的块中的任何代码视为潜在地引发结构化异常,因此编译器无法执行优化。 Seh 异常也由异常代码捕获。

SEH 的语言扩展是对 Microsoft C/C++ 的扩展,也就是说,它们既可以在 C++ 中使用,也可以在 C++ 中使用,因此处理基础结构不知道对象析构函数。 此外,当您捕获 SEH 异常时,不会对堆栈或进程的任何其他部分的状态做出任何假设。

尽管大多数 Windows 函数将以适当的方式捕获内核生成的 SEH 异常,但有些函数故意允许它们传播(例如,远程过程调用(RPC)函数,或用于内存管理的函数)。 使用某些 Windows 函数,您可以显式请求使用 SEH 异常处理错误。 例如,HeapCreate组函数将允许 Windows 应用创建私有堆,您可以传递HEAP_GENERATE_EXCEPTIONS标志来指示创建堆以及在私有堆中分配或重新分配内存时出现的错误将生成 SEH 异常。 这是因为调用这些函数的开发人员可能会认为故障非常严重,无法恢复,因此进程应该终止。 由于 SEH 是如此严重的情况,您应该仔细检查除了报告异常的详细信息并终止该过程之外,是否适合(并非完全不可能)做更多的事情。

SEH 异常本质上是低级操作系统异常,但熟悉语法很重要,因为它看起来类似于 C++ 异常。 例如:

    char* pPageBuffer; 
    unsigned long curPages = 0; 
    const unsigned long PAGESIZE = 4096; 
    const unsigned long PAGECOUNT = 10; 

    int main() 
    { 
        void* pReserved = VirtualAlloc( 
        nullptr, PAGECOUNT * PAGESIZE, MEM_RESERVE, PAGE_NOACCESS); 
        if (nullptr == pReserved)  
        { 
            cout << "allocation failed" << endl; 
            return 1; 
        } 

        char *pBuffer = static_cast<char*>(pReserved); 
        pPageBuffer = pBuffer; 

        for (int i = 0; i < PAGECOUNT * PAGESIZE; ++ i) 
        { 
            __try { pBuffer[i] = 'X'; } __except (exception_filter(GetExceptionCode())) { cout << "Exiting process.n"; ExitProcess(GetLastError()); } 
        } 
        VirtualFree(pReserved, 0, MEM_RELEASE); 
        return 0; 
    }

此处突出显示 SEH 异常代码。 此代码使用 WindowsVirtualAlloc函数保留若干页内存。 保留不会分配内存,该操作必须在称为提交内存的单独操作中执行。 Windows 将在称为的块中保留(和提交)内存,并且在大多数系统上,正如这里假设的那样,页是 4096 字节。 对VirtualAlloc函数的调用表明它应该保留 10 个 4096 字节的页面,这些页面将在以后提交(和使用)。

VirtualAlloc的第一个参数指示内存的位置,但由于我们是在保留内存,所以这并不重要,因此传递了nullptr。 如果保留成功,则向内存返回一个指针。 for循环每次只向内存写入一个字节的数据。 突出显示的代码通过结构化异常处理保护这种内存访问。 受保护的块以__try关键字开头。 当引发 SEH 时,执行转到__except块。 这与 C++ 异常中的catch块非常不同。 首先,__except异常处理程序接收三个值中的一个,以指示它应该如何行为。 只有当这是EXCEPTION_EXECUTE_HANDLER时,处理程序块中的代码才会运行(在此代码中,突然关闭进程)。 如果值为EXCEPTION_CONTINUE_SEARCH,则不识别异常,搜索将继续向上堆栈*,但不展开 C++ 堆栈*。 令人惊讶的值是EXCEPTION_CONTINUE_EXECUTION,,因为这会忽略异常,__try块中的执行将继续。 对于 C++ 异常,您不能这样做。 通常,SEH 代码将使用异常筛选器函数来确定__except处理程序需要执行什么操作。 在此代码中,此筛选器称为exception_filter,,向其传递通过调用 Windows 函数GetExceptionCode获得的异常代码。 此语法非常重要,因为此函数只能在__except上下文中调用。

循环第一次运行时没有提交内存,因此写入内存的代码将引发异常:页面错误。 执行将传递到异常处理程序并传递到exception_filter

    int exception_filter(unsigned int code) 
    { 
        if (code != EXCEPTION_ACCESS_VIOLATION) 
        { 
            cout << "Exception code = " << code << endl; 
            return EXCEPTION_EXECUTE_HANDLER; 
        } 

        if (curPage >= PAGECOUNT) 
        { 
            cout << "Exception: out of pages.n"; 
            return EXCEPTION_EXECUTE_HANDLER; 
        } 

        if (VirtualAlloc(static_cast<void*>(pPageBuffer), PAGESIZE, 
         MEM_COMMIT, PAGE_READWRITE) == nullptr) 
        { 
            cout << "VirtualAlloc failed.n"; 
            return EXCEPTION_EXECUTE_HANDLER; 
        } 

        curPage++ ; 
        pPageBuffer += PAGESIZE; 
        return EXCEPTION_CONTINUE_EXECUTION; 
    }

在 SEH 代码中,重要的是只处理您知道的异常,并且只有在您知道条件已经完全解决的情况下才使用异常。 如果您访问尚未提交的 Windows 内存,操作系统会生成一个称为页面错误的异常。 在这段代码中,测试异常代码以确定它是否是页面错误,如果不是,筛选器返回,通知异常处理程序运行终止进程的异常处理程序块中的代码。 如果异常是页面错误,那么我们可以提交下一页。 首先,要测试页码是否在我们将使用的范围内(如果不在,则关闭该进程)。 然后,通过另一次调用VirtualAlloc来提交下一页,以标识要提交的页以及该页中的字节数。 如果函数成功,它将返回指向提交页的指针或空值。 只有在提交页面成功时,筛选器才会返回值EXCEPTION_CONTINUE_EXECUTION,这表示异常已得到处理,并且可以在引发异常的时间点继续执行。 此代码是使用VirtualAlloc的标准方式,因为它意味着只有在需要内存分页时才会提交它们。

SEH 还有终止处理程序的概念。 当执行通过调用return离开__try代码块时,或者通过完成块中的所有代码,或者通过调用 Microsoft 扩展__leave指令,或者引发 SEH,则调用标记为__finally的终止处理程序代码块。 由于终止处理程序总是被调用,所以无论如何退出__try块,都可以将其用作释放资源的一种方式。 然而,由于 SEH 不执行 C++ 堆栈展开(也不调用析构函数),这意味着您不能在具有 C++ 对象的函数中使用此代码。 事实上,编译器将拒绝编译具有 SEH 并创建了 C++ 对象的函数,无论是在函数堆栈上还是在堆上分配的。 (但是,您可以使用全局对象或在调用函数时分配并作为参数传入的对象。)。 __try/__finally结构看起来很有用,但受到不能与创建 C++ 对象的代码一起使用的要求的限制。

编译器异常开关

在这一点上,有必要解释一下为什么要使用/EHsc开关编译代码。 简单的答案是,如果您不使用此开关,编译器将从标准库代码发出警告,并且由于标准库使用异常,因此您必须使用/EHsc开关。 警告告诉您要这样做,所以这就是您要做的。

长长的答案是,/EH开关有三个参数,您可以使用它们来影响异常的处理方式。 使用s参数告诉编译器为同步异常提供基础结构,即可能在try块中抛出并在catch块中处理的 C++ 异常,并且具有调用自动 C++ 对象析构函数的堆栈展开。 c参数指示extern C函数(即所有 Windows SDK 函数)从不抛出 C++ 异常(因此编译器可以执行更高级别的优化)。 因此,您可以使用/EHs/EHsc编译标准库代码,但后者将生成更优化的代码。 还有一个额外的参数,其中/EHa表示代码将使用try/catch块捕获同步和异步异常(SEH)。

混合使用 C++ 和 SEH 异常处理

RaiseExceptionWindows 函数将抛出 SEH 异常。 第一个参数是异常代码,第二个参数表示处理此异常后进程是否可以继续(0表示可以)。 第三个和第四个参数提供有关异常的附加信息。 第四个参数是指向具有这些附加参数的数组的指针,参数的数量在第三个参数中给出。

使用/EHa,您可以编写如下代码:

    try  
    { 
        RaiseException(1, 0, 0, nullptr); 
    } 
    // legal code, but don't do it 
    catch(...) 
    { 
        cout << "SEH or C++ exception caught" << endl; 
    }

此代码的问题在于它处理所有 SEH 异常。 这相当危险,因为某些 SEH 异常可能指示进程状态已损坏,因此进程继续运行是危险的。 C 运行库提供了一个名为_set_se_translator的函数,该函数提供了一种机制来指示哪些 SEH 异常由try处理。 使用此原型编写的函数会向此函数传递一个指针:

    void func(unsigned int, EXCEPTION_POINTERS*);

第一个参数是异常代码(将从GetExceptionCode函数返回),第二个参数是从GetExceptionInformation函数返回的,并且具有与异常相关的任何附加参数(例如,通过RaiseException中的第三个和第四个参数传递的参数)。 您可以使用这些值在 SEH 的位置抛出 C++ 异常。 如果您提供此功能:

    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) 
    { 
        if (code == 1) throw exception("my error"); 
    }

现在,您可以在处理 SEH 异常之前注册该函数:

    _set_se_translator(seh_to_cpp); 
    try  
    { 
        RaiseException(1, 0, 0, nullptr); 
    } 
    catch(exception& e) 
    { 
        cout << e.what() << endl; 
    }

在这段代码中,RaiseException函数产生一个值为 1 的自定义 SEH。这个转换可能不是最有用的,但它说明了这一点。 winnt.h头文件定义了可以在 Windows 代码中引发的标准 SEH 异常的异常代码。 更有用的翻译功能是:

    double reciprocal(double d) 
    { 
        return 1.0 / d; 
    } 

    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) 
    { 
        if (STATUS_FLOAT_DIVIDE_BY_ZERO == code || 
            STATUS_INTEGER_DIVIDE_BY_ZERO == code) 
        { 
            throw invalid_argument("divide by zero"); 
        } 
    }

这允许您按如下方式调用倒数函数:

    _set_se_translator(seh_to_cpp); 
    try  
    { 
        reciprocal(0.0); 
    } 
    catch(invalid_argument& e) 
    { 
        cout << e.what() << endl; 
    }