使用 C 和 C++ 的程式語言概念/C++ 中的異常處理
類似於 Java,C++ 中的異常通常(並非總是!)是類型別的物件。也就是說,除了返回特定型別的返回值之外,函式還可以返回異常物件。但是,也可以丟擲原始型別的異常物件。以下是一個不常見情況的示例。
示例:丟擲非物件型別的異常。 enum ERESULT { arg_neg = -1, arg_toobig = -2}; long fact(short n) { if (n < 0) throw arg_neg; if (n > MAX_ARG) throw arg_toobig; if (n == 0 || n == 1) return 1; else return (n * fact(n – 1)); } // end of long fact(short)
C++ 中異常的其他特性與它們被指定和處理的方式有關。除了透過異常規範列出從函式丟擲的異常的精確列表之外,還可以選擇刪除規範並獲得丟擲任何異常的自由。
示例:異常規範。 // f1 可以丟擲型別為 E1 和 E2 的異常 void f1(...) throw(E1, E2); // f2 根本不丟擲任何異常 void f2(...) throw(); // f3 可以丟擲任何型別的異常。這可能是專案初始階段的一個不錯的選擇。 void f3(...);
如果我們顯式列出從函式丟擲的異常,並且事實證明丟擲了一個意外異常(即規範中未列出的異常)並且未在函式呼叫鏈中處理,則會呼叫在 C++ 標準庫中定義的 unexpected()。換句話說,規範違規的檢測在執行時進行。如果控制流從未到達丟擲意外異常的點,程式將執行而不會出現問題。
與它的設計理念一致,C++ 並不強制要求具有丟擲異常潛力的語句必須在 try 塊中發出。類似於從 Java 中的 RuntimeException 派生的異常,C++ 異常不需要被保護。如果我們能夠弄清楚異常永遠不會發生,我們可以刪除 try-catch 關鍵字並獲得更簡潔的程式碼。
示例:沒有強制的 try-catch 塊。 Rational divide_by_five(double x) { Rational rat1 = Rational(x); Rational nonzero_rat = Rational(5); Rational ret_rat = rat1.divide(nonzero_rat); return ret_rat; } // end of Rational divide_by_five(double)
#ifndef QUEUE_EXCEPTIONS_HXX
#define QUEUE_EXCEPTIONS_HXX
#include <iostream>
using namespace std;
namespace CSE224 {
namespace DS {
namespace Exceptions {
請注意,我們的異常類沒有任何成員欄位。換句話說,我們沒有辦法識別情況的詳細資訊。我們只知道出了問題,僅此而已!雖然在我們的例子中我們不需要任何關於問題性質的細節,但這並不總是這樣。
例如,以階乘為例。我們可能希望傳遞導致異常情況的引數值。這等效於說我們想區分同一個類的異常物件。實際上,我們可以將問題表述為區分同一個類的物件,無論是異常物件類還是其他任何類。我們可以透過簡單地向類定義新增欄位來實現這一點。
class Negative_Arg { public: void error(void) { cerr << "負引數 " << _arg_value << endl; } // end of void error(void) Negative_Arg(short arg) { _arg_value = arg; } private: short _arg_value; } // end of class Negative_Arg class TooBig_Arg { ... }
... long fact(short n) throw(Negative_Arg, TooBig_Arg) { if (n < 0) throw Negative_Arg(n); if (n > MAX_ARG) throw TooBig_Arg(n); if (n == 0 || n == 1) return 1; else return(n * fact(n – 1)); } // end of long fact(short) throw(Negative_Arg, TooBig_Arg) ...
class Queue_Empty {
public:
請注意,我們類的唯一函式被宣告為 static,這意味著我們可以透過類作用域運算子呼叫它,而無需建立類的例項。類似地,可以定義 static 資料欄位,這些欄位由類的所有例項共享,並且沒有義務透過類的物件訪問這些欄位。
static void error(void) { cerr << "Queue Empty!!!" << endl; }
}; // end of class Queue_Empty
} // end of namespace Exceptions
} // end of namespace DS
} // end of namespace CSE224
#endif
#ifndef QUEUE_HXX
#define QUEUE_HXX
#include <iostream>
using namespace std;
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
以下是前向類宣告。它的目的與 C 中的前向宣告類似:我們聲明瞭要使用名為 Queue_Node 的類的意圖,並將其定義推遲到其他地方。
請注意,我們不能宣告這種型別的物件。這是因為 C++ 不允許您宣告變數為其定義尚未完成的型別。因為編譯器無法確定物件所需的記憶體量。但是,我們可以宣告變數為指向這種類的指標或引用——理解為“常量指標”。
class Queue_Node;
class Queue {
在某些情況下,允許某個函式/類訪問類的非公開成員,而不允許程式中的其他函式/類訪問,這很方便。C++ 中的friend 機制允許類授予函式/類對其非public 成員的自由訪問權。
friend 宣告以關鍵字 friend 開頭。它只能出現在類定義中。換句話說,是類宣告某個函式/類為其 friend,而不是反過來。也就是說,您不能簡單地將某個類宣告為您的 friend 並訪問它的欄位。
由於 friend 不是授予友誼的類的成員,因此它們不受 public、protected 或 private 部分的影響,它們在類體中宣告。也就是說,friend 宣告可以在類定義中的任何地方出現。
根據以下宣告,過載的移位運算子 (<<) 可以自由訪問 Queue 物件的內部,該物件的引用作為第二個引數傳遞,就像它們是 public 一樣。
如果我們選擇將移位運算子設為例項函式,我們就無法實現我們的目標。以以下示例為例
cout << q1 << q2;
此語句將首先將 q1 列印到標準輸出檔案,然後列印 q2。我們可以透過以下語句達到相同的效果。
cout << q1; cout << q2;
事實上,這就是幕後發生的事情。我們可以透過應用以下轉換來了解發生了什麼。
cout << q1 << q2; ⇒ cout.operator<<(q1).operator<<(q2); ⇒ x.operator<<(q2);
移位訊息傳送了兩次:一次傳送到名為 cout 的物件,一次傳送到第一次呼叫相應函式 (x) 返回的物件。這意味著我們需要一個函式簽名,其中返回值和第一個引數型別相同:ostream 或 ostream&。瞭解例項函式以指向正在定義的類的例項的指標作為其隱式第一個引數,我們得出結論,移位運算子不能是 Queue 類的例項函式。解決方法是提供 friend 宣告,如下所示。
friend ostream& operator<<(ostream&, const Queue&);
public:
Queue(void) : _front(NULL), _rear(NULL), _size(0) { }
Queue(const Queue&);
~Queue(void);
Queue& operator=(const Queue&);
bool operator==(const Queue&);
請注意異常說明中使用的型別。第一個函式可以異常返回,丟擲 Queue_Empty 物件,而第二個函式將返回指向此類物件的指標。這並不奇怪。與僅在堆中建立物件的 Java 不同,C++ 允許您在所有三個區域建立物件——即堆、執行時堆疊和靜態資料區域。由於異常物件本質上是 C++ 物件,因此您可以在您喜歡的任何資料區域中建立它。
只要相應地宣告異常處理程式,以下異常說明之間幾乎沒有區別。[1] 第一個的處理程式將期望一個物件,而第二個將期望一個指向堆中某個區域的指標。[2]
double peek(void) throw(Queue_Empty);
double remove(void) throw(Queue_Empty*);
void insert(double);
bool empty(void);
private:
Queue_Node *_front, *_rear;
unsigned int _size;
}; // end of class Queue
請注意,以下類定義的所有欄位都是 private。也沒有函式來操作物件。因此,看起來我們需要一些魔法來建立和操作該類的物件。答案在於 Queue_Node 與 Queue 的關係:Queue_Node 與 Queue 密切耦合。一個 Queue_Node 物件只能在 Queue 物件的上下文中存在。friend 宣告反映了這一事實。由於此宣告,我們可以 [間接] 透過對某個 Queue 物件的操作來操作一個 Queue_Node 物件。
class Queue_Node {
friend class Queue;
下一條語句宣告移位運算子是 Queue_Node 類的 friend。在 Queue 類中也做出了類似的宣告,這意味著一個函式將有權檢視兩個不同類的內部。
friend ostream& operator<<(ostream&, const Queue&);
private:
double _item;
Queue_Node *_next;
Queue_Node(double val = 0) : _item(val), _next(NULL) { }
}; // end of class Queue_Node
} // end of namespace DS
} // end of namespace CSE224
#endif
#include <iomanip>
#include <iostream>
using namespace std;
#include "ds/Queue"
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
namespace CSE224 {
namespace DS {
Queue::
Queue(const Queue& rhs) : _front(NULL), _rear(NULL), _size(0) {
Queue_Node *ptr = rhs._front;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert(ptr->_item);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
} // end of copy constructor
我們的解構函式由程式設計師隱式呼叫(透過 delete 在釋放堆物件時)或由編譯器合成的程式碼呼叫(在釋放靜態和執行時堆疊物件的過程中),它刪除佇列中的所有節點,然後繼續清理為欄位預留的空間。如果我們忘記刪除這些專案,我們將最終得到下面給出的圖片,這實際上與沒有解構函式的情況下得到的圖片相同。

請注意,陰影區域表示由 delete 運算子本身返回給分配器的記憶體,而不是解構函式。[3] 只能透過陰影區域中的欄位訪問的所有佇列節點現在都變成了垃圾。因此,在刪除佇列時,我們必須刪除所有佇列專案,這正是我們在解構函式體中所做的。
另請注意,我們沒有在 try-catch 塊中編寫程式碼。與 Java 不同,這對 C++ 來說是可以的;如果您認為它們永遠不會發生,可以選擇省略 try-catch 塊。在這種情況下,刪除的次數保證與佇列中的專案數量一樣多,這不會產生任何異常情況。
Queue::
~Queue(void) {
unsigned int size = _size;
for(unsigned int i = 0; i < size; i++) remove();
} // end of destructor
Queue& Queue::
operator=(const Queue& rhs) {
if (this == &rhs) return (*this);
for(unsigned int i = _size; i > 0; i--) remove();
Queue_Node *ptr = rhs._front;
for(unsigned int i = 0; i < rhs._size; i++) {
this->insert(ptr->_item);
ptr = ptr->_next;
} // end of for(unsigned int i = 0; i < rhs._size; i++)
if (rhs._size == 0) {
_front = _rear = NULL;
_size = 0;
return(*this);
} // end of if(rhs._size == 0)
return (*this);
} // end of assignment operator
bool Queue::
operator==(const Queue& rhs) {
if (_size != rhs._size) return false;
if (_size == 0 || this == &rhs) return true;
Queue_Node *ptr = _front;
Queue_Node *ptr_rhs = rhs._front;
for (unsigned int i = 0; i < _size; i++) {
if (ptr->_item != ptr_rhs->_item)
return false;
ptr = ptr->_next;
ptr_rhs = ptr_rhs->_next;
} // end of for(unsigned int i = 0; i < _size; i++)
return true;
} // end of equality-test operator
double Queue::
peek(void) throw(Queue_Empty) {
if (empty()) throw Queue_Empty();
return(_front->_item);
} // end of double Queue::peek(void)
double Queue::
remove(void) throw(Queue_Empty*) {
if (empty()) throw new Queue_Empty();
double ret_val = _front->_item;
Queue_Node *temp_node = _front;
if (_front == _rear) _front = _rear = NULL;
else _front = _front->_next;
delete temp_node;
_size--;
return ret_val;
} // end of double Queue::remove(void)
void Queue::
insert(double value) {
Queue_Node *new_node = new Queue_Node(value);
if (empty()) {
_front = _rear = new_node;
_size = 1;
return;
} // end of if (empty())
_rear->_next = new_node;
_rear = _rear->_next;
_size++;
} // end of void Queue::insert(double)
bool Queue::
empty(void) { return (_size == 0); }
以下輸出運算子定義同時使用了 Queue 和 Queue_Node 類。它首先透過使用 Queue 類的私有欄位列印佇列的長度,然後透過遍歷每個節點(它們是 Queue_Node 型別)輸出相應佇列的內容。為此,我們必須使此函式成為兩個類的 friend。
ostream& operator<<(ostream& os, const Queue& rhs) {
os << "( " << rhs._size << " )";
if (rhs._size == 0) {
os << endl;
return(os);
} // end of if (rhs._size == 0)
os << "(front: ";
Queue_Node *iter = rhs._front;
while(iter != NULL) {
os << iter->_item << " ";
iter = iter->_next;
} // end of while(*iter != NULL)
os << " :rear )\n";
return(os);
} // end of ostream& operator<<(ostream&, const Queue&)
} // end of namespace DS
} // end of namespace CSE224
#include <fstream>
#include <string>
using namespace std;
#include "ds/Queue"
using namespace CSE224::DS;
#include "ds/exceptions/Queue_Exceptions"
using namespace CSE224::DS::Exceptions;
int main(void) {
Queue q1;
string fname("Queue_Test.input");
ifstream infile(fname.c_str());
if (!infile) {
cout << "Unable to open file: " << fname << endl;
return 1;
} // end of if(!infile)
現在,處理程式的引數 (q) 指向某個堆記憶體,我們必須在完成異常處理後立即銷燬該區域。這就是我們在處理程式中使用 delete 運算子所做的。
如果我們更傾向於傳遞一個物件而不是一個指向物件的指標,就像我們在 peek 中所做的那樣,那麼就不需要這樣的清理活動;由於編譯器合成的程式碼,它會在退出處理程式時自動執行。
請注意,我們可以將處理程式的第一個語句寫成 Queue_Empty::error(); 這樣做是可以的,因為我們異常類中的唯一函式是 static,這意味著我們可以透過類名呼叫它。
try { q1.remove(); }
catch(Queue_Empty* q) { q->error(); delete q; }
for (int i = 0; i < 10; i++) {
double val;
infile >> val;
q1.insert(val);
} // end of for(int i = 0; i < 10; i++)
infile.close();
cout << q1;
Queue q2 = q1;
cout << "Queue 1: " << q1;
cout << "Queue 2: " << q2;
if (q1 == q2) cout << "OK" << endl;
else cout << "Something wrong with equality testing!" << endl;
q2.remove(); q2.remove();
cout << "Queue 2: " << q2;
if (q1 == q2) cout << "Something wrong with equality testing!" << endl;
else cout << "OK" << endl;
return(0);
} // end of int main(void)
C++ 中的輸入/輸出
[edit | edit source]C++ 中的輸入/輸出功能,作為標準庫的一部分,是透過iostream 庫提供的,該庫作為類層次結構實現,利用多重繼承和虛繼承。此層次結構包括處理來自使用者終端、磁碟檔案和記憶體緩衝區的輸入和/或輸出的類。

