(十五)C++學習 | 強制型別轉換 異常處理

Skies_發表於2020-10-26


1. 強制型別轉換

C {\rm C} C++中存在四種形式的強制型別轉換:static_castinterpret_castconst_castdynamic_cast

1.1 static_cast

用來進行比較自然低風險的轉換,比如整型和實數型、字元型之間相互轉換等。static_cast不能在不同型別的指標之間相互轉換,也不能用於整型和指標之間的相互轉換,也不能用於不同型別的引用之間的相互轉換。示例程式:

#include<iostream>

using namespace std;

class A {
public:
	// 過載int(),並返回1
	operator int() {
		return 1;
	}
	// 過載char*(),並返回NULL
	operator char* () {
		return NULL;
	}
};

int main() {
	A a;
	int n;  char* p = (char*)"New Good.";
	// 將3.14強制轉換為3
	n = static_cast<int>(3.14);
	// 將a物件轉化成int,呼叫int(),n的值變為1
	n = static_cast<int>(a);
	// p的值變成NULL
	p = static_cast<char*>(a);
	// 編譯錯誤,static_cast不能將指標轉換成整型
	n = static_cast<int>(p);
	// 編譯錯誤,static_cast不能將整型轉換成指標
	p = static_cast<char*>(n);
	return 0;
}

1.2 reinterpret_cast

用來進行各種不同型別的指標之間的轉換,不同型別的引用之間的轉換,指標和能容納得下指標的整數型別之間的轉換。轉換的時候,執行的是逐個位元拷貝的操作。示例程式:

#include<iostream>

using namespace std;

class A {
public:
	int i; int j;
	A(int n):i(n),j(n){}	// 初始化列表
};
int main() {
	A a(100);
	// 強行讓r引用a
	int& r = reinterpret_cast<int&>(a);
	// r引用的內容變成200,r引用a前四個位元組,即i的值
	r = 200;
	// 200 100
	cout << a.i << " " << a.j;
	int n = 300;
	// 強行讓pa指向n
	A* pa = reinterpret_cast<A*>(&n);
	// n變成400
	pa->i = 400;
	// 語句不安全,可能導致程式崩潰
	pa->j = 500;
	long long la = 0x12345678abcdLL;
	// la太長,只取低32位0x5678abcd拷貝給pa
	pa = reinterpret_cast<A*>(la);
	// pa逐個位元拷貝到u
	unsigned int u = reinterpret_cast<unsigned int>(pa);
	// 5678abcd
	cout << hex << u;
	typedef void(*PF1)(int);
	typedef int(*PF2)(int, char*);
	PF1 pf1; PF2 pf2;
	// 兩個不同型別的函式指標之間的轉換
	pf2 = reinterpret_cast<PF2>(pf1);
	return 0;
}

1.3 const_cast

用來去除const屬性,將常引用轉換成同型別的非常引用,將常指標轉換成同型別的非常指標。例如:

const string s = "Inception";
// 將s轉換成非常引用
string& p = const_cast<string&>(s);
// 將s轉換成非常指標
string* ps = const_cast<string*>(&s);

1.4 dynamic_cast

專門用於將多型基類(包含虛擬函式的基類)的指標或引用,強制轉換為派生類的指標或引用,而且能夠檢查型別轉換的安全性。對於不安全的轉換,轉換結果會返回NULLdynamic_cast不能用於將非多型基類的指標或引用,強制轉換為派生類的指標或引用。示例程式:

#include<string>
#include<iostream>

using namespace std;

class Base {
public:
	// 虛解構函式
	virtual ~Base() {}
};
// Derived由Base派生而來
class Derived:public Base {};

int main() {
	Base b;
	Derived d;
	Derived* pd;
	// 將基類指標b轉化成派生類指標pd
	pd = reinterpret_cast<Derived*>(&b);
	// reinterpret_cast不檢查安全性,不執行下列語句
	if (pd == NULL) {
		cout << "unsafe1";
	}
	// 將基類指標b轉化成派生類指標pd,不安全
	pd = dynamic_cast<Derived*>(&b);
	// unsafe2
	if (pd == NULL) {
		cout << "unsafe2";
	}
	// 將基類指標pb(指向派生類物件)轉化成派生類指標pd,安全
	Base* pb = &d;
	pd = dynamic_cast<Derived*>(pb);
	if (pd == NULL) {
		cout << "unsafe2";
	}
	return 0;
}

2. 異常處理

程式執行過程中難免會發生各種各樣的錯誤,如陣列元素的下標越界、空指標、除數為零等,而造成這些錯誤的原因主要有程式碼質量不高存在 B U G {\rm BUG} BUG、輸入資料不符合程式要求等。而我們總是希望在發生異常情況時,不只是簡單地終止程式的執行,而是能夠反饋異常情況的資訊,並且能夠對程式執行中已發生的事故進行處理。

C {\rm C} C++中,解決異常的通用做法有以下幾種:在預計會發生異常的地方,加入相應的提示程式碼,但這種程式碼通常不是適用的;把異常與函式介面分開,並且能夠區分不同的異常,並且在函式體外捕獲所發生的異常,並提供更多的異常資訊。對於後者, C {\rm C} C++提供了try、catch語句用於處理異常。

2.1 try、catch處理異常

示例程式:

#include<iostream>

