首頁 後端開發 C#.Net教程 C++例外處理

C++例外處理

Dec 13, 2016 pm 02:38 PM
c++例外處理

引言

異常,讓一個函數可以在發現自己無法處理的錯誤時拋出一個異常,希望它的呼叫者可以直接或間接處理這個問題。而傳統錯誤處理技術,檢查到一個局部無法處理的問題時:

1.終止程序(例如atol,atoi,輸入NULL,會產生段錯誤,導致程序異常退出,如果沒有core文件,找問題的人一定會發瘋)

2.傳回一個表示錯誤的值(很多系統函數都是這樣,例如malloc,記憶體不足,分配失敗,回傳NULL指標)

3.傳回一個合法值,讓程式處於某種非法的狀態(最坑爹的東西,有些第三方庫真會這樣)

4.調用一個預先準備好在出現"錯誤"的情況下用的函數。

第一種情況是不允許的,無條件終止程式的函式庫無法運用到不能當機的程式裡。第二種情況,比較常用,但是有時不合適,例如返回錯誤碼是int,每個調用都要檢查錯誤值,極不方便,也容易讓程式規模加倍(但是要精確控制邏輯,我覺得這種方式不錯)。第三種情況,很容易誤導呼叫者,萬一呼叫者沒有去檢查全域變數errno或透過其他方式檢查錯誤,那是一個災難,而且這種方式在並發的情況下不能很好工作。至於第四種情況,本人覺得比較少用,回呼的程式碼不該多出現。

使用異常,就把錯誤和處理分開來,由庫函數拋出異常,由調用者捕獲這個異常,調用者就可以知道程序函數庫調用出現錯誤了,並去處理,而是否終止程序就把握在調用者手裡了。

但是,錯誤的處理依然是一件很困難的事情,C++的異常機制為程式設計師提供了一種處理錯誤的方式,使程式設計師可以更自然的方式處理錯誤。

異常實戰入門

假設我們寫一個程序,把用戶輸入的兩個字符串轉換為整數,相加輸出,一般我們會這麼寫

char *str1 = "1", *str2 = "2";
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);
登入後複製

假設用戶輸入的是str1,str2,如果str1和str2都是整數類型的字串,這段代碼是可以正常工作的,但是用戶的輸入有可能誤操作,輸入了非法字符,例如

char *str1 = "1", *str2 = "a";
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);
登入後複製

這個時候結果是1,因為atoi(str2)返回0。

如果使用者輸入是這樣:

char *str1 = "1", *str2 = NULL;
int num1 = atoi(str1);
int num2 = atoi(str2);
printf("sum is %d\n", num1 + num2);
登入後複製

那麼這段程式碼會出現段錯誤,程式異常退出。

atoi我覺得是一個比較危險的函數,如果在一個重要係統中,調用者不知情,傳入了一個NULL字符,程序就異常退出了,導致服務中斷,或者傳入非法字符,結果返回0 ,程式碼繼續走下去,在複雜的系統中想要定位這個問題,真是很不容易。

所以比較適合的方式,是我們用異常處理改造一個安全的atoi方法,叫parseNumber。