特定流型別的屬性以某種方式混雜在它的名稱中。例如,ifstream 代表一個檔案流,我們將其用作輸入源。類似地,ostringstream 是一個記憶體緩衝區——一個字串物件——流,用作輸出接收器。
基流類:ios
[edit | edit source]無論使用的類名是什麼,它最終都源自 ios,iostream 庫的基類。此類包含所有流共有的功能,例如用於操作狀態和格式的訪問器-修改器函式。在前面組中包含以下函式
iostate rdstate() const:返回當前流物件的狀態,可以是以下任何組合:good、eof、fail和bad。void setstate(iostate new_state):除了已設定的標誌外,還將流的狀態設定為new_state。請注意,此函式不能用於取消設定標誌值。void clear(iostate new_state = ios::goodbit):將狀態設定為在new_state中傳遞的值。int good(void):如果流上的最後一次操作成功,則返回true。int eof(void):如果流上的最後一次操作找到檔案末尾,則返回true。int fail(void):如果流上的最後一次操作不成功並且由於操作沒有丟失資料,則返回true。int bad(void):如果流上的最後一次操作不成功並且由於操作而丟失了資料,則返回true。
為了操作格式,我們有
char fill(void) const:返回當前使用的填充字元。預設字元為空格。char fill(char new_pad_char):將填充字元設定為new_pad_char並返回先前的值。int precision(void) const:返回用於輸出浮點數的有效數字數。預設值為 6。int precision(int new_pre):將精度設定為new_pre並返回先前的值。int width(void) const:返回輸出欄位寬度。預設值為 0,這意味著使用盡可能多的字元。int width(int new_width):將寬度設定為new_width並返回先前的值。fmtflags setf(fmtflags flag):設定一個標誌,用於控制輸出的生成方式。flag可以是以下之一:(用於輸出整數值的基值)ios::dec、ios::oct、ios::hex、(用於顯示浮點值)ios::scientific、ios::fixed、(用於對齊文字)ios::left、ios::right、ios::internal、(用於顯示額外資訊)ios::showbase、ios::showpoint、ios::showpos、ios::uppercase。就像接下來的四個函式一樣,此函式返回呼叫之前生效的狀態。fmtflags setf(fmtflags flag, fmtflags mask):清除在 mask 中傳遞的標誌組合,然後設定在flag中傳遞的標誌。fmtflags unsetf(fmtflags flag):setf的反向,此函式確保在flag中傳遞的標誌組合未設定。fmtflags flags(void) const: 返回當前的格式狀態。fmtflags flags(fmtflags new_flags): 將格式狀態設定為new_flags。
輸入流
[edit | edit source]除了上一節中列出的功能外,C++ 中的所有輸入流都支援以下函式。
istream& operator>>(type data): 過載的移入(或 *提取*)運算子用於讀取各種型別的數值,並且可以由程式設計師進一步過載。它可以級聯使用,如果輸入操作不成功,它將返回false,這意味著它也可以在布林表示式中使用。int get(void): 返回讀頭下的字元,並將其前進一位。int peek(void): 與前一個函式類似,peek返回讀頭下的字元,但不會移動它。也就是說,peek不會改變流的內容。istream& get(char& c):get(void)的級聯版本,此函式等效於operator>>(char&)。也就是說
in_str.get(c1).get(c2).get(c3);≡in_str >> c1 >> c2 >> c3;
istream& get(char* str, streamsize len, char delim = '\n'): 將一個以null結尾的字串讀入str。此字串的長度取決於第二個和第三個引數,它們分別儲存緩衝區的大小和哨兵字元。如果掃描len - 1而不讀取哨兵字元,則在緩衝區中追加'\0'並返回第一個引數。如果在填充緩衝區之前遇到哨兵字元,則讀頭將停留在哨兵字元上,並且所有讀入該點的所有內容以及終止符'\0'將返回到緩衝區中。istream& getline(ctype* str, streamsize len, char delim = '\n'): 與前一個函式類似,getline用於將一個以null結尾的字串讀入其第一個引數。但是,如果在填充緩衝區之前遇到哨兵字元,則哨兵字元不會保留在流中,而是被讀取和丟棄。注意第一個引數的型別是指向char、unsigned char或signed char之一。istream& read(void* buf, streamsize len): 將len位元組讀入buf,除非輸入先結束。如果在讀取len位元組之前輸入結束,此函式將設定ios::fail標誌並返回不完整的結果。istream& putback(char c): 對應於 C 的ungetc(char),此函式嘗試回退一個字元,並將回退的字元替換為 c。注意此操作僅保證能工作一次。連續使用它可能會或可能不會起作用。istream& unget(void): 嘗試回退一個字元。istream& ignore(streamsize len, char delim = traits::eof): 此函式讀取並丟棄多達len個字元,或所有字元,直到且包括delim。
輸出流
[edit | edit source]對上一節中列出的操作進行補充的是對輸出流執行的操作。在我們列出這些操作之前,我們應該提到一個關鍵點:為了使輸出操作生效,必須滿足以下條件之一
- 一個
endl操縱器或'\n'被插入到流中。 - 一個
flush操縱器被插入到流中或向流傳送一個flush()訊息。 - 附加到流的緩衝區已滿。
- 與流繫結的
istream物件執行輸入操作。繫結兩個流意味著它們的運算將同步。一個流行的例子是cin-cout對:在向cin傳送訊息之前,cout將被重新整理。也就是說,
cout << "Your name:"; ≡ cout << "Your name:"; cout.flush(); ≡ cout << "Your name" << flush; cin >> name; cin >> name; cin >> name;
ostream& operator<<(type data): 過載的移出(或 *插入*)運算子用於寫入各種型別的數值,並且可以由程式設計師進一步過載。與提取運算子一樣,它可以級聯使用。ostream& put(char c): 將c插入到當前流中。ostream& write(string str, streamsize len): 將str中的len個字元插入到當前流中。由於string物件可以從[const] char*構造,因此第一個引數也可以是 C 樣式的字元字串。
在繼續討論面向檔案的流之前,我們應該提到 istream 和 ostream 的功能在 iostream 類中被組合起來,它派生於這兩個類。也就是說,可以使用同一個流同時進行輸入和輸出。
檔案輸入和輸出
[edit | edit source]使用 ifstream 和 ofstream 可以從檔案讀取和寫入檔案。由於這些類繼承了相關的流類——分別是 istream 和 ostream——它們的例項可以接收前面各節中給出的訊息。除了這些,還可以使用以下列表。
ifstream(const char* fn, int mde = ios::in, int prt = 644),ofstream(const char* fn, int mde = ios::out, int prt = 644): 將正在構造的流連線到名為fn的磁碟檔案。第二個和第三個引數是可選的,用於指定流的使用方式。第三個引數特定於基於 Unix 的作業系統,表示檔案保護位。第二個引數指定如何開啟磁碟檔案,可以是以下內容的 [合理] 組合ios::in: 以輸入模式開啟檔案,並將讀指標定位在檔案開頭。ios::out: 以輸出模式開啟檔案。這樣做時,檔案將被截斷。ios::app: 以輸出模式開啟檔案。檔案內容不會被破壞,每次輸出操作都會將資料插入到檔案末尾。ios::bin: 將檔案內容視為原始資料。在'\n'對映到單個位元組的環境中,這並不需要。
ifstream(void)& ofstream(void): 建立一個流物件,但不將其連線到磁碟檔案。void open(const char* fn, int mde = def_mde, int prt = 644): 將先前構造的 [未連線] 流物件連線到磁碟檔案。ios::pos_type tellg/tellp(void): 返回檔案標記的位置。這些函式的最後字母,g表示獲取,p表示放置,用於提醒檔案標記是讀指標還是寫指標。void seekg/seekp(pos_type n_p): 這些函式將檔案標記(即讀指標或寫指標)移動到由n_p指定的絕對位元組號。seekg(讀作“尋求下一個獲取的新位置”)影響讀指標,而 seekp(讀作“尋求下一個放置的新位置”)影響寫指標。void seekg/seekp(off_type offset, ios::seekdir dir): 相對於dir指定的位置移動最多offset個位元組,dir可以取以下值之一: [檔案開頭]ios::beg, [當前檔案標記位置]ios::cur, 和 [檔案結尾]ios::end。
作為這份講義的結束語,我們應該提到同時從同一個檔案讀取和寫入的可能性。在這種情況下,可以構造一個 fstream 物件並使用它來實現我們的目標。
註釋
[edit | edit source]- ↑ 實際上,存在差異。無論是普通物件還是異常物件,在堆中建立的物件都由程式設計師管理,必須由她釋放。
- ↑ 事實上,你可以傳遞指向地址空間其他部分(例如靜態資料或執行時堆疊區域)的指標。但隨後如何決定是否釋放該區域?如果它指向堆中的某個位置,則程式設計師有責任,她必須釋放該物件;如果指向的物件不在堆中,其生命週期將由編譯器管理。我們最好更確定性,在堆中建立所有此類物件,或者讓處理程式接受一個額外的引數。或者更好的是,選擇傳遞物件,而不是指向物件的指標。
- ↑ 觀察到這基本上與在
malloced 堆物件的情況下使用free返回的區域相同:指標指向的區域。這種相似性使我們得出一個非正式定義:delete運算子是解構函式的隱式呼叫加上free。