理解智能指针
管理堆(或自由存储区)中的内存时,C++程序员并非一定要使用常规指针,而可使用智能指针。
什么是智能指针
简单地说,C++智能指针是包含重载运算符的类,其行为像常规指针,但智能指针能够及时、妥善地销毁动态分配的数据,并实现了明确的对象生命周期,因此更有价值。
常规(原始)指针存在的问题
与其他现代编程语言不同,C++在内存分配、释放和管理方面向程序员提供了全面的灵活性。不幸的是,这种灵活性是把双刃剑,一方面,它使 C++成为一种功能强大的语言,另一方面,它让程序员能够制造与内存相关的问题,如动态分配的对象没有正确地释放时将导致内存泄露。
智能指针有何帮助
鉴于使用常规指针以及常规的内存管理方法存在的问题,当 C++程序员需要管理堆(自由存储区)中的数据时,并非一定要使用它们,而可在程序中使用智能指针,以更智能的方式分配和管理内存。
智能指针的行为类似常规指针(这里将其称为原始指针),但通过重载的运算符和析构函数确保动态分配的数据能够及时地销毁,从而提供了更多有用的功能。
智能指针是如何实现的
这个问题暂时可以简化为:“智能指针 spData 是如何做到像常规指针那样的?”
答案如下:智能指针类重载了解除引用运算符(*)和成员选择运算符(->),让程序员可以像使用常规指针那样使用它们。
另外,为了让您能够在堆中管理各种类型,几乎所有良好的智能指针类都是模板类,包含其功能的泛型实现。由于是模板,它们是通用的,可以根据要管理的对象类型进行具体化。
一个简单智能指针类的实现:
template<typename T>
class smart_pointer {
private:
T* rawPtr; //原始指针
public:
smart_pointer(T* pData):rawPtr(pData){} //构造器
~smart_pointer() { delete rawPtr; } //析构函数
//拷贝构造
smart_pointer(const smart_pointer& anotherSP);
//赋值运算符重载
smart_pointer& operator=(const smart_pointer& anotherSP);
T& operator*() const { //解引用运算符
return *rawPtr;
}
T* operator->() const { //成员选择运算符
return rawPtr;
}
};
该智能指针类实现了两个运算符:* 和 -> ,它们让这个类能够用作常规意义上的“指针”。
例如,如果有一个 Tuna 类,则可这样对该类型的对象使用智能指针:
smart_pointer <Tuna> smartTuna (new Tuna);
smartTuna->Swim();
// 另一种调用方式
(*smartTuna).Swim ();
这个 smart_pointer 类还没有实现使其非常智能,从而胜于常规指针的功能。构造函数接受一个指针,并将其保存到该智能指针类内部的一个指针对象中。析构函数释放该指针,从而实现了自动内存释放。
智能指针类型
内存资源管理(即实现的内存所有权模型)是智能指针类与众不同的地方。智能指针决定在复制和赋值时如何处理内存资源。最简单的实现通常会导致性能问题,而最快的实现可能并非适合所有应用程序。因此,在应用程序中使用智能指针前,程序员应理解其工作原理。
智能指针的分类实际上就是内存资源管理策略的分类,可分为如下几类:
• 深复制;
• 写时复制(Copy on Write,COW);
• 引用计数;
• 引用链接;
• 破坏性复制。
下面首先简要地介绍一下这些策略,再探索 C++标准库提供的智能指针 std::unique_ptr。
深复制
在实现深复制的智能指针中,每个智能指针实例都保存一个它管理的对象的完整副本。每当智能指针被复制时,将复制它指向的对象(因此称为深复制)。每当智能指针离开作用域时,将(通过析构函数)释放它指向的内存。
虽然基于深复制的智能指针看起来并不比按值传递对象优越,但在处理多态对象时,其优点将显现出来。使用智能指针可避免切除(slicing)问题。
在C++中,“切除”通常指的是在对象拷贝过程中,派生类对象被当作基类对象处理时,派生类特有的部分(即基类中没有的部分)被“切除”或丢失了。这通常发生在以下几种情况:
1、按值传递派生类对象给接受基类对象的函数:
当你将一个派生类对象按值传递给一个接受基类对象的函数时,派生类对象会被隐式转换为基类对象,派生类特有的部分不会被传递。
2、从派生类对象赋值给基类对象:
类似地,当你尝试将一个派生类对象赋值给一个基类对象时,也会发生切除。
如果程序员选择使用深复制智能指针,便可解决切除问题。
使用基于深复制的智能指针将多态对象作为基类对象进行传递:
template<typename T>
class deepcopy_smart_ptr {
private:
T* object;
public:
//...other function
//对指针进行深复制
deepcopy_smart_ptr(const deepcopy_smart_ptr& source) {
//Clone() 是一个虚构的,确保派生类对象执行了深复制
object = source->Clone();
}
//赋值运算符重载
deepcopy_smart_ptr& operator=(const deepcopy_smart_ptr& source) {
if (object) {
delete object;
}
object = source->Clone();
}
};
可以看到,deepcopy_smart_ptr 实现了一个复制构造函数,使得能够通过函数 Clone( ) 函数对多态对象进行深复制—类必须实现函数 Clone( )。另外,它还实现了复制赋值运算符。为了简单起见,这里假设基类 Fish 实现的虚函数为 Clone()。通常,实现深复制模型的智能指针通过模板参数或函数对象提供该函数。
下面是 deepcopy_smart_ptr 的一种用法:
deepcopy_smart_ptr<Carp> freshWaterFish(new Carp);
MakeFishSwim (freshWaterFish); // Carp will not be 'sliced'
构造函数实现的深复制将发挥作用,确保传递的对象不会出现切除问题—虽然从语法上说,目标函数 MakeFishSwim( )只要求基类部分。
基于深复制的机制的不足之处在于性能。对有些应用程序来说,这可能不是问题,但对于其他很多应用程序来说,这可能导致程序员不使用智能指针,而将指向基类的指针(常规指针 Fish*)传递给函数,如 MakeFishSwim( )。其他指针类型以各种方式试图解决这种性能问题。
写时复制机制
写时复制机制(Copy on Write,COW)试图对深复制智能指针的性能进行优化,它共享指针,直到首次写入对象。首次调用非 const 函数时,COW 指针通常为该非 const 函数操作的对象创建一个副本,而其他指针实例仍共享源对象。
COW 深受很多程序员的喜欢。实现 const 和非 const 版本的运算符*和->,是实现 COW 指针功能的关键。非 const 版本用于创建副本。
重要的是,选择 COW 指针时,在使用这样的实现前务必理解其实现细节。否则,复制时将出现复制得太少或太多的情况。
引用计数智能指针
引用计数是一种记录对象的用户数量的机制。当计数降低到零后,便将对象释放。因此,引用计数提供了一种优良的机制,使得可共享对象而无法对其进行复制。如果读者使用过微软的 COM 技术,肯定知道引用计数的概念。
这种智能指针被复制时,需要将对象的引用计数加 1。至少有两种常用的方法来跟踪计数:
• 在对象中维护引用计数;
• 引用计数由共享对象中的指针类维护。
前者称为入侵式引用计数,因为需要修改对象以维护和递增引用计数,并将其提供给管理对象的智能指针。COM 采取的就是这种方法。后者是智能指针类将计数保存在自由存储区(如动态分配的整型),复制时复制构造函数将这个值加 1。
因此,使用引用计数机制,程序员只应通过智能指针来处理对象。在使用智能指针管理对象的同时让原始指针指向它是一种糟糕的做法,因为智能指针将在它维护的引用计数减为零时释放对象,而原始指针将继续指向已不属于当前应用程序的内存。引用计数还有一个独特的问题:如果两个对象分别存储指向对方的指针,这两个对象将永远不会被释放,因为它们的生命周期依赖性导致其引用计数最少为 1。
引用链接智能指针
引用链接智能指针不主动维护对象的引用计数,而只需知道计数什么时候变为零,以便能够释放对象。
之所以称为引用链接,是因为其实现是基于双向链表的。通过复制智能指针来创建新智能指针时,新指针将被插入到链表中。当智能指针离开作用域进而被销毁时,析构函数将把它从链表中删除。与引用计数的指针一样,引用链接指针也存在生命周期依赖性导致的问题。
破坏性复制
破坏性复制是这样一种机制,即在智能指针被复制时,将对象的所有权转交给目标指针并重置原来的指针。
destructive_copy_smartptr <SampleClass> smartPtr (new SampleClass ());
SomeFunc (smartPtr); // Ownership transferred to SomeFunc
// Don't use smartPtr in the caller any more!
虽然破坏性复制机制使用起来并不直观,但它有一个优点,即可确保任何时刻只有一个活动指针指向对象。因此,它非常适合从函数返回指针以及需要利用其“破坏性”的情形。
使用 std::unique_ptr
std::unique_ptr 是 C++11 新增的,与 auto_ptr 稍有不同,因为它不允许复制和赋值。
使用 std::unique_ptr:
#include <iostream>
#include <memory>
using namespace std;
class Fish {
public:
Fish(){cout << "Fish: Constructed!" << endl;}
~Fish() { cout << "Fish: Destructed!" << endl; }
void Swim() const { cout << "Fish swims in water" << endl; }
};
void MakeFishSwim(const unique_ptr<Fish>& inFish){
inFish->Swim();
}
int main() {
unique_ptr<Fish> smartFish(new Fish);
smartFish->Swim();
MakeFishSwim(smartFish);
unique_ptr<Fish> copySmartFish;
//copySmartFish = smartFish; error: operator= is private
return 0;
}
从输出可知,虽然 smartFish 指向的对象是在 main( )中创建的,但它被自动销毁,您无需调用 delete 运算符。这是 unique_ptr 的行为:当指针离开作用域时,将通过析构函数释放它拥有的对象。注意到上面代码中将 smartFish 作为参数传递给了 MakeFishSwim( ),这样做不会导致复制,因为 MakeFishSwim( ) 的参数为引用。如果删除引用符号&,将出现编译错误,因为复制构造函数是私有的。同样,不能将一个 unique_ptr 对象赋给另一个 unique_ptr 对象,因为复制赋值运算符是私有的。
总之,unique_ptr 比 C++11 已摒弃的 auto_ptr 更安全,因为复制和赋值不会导致源智能指针对象无效。它在销毁时释放对象,可帮助您进行简单的内存管理。
深受欢迎的智能指针库
显然,C++标准库提供的智能指针并不能满足所有程序员的需求,这就是还有很多其他智能指针库的原因。
Boost(www.boost.org)提供了一些经过测试且文档完善的智能指针类,还有很多其他的实用类。有关 Boost 智能指针的更详细信息,请访问 http://www.boost.org/libs/ smart_ptr/smart_ptr.htm,在这里还可下载相关的库。
总结
本章介绍了使用正确的智能指针有助于编写使用指针的代码,并有助于减少与内存分配和对象拥有权相关的问题。本章还介绍了各种智能指针类型,并指出在应用程序中使用智能指针类前务必要了解其行为。现在您知道,不应使用 std::auto_ptr,因为它在复制和赋值时导致源指针无效。您还学习了智能指针类 std::unique_ptr,这是 C++11 新增的。
使用流进行输入和输出
流的概述
假设您要开发一个程序,它从磁盘读取数据,将数据显示到屏幕上,从键盘读取用户输入以及将数据存储到磁盘中。在这种情况下,倘若不管数据来自或前往什么设备或位置,都能以相同的方式处理读写操作,那该有多好!这正是 C++流提供的功能。
C++流是读写(输入和输出)逻辑的通用实现,让您能够用统一的模式读写数据。
您只需使用合适的流类,类的实现将负责处理与设备和操作系统相关的细节。
std::cout << "Hello World!" << std::endl;
std:cout 是 ostream 类的一个对象,用于输出到控制台。要使用 std::cout,需要包含提供它的头文件<iostream>,这个头文件还提供了 std::cin,让您能够从流中读取数据。
那么,我说流让您能够以一致的方式访问不同的设备时,是什么意思呢?如果要将 Hello World 写入文本文件,可将同样的语法用于文件流对象 fsHello:
fsHello << "Hello world" << endl; //"Hello world"将被写入文件流中
选择正确的流类后,将 Hello World 写入文件与将其显示到屏幕上并没有太大的不同。
本章将从实用的角度探讨流。
重要的 C++流类和流对象
C++提供了一组标准类和头文件,可帮助您执行重要而常见的输入/输出操作。
使用流类时,可指定为您执行特定操作的控制符(manipulator)。std::endl 就是一个这样的控制符,您一直在使用它来插入换行符。
使用 std::cout 将指定格式的数据写入控制台
std::cout 用于写入到标准输出流,它可能是本书前面使用得最多的流。下面更详细地介绍它,并使用一些控制符来改变数据的对齐和显示方式。
使用 std::cout 修改数字的显示格式
可以让 cout 以十六进制或八进制方式显示整数。程序清单 27.1 演示了如何使用 cout 以各种格式显
示输入的数字。
#include <iostream>
#include <iomanip> //控制符的头文件
using namespace std;
int main() {
cout << "Enter an integer: ";
int input = 0;
cin >> input;
cout << "八进制:" << oct << input << endl;
cout << "十六进制: " << hex << input << endl;
cout << "Integer in hex using base notation: ";
// 让 cout 以十六进制方式(并使用大写字母)显示该数字,其结果是 cout 将 253 显示为 OXFD
cout << setiosflags(ios_base::hex | ios_base::showbase | ios_base::uppercase);
cout << input << endl;
cout << "Integer after resetting I/O flags: ";
// 使用resetiosflags(),其效果是再次使用 cout 显示该整数时,将显示为十进制
cout << resetiosflags(ios_base::hex | ios_base::showbase | ios_base::uppercase);
cout << input << endl;
return 0;
}
对于诸如 Pi 等数字,可指定 cout 显示它们时使用的精度(小数点后面的位数),还可指定以定点表示法或科学表示法显示它们。程序清单 27.2 演示了如何设置这些格式。
#include <iostream>
#include <iomanip> //控制符的头文件
using namespace std;
int main() {
const double Pi = (double)22.0 / 7;
cout << "Pi = " << Pi << endl;
cout << endl << "显示七位有效数字:" << endl;
// 设置显示七位有效数字
cout << setprecision(7);
cout << "Pi: " << Pi << endl;
// 让流以定点表示法显示数据
cout << fixed << "Fixed Pi = " << Pi << endl;
// 让流以科学计数法显示数据
cout << scientific << "Scientific Pi = " << Pi << endl;
return 0;
}
使用 std::cout 对齐文本和设置字段宽度
可使用 setw( )控制符来设置字段宽度,插入到流中的内容将在指定宽度内右对齐。在这种情况下,还可使用 setfill( )指定使用什么字符来填充空白区域:
#include <iostream>
#include <iomanip> //控制符的头文件
using namespace std;
int main() {
cout << "Hey - default!" << endl;
//插入到流中的内容将在指定宽度内右对齐
cout << setw(35); //设置35个字段宽度
cout << "Hey - right aligned!" << endl;
//setfill( )指定使用什么字符来填充空白区域
cout << setw(35) << setfill('*');
cout << "Hey - right aligned!" << endl;
cout << "Hey - back to default!" << endl;
return 0;
}
使用 std::cin 进行输入
std::cin 用途广泛,让您能够将输入读取到基本类型(如 int、double 和 char*)变量中。您还可使用 getline( )从键盘读取一行输入。
使用 std::cin 将输入读取到基本类型变量中
比较基础,略。
使用 std::cin:get 将输入读取到 char* 缓冲区中
cin 让您能够将输入直接写入 int 变量,也可将输入直接写入 char 数组(C 风格字符串):
cout << "Enter a line: " << endl;
char charBuf [10] = {0}; // can contain max 10 chars
cin >> charBuf; // Danger: user may enter more than 10 chars
写入 C 风格字符串缓冲区时,务必不要超越缓冲区的边界,以免导致程序崩溃或带来安全隐患,这至关重要。因此,将输入读取到 char 数组(C 风格字符串)时,下面是一种更好的方法:
cout << "Enter a line: " << endl;
char charBuf[10] = {0};
cin.get(charBuf, 9); // stop inserting at the 9th character
这种将文本插入到 char 数组(C 风格字符串)的方式更安全。
使用 std::cin 将输入读取到 std::string 中
cin 用途广泛,甚至可使用它将用户输入的字符串直接读取到 std::string 中:
std::string input;
cin >> input; // stops insertion at the first space
但是要注意:cin 遇到空白后将停止插入。
要读取整行输入(包括空白),需要使用 getline( ):
string name;
getline(cin, name);
使用 getline( )和 cin 读取整行用户输入:
#include <iostream>
#include <string>
using namespace std;
int main() {
cout << "Enter your name: ";
string name;
getline(cin, name);
cout << "Hi " << name << endl;
return 0;
}
getline( )确保不跳过空白字符,现在输出包含整行用户输入。
使用 std::fstream 处理文件
C++提供了 std::fstream,旨在以独立于平台的方式访问文件。std::fstream 从 std::ofstream 那里继承了写入文件的功能,并从 std::ifstream 那里继承了读取文件的功能。
换句话说,std::fstream 提供了读写文件的功能。
使用 open( )和 close( )打开和关闭文件
要使用 fstream、ofstream 或 ifstream 类,需要使用方法 open( )打开文件:
fstream myFile;
myFile.open("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
if (myFile.is_open()) // check if open() succeeded
{
// do reading or writing here
myFile.close();
}
open( )接受两个参数:第一个是要打开的文件的路径和名称(如果没有提供路径,将假定为应用程序的当前目录设置);第二个是文件的打开模式。在上述代码中,指定了模式 ios_base::trunc(即便指定的文件存在,也重新创建它)、ios_base::in(可读取文件)和 ios_base::out(可写入文件)。
注意到在上述代码中使用了 is_open( ),它检测 open( )是否成功。
还有另一种打开文件流的方式,那就是使用构造函数:
fstream myFile("HelloFile.txt",ios_base::in|ios_base::out|ios_base::trunc);
如果只想打开文件进行写入,可使用如下代码:
ofstream myFile("HelloFile.txt", ios_base::out);
如果只想打开文件进行读取,可使用如下代码:
ifstream myFile("HelloFile.txt", ios_base::in);
另外可在下述各种模式下打开文件流。
• ios_base::app:附加到现有文件末尾,而不是覆盖它。
• ios_base::ate:切换到文件末尾,但可在文件的任何地方写入数据。
• ios_base::trunc:导致现有文件被覆盖,这是默认设置。
• ios_base::binary:创建二进制文件(默认为文本文件)。
• ios_base::in:以只读方式打开文件。
• ios_base::out:以只写方式打开文件。
使用 open( )创建文本文件并使用运算符<<写入文本
有打开的文件流后,便可使用运算符<<向其中写入文本。
使用 open( )和运算符>>读取文本文件
要读取文件,可使用 fstream 或 ifstream,并使用标志 ios_base::in 打开它。
读写二进制文件
写入二进制文件的流程与前面介绍的流程差别不大,重要的是在打开文件时使用 ios_base::binary 标志。通常使用 ofstream::write 和 ifstream::read 来读写二进制文件。
关于记忆文件 IO 的一个技巧
举例:
ofstream myFile;
myFile.open("HelloFile.txt", ios_base::out);
如果指定的 out,那么意味着从程序内部输出内容到文件 HelloFile.txt 中。
ifstream myFile;
myFile.open("HelloFile.txt", ios_base::in);
如果指定的 in,那么意味着程序要从文件 HelloFile.txt 中读取内容进入程序中。
使用 std::stringstream 对字符串进行转换
假设您有一个字符串,它包含字符串值 45,如何将其转换为整型值 45 呢?如何将整型值 45 转换为字符串 45 呢?C++提供的 stringstream 类是最有用的工具之一,让您能够执行众多的转换操作。
演示了一些简单的 stringstream 操作:
#include <iostream>
#include <fstream>
#include <sstream>
using namespace std;
int main() {
cout << "Enter an integer: ";
int input = 0;
cin >> input;
//用户输入一个整型值,并使用运算符<<将其插入到一个 stringstream 对象中
stringstream converterStream;
converterStream << input;
string inputAsStr;
//使用提取运算符将这个整数转换为 string
converterStream >> inputAsStr;
cout << "Integer Input = " << input << endl;
cout << "String gained from integer = " << inputAsStr << endl;
//将存储在 inputAsStr 中的字符串转换为整数,并将其存储到 Copy 中
stringstream anotherStream;
anotherStream << inputAsStr;
int Copy = 0;
anotherStream >> Copy;
cout << "Integer gained from string, Copy = " << Copy << endl;
return 0;
}
说白了就是流与流之间的转换罢了。
总结
本章从实用的角度介绍了流。您了解到,从本书开头起,您就一直在使用输入/输出流,如 cout和 cin。现在,您知道了如何创建简单的文本文件以及如何读写这种文件。您了解到,stringstream 可帮助您在简单类型(如整型)和字符串之间进行转换。
异常处理
什么是异常
假设您的程序分配内存、读写数据、保存到文件,一切都在开发环境中完美地执行;您的应用程序使用了数 GB 内存,却没有泄露一字节,对此您很是自豪!您发布该应用程序,用户将其部署到各种工作站。有些工作站已购买 10 年。不久后您就收到了抱怨邮件,有些用户抱怨说“访问违规”,有些说出现“未处理的异常”。
“未处理”和“异常”,就是这样。显然,程序在开发环境中表现不错,为何麻烦不断呢?
现实世界千差万别,没有两台计算机是相同的,即便硬件配置一样。这是因为在特定时间,可用的资源量取决于计算机运行的软件及其状态,因此即便在开发环境中内存分配完美无缺,在其他环境中也可能出问题。
这些问题导致“异常”。
异常会打断应用程序的正常流程。毕竟,如果没有内存可用,应用程序就无法完成分配给它的任务。然而,应用程序可处理这种异常:向用户显示一条友好的错误消息、采取必要的挽救措施并妥善地退出。
通过对异常进行处理,有助于避免出现“访问违规”和“未处理的异常”等屏幕,还可避免收到相关的抱怨邮件。下面来看看 C++都向您提供了哪些应对意外的工具。
导致异常的原因
异常可能是外部因素导致的,如系统没有足够的内存;也可能是应用程序内部因素导致的,如使用的指针包含无效值或除数为零。为了向调用者指出错误,有些模块引发异常。
使用 try 和 catch 捕获异常
在捕获异常方面,try 和 catch 是最重要的 C++关键字。要捕获语句可能引发的异常,可将它们放在 try 块中,并使用 catch 块对 try 块可能引发的异常进行处理:
void SomeFunc() {
try {
int* numPtr = new int;
*numPtr = 999;
delete numPtr;
}
catch(...) { // ... catches all exceptions
cout << "Exception in SomeFunc(), quitting" << endl;
}
}
使用 catch(…)处理所有异常
使用 try 和 catch 捕获并处理内存分配异常:
#include <iostream>
using namespace std;
int main() {
cout << "Enter number of integers you wish to reserve: ";
try {
int input = 0;
cin >> input;
// Request memory space and then return it
int* numArray = new int[input];
delete[] numArray;
}
// catch( )像函数一样接受参数,参数…意味着 catch 块将捕获所有的异常。
catch (...) {
cout << "Exception occurred. Got to end, sorry!" << endl;
}
return 0;
}
在这个示例中,我请求为−1 个整数预留内存。这很荒谬,但用户经常做荒谬的事。如果没有异常处理程序,该程序将以讨厌的方式终止。但由于有异常处理程序,程序显示了一条得体的消息:Got to end, sorry!。
然而,在这个示例中,您可能想指定特定的异常类型 std::bad_alloc,因为这是 new 失败时引发的异常。通过捕获特定类型的异常,有助于处理这种类型的异常,如显示一条消息,准确地指出出现了什么问题。
捕获特定类型的异常
上面程序所示的异常是由 C++标准库引发的。这种异常的类型是已知的,在这种情况下,更好的选择是只捕获这种类型的异常,因为您能查明导致异常的原因,执行更有针对性的清理工作,或至少是向用户显示一条准确的消息,如下面的代码所示:
#include <iostream>
using namespace std;
int main() {
cout << "Enter number of integers you wish to reserve: ";
try {
int input = 0;
cin >> input;
// Request memory space and then return it
int* numArray = new int[input];
delete[] numArray;
}
catch (std::bad_alloc& exp) {
cout << "Exception encountered: " << exp.what() << endl;
cout << "Got to end, sorry!" << endl;
}
// catch( )像函数一样接受参数,参数…意味着 catch 块将捕获所有的异常。
catch (...) {
cout << "Exception occurred. Got to end, sorry!" << endl;
}
return 0;
}
不难发现现在能够提供应用程序中断的准确原因,即 bad array new length。这是因为新增了一个 catch(是的,有两个 catch 块),其中一个捕获类型为 bad_alloc&的异常,这种异常是由 new 引发的。
一般而言,可根据可能出现的异常添加多个 catch( )块,这将很有帮助。如上面的程序所示,catch(…) 捕获未被其他 catch 块显式捕获的所有异常。
使用 throw 引发特定类型的异常
上面的程序捕获 std::bad_alloc 时,实际上是捕获 new 引发的 std::bad_alloc 类对象。您可以引发自己选择的异常,为此只需使用关键字 throw:
void DoSomething() {
if(something_unwanted)
throw object;
}
下面的程序将两个数相除,演示了如何使用 throw 引发自定义除零异常:
#include <iostream>
using namespace std;
double Divide(double dividend, double divisor) {
if (divisor == 0) {
throw "Dividing by 0 is a crime";
}
return (dividend / divisor);
}
int main() {
cout << "Enter dividend: ";
double dividend = 0;
cin >> dividend;
cout << "Enter divisor: ";
double divisor = 0;
cin >> divisor;
try {
cout << "Result is: " << Divide(dividend, divisor);
}
catch (const char* exp) {
cout << "Exception: " << exp << endl;
cout << "Sorry, can't continue!" << endl;
}
return 0;
}
上述代码表明,通过捕获类型为 char*的异常可捕获调用函数 Divide( )可能引发的异常。另外,这里没有将整个 main( ) 都放在 try{ };中,而只在其中包含可能引发异常的代码。这通常是一种不错的做法,因为异常处理也可能降低代码的执行性能。
异常处理的工作原理
在上面的程序中,您在函数 Divide( ) 中引发了一个类型为 char*的异常,并在函数 main( )中使用处理程序 catch(char*)捕获它。
每当您使用 throw 引发异常时,编译器都将查找能够处理该异常的 catch(Type)。异常处理逻辑首先检查引发异常的代码是否包含在 try 块中,如果是,则查找可处理这种异常的 catch(Type)。如果 throw 语句不在 try 块内,或者没有与引发的异常兼容的 catch( ),异常处理逻辑将继续在调用函数中寻找。因此,异常处理逻辑沿调用栈向上逐个地在调用函数中寻找,直到找到可处理异常的 catch(Type)。在退栈过程的每一步中,都将销毁当前函数的局部变量,因此这些局部变量的销毁顺序与创建顺序相反。下面的程序演示了这一点。
出现异常时销毁局部对象的顺序:
#include <iostream>
using namespace std;
struct StructA{
StructA() { cout << "StructA constructor" << endl; }
~StructA() { cout << "StructA destructor" << endl; }
};
struct StructB{
StructB() { cout << "StructB constructor" << endl; }
~StructB() { cout << "StructB destructor" << endl; }
};
void FuncB() { // throws
cout << "In Func B" << endl;
StructA objA;
StructB objB;
cout << "About to throw up!" << endl;
throw "Throwing for the heck of it";
}
void FuncA(){
try{
cout << "In Func A" << endl;
StructA objA;
StructB objB;
FuncB();
cout << "FuncA: returning to caller" << endl;
}
catch (const char* exp){
cout << "FuncA: Caught exception: " << exp << endl;
cout << "Handled it, will not throw to caller" << endl;
// throw; // 如果这行取消注释将会把异常抛给 main() 函数
}
}
int main() {
cout << "main(): Started execution" << endl;
try{
FuncA();
}
catch (const char* exp){
cout << "Exception: " << exp << endl;
}
cout << "main(): exiting gracefully" << endl;
return 0;
}
上面的程序表明,main( ) 调用了 FuncA( ),FuncA( )调用了 FuncB( ),而 FuncB( ) 引发异常。
函数 FuncA( )和 main( )都能处理这种异常,因为它们都包含 catch(const char*)。引发异常的 FuncB( )没有 catch( )块,因此 FuncB( )引发的异常将首先由 FuncA( )中的 catch 块处理,因为是 FuncA( )调用了 FuncB( )。
注意到 FuncA( )认为这种异常不严重,没有继续将其传播给main( )。因此,在 main( )看来,就像没有问题发生一样。如果解除对第 38 行的注释,异常将传播给FuncA 的调用者,即 main( )也将收到这种异常。
输出指出了对象的创建顺序(与实例化它们的代码的排列顺序相同),还指出了引发异常后对象被销毁的顺序(与实例化顺序相反)。不仅在引发异常的 FuncB( )中创建的对象被销毁,在调用 FuncB( )并处理异常的 FuncA( )中创建的对象也被销毁。
std::exception 类
之前的程序捕获 std::bad_alloc 时,实际上是捕获 new 引发的 std::bad_alloc 对象。std::bad_alloc 继承了 C++标准类 std::exception,而 std::exception 是在头文件<exception>中声明的。
下述重要异常类都是从 std::exception 派生而来的。
• bad_alloc:使用 new 请求内存失败时引发。
• bad_cast:试图使用 dynamic_cast 转换错误类型(没有继承关系的类型)时引发。
• ios_base::failure:由 iostream 库中的函数和方法引发。
std::exception 类是异常基类,它定义了虚方法 what( );这个方法很有用且非常重要,详细地描述了导致异常的原因。
由于 std::exception 是众多异常类型的基类,因此可使用 catch(const exception&)捕获所有将 std::exception 作为基类的异常:
void SomeFunc() {
try {
// code made exception safe
}
catch (const std::exception& exp){ // catch bad_alloc, bad_cast, etc
cout << "Exception encountered: " << exp.what() << endl;
}
}
从 std::exception 派生出自定义异常类
可以引发所需的任何异常。然而,让自定义异常继承 std::exception 的好处在于,现有的异常处理程序 catch(const std::exception&)不但能捕获 bad_alloc、bad_cast 等异常,还能捕获自定义异常,因为它们的基类都是 exception。
#include <iostream>
#include <exception>
#include <string>
using namespace std;
class CustomException :public std::exception {
string reason;
public:
//构造器,需要 reason
CustomException(const char* why):reason(why){}
//重定义虚函数 what() 方法去返回 reason
virtual const char* what() const throw() {
return reason.c_str();
}
};
double Divide(double dividend, double divisor){
if (divisor == 0)
throw CustomException("CustomException: Dividing by 0 is a crime");
return (dividend / divisor);
}
int main() {
cout << "Enter dividend: ";
double dividend = 0;
cin >> dividend;
cout << "Enter divisor: ";
double divisor = 0;
cin >> divisor;
try {
cout << "Result is: " << Divide(dividend, divisor);
}
catch (exception& exp) { //catch CustomException, bad_alloc, etc
cout << exp.what() << endl;
cout << "Sorry, can't continue!" << endl;
}
return 0;
}
上面的程序在除以零时引发简单的 char* 异常,这里对其进行了修改,实例化了 CustomException 类的一个对象,它继承了 std::exception。注意到这个自定义异常类实现了虚函数 what( );该函数返回引发异常的原因。在 main( )中,catch(exception&) 不但处理异常 CustomException,还处理 bad_alloc 等其他将 exception 作为基类的异常。
总结
本章介绍了 C++编程的一个重要部分。确保应用程序离开开发环境后依然稳定很重要,这有助于提高用户满意度,并提供直观的用户体验,而这正是异常的用武之地。您会发现,分配资源或内存的代码可能失败,因此需要处理它们可能引发的异常。您学习了 C++异常类 std::exception;如果需要编写自定义异常类,最好继承 std::exception。