using namespace std;

int main() {
	double m, n;
	cin >> m >> n;
	// 將可能出現異常的語句放入try內
	try {
		cout << "before dividing." << endl;
		// 除零錯誤,丟擲異常
		if (n == 0) {
			throw - 1;
		}
		else
		{
			cout << m / n << endl;
		}
		cout << "after dividing." << endl;
	}
	// 沒有丟擲異常時不會執行catch塊語句,否則根據try塊中的異常值進入catch分支
	// 如上述丟擲異常資訊為-1,則進入catch (int e){ };塊中
	catch (double d) {
		cout << "catch(double)" << d << endl;
	}
	catch (int e) {
		cout << "catch(int)" << e << endl;
	}
	cout << "finished." << endl;
	return 0;
}

下列語句可以捕獲任何形式的異常:

catch (...) {
 cout << catch(...) << endl;
}

2.2 異常再丟擲

如果一個函式在執行過程中,丟擲的異常在本函式內就被捕獲並處理了,那麼該異常就不會拋給這個函式的呼叫者(山一層函式);如果異常在本函式內沒有被處理,則該異常就會繼續被拋給上一層函式。示例程式:

#include<string>
#include<iostream>

using namespace std;

class CException {
public:
	string msg;
	CException(string s) :msg(s) {}	// 初始化列表
};
double Devide(double x, double y) {
	// 丟擲異常為CException類物件
	if (y == 0) {
		throw CException("divided by zero");
	}
	cout << "in Devide" << endl;
	return 0;
}
int CountTax(int salary) {
	try {
		// 如果salary小於零則丟擲整型異常
		if (salary < 0) {
			throw - 1;
		}
		cout << "counting tax" << endl;
	}
	catch (int) {
		cout << "salary 0" << endl;
	}
	cout << "tex counted." << endl;
	return salary * 0.15;
}
int main() {
	double f = 1.2;
	try {
		CountTax(-1);	// 引數小於零會引發異常,輸出salary 0
		f = Devide(3, 0);	// 除數為零會引發異常
		cout << "end of try block." << endl;
	}
	// CException型別異常,輸出divided by zero
	catch (CException e) {
		cout << e.msg << endl;
	}
	cout << "f=" << f << endl;
	cout << "finished." << endl;
	return 0;
}

3. STL中的異常類

標準模板庫中有關異常的類都繼承自基類exception,有bad_typeidbad_castbad_allocios_bas::failurelogic_error等。

3.1 bad_cast

在使用dynamic_cast進行從多型基類物件或引用到派生類的引用的強制型別轉換,如果轉換是不安全的,則會丟擲此異常。示例程式:

#include<typeinfo>
#include<iostream>
#include<stdexcept>

using namespace std;
// 有虛成員函式,是多型基類
class Base {
	virtual void func() {}
};
// Derived繼承自Base
class Derived :public Base {
public:
	void Print() {}
};
void PrintObj(Base& b) {
	try {
		// 強制型別轉換,將b轉換成Derived&,即基類引用->派生類引用
		Derived& rd = dynamic_cast<Derived&>(b);
		// 若轉換不安全,會丟擲bad_cast異常
		rd.Print();
	}
	// 如果b引用的是基類物件,則轉換是不安全的,輸出Bad dynamic_cast
	catch (bad_cast& e) {
		cerr << e.what() << endl;
	}
}
int main() {
	Base b;
	PrintObj(b);
	return 0;
}

3.2 bad_alloc

在使用new關鍵字進行動態記憶體分配時,如果沒有足夠的記憶體供使用,則會引發此異常。示例程式:

#include<iostream>
#include<stdexcept>

using namespace std;

int main() {
	try {
		// 無法分配這麼多空間,丟擲異常
		char* p = new char[0x7fffffff];
	}
	// 輸出bad allocation
	catch (bad_alloc& e) {
		cerr << e.what() << endl;
	}
	return 0;
}

3.3 out_of_range

vectorstringat成員函式根據下標訪問元素時,如果下標越界,則會丟擲此異常。示例程式:

#include<vector>
#include<string>
#include<iostream>
#include<stdexcept>

using namespace std;

int main() {
	vector<int> v(10);
	try {
		// 丟擲out_of_range異常
		v.at(100) = 100;
	}
	// 捕獲異常,輸出invalid vector subscript
	catch (out_of_range& e) {
		cerr << e.what() << endl;
	}
	string s = "hello";
	try {
		// 丟擲out_of_range異常
		char c = s.at(100);
	}
	// 捕獲異常,輸出invalid string position
	catch (out_of_range& e) {
		cerr << e.what() << endl;
	}
	return 0;
}

4. 總結

C {\rm C} C++中,我們可以簡單地使用int(...)等形式進行強制型別轉換,但這種方式不檢查轉換的安全性,本文介紹的幾種強制型別轉換方式適用於不同的場景。在不同場景使用不同的強制型別轉換方式,可以有效地提高程式的可讀性。對於異常處理,主要依靠try、catch塊處理,在編寫大型程式時,對於利用好異常處理非常重要。因此健壯的程式總是能夠自行發現錯誤,並給出修改意見,而異常處理模組在其中發揮著舉足輕重的重要。


參考

  1. 北京大學公開課:程式設計與演算法(三)C++物件導向程式設計.


相關文章