C++基础教程面向对象(学习笔记(77))

异常,类和继承

例外和成员函数

到目前为止,在本教程中,您只看到了非成员函数中使用的异常。但是,异常在成员函数中同样有用,甚至在重载运算符中也是如此。将以下重载的[]运算符视为简单整数数组类的一部分:

int& IntArray::operator[](const int index)
{
    return m_data[index];
}

虽然只要index是一个有效的数组索引,这个函数就会很好用,但是这个函数在一些好的错误检查中非常缺乏。我们可以添加一个断言语句来确保索引有效:

int& IntArray::operator[](const int index)
{
    assert (index >= 0 && index < getLength());
    return m_data[index];
}

现在,如果用户传入无效索引,程序将导致断言错误。虽然这有助于向用户表明出现了问题,但有时更好的做法是无声地失败并让调用者知道出错了所以他们可以适当地处理它。

不幸的是,因为重载运算符对它们可以获取和返回的参数的数量和类型有特定的要求,所以没有灵活性将错误代码或布尔值传递给调用者。但是,由于异常不会更改函数的签名,因此可以在此处使用它们。这是一个例子:

int& IntArray::operator[](const int index)
{
    if (index < 0 || index >= getLength())
        throw index;
 
    return m_data[index];
}

现在,如果用户传入无效索引,operator []将抛出一个int异常。

当构造函数失败时

构造函数是另一个类,其中异常非常有用。如果构造函数失败,只需抛出异常以指示对象无法创建。对象的构造被中止,它的析构函数永远不会被执行(注意:这意味着你的构造函数应该在抛出异常之前处理垃圾内存)。

异常类

使用基本数据类型(例如int)作为异常类型的一个主要问题是它们本质上是模糊的。更大的问题是当try块中存在多个语句或函数调用时,异常意味着消歧。

//使用上面的IntArray重载operator []
 
try
{
    int *value = new int(array[index1] + array[index2]);
}
catch (int value)
{
    //我们在这里catch到什么?
}

在这个例子中,如果我们要捕获一个int异常,那真的告诉我们什么?其中一个数组索引是否超出界限operator+导致整数溢出吗?操作员新操作失败是因为内存不足吗?不幸的是,在这种情况下,没有简单的方法来消除歧义。虽然我们可以抛出const char *异常来解决识别出错的问题,但这仍然无法让我们以不同的方式处理来自各种来源的异常。

解决此问题的一种方法是使用异常类。一个异常类仅仅是被专门设计为作为异常抛出一个普通的类。让我们设计一个与IntArray类一起使用的简单异常类:

#include <string>
 
class ArrayException
{
private:
    std::string m_error;
 
public:
    ArrayException(std::string error)
        : m_error(error)
    {
    }
 
    const char* getError() { return m_error.c_str(); }
};

这是使用此类的完整程序:

#include <iostream>
#include <string>
 
class ArrayException
{
private:
	std::string m_error;
 
public:
	ArrayException(std::string error)
		: m_error(error)
	{
	}
 
	 const char* getError() { return m_error.c_str(); }
};
 
class IntArray
{
private:
 
	int m_data[3]; // 假设数组长度为3,以简化
public:
	IntArray() {}
	
	int getLength() { return 3; }
 
	int& operator[](const int index)
	{
		if (index < 0 || index >= getLength())
			throw ArrayException("Invalid index");
 
		return m_data[index];
	}
 
};
 
int main()
{
	IntArray array;
 
	try
	{
		int value = array[5];
	}
	catch (ArrayException &exception)
	{
		std::cerr << "An array exception occurred (" << exception.getError() << ")\n";
	}
}

使用这样的类,我们可以让异常返回发生的问题的描述,它提供了出错的上下文。由于ArrayException是它自己的唯一类型,我们可以专门捕获数组类抛出的异常,如果我们愿意,可以将它们与其他异常区别对待。

请注意,异常处理程序应通过引用而不是按值来捕获类异常对象。这可以防止编译器创建异常的副本,当异常是类对象时这可能很昂贵,并且在处理派生的异常类时会阻止对象切片(我们稍后会讨论)。除非您有特定的理由,否则通常应避免通过指针捕获异常。

例外和继承

由于可以将类作为异常抛出,并且可以从其他类派生类,因此我们需要考虑当使用继承类作为异常时会发生什么。事实证明,异常处理程序不仅匹配特定类型的类,它们还匹配从该特定类型派生的类!请考虑以下示例:

class Base
{
public:
    Base() {}
};
 
class Derived: public Base
{
public:
    Derived() {}
};
 
int main()
{
    try
    {
        throw Derived();
    }
    catch (Base &base)
    {
        cerr << "caught Base";
    }
    catch (Derived &derived)
    {
        cerr << "caught Derived";
    }
 
    return 0;
}	

在上面的例子中,我们抛出Derived类型的异常。但是,该程序的输出是:

caught Base

发生了什么?
首先,如上所述,派生类将由基类型的处理程序捕获。因为Derived是从Base派生的,Derived is-a Base(它们具有is-a关系)。其次,当C ++试图为引发的异常找到处理程序时,它会按顺序执行。因此,C ++做的第一件事就是检查Base的异常处理程序是否与Derived异常匹配。因为Derived is-a Base,答案是肯定的,它执行Base类型的catch块!Derived的catch块在这种情况下甚至都没有测试过。

为了使这个例子按预期工作,我们需要翻转catch块的顺序:

class Base
{
public:
    Base() {}
};
 
class Derived: public Base
{
public:
    Derived() {}
};
 
int main()
{
    try
    {
        throw Derived();
    }
    catch (Derived &derived)
    {
        cerr << "caught Derived";
    }
    catch (Base &base)
    {
        cerr << "caught Base";
    }
 
    return 0;
}	

