C++ STL——異常



注:原創不易,轉載請務必注明原作者和出處,感謝支持!

注:內容來自某培訓課程,不一定完全正確!

一 C++異常機制概述

什么是異常處理?一句話,異常處理就是處理程序中的錯誤。

為什么需要異常處理以及異常處理的基本思想?

C++之父Bjarne Stroustrup在《The C++ Programming Language》中講到:一個庫的作者可以檢測出發生了運行時錯誤,但一般不知道怎樣去處理它們(因為和用戶具體的應用有關);另一方面,庫的用戶知道怎樣處理這些錯誤,但卻無法檢查它們何時發生(如果能檢測,就可以在用戶的代碼里處理了,不用留給庫去發現)。

Bjane Stroustrup說:提供異常的基本目的就是為了處理上面的問題。基本思想是:讓一個函數在發現了自己無法處理的錯誤時拋出(throw)一個異常,然后它的(直接或間接)調用者能夠處理這個問題。也就是說,C++將問題的檢測與問題的處理相分離。

在異常處理機制出現之前的錯誤處理方式?在C語言中,對錯誤的處理總是圍繞着兩種方法:一是使用整型的返回值標識錯誤;二是使用errno宏(可以簡單理解為一個全局整型變量)去記錄錯誤。當然C++中仍然可以使用這兩種方法。但是這兩種方法最大的缺陷就是會出現不一致問題。例如有些函數返回1表示成功,返回0表示出錯;而有些函數返回0表示成功,返回非0表示出錯。還有一個缺點是一個函數的返回值只有一個,你通過函數的返回值標識錯誤代碼,那么函數就不能返回其他的值。當然,你也可以通過指針或者C++的引用來返回另外的值,但這可能會令你的程序略微晦澀難懂。

異常的優點在哪里?

(1)函數的返回值可以忽略,但異常不可以忽略。如果程序出現異常,但是沒有被捕獲,程序就會終止,這多少回促使程序員開發出來的程序更健壯一點。而如果使用C語言的errno宏或者函數返回值,調用者都有可能忘記檢查,從而沒有對錯誤進行處理,結果造成程序莫名其妙地終止或者出現錯誤的結果。
(2)整型返回值沒有任何語義信息。而異常卻包含語義信息,有時你從類名就能夠體現出來。
(3)整型返回值缺乏相關的上下文信息。異常作為一個類,可以擁有自己的成員,這些成員就可以傳遞足夠的信息。
(4)異常處理可以在調用時跳級。這是一個代碼編寫時的問題:假設在有多個函數的調用棧中出現了某個錯誤,使用整型返回碼要求你在每一級函數中都要進行處理。而使用異常處理的棧展開機制,只需在一處進行處理就可以了,不需要每級函數都處理。

下面是一個異常的基本語法實例。如果除數y為0,則拋出y的值。在Test1()中嘗試捕獲異常。

// 異常的基本語法
int Divide(int x, int y)
{
	if (y == 0)
	{
		// 拋出異常
		throw y;
	}
	return x / y;
}

void Test1(void)
{
	// 試着捕獲異常
	try
	{
		Divide(10, 0);
	}
	// 異常是根據類型進行匹配的
	catch (int e)
	{
		cout << "Error : 除數為" << e << endl;
	}
}

異常可以跳級處理。比如在下面的這個例子中,函數的調用順序為Test2() --> CallDivide() --> Divide()。異常在Divide()函數中被拋出,但異常的捕獲卻沒有在CallDivide()中進行,而是放到了Test2()中進行。

int Divide(int x, int y)
{
	if (y == 0)
	{
		// 拋出異常
		throw y;
	}
	return x / y;
}

// CallDivide()並未對異常進行處理
void CallDivide(int x, int y)
{
	Divide(x, y);
}

// Divide()所拋出的異常在函數調用頂層Test2()中被捕獲
// 如果Test2()中仍然沒有捕獲到Divide()所拋出的異常,則
// 異常會被拋到函數調用的最頂層main()函數中,如果在main()
// 中異常還沒有被捕獲並處理,則程序會終止執行!
void Test2(void)
{
	try
	{
		CallDivide(10, 0);
	}
	catch (int e)
	{
		cout << "錯誤:除數為" << e << endl;
	}
}

