目錄
拋出的異常類型大致可以分為三種。?
第一種? ? 基本類型
1. 可以直接拋出常量?
2. 也可以拋出定義好的變量?
3. 如果我們使用const,會不會影響到異常的匹配。
第二種? ? 字符串類型以及指針類型
1. 使用字符指針?
注意:?
2. 使用string類型?
第三種? 自定義類型(類類型)?
?
注意事項:
?
catch是根據我們拋出的異常信息類型來捕獲的。?
拋出的異常類型大致可以分為三種。?
第一種? ? 基本類型
?int, char, float,double等類型。
以拋出int類型的數據為例?
1. 可以直接拋出常量?
void func1() {throw - 1;printf("func1");
}int main(void) {try {func1();}catch (int error) {printf("異常處理 %d\n",error);}system("pause");return 0;
}
2. 也可以拋出定義好的變量?
void func1() {int err = -1; // 定義變量throw err;printf("func1");
}int main(void) {try {func1();}catch (int error) {printf("異常處理 %d\n",error);}system("pause");return 0;
}
3. 如果我們使用const,會不會影響到異常的匹配。
我們對上面的代碼進行修改,?我們在func1定義err時和catch參數中兩個地方加上const,或者一個加一個不加。執行代碼,會發現依然能匹配成功。所以對于普通類型的數據,有沒有const都不會影響到異常的匹配的。
第二種? ? 字符串類型以及指針類型
首先C語言的字符串類型是字符指針,c++的字符串類型string(當然c++中包含C語言指針)。?
1. 使用字符指針?
第一種:? 直接拋出字符串常量?
對于字符串常量,我們直接使用非const指針指向它是不安全的,但是有的編譯器允許這么做。所以有的編譯器char*類型也可以與字符串常量匹配,但是有的編譯器認為字符串常量必須使用const的字符指針指向才行,所以只能與const char*匹配。
void func1() {throw "異常";printf("func1");
}int main(void) {try {func1();}catch (const char* error) {printf("異常處理 %s\n",error);}system("pause");return 0;
}
注意:?
雖然在c++中我們可以使用string定義字符串,但是string只是c++封裝的一個類型。字符串常量或者字符指針,本身表示的是一個地址,所以它是沒有辦法與string類型匹配成功的,需要使用字符指針。?
第二種:? 使用字符指針指向或者字符數組
?字符數組:??
void func1() {char arr[] = "異常";throw arr;printf("func1");
}int main(void) {try {func1();}catch (const char* error) {printf("異常處理 %s\n",error);}system("pause");return 0;
}
字符指針:? ?
void func1() {char* arr = (char*)"異常";throw arr;printf("func1");
}int main(void) {try {func1();}catch (char* error) {printf("異常處理 %s\n",error);}system("pause");return 0;
}
?無論是字符數組還是字符指針:
1. 如果我們拋出的是const 修飾的,那么只能和catch中const修飾的char*匹配。?
2. 如果我們拋出的是非const修飾的,那么catch中用不用const修飾都可以匹配成功。?
3. 其實和賦值時,const修飾的不能賦值給非const修飾的,非const修飾的可以賦值給const修飾的是一個道理的。
4. 當然如果const在*后面修飾,那么就不會影響匹配。char*const可以和char*匹配成功。
2. 使用string類型?
string類型其實和普通類型差不多,string*和char*也類似。但是string類型和char*類型是無法匹配的,雖然它們都可以表示字符串,但是是不同的類型。
第三種? 自定義類型(類類型)?
我們可以將相應的異常封裝成一個類,類中封裝一些方法,在出現這類異常之后,?可以拋出一個此類的對象。
?看下面這段代碼,將打開文件異常和寫入文件異常封裝成兩個類,然后拋出它們的對象。
#define BUFFER_SIZE 1024class OpenFileError {
public:OpenFileError(int err) :errorData(err) {};void print() {switch (errorData) {case -1:printf("源文件打開失敗 %d\n", errorData);break;case -2:printf("目的文件打開失敗 %d\n", errorData);break;}}
private:int errorData;
};class WriteFileError {
public:void print() {printf("文件寫入失敗");}
};// 將一個文件中的內容拷貝到另外一個文件中去
int makeFile(const char* dest, const char* src) {// 定義文件指針FILE* fp1 = NULL, * fp2 = NULL;// 打開文件, 以只讀二進制形式打開文件,打開失敗返回NULLfp1 = fopen(src, "rb"); // 判斷文件是否成功打開if (!fp1) {throw OpenFileError(-1);}// 打開文件,以只寫二進制形式打開文件,打開失敗返回NULLfp2 = fopen(dest, "wb");// 判斷文件是否成功打開if (!fp2) {throw OpenFileError(-2); // 返回錯誤標記,表示目標文件打開失敗}// 進行文件的拷貝char buffer[BUFFER_SIZE]; // 1024字節的緩存int readLen, writeLen; // 每次讀取的長度和寫入的長度// 讀取的長度大于0,說明有內容可以寫入,執行循環體的寫入內容while ((readLen = fread(buffer, 1, BUFFER_SIZE, fp1)) > 0) {writeLen = fwrite(buffer, 1, readLen, fp2);// 如果一次寫入的長度和讀取的長度不等,那么說明寫入失敗if (readLen != writeLen) {throw WriteFileError(); }}// 關閉文件fclose(fp1);fclose(fp2);return 0; // 一切正常返回0
}int makeFile2(const char* dest, const char* src) {int ret;ret = makeFile(dest, src);printf("makeFile2 函數被調用");return ret;
}int main(void) {int ret = 0;try {ret = makeFile2("dest.txt", "src.txt");}catch (OpenFileError& error) {error.print();}catch (WriteFileError& error) {error.print();}system("pause");return 0;
}
其實拋出類對象的寫法不止一種,但是我們為什么選擇使用上面的方式呢????
我們使用下面的代碼進行說明。
class Error {
public:Error(int err) :errorData(err) {cout << "構造函數" << errorData << endl;};~Error() {cout << "析構函數" << errorData << endl;};Error(const Error& error) {errorData = error.errorData;cout << "拷貝構造函數"<< errorData << endl;};void print() {printf("異常:%d", errorData);}
public:int errorData;
};void func1() {Error err(-1);throw err;printf("func1");
}int main(void) {try {func1();}catch (Error error) {error.errorData = 10;printf("異常處理 %d\n",error);}system("pause");return 0;
}
運行結果:?
?
分析:?
上面代碼,我們拋出異常時是拋出的定義的對象,在catch接收的時候也是直接使用的普通參數形式(Error error)。 我們使用類對象的構造,析構,拷貝來觀察拋出的過程。
運行結果:??
第一個構造函數是用來構造我們的func1函數中的error對象的。?
第一個拷貝構造函數是在拋出的時候,編譯器會根據我們拋出的error對象,創建一個匿名對象進行拋出,所以會調用一次拷貝構造函數。?
第二個拷貝構造函數是我們拋出的匿名對象,在與catch中的參數Error error配對之后,直接將匿名對象在error初始化時賦值給它。?
第一個析構函數是我們拋出創建的匿名對象后,func1函數就執行結束了,error是其內部的局部變量,就會被銷毀,所以這個析構函數是用來銷毀func1中的error對象的。?
異常處理 10是我們配對成功,之后執行的異常處理代碼。?
析構函數 10是我們在和catch匹配的時候,根據其參數創建了對象error,其作用域就是這個catch開始到結束,catch的代碼執行完,結束的時候,這個對象的生命周期也就結束了,所以調用析構函數?
析構函數 -1是用來銷毀系統拋出的匿名對象,而調用的構造函數。
?
說明:?
最后兩個析構函數我們怎么能確定是來銷毀哪個對象的呢?在代碼中我們在對應的構造函數中打印了數據errorData的值,我們在func1中創建對象時將其初始化為-1,然后編譯器會將其拷貝給匿名對象,匿名對象內的屬性值我們無法處理,但是在catch接收匿名對象的時候,定義了另外一個對象error,我們將這個對象的值顯示修改為10。所以,析構函數 10就是用來析構它的。
對比:?
我們將第二段代碼進行修改 --? 在拋出時,使用匿名對象,接收是使用引用
class Error {
public:Error(int err) :errorData(err) {cout << "構造函數" << errorData << endl;};~Error() {cout << "析構函數" << errorData << endl;};Error(const Error& error) {errorData = error.errorData;cout << "拷貝構造函數"<< errorData << endl;};void print() {printf("異常:%d", errorData);}
public:int errorData;
};void func1() {throw Error(-1);printf("func1");
}int main(void) {try {func1();}catch (Error& error) {error.errorData = 10;printf("異常處理 %d\n",error);}system("pause");return 0;
}
運行結果:?
?
分析:? ?
首先第一眼看,這個運行的效率就比前面的效率高很多。?就是在拋出類對象的時候,直接拋出匿名對象,在catch的參數中寫使用類的引用接收。
?
運行結果:?
構造函數 -1:? ?創建匿名對象并拋出。?
異常處理 10:? ?匹配成功,執行異常處理代碼。?
析構函數 10:?? 調用析構函數,銷毀匿名對象。
?
說明:??
為什么構造函數和析構函數打印出來的errorData不一樣?因為我們在構造匿名函數時將errorData初始化為-1,但是在catch捕獲異常的時候,我們將其修改為了10。因為我們catch中使用的是引用,所以error還是表示哪個匿名對象,所以在析構時errorData變為了10。?
?
那么為什么可以使用引用來接收函數拋出的匿名對象呢?
因為,我們在第二段代碼中知道,在異常機制中,函數拋出的匿名對象析構要在catch中創建的對象析構之后。所以,我們使用引用來接收函數返回的匿名對象之后,在catch語句結束時,是引用的量先被釋放,匿名對象后被釋放。這樣就不會存在引用指向局部變量的問題了。
?
總結:? ??
綜上所述,我們在使用自定義類拋出異常的時候,應該直接拋出其匿名對象,并且使用引用來接收。這樣就會少調用幾次構造函數和析構函數,那么就大大提高了效率。
?
注意事項:
1.? 上面說到,catch的參數中對象的引用和對象的定義都可以與拋出的匿名對象進行匹配,那么如果同時存在兩個catch,參數分別為這兩種,那么發生什么??
會出錯,因為兩個都能匹配,編譯器區分不了,自然就報錯了。?
2.? ?1.中的情況,不僅是類類型,對于普通類型和字符串類型也是一樣的。?
3.? ?上面說到,普通類型,catch中參數加const和不加const都能匹配,所以兩者都存在的話,編譯器也無法區分,會報錯。?但是對于指針就不會報錯。
4.? ?其它情況也還類似。?