这样,Derived处理程序将首先捕获Derived类型的对象(在Base的处理程序之前)。Base类型的对象与Derived处理程序(Derived is-a Base,但Base不是Derived)不匹配,因此将“落入”Base处理程序。

规则:派生异常类的处理程序应列在基类的处理程序之前。

处理程序使用基类的处理程序捕获派生类型的异常的能力结果非常有用。

std ::例外

标准库中的许多类和运算符都会在失败时抛出异常类。例如,operator new和std :: string如果无法分配足够的内存,则可以抛出std :: bad_alloc。失败的dynamic_cast会抛出std :: bad_cast。等等。从C ++ 14开始,可以抛出21种不同的异常类,其中更多的是C ++ 17。

好消息是所有这些异常类都派生自一个名为std :: exception的类。std :: exception是一个小型接口类,旨在作为C ++标准库抛出的任何异常的基类。

大多数情况下,当标准库抛出异常时,我们不会关心它是一个糟糕的分配,一个糟糕的演员还是别的东西。我们只关心发生灾难性事故,现在我们的计划正在爆炸式增长。感谢std :: exception,我们可以设置一个异常处理程序来捕获std :: exception类型的异常,并且我们最终会在一个地方一起捕获std :: exception和所有(21+)派生异常。很简单!

#include <iostream>
#include <exception> // std::exception
#include <string> // 这个例子
int main()
{
	try
	{
		// 您使用标准库的代码就在这里
		// 出于示例的目的,我们将故意触发其中一个例外
                std::string s;
                s.resize(-1); // 将触发std :: bad_alloc
	}
	// 此处理程序将捕获std :: exception和所有派生的异常
	catch (std::exception &exception)
	{
		std::cerr << "Standard exception: " << exception.what() << '\n';
	}
 
	return 0;
}

以上程序打印:

Standard exception: string too long

上面的例子应该非常简单。值得注意的一点是,std :: exception有一个名为what()的虚拟成员函数,它返回异常的C风格字符串描述。大多数派生类都会覆盖what()函数来更改消息。请注意,此字符串仅用于描述性文本 - 不要将其用于比较,因为不能保证它们在编译器之间是相同的。

有时我们会想要以不同方式处理特定类型的异常。在这种情况下,我们可以为该特定类型添加一个处理程序,并让所有其他人“通过”到基本处理程序。考虑:

try
{
     // 使用标准库的代码在这里
}
// 此处理程序将捕获std :: bad_alloc(以及从中派生的任何异常)
catch (std::bad_alloc &exception)
{
    std::cerr << "You ran out of memory!" << '\n';
}
// 这个处理程序将捕获掉落的std :: exception(以及从它派生的任何异常)
catch (std::exception &exception)
{
    std::cerr << "Standard exception: " << exception.what() << '\n';
}

在此示例中,类型为std :: bad_alloc的异常将被第一个处理程序捕获并在那里处理。类型为std :: exception和所有其他派生类的异常将被第二个处理程序捕获。

这种继承层次结构允许我们使用特定处理程序来定位特定的派生异常类,或者使用基类处理程序来捕获整个异常层次结构。这使我们可以很好地控制我们想要处理的异常类型,同时确保我们不必做太多工作来捕获层次结构中的“其他所有内容”。

直接使用标准例外

什么都没有直接抛出std :: exception,你也不应该。但是,如果它们足以代表您的需求,您可以随意将标准库中的其他标准异常类抛出。您可以在cppreference上找到所有标准异常的列表。

std :: runtime_error(作为stdexcept头部分包含在内)是一种流行的选择,因为它具有通用名称,并且其构造函数采用可自定义的消息:

#include <iostream>
#include <stdexcept>
 
int main()
{
	try
	{
		throw std::runtime_error("Bad things happened");
	}
	// 此处理程序将捕获std :: exception和所有派生的异常
	catch (std::exception &exception)
	{
		std::cerr << "Standard exception: " << exception.what() << '\n';
	}
 
	return 0;
}

这打印:

Standard exception: Bad things happened

从std :: exception派生自己的类
当然,您可以从std :: exception派生自己的类,并覆盖虚拟的what()const成员函数。这是与上面相同的程序,其中ArrayException派生自std :: exception:

#include <iostream>
#include <string>
#include <exception> // std::exception
 
class ArrayException: public std::exception
{
private:
	std::string m_error;
 
public:
	ArrayException(std::string error)
		: m_error(error)
	{
	}
 
	// 将std :: string作为const C风格的字符串返回
//	const char* what() const { return m_error.c_str(); } //先前C ++ 11版本
	const char* what() const noexcept { return m_error.c_str(); } // C++11版本
};
 
class IntArray
{
private:
 
	int m_data[3]; //假设数组长度为3,以简化
public:
	IntArray() {}
	
	int getLength() { return 3; }
 
	int& operator[](const int index)
	{
		if (index < 0 || index >= getLength())
			throw ArrayException("Invalid index");
 
		return m_data[index];
	}
 
};
 
int main()
{
	IntArray array;
 
	try
	{
		int value = array[5];
	}
	catch (ArrayException &exception) // 派生的catch块首先出现
	{
		std::cerr << "An array exception occurred (" << exception.what() << ")\n";
	}
	catch (std::exception &exception)
	{
		std::cerr << "Some other std::exception occurred (" << exception.what() << ")\n";
	}
}

在C ++ 11中,虚函数what()被更新为具有说明符noexcept(这意味着函数不会抛出异常本身)。因此,在C ++ 11及更高版本中,我们的覆盖也应该具有说明符noexcept。

您是否想要创建自己的独立异常类,使用标准异常类或从std :: exception派生自己的异常类取决于您。根据您的目标,所有的都是有效的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章