二 棧解旋(unwinding)

棧解旋是指當異常被拋出后,從進入try塊起,到異常被拋出前,這期間在棧上構造的所有對象,都會被自動析構,析構的順序與構造的順序相反。這一過程被稱為棧的解旋(unwinding)。

比如下面的例子,進入try語句塊之后,先調用的是CallDivide(),在CallDivide()當中先是構建了Person對象p3,然后調用Divide(),在Divide()當中構建了對象p1和p2,直到異常的發生。

class Person
{
public:
	Person(string nm)
	{
		name = nm;
		cout << "Person對象" << name << "構建" << endl;
	}

	~Person()
	{
		cout << "Person對象" << name << "析構" << endl;
	}

private:
	string name;
};

int Divide(int x, int y)
{
	Person p1("p1");
	Person p2("p2");

	if (y == 0)
		throw y;

	return x / y;
}

void CallDivide()
{
	Person p3("p3");
	Divide(10, 0);
}

void Test01()
{
	try
	{
		CallDivide();
	}
	catch (int e)
	{
		cout << "有異常發生!" << endl;
	}
}

上面程序的輸出如下,可以看到構建的順序和析構的順序剛好相反。

Person對象p3構建
Person對象p1構建
Person對象p2構建
Person對象p2析構
Person對象p1析構
Person對象p3析構
有異常發生!

三 異常接口的聲明

(1)為了加強程序的可讀性,可以在函數聲明中列出可能拋出異常的所有類型,例如:void func() throw (A, B, C),這個函數func能夠且只能拋出類型A,B,C及其子類型的異常。
(2)如果在函數聲明中沒有包含異常接口聲明,則此函數可以拋出任何類型的異常
(3)一個不拋出任何異常的函數可以聲明為:void func() throw ()
(4)如果一個函數拋出了它的異常接口聲明所不允許拋出的異常,則unexcepted函數會被調用,該函數默認行為調用terminate()函數中斷程序。

下面是一個異常接口聲明的實例。

// 這個函數只能拋出int float char三種類型的異常,拋出其他異常就報錯
void func1() throw (int, float, char)
{
	// 拋出char *會導致程序異常退出
	throw string("abc");
}

// 這個函數不能拋出任何異常
void func2() throw ()
{
	// 拋出異常會導致程序異常退出
	throw 1;
}

// 這個函數可以拋出任何類型異常
void func3()
{
	;
}

四 異常類型和異常變量的生命周期

throw的異常是有類型的,可以是數字、字符串、和類對象等。catch需要嚴格地匹配異常類型。

void fun1()
{
	throw 1;
}

void fun2()
{
	throw "exception";
}

class MyException 
{
public:
	MyException(string msg)
	{
		error = msg;
	}

	void what()
	{
		cout << error << endl;
	}

private:
	string error;
};

void fun3()
{
	// 拋出匿名對象
	throw MyException("我剛寫的異常!");
}

void Test1()
{
	try
	{
		fun1();
	}
	catch (int e)
	{
		cout << "int型異常!" << endl;
	}

	try
	{
		fun2();
	}
	catch (char *e)
	{
		cout << "char *型異常!" << endl;
	}

	try
	{
		fun3();
	}
	catch (MyException e)
	{
		// 對象MyException里封裝了異常的相關信息
		e.what();
	}
}

下面進行異常變量生命周期分析。首先是使用普通的匿名對象去接拋出異常的情況。

class MyException
{
public:
	MyException()
	{
		cout << "MyException構造函數被調用" << endl;
	}

	MyException(const MyException & ex)
	{
		cout << "MyException拷貝構造函數被調用" << endl;
	}
	~MyException()
	{
		cout << "MyException析構函數被調用" << endl;
	}
};

