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派生自己的異常類取決於您。根據您的目標,所有的都是有效的方法。

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