class NumberParseException {};
bool isNumber(char * str) {
     using namespace std;
     if (str == NULL) 
        return false;
     int len = strlen(str);
     if (len == 0) 
        return false;
     bool isaNumber = false;
     char ch;
     for (int i = 0; i < len; i++) {
         if (i == 0 && (str[i] == &#39;-&#39; || str[i] == &#39;+&#39;)) 
            continue;
         if (isdigit(str[i])) {
            isaNumber = true;
         } else {
           isaNumber = false;
           break;
         }
     }
     return isaNumber;
}
int parseNumber(char * str) throw(NumberParseException) {
    if (!isNumber(str)) 
       throw NumberParseException();
    return atoi(str);
}
登入後複製

上述程式碼中NumberParseException是自訂的異常類,當我們偵測的時候傳入的str不是一個數字時,就拋出一個數字轉換異常,讓呼叫者處理錯誤,這比傳入NULL字串,導致段錯誤結束程序好得多,呼叫者可以捕獲這個異常,決定是否結束程序,也比傳入一個非整數字串,返回0要好,程序出現錯誤,卻繼續無聲無息執行下去。

於是我們之前寫的程式碼可以改造如下:

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } catch (NumberParseException) {
        printf("输入不是整数\n");
    }
登入後複製

這段程式碼的結果是列印出"輸入不是整數".假設這段程式碼是運行在一個遊戲統計系統中,系統需要定時從大量檔案中統計大量使用者進入遊戲頻道1和遊戲頻道2的次數,str1代表進入遊戲頻道1的次數,str2表示進入頻道2的次數,如果不是使用異常,當輸入是NULL程式會導致整個系統宕機,當輸入是非法整數,計算結果全部是錯誤的,當時程式仍然無聲無息"正確執行"。

輸入非法,拋出NumberParseException,即使呼叫者沒有考慮輸入是非法的,例如是:

 char *str1 = "1", *str2 = "12,";
    int num1 = parseNumber(str1);
    int num2 = parseNumber(str2);
    printf("sum is %d\n", num1 + num2);
登入後複製

就算調用者比較粗心,沒有捕獲異常,程序運行中會拋出NumberParseException,程序宕機,會留下coredump文件,呼叫者透過"gdb 程式名稱coredump檔",查看程式宕機時的堆疊,就知道程式運作中,出現了非法整數位符,那麼他就很快知道問題所在,會學乖,把上述程式碼改成

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } catch (NumberParseException) {
        printf("输入不是整数\n"); 
        //打印文件的路径,行号,str1,str2等信息足够自己去定位问题所在 
    }
登入後複製

這樣,下次程式出現問題時,呼叫者就可以定位問題所在了,這就是異常的錯誤處理方式,把錯誤的發現(parseNumber)和錯誤的處理(遊戲統計代碼)分開。和捕獲,還有異常的使用場景,接下來就開始一步步講解C++異常。

異常的描述

函數和函數可能拋出的異常集合作為函數聲明的一部分是有價值的,例如

void f(int a) throw (x2,x3);
登入後複製

表示f()只能抛出两个异常x2,x3,以及这些类型派生的异常,但不会抛出其他异常。如果f函数违反了这个规定,抛出了x2,x3之外的异常,例如x4,那么当函数f抛出x4异常时,
会转换为一个std::unexpected()调用,默认是调用std::terminate(),通常是调用abort()。

如果函数不带异常描述,那么假定他可能抛出任何异常。例如:

int f();  //可能抛出任何异常
登入後複製

带任何异常的函数可以用空表表示:

int g() throw (); // 不会抛出任何异常
登入後複製

捕获异常

捕获异常的代码一般如下:

try {
    throw E();
}
catch (H h) {
     //何时我们可以能到这里呢
}
登入後複製

1.如果H和E是相同的类型

2.如果H是E的基类

3.如果H和E都是指针类型,而且1或者2对它们所引用的类型成立

4.如果H和E都是引用类型,而且1或者2对H所引用的类型成立

从原则上来说,异常在抛出时被复制,我们最后捕获的异常只是原始异常的一个副本,所以我们不应该抛出一个不允许抛出一个不允许复制的异常。

此外,我们可以在用于捕获异常的类型加上const,就像我们可以给函数加上const一样,限制我们,不能去修改捕捉到的那个异常。

还有,捕获异常时如果H和E不是引用类型或者指针类型,而且H是E的基类,那么h对象其实就是H h = E(),最后捕获的异常对象h会丢失E的附加携带信息。

异常处理的顺序
我们之前写的parseNumber函数会抛出NumberParseException,这个函数只是判断是否数字才抛出异常,但是没有考虑,但这个字符串表示的整数太大,溢出,抛出异常Overflow.表示如下:

class NumberParseException {};
class Overflow : public NumberParseException {};
登入後複製

假设我们parseNumber函数已经为字符串的整数溢出做了检测,遇到这种情况,会抛出Overflow异常,那么异常捕获代码如下:

char *str1 = "1", *str2 = NULL;
    try {
        int num1 = parseNumber(str1);
        int num2 = parseNumber(str2);
        printf("sum is %d\n", num1 + num2);
    } 
    catch (Overflow) {
        //处理Overflow或者任何由Overflow派生的异常
    }
    catch (NumberParseException) {
         //处理不是Overflow的NumberParseException异常
    }
登入後複製

异常组织这种层次结构对于代码的健壮性很重要,因为库函数发布之后,不可能不加入新的异常,就像我们的parseNumber,第一次发布时只是考虑输入是否一个整数的错误,第二次发布时就考虑了判断输入的一个字符串作为整数是否太大溢出,对于一个函数发布之后不再添加新的异常,几乎所有的库函数都不能接受。

如果没有异常的层次结构,当函数升级加入新的异常描述时,我们可能都要修改代码,为每一处调用这个函数的地方加入对应的catch新的异常语句,这很让你厌烦,程序员也很容易忘记把某个异常加入列表,导致这个异常没有捕获,异常退出。

而有了异常的层次结构,函数升级之后,例如我们的parseNumber加入了Overflow异常描述,函数调用者只需要在自己感兴趣的调用场景加入catch(Overflow),并做处理就行了,如果根据不关心Overflow错误,甚至不用修改代码。

未捕获的异常

如果抛出的异常未被捕捉,那么就会调用函数std::terminate(),默认情况是调用abort,这对于大部分用户是正确选择,特别是排错程序错误的阶段(调用abort会产生coredump文件,coredump文件的使用可以参考博客的"学会用core dump调试程序错误")。

如果我们希望在发生未捕获异常时,保证清理工作,可以在所有真正需要关注的异常处理之外,再在main添加一个捕捉一切的异常处理,例如:

int main() {
    try {
        //...
     }
    catch (std::range_error) {
        cerr << "range error\n";
     } catch (std::bad_alloc) {
        cerr << "new run out of memory\n";
     } catch (...) {
       //..
     }
}
登入後複製

这样就可以捕捉所有的异常,除了那些在全局变量构造和析构的异常(如果要获得控制,唯一方式是set_unexpected)。
其中catch(...)表示捕捉所有异常,一般会在处理代码做一些清理工作。

重新抛出

当我们捕获了一个异常,却发现无法处理,这种情况下,我们会做完局部能够做的事情,然后再一次抛出这个异常,让这个异常在最合适的地方地方处理。例如:

void downloadFileFromServer() {
    try {
          connect_to_server();
          //...
     } 
      catch (NetworkException) {
           if (can_handle_it_completely) {
               //处理网络异常,例如重连
           } else {
                throw;
            }
       }
}
登入後複製

这个函数是从远程服务器下载文件,内部调用连接到远程服务器的函数,但是可能存在着网络异常,如果多次重连无法成功,就把这个网络异常抛出,让上层处理。

重新抛出是采用不带运算对象的throw表示,但是如果重新抛出,又没有异常可以重新抛出,就会调用terminate();

假设NetworkException有两个派生异常叫FtpConnectException和HttpConnectException,调用connect_to_server时是抛出HttpConnectException,那么调用downloadFileFromServer仍然能捕捉到异常HttpConnectException。

标准异常

到了这里,你已经基本会使用异常了,可是如果你是函数开发者,并需要把函数给别人使用,在使用异常时,会涉及到自定义异常类,但是C++标准已经定义了一部分标准异常,请尽可能复用这些异常,标准异常参考http://www.cplusplus.com/reference/std/stdexcept/

雖然C++標準異常比較少,但是作為函數開發者,盡可能還是複用c++標準異常,作為函數調用者就可以少花時間去了解的你自定義的異常類,更好的去調用你開發的函數。

總結

本文只是簡單從異常的使用場景,再介紹異常的基本使用方法,一些高級的異常用法沒有羅列,詳細資料可以參考c++之父的C++程式設計語言的異常處理。


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

char在C語言字符串中的作用是什麼 char在C語言字符串中的作用是什麼 Apr 03, 2025 pm 03:15 PM

在 C 語言中,char 類型在字符串中用於:1. 存儲單個字符;2. 使用數組表示字符串並以 null 終止符結束;3. 通過字符串操作函數進行操作;4. 從鍵盤讀取或輸出字符串。

C語言各種符號的使用方法 C語言各種符號的使用方法 Apr 03, 2025 pm 04:48 PM

C 語言中符號的使用方法涵蓋算術、賦值、條件、邏輯、位運算符等。算術運算符用於基本數學運算,賦值運算符用於賦值和加減乘除賦值,條件運算符用於根據條件執行不同操作,邏輯運算符用於邏輯操作,位運算符用於位級操作,特殊常量用於表示空指針、文件結束標記和非數字值。

char在C語言中如何處理特殊字符 char在C語言中如何處理特殊字符 Apr 03, 2025 pm 03:18 PM

C語言中通過轉義序列處理特殊字符,如:\n表示換行符。 \t表示製表符。使用轉義序列或字符常量表示特殊字符,如char c = '\n'。注意,反斜杠需要轉義兩次。不同平台和編譯器可能有不同的轉義序列,請查閱文檔。

c#多線程和異步的區別 c#多線程和異步的區別 Apr 03, 2025 pm 02:57 PM

多線程和異步的區別在於,多線程同時執行多個線程,而異步在不阻塞當前線程的情況下執行操作。多線程用於計算密集型任務,而異步用於用戶交互操作。多線程的優勢是提高計算性能,異步的優勢是不阻塞 UI 線程。選擇多線程還是異步取決於任務性質:計算密集型任務使用多線程,與外部資源交互且需要保持 UI 響應的任務使用異步。

char與wchar_t在C語言中的區別 char與wchar_t在C語言中的區別 Apr 03, 2025 pm 03:09 PM

在 C 語言中,char 和 wchar_t 的主要區別在於字符編碼:char 使用 ASCII 或擴展 ASCII,wchar_t 使用 Unicode;char 佔用 1-2 個字節,wchar_t 佔用 2-4 個字節;char 適用於英語文本,wchar_t 適用於多語言文本;char 廣泛支持,wchar_t 依賴於編譯器和操作系統是否支持 Unicode;char 的字符範圍受限,wchar_t 的字符範圍更大,並使用專門的函數進行算術運算。

char在C語言中如何進行類型轉換 char在C語言中如何進行類型轉換 Apr 03, 2025 pm 03:21 PM

在 C 語言中,char 類型轉換可以通過:強制類型轉換:使用強制類型轉換符將一種類型的數據直接轉換為另一種類型。自動類型轉換:當一種類型的數據可以容納另一種類型的值時,編譯器自動進行轉換。

char數組在C語言中如何使用 char數組在C語言中如何使用 Apr 03, 2025 pm 03:24 PM

char 數組在 C 語言中存儲字符序列,聲明為 char array_name[size]。訪問元素通過下標運算符,元素以空終止符 '\0' 結尾,用於表示字符串終點。 C 語言提供多種字符串操作函數,如 strlen()、strcpy()、strcat() 和 strcmp()。

C語言 sum 的作用是什麼? C語言 sum 的作用是什麼? Apr 03, 2025 pm 02:21 PM

C語言中沒有內置求和函數,需自行編寫。可通過遍歷數組並累加元素實現求和:循環版本:使用for循環和數組長度計算求和。指針版本:使用指針指向數組元素,通過自增指針遍歷高效求和。動態分配數組版本:動態分配數組並自行管理內存,確保釋放已分配內存以防止內存洩漏。

See all articles