void fun()
{
	// 拋出匿名異常對象
	throw MyException();
}
void Test()
{
	try
	{
		fun();
	}
	// 使用普通對象去接拋出的異常
	catch (MyException e)
	{
		cout << "異常捕獲!" << endl;
	}
}

此時,程序輸出結果如下。程序首先是在fun()中調用了MyException的構造函數創建了一個匿名對象,然后將該匿名對象拋出。然后在使用普通元素e接收拋出的異常對象時,調用了拷貝構造函數將匿名對象拷貝至e當中。然后是捕獲異常,捕獲之后,在catch語句中進行處理。當catch中處理完了異常之后再將對象e和匿名對象析構,所以輸出兩次“MyException析構函數被調用”。

MyException構造函數被調用
MyException拷貝構造函數被調用
異常捕獲!
MyException析構函數被調用
MyException析構函數被調用

接下來是使用引用去接拋出的異常對象的情況。

void Test()
{
	try
	{
		fun();
	}
	// 使用引用去接拋出的異常
	catch (const MyException &e)
	{
		cout << "異常捕獲!" << endl;
	}
}

此時程序輸出如下。和使用普通元素e接收拋出異常對象相比,使用引用去接收拋出對象少了調用拷貝構造函數的步驟。因為,在catch時直接引用了匿名對象,所以從始至終,只要匿名對象被創建和析構。

我比較菜,我有疑問:參考下面的用指針去接收拋出的異常部分,為什么這里可以在catch語句塊里去引用匿名對象?這個匿名對象不是在fun()里創建的嗎?隨着fun()調用完畢,退棧之后,匿名對象還存在嗎?如果不存在,那你還怎么能夠引用呢?如果存在,那說明這個匿名異常對象肯定不在棧內存中,不在棧內存中,那它在哪里?在堆區嗎?

MyException構造函數被調用
異常捕獲!
MyException析構函數被調用

下面是使用指針去接收拋出異常對象的錯誤示范。

void fun()
{
	// 拋出匿名對象的地址
	throw &(MyException());
}
void Test()
{
	try
	{
		fun();
	}
	// 使用指針去接拋出的異常
	catch (const MyException *e)
	{
		cout << "異常捕獲!" << endl;
	}
}

此時,程序輸出結果如下。可以看到,在catch塊中進行異常處理之前,匿名對象就已經被析構了!此時指針e成了一個野指針,你再也無法通過e來獲取e中封裝的異常信息了!

MyException構造函數被調用
MyException析構函數被調用
異常捕獲!

下面是使用指針去接收拋出異常對象的正確示范。為了防止匿名對象在進行異常處理之前被析構,你需要在堆中創建匿名對象,這也就意味着你需要手動地管理內存!

void fun()
{
	// 拋出匿名對象的地址,匿名對象在堆中創建
	throw new MyException();
}
void Test()
{
	try
	{
		fun();
	}
	// 使用指針去接拋出的異常
	catch (const MyException *e)
	{
		cout << "異常捕獲!" << endl;
		// 千萬別忘記手動釋放匿名對象的內存!
		delete e;
	}
}

此時,程序終於輸出正確了!

MyException構造函數被調用
異常捕獲!
MyException析構函數被調用

五 C++標准異常庫

C++標准異常庫的成員

(1)在上述的繼承體系中,每個類都提供了構造函數、拷貝構造函數和賦值操作符重載
(2)logic_error類及其子類、runtime_error類及其子類,它們的構造函數是接受一個string類型的形式參數,用於異常信息的描述
(3)所有的異常類都有一個what()方法,返回const char *類型(C風格字符串)的值,描述異常信息。

標准異常類的具體描述

異常名稱 描述
exception 所有標准異常類的父類
bad_alloc 當operator new and operator new[]請求分配失敗時
bad_exception 這是個特殊的異常類,如果函數的異常拋出列表里聲明了bad_exception異常,當函數內部拋出了異常列表中沒有的異常,這時調用的unexcepted函數中若拋出異常,不論什么類型,都會被替換為bad_exception類型
bad_type 使用typeid操作符,操作一個NULL指針,而該指針是帶有虛函數的類,這時拋出bad_typeid異常
bad_cast 使用dynamic_cast轉換引用失敗時
ios_base::failure io操作過程中出現錯誤
logic_error 邏輯錯誤,可以在運行前檢測的錯誤
runtime_error 運行時錯誤,僅在運行時才可以檢測的錯誤

logic_error的子類:

異常名稱 描述
length_error 試圖生成一個超出該類型最大長度的對象時,例如vector的resize操作
domain_error 參數的值域錯誤,主要用在數學函數中。例如使用一個負值調用只能操作非負值的函數
out_of_range 超出有效范圍
invalid_argument 參數不合適。在標准庫中,當利用string對象構造bitset時,而string中的字符不是'0'或者'1'的時候,拋出該異常

runtime_error的子類:

異常名稱 描述
range_error 計算結果超出了有意義的值域范圍
overflow_error 算數計算上溢
underflow_error 算數計算下溢
invalid_argument 參數不合適。在標准庫中,當利用string對象構造bitset時,而string中的字符不是'0'或者'1'的時候,拋出該異常

編寫自己的異常類
為什么要編寫自己的異常類?
(1)標准庫中的異常類是有限的
(2)在自己的異常類中,可以添加自己的信息(標准庫中的異常類值允許設置一個用來描述異常的字符串)

如何編寫自己的異常類?
(1)建議自己的異常類要繼承標准異常類。因為C++中可以拋出任何類型的異常,所以我們的異常類可以不繼承自標准異常,但是這樣可能會導致程序混亂,尤其是當我們多人協同開發時
(2)當繼承標准異常類時,應該重載父類的what()函數和虛析構函數
(3)因為棧展開的過程中,要復制異常類型,那么要根據你在類中添加的成員考慮是否提供自己的復制構造函數。

下面是一個C++標准異常庫的應用實例

class Person
{
public:
	Person()
	{
		mAge = 0;
	}

	void setAge(int age)
	{
		if (age < 0 || age > 100)
			throw out_of_range("年齡應該在0到100之間");

		this->mAge = age;
	}

private:
	int mAge;
};


void Test()
{
	Person p;

	try
	{
		p.setAge(1000);
	}
	catch (const exception &e)
	{
		cout << e.what() << endl;
	}
}

下面是自己手寫的異常類的應用實例。

class MyOutOfRangeException : public exception
{
public:
	MyOutOfRangeException(char *error)
	{
		pError = new char[strlen(error) + 1];
		strcpy_s(pError, strlen(error) + 1, error);
	}
	~MyOutOfRangeException()
	{
		if (pError != nullptr)
		{
			delete[] pError;
		}
	}
	virtual const char *what() const
	{
		return pError;
	}

private:
	char *pError;

};

void Call(void)
{
	throw MyOutOfRangeException("我自己的異常類!");
}

void Test()
{
	try
	{
		Call();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
}

六 異常的繼承

下面是一個自己手寫異常繼承的應用案例。


// 異常基類
class BaseMyException
{
public:
	virtual void what() = 0;
	virtual ~BaseMyException() {}
};

// 繼承
class TargetSpaceNullException : public BaseMyException
{
public:
	virtual void what()
	{
		cout << "目標空間為空!" << endl;
	}
	~TargetSpaceNullException() {}
};

// 繼承
class SourceSpaceNullException : public BaseMyException
{
public:
	virtual void what()
	{
		cout << "源空間為空!" << endl;
	}
	~SourceSpaceNullException() {}
};

void copy_str(char *target, char *source)
{
	if (target == nullptr)
	{
		throw TargetSpaceNullException();
	}

	if (source == nullptr)
	{
		throw SourceSpaceNullException();
	}

	while (*source != '\0')
	{
		*target = *source;
		target++;
		source++;
	}
	*target = '\0';
}

int main(int argc, char **argv)
{
	char *source = "abcdefg";
	char buf[1024] = { 0 };
	try
	{
		copy_str(nullptr, source);
	}
	catch (BaseMyException &e)
	{
		e.what();
	}

	cout << buf << endl;
	getchar();
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM