C++ 程式設計 - 第 4 章
模板是一種使程式碼更具可重用性的方法。簡單示例包括建立可以儲存任意資料型別的通用資料結構。模板對程式設計師非常有用,尤其是在與多種 繼承 和 運算子過載 相結合時。標準模板庫 (STL) 在一個連線的模板框架內提供了許多有用的函式。
由於模板非常具有表現力,因此可以用於除通用程式設計以外的用途。其中一種用途稱為 模板超程式設計,它是一種在編譯時而不是執行時預先評估部分程式碼的方法。此處進一步討論僅與模板作為一種通用程式設計方法相關。
到目前為止,您應該已經注意到,執行相同任務的函式往往看起來很相似。例如,如果您編寫了一個列印整數的函式,則必須先宣告該整數。這樣,可以減少程式碼中出錯的可能性,但是,為了處理您使用的所有不同資料型別而必須建立函式的不同版本,這有點煩人。例如,您可能希望該函式僅列印輸入變數,無論該變數是什麼型別。為每種可能的輸入型別編寫不同的函式(double, char *等...)將非常麻煩。這就是模板的用武之地。
模板解決了一些與宏相同的問題,在編譯時生成“最佳化”程式碼,但受限於 C++ 的嚴格型別檢查。
引數化型別,更廣為人知的是模板,允許程式設計師建立一個可以處理許多不同型別的函式。無需考慮每種資料型別,您只需使用一個任意引數名稱,編譯器就會用您希望函式使用、操作等的不同資料型別替換它。
- 模板在編譯時使用原始碼例項化。
- 模板是型別安全的。
- 模板允許使用者定義的特化。
- 模板允許非型別引數。
- 模板使用“延遲結構約束”。
- 模板支援混合。
- 模板語法
模板使用起來相當容易,只需檢視語法即可
template <class TYPEPARAMETER>
(或者,等效地,一些人更喜歡)
template <typename TYPEPARAMETER>
有兩種型別的模板。一個函式模板的行為類似於一個可以接受多種不同型別引數的函式。例如,標準模板庫包含函式模板max(x, y)返回x或y,以較大者為準。max()可以像這樣定義
template <typename TYPEPARAMETER>
TYPEPARAMETER max(TYPEPARAMETER x, TYPEPARAMETER y)
{
if (x < y)
return y;
else
return x;
}
該模板可以像函式一樣呼叫
std::cout << max(3, 7); // outputs 7
編譯器透過檢查引數來確定這是一個呼叫max(int, int)並例項化函式的版本,其中型別TYPEPARAMETER是int.
無論引數x和y是整數、字串還是任何其他型別,只要可以說“x < y”就適用。如果您已定義自己的資料型別,則可以使用運算子過載來定義<對您的型別的含義,從而允許您使用max()函式。雖然這在孤立的示例中可能看起來是一個小的優勢,但在 STL 等綜合庫的背景下,它允許程式設計師獲得新資料型別的廣泛功能,只需為它定義幾個運算子即可。僅僅定義<允許一種型別與標準sort(), stable_sort()和binary_search()演算法一起使用;資料結構,如set、堆和關聯陣列;等等。
作為反例,標準型別complex沒有定義<運算子,因為 複數 沒有嚴格的順序。因此,如果x 和 y 是complex值,則max(x, y) 會導致編譯錯誤。同樣,其他依賴於<的模板不能應用於complex資料。不幸的是,編譯器在歷史上為這種型別的錯誤生成了比較晦澀難懂且無用的錯誤訊息。確保某個物件符合 方法協議 可以緩解這個問題。
{TYPEPARAMETER}只是您要在函式中使用的任意TYPEPARAMETER名稱。一些程式設計師更喜歡只使用T來代替TYPEPARAMETER.
假設您想建立一個能夠處理多種資料型別的交換函式...看起來像這樣
template <class SOMETYPE>
void swap (SOMETYPE &x, SOMETYPE &y)
{
SOMETYPE temp = x;
x = y;
y = temp;
}
您在上面看到的函式看起來非常類似於任何其他交換函式,不同之處在於函式定義之前的模板 <class SOMETYPE> 行以及程式碼中的 SOMETYPE 例項。在您通常需要使用的資料型別名稱或類的任何地方,您現在都用您在模板 <class SOMETYPE> 中使用的任意名稱替換。例如,如果您使用 **SUPERDUPERTYPE** 而不是 **SOMETYPE**,程式碼將如下所示
template <class SUPERDUPERTYPE>
void swap (SUPERDUPERTYPE &x, SUPERDUPERTYPE &y)
{
SUPERDUPERTYPE temp = x;
x = y;
y = temp;
}
如您所見,您可以使用您想要的任何標籤作為模板 **TYPEPARAMETER**,只要它不是保留字。
類模板
[edit | edit source]一個 *類模板* 將相同概念擴充套件到類。類模板通常用於建立泛型容器。例如,STL 有一個 連結串列 容器。要建立一個整數的連結串列,可以寫list<int>. 字串的連結串列表示為list<string>. 一種list有一組與之關聯的標準函式,無論您在方括號中放入什麼,這些函式都起作用。
如果您想擁有多個模板 **TYPEPARAMETER**,那麼語法將是
template <class SOMETYPE1, class SOMETYPE2, ...>
- 模板和類
假設您不想建立簡單的模板化函式,而是想將模板用於類,以便該類可以處理多種資料型別。您可能已經注意到,某些類能夠接受型別作為引數並根據該型別建立物件的變體(例如 STL 容器類層次結構的類)。這是因為它們被宣告為模板,使用類似於下面呈現的語法
template <class T> class Foo
{
public:
Foo();
void some_function();
T some_other_function();
private:
int member_variable;
T parametrized_variable;
};
定義模板類的成員函式有點像定義函式模板,只是您使用範圍解析運算子來指示這是模板類的成員函式。一個重要且非顯而易見的細節是在類名之後使用包含引數化型別名的模板運算子。
以下示例透過定義來自上面示例類的函式來描述所需的語法。
template <class T> Foo<T>::Foo()
{
member_variable = 0;
}
template <class T> void Foo<T>::some_function()
{
cout << "member_variable = " << member_variable << endl;
}
template <class T> T Foo<T>::some_other_function()
{
return parametrized_variable;
}
如您可能已經注意到,如果您要宣告一個返回引數化型別物件的函式,您只需要使用該引數的名稱作為函式的返回型別。
優點和缺點
[edit | edit source]模板的一些用法,例如max()函式,以前由類似函式的 預處理器 宏 填充。
// a max() macro
#define max(a,b) ((a) < (b) ? (b) : (a))
宏和模板都在編譯時擴充套件。宏總是內聯擴充套件;模板也可以作為行內函數擴充套件,只要編譯器認為合適。因此,類似函式的宏和函式模板都沒有執行時開銷。
然而,模板通常被認為比宏更適合這些用途。模板是型別安全的。模板避免了在大量使用類似函式的宏的程式碼中發現的一些常見錯誤。也許最重要的是,模板旨在應用於比宏更大的問題。類似函式的宏的定義必須放在程式碼的單行邏輯行上。
使用模板有三個主要缺點。首先,歷史上許多編譯器對模板的支援非常差,因此使用模板會使程式碼的可移植性降低。其次,幾乎所有編譯器在模板程式碼中檢測到錯誤時都會產生令人困惑、無用的錯誤訊息。這使得模板難以開發。第三,模板的每次使用都可能導致編譯器生成額外的程式碼(模板的 *例項化*),因此不加選擇地使用模板會導致 程式碼膨脹,從而導致可執行檔案過大。
模板的另一個主要缺點是,無法替換類似 #define 的 max,它與不同型別或函式呼叫相同地起作用。模板已經取代了使用 #define 來建立複雜函式,但沒有取代像 max(a,b) 這樣的簡單函式。有關嘗試為 #define max 建立模板的完整討論,請參閱 Scott Meyer 為 *C++ Report* 在 1995 年 1 月撰寫的論文 "Min, Max and More"。
使用模板的最大優勢是,複雜的演算法可以具有簡單的介面,然後編譯器使用該介面根據引數的型別選擇正確的實現。例如,搜尋演算法可以利用被搜尋容器的屬性。這種技術貫穿整個 C++ 標準庫。
連結問題
[edit | edit source]在連結一個基於模板的程式時,該程式由分佈在幾個檔案中的多個模組組成,經常會遇到一個令人費解的情況,即模組的目的碼無法連結,原因是“對(在...中插入模板成員函式名稱)的未解析引用”。錯誤的函式實現就在那裡,那麼為什麼它在目的碼中缺失了呢?讓我們停下來思考一下,這怎麼可能發生。
假設您建立了一個名為 Foo 的基於模板的類,並將它的宣告放在檔案 Util.hpp 中,以及另一個名為 Bar 的普通類
template <class T> Foo
{
public:
Foo();
T some_function();
T some_other_function();
T some_yet_other_function();
T member;
};
class Bar
{
Bar();
void do_something();
};
現在,為了遵守所有藝術規則,您建立一個名為 Util.cc 的檔案,將所有函式定義(模板或其他)都放在其中
#include "Util.hpp"
template <class T> T Foo<T>::some_function()
{
...
}
template <class T> T Foo<T>::some_other_function()
{
...
}
template <class T> T Foo<T>::some_yet_other_function()
{
...
}
最後
void Bar::do_something()
{
Foo<int> my_foo;
int x = my_foo.some_function();
int y = my_foo.some_other_function();
}
接下來,您編譯模組,沒有錯誤,您很高興。但是,假設程式中還有另一個(主)模組,位於 MyProg.cc 中
#include "Util.hpp" // imports our utility classes' declarations, including the template
int main()
{
Foo<int> main_foo;
int z = main_foo.some_yet_other_function();
return 0;
}
這也乾淨地編譯成目的碼。但是,當您嘗試將這兩個模組連結在一起時,您會收到一個錯誤,提示 MyProg.cc 中存在對 Foo<int>::some_yet_other function() 的未定義引用。您已正確定義模板成員函式,那麼問題出在哪裡?
如您所知,模板在編譯時例項化。這有助於避免程式碼膨脹,程式碼膨脹將是為所有可能的型別生成所有可能的類和函式變體的結果。因此,當編譯器處理 Util.cc 程式碼時,它看到 Foo 類的唯一變體是 Foo<int>,唯一需要的函式是
int Foo<int>::some_function();
int Foo<int>::some_other_function();
Util.cc 中的程式碼不需要 Foo 或其方法的任何其他變體存在,因此編譯器只生成了這些程式碼。目的碼中沒有 some_yet_other_function() 的實現,就像沒有
double Foo<double>::some_function();
或
string Foo<string>::some_function();
MyProg.cc 程式碼編譯沒有錯誤,因為 Foo 使用的成員函式在 Util.hpp 標頭檔案中正確宣告,並且預期它將在連結時可用。但它不存在,因此出現了錯誤,如果您是模板新手並且開始在程式碼中尋找錯誤,就會帶來很多麻煩,而您的程式碼實際上是完全正確的。
解決方案在一定程度上取決於編譯器。對於 GNU 編譯器,嘗試使用 -frepo 標誌進行實驗,還可以閱讀 “info gcc” 中與模板相關的部分(節點“模板例項化”:“模板在哪裡?”)可能會有所啟發。在 Borland 中,據說連結器選項中有一個選項可以啟用“智慧”模板,專門針對這類問題。
您還可以嘗試另一種方法,稱為顯式例項化。您所做的是在包含模板的模組中建立一些虛擬程式碼,它建立模板類的所有變體並呼叫您知道在其他地方需要的其所有成員函式變體。顯然,這需要您瞭解程式碼中需要哪些變體。在我們這個簡單的例子中,這將如下所示
1. 將以下類宣告新增到 Util.hpp
class Instantiations
{
private:
void Instantiate();
};
2. 將以下成員函式定義新增到 Util.cc
void Instantiations::Instantiate()
{
Foo<int> my_foo;
my_foo.some_yet_other_function();
// other explicit instantiations may follow
}
當然,您永遠不需要實際例項化 Instantiations 類,也不需要呼叫其任何方法。它們在程式碼中存在的事實會使編譯器生成所需的模板的所有變體。現在目的碼將能夠成功連結。
還有一種解決方案,雖然不是最優雅的,但仍然可行。只需將所有模板函式的定義程式碼移到 Util.hpp 標頭檔案。這並不漂亮,因為標頭檔案用於宣告,而實現應該在其他地方定義,但它在這種情況下確實有效。在編譯 MyProg.cc(以及包含 Util.hpp 的任何其他模組)程式碼時,編譯器將生成所有需要的模板變體,因為定義已經可用。
模板超程式設計概述
[edit | edit source]模板超程式設計 (TMP) 指的是使用 C++ 模板系統在程式碼中執行編譯時計算。在大多數情況下,可以將其視為“用型別程式設計”——因為 TMP 主要使用的“值”是特定的 C++ 型別。使用型別作為計算的基本物件允許將型別推斷規則的全部功能用於通用計算。
編譯時程式設計
[edit | edit source]預處理器允許在編譯時執行某些計算,這意味著在程式碼編譯完成後,決策已經做出,可以從編譯的可執行檔案中省略。以下是一個非常牽強的例子
#define myvar 17
#if myvar % 2
cout << "Constant is odd" << endl;
#else
cout << "Constant is even" << endl;
#endif
這種構造除了條件包含平臺特定程式碼之外,沒有太多應用。特別是沒有辦法迭代,因此不能用於通用計算。使用模板進行編譯時程式設計的工作方式類似,但功能要強大得多,實際上它是 *圖靈完備* 的。
特徵類是簡單形式的模板超程式設計的一個熟悉示例:給定型別的輸入,它們將與該型別關聯的屬性計算為輸出(例如,std::iterator_traits<> 接受迭代器型別作為輸入,並計算屬性,例如迭代器的 difference_type、value_type 等等)。
模板超程式設計的本質
[edit | edit source]模板超程式設計更接近函數語言程式設計,而不是普通的慣用 C++。 這是因為“變數”都是不可變的,因此需要使用遞迴而不是迭代來處理集合中的元素。 這對學習 TMP 的命令式程式設計師來說增加了另一層挑戰:除了學習它的機制外,他們還必須學會用不同的方式思考。
模板超程式設計的侷限性
[edit | edit source]由於模板超程式設計是從模板系統的意外使用中發展而來,因此它經常很繁瑣。 通常很難讓維護人員清楚地瞭解程式碼的意圖,因為所使用程式碼的自然含義與程式碼的用途截然不同。 處理此問題的最有效方法是依賴習慣用法;如果你想成為一名高效的模板元程式設計師,你必須學會識別常見的習慣用法。
它還挑戰了舊編譯器的能力;一般來說,2000 年左右或之後的編譯器能夠處理大多數實用的 TMP 程式碼。 即使編譯器支援它,編譯時間也可能非常長,並且在編譯失敗的情況下,錯誤訊息通常難以理解。 有關模板例項化偵錯程式,請參閱 TempLight。
某些編碼標準甚至可能禁止模板超程式設計,至少在 Boost 等第三方庫之外是如此。
TMP 的歷史
[edit | edit source]從歷史上看,TMP 是一種意外;在標準化 C++ 語言的過程中,人們發現它的模板系統碰巧是圖靈完備的,即原則上能夠計算任何可計算的東西。 這方面的第一個具體證明是由 Erwin Unruh 編寫的程式,該程式計算了素數,儘管它實際上沒有完成編譯:素數列表是編譯器在嘗試編譯程式碼時生成的錯誤訊息的一部分。 [1] 從那時起,TMP 已經有了相當大的進步,現在已成為 C++ 中庫構建者的實用工具,但其複雜性意味著它通常不適用於大多數應用程式或系統程式設計環境。
#include <iostream>
template <int p, int i>
class is_prime {
public:
enum { prim = ( (p % i) && is_prime<p, i - 1>::prim ) };
};
template <int p>
class is_prime<p, 1> {
public:
enum { prim = 1 };
};
template <int i>
class Prime_print { // primary template for loop to print prime numbers
public:
Prime_print<i - 1> a;
enum { prim = is_prime<i, i - 1>::prim };
void f() {
a.f();
if (prim)
{
std::cout << "prime number:" << i << std::endl;
}
}
};
template<>
class Prime_print<1> { // full specialization to end the loop
public:
enum { prim = 0 };
void f() {}
};
#ifndef LAST
#define LAST 18
#endif
int main()
{
Prime_print<LAST> a;
a.f();
}
構建塊
[edit | edit source]值
[edit | edit source]TMP 中的“變數”並不是真正的變數,因為它們的值不能改變,但你可以擁有命名的值,你可以像在普通程式設計中使用變數一樣使用它們。 在使用型別程式設計時,命名值是型別定義
struct ValueHolder
{
typedef int value;
};
您可以將其視為“儲存”int 型別,以便可以在 value 名稱下訪問它。 整數值通常儲存為列舉中的成員
struct ValueHolder
{
enum { value = 2 };
};
這同樣儲存了值,以便可以在 value 名稱下訪問它。 這些示例中的任何一個本身都沒有用,但它們構成了大多數其他 TMP 的基礎,因此它們是需要意識到的重要模式。
函式
[edit | edit source]函式將一個或多個輸入引數對映到輸出值。 TMP 與此相似的模板是模板類
template<int X, int Y>
struct Adder
{
enum { result = X + Y };
};
這是一個將兩個引數相加並將結果儲存在 result enum 成員中的函式。 您可以在編譯時使用類似 Adder<1, 2>::result 的內容呼叫它,這將在編譯時擴充套件並與程式中的文字 3 完全相同。
分支
[edit | edit source]條件分支可以透過編寫模板類的兩個備用特化來構造。 編譯器將選擇適合所提供型別的那個,然後可以訪問例項化類中定義的值。 例如,考慮以下部分特化
template<typename X, typename Y>
struct SameType
{
enum { result = 0 };
};
template<typename T>
struct SameType<T, T>
{
enum { result = 1 };
};
這告訴我們它例項化的兩個型別是否相同。 這似乎並不太有用,但它可以識別可能隱藏型別是否相同的型別定義,並且可以用於模板程式碼中的模板引數。 你可以像這樣使用它
if (SameType<SomeThirdPartyType, int>::result)
{
// ... Use some optimised code that can assume the type is an int
}
else
{
// ... Use defensive code that doesn't make any assumptions about the type
}
上面的程式碼不太慣用:因為型別可以在編譯時識別,所以 if() 塊將始終具有一個微不足道的條件(它將始終解析為 if (1) { ... } 或 if (0) { ... })。 然而,這確實說明了可以實現的那種事情。
遞迴
[edit | edit source]由於在使用模板進行程式設計時沒有可變變數可用,因此無法遍歷一系列值。 可以在標準 C++ 中使用迭代來完成的任務必須用遞迴重新定義,即一個函式呼叫自身。 這通常採用模板類的形式,其輸出值遞迴地引用自身,以及一個或多個特化,它們為防止無限遞迴提供固定值。 你可以將其視為上面描述的函式和條件分支思想的結合。
計算階乘自然地以遞迴方式完成:,對於 ,。 在 TMP 中,這對應於一個類模板“階乘”,其一般形式使用遞迴關係,其特化終止遞迴。
首先,一般(非特化)模板表示 factorial<n>::value 由下式給出n*factorial<n-1>::value:
template <unsigned n>
struct factorial
{
enum { value = n * factorial<n-1>::value };
};
接下來,零的特化表示 factorial<0>::value 評估為 1
template <>
struct factorial<0>
{
enum { value = 1 };
};
現在,一些在編譯時“呼叫”階乘模板的程式碼
int main() {
// Because calculations are done at compile-time, they can be
// used for things such as array sizes.
int array[ factorial<7>::value ];
}
觀察到 factorial<N>::value 成員是用 factorial<N> 模板表示的,但這不可能無限期地繼續:每次評估時,它都會使用越來越小的(但非負)數字呼叫自身。 這最終必須達到零,此時特化開始,評估不會再遞迴。
示例:編譯時“If”
[edit | edit source]以下程式碼定義了一個名為“if_”的元函式;這是一個類模板,可用於根據編譯時常量在兩種型別之間進行選擇,如下面的main中所示
template <bool Condition, typename TrueResult, typename FalseResult>
class if_;
template <typename TrueResult, typename FalseResult>
struct if_<true, TrueResult, FalseResult>
{
typedef TrueResult result;
};
template <typename TrueResult, typename FalseResult>
struct if_<false, TrueResult, FalseResult>
{
typedef FalseResult result;
};
int main()
{
typename if_<true, int, void*>::result number(3);
typename if_<false, int, void*>::result pointer(&number);
typedef typename if_<(sizeof(void *) > sizeof(uint32_t)), uint64_t, uint32_t>::result
integral_ptr_t;
integral_ptr_t converted_pointer = reinterpret_cast<integral_ptr_t>(pointer);
}
在第 18 行,我們使用真值對if_模板進行求值,因此使用的型別是提供的第一個值。因此整個表示式if_<true, int, void*>::result求值為int。類似地,在第 19 行,模板程式碼求值為void *。這些表示式與在原始碼中將型別寫為文字值的效果完全相同。
第 21 行是它開始變得巧妙的地方:我們定義了一種型別,它取決於與平臺相關的sizeof表示式的值。在指標為 32 位或 64 位的平臺上,這將選擇正確的型別編譯時沒有任何修改,也無需預處理器宏。選擇型別後,就可以像使用任何其他型別一樣使用它。
為了比較,這個問題在 C90 中最好按如下方式解決
# include <stddef.h>
typedef size_t integral_ptr_t;
typedef int the_correct_size_was_chosen [sizeof (integral_ptr_t) >= sizeof (void *)? 1: -1];
碰巧的是,庫定義的型別size_t應該是任何平臺上這個問題的正確選擇。為了確保這一點,第 3 行用作編譯時檢查,以檢視所選型別是否實際上足夠大;如果不是,陣列型別the_correct_size_was_chosen將定義為負長度,導致編譯時錯誤。在 C99 中,<stdint.h>可能會定義型別intptr_t和uintptr_t。
除錯 TMP
[edit | edit source]截至 2017 年,這無法以任何有意義的方式完成。通常,拋棄模板並重新開始比試圖破譯從模板元程式中的單個位元組拼寫錯誤產生的編譯器輸出的錯綜複雜的迷宮更容易。
考慮 C++ 標準化委員會秘書 Herb Sutter 的以下觀察結果
Herb: Boost.Lambda, is a marvel of engineering… and it worked very well if … if you spelled it exactly right the first time, and didn’t mind a 4-page error spew that told you almost nothing about what you did wrong if you spelled it a little wrong. …
Charles: talk about giant error messages… you could have templates inside templates, you could have these error messages that make absolutely no sense at all.
Herb: oh, they are baroque.
來源:https://ofekshilon.com/2012/09/01/meta-programming-is-still-evil/
"結構化" TMP 的約定
[edit | edit source]標準模板庫 (STL)
[edit | edit source]標準模板庫 (STL) 是C++ 標準庫的一部分,它提供演算法、容器、迭代器和其他基本元件的集合,這些元件以模板、類和函式的形式實現,對於擴充套件 C++ 的功能和標準化至關重要。STL 的主要目標是透過強調效能和正確性來提供改進的實現標準化。
不用再擔心您的陣列是否需要容納 257 條記錄,也不用再擔心字串緩衝區溢位,您可以享受vector和string,它們會自動擴充套件以包含更多記錄或字元。例如,vector 就像陣列一樣,不同的是 vector 的大小可以擴充套件以容納更多單元格,或者在需要更少單元格時縮小。必須牢記,STL 不會與 OOP 衝突,但本身不是面向物件的;尤其是它沒有使用執行時多型性(即沒有虛擬函式)。
STL 的真正力量不在於它的容器類,而在於它是一個框架,它透過迭代器結合了演算法和資料結構,允許對高階演算法進行通用實現,從而高效地處理各種形式的資料。舉個簡單的例子,同一個std::copy函式可用於將一個數組中的元素複製到另一個數組中,或者複製檔案的位元組,或者將“類似這樣的文字”中的空格分隔的單詞複製到std::vector<std::string>之類的容器中。
// std::copy from array a to array b
int a[10] = { 3,1,4,1,5,9,2,6,5,4 };
int b[10];
std::copy(&a[0], &a[9], b);
// std::copy from input stream a to an arbitrary OutputIterator
template <typename OutputIterator>
void f(std::istream &a, OutputIterator destination) {
std::copy(std::istreambuf_iterator<char>(a),
std::istreambuf_iterator<char>(),
destination);
}
// std::copy from a buffer containing text, inserting items in
// order at the back of the container called words.
std::istringstream buffer("text like this");
std::vector<std::string> words;
std::copy(std::istream_iterator<std::string>(buffer),
std::istream_iterator<std::string>(),
std::back_inserter(words));
assert(words[0] == "text");
assert(words[1] == "like");
assert(words[2] == "this");
歷史
[edit | edit source]
C++ 標準庫包含 STL 的一部分(由SGI/惠普公司釋出為軟體庫)。C++ 標準模板庫的主要實現者是亞歷山大·斯特潘諾夫。
如今,我們稱 STL 為被納入 C++ 標準的內容。ISO C++ 不會指定標頭檔案內容,並且允許在標頭檔案中或在真正的庫中實現 STL。
編譯器已經包含一個作為 C++ 標準的一部分的實現(例如,MS Visual Studio 使用 Dinkum STL)。所有實現都必須符合標準關於功能和行為的要求,但程式在所有主要硬體實現、作業系統和編譯器之間的一致性也取決於 STL 實現的可移植性。它們還可以提供擴充套件的功能或針對不同的設定進行最佳化。
STL 有許多不同的實現,它們都基於語言標準,但仍然彼此不同,對程式設計師來說是透明的,使程式碼庫的專業化和快速演變成為可能。許多開源實現可用,可以用來參考。
- STL 實現列表。
- 來自 gnu 的 libstdc++(曾經是 libg++ 的一部分)
- SGI STL 庫 (http://www.sgi.com/tech/stl/) 免費 STL 實現。
- Rogue Wave 標準庫(HP、SGI、SunSoft、Siemens-Nixdorf)/Apache C++ 標準庫 (STDCXX)
- P.J. Plauger 的 Dinkum STL 庫 (http://www.dinkumware.com/) 商業 STL 實現,使用廣泛,因為它在與 Microsoft 共同維護的許可下,是隨 Visual Studio 提供的 STL 實現。
- Apache C++ 標準庫 (http://stdcxx.apache.org/) (開源)
- STLport STL 庫 (http://www.stlport.com/) 開源且高度跨平臺的實現,基於 SGI 實現。
容器
[edit | edit source]我們將在本書的這一部分中討論的容器是標準名稱空間 (std::) 的一部分。它們都起源於 STL 的原始 SGI 實現。
序列容器
[edit | edit source]- 序列 - 比陣列更易於使用
序列類似於 C 陣列,但更易於使用。Vector 通常是學習的第一個序列。其他序列,列表和雙端佇列,與 vector 類似,但在某些特殊情況下更高效。(它們的行為在迭代器在容器更改時有效性的重要方面也存在差異;迭代器有效性是使用 C++ 中的容器時一個重要的但有點高階的概念。)
- vector - "一個易於使用的陣列"
- list - 實際上是一個雙向連結串列
- deque - 雙端佇列(正確讀作“deck”,常被誤讀為“dee-queue”)
vector
[edit | edit source]vector 本身是一個類模板,它是一個序列容器,允許您輕鬆建立幾乎任何資料型別的元素(每個例項一個型別)或使用它時程式中的物件。vector 類為您處理大多數記憶體管理。
由於向量包含連續的元素,因此它是替換舊的 C 樣式陣列的理想選擇,在需要儲存資料的情況下,並且在需要將動態資料儲存為在程式執行期間大小會改變的陣列的情況下(舊的 C 樣式陣列無法做到這一點)。 但是,與靜態陣列相比,向量會產生非常小的開銷(取決於編譯器的質量),並且不能透過初始化列表進行初始化。
訪問向量的成員或追加元素需要固定時間,無論向量有多大,而查詢向量元素中的特定值或將元素插入向量需要的時間與它在向量中的位置成正比(取決於大小)。
- 示例
/*
David Cary 2009-03-04
quick demo for wikibooks
*/
#include <iostream>
#include <vector>
using namespace std;
vector<int> pick_vector_with_biggest_fifth_element(vector<int> left,vector<int> right)
{
if(left[5] < right[5])
{
return( right );
}
// else
return left ;
}
int* pick_array_with_biggest_fifth_element(int * left,int * right)
{
if(left[5] < right[5])
{
return( right );
}
// else
return left ;
}
int vector_demo(void)
{
cout << "vector demo" << endl;
vector<int> left(7);
vector<int> right(7);
left[5] = 7;
right[5] = 8;
cout << left[5] << endl;
cout << right[5] << endl;
vector<int> biggest(pick_vector_with_biggest_fifth_element( left, right ) );
cout << biggest[5] << endl;
return 0;
}
int array_demo(void)
{
cout << "array demo" << endl;
int left[7];
int right[7];
left[5] = 7;
right[5] = 8;
cout << left[5] << endl;
cout << right[5] << endl;
int * biggest =
pick_array_with_biggest_fifth_element( left, right );
cout << biggest[5] << endl;
return 0;
}
int main(void)
{
vector_demo();
array_demo();
}
- 成員函式
vector 類模擬了容器 概念,這意味著它具有begin()、end()、size()、max_size()、empty() 和 swap() 方法。
- 資訊
vector::front- 返回對向量第一個元素的引用。vector::back- 返回對向量最後一個元素的引用。vector::size- 返回向量中的元素數量。vector::empty- 如果向量沒有元素,則返回 true。
- 標準操作
vector::insert- 將元素插入向量(單個 & 範圍),將後面的元素向上移動。效率低下。vector::push_back- 將元素追加(插入)到向量的末尾,如果需要,為其分配記憶體。攤銷 O(1) 時間。vector::erase- 從向量中刪除元素(單個 & 範圍),將後面的元素向下移動。效率低下。vector::pop_back- 刪除向量的最後一個元素(可能減少容量 - 通常不會減少,但這取決於特定的 STL 實現)。攤銷 O(1) 時間。vector::clear- 刪除所有元素。但是請注意,如果資料元素是指向動態建立的記憶體的指標(例如,使用了new 運算子),則記憶體不會被釋放。
- 分配/大小修改
vector::assign- 用於刪除原始向量並將指定元素複製到一個空目標向量。vector::reserve- 更改向量的容量(分配更多記憶體),如果需要。在許多 STL 實現中,容量只能增長,永遠不會減少。vector::capacity- 返回向量的當前容量(已分配的記憶體)。vector::resize- 更改向量的大小。
- 迭代
vector::begin- 返回一個指向向量開始遍歷的迭代器。vector::end- 返回一個指向向量末尾的迭代器。vector::at- 返回對向量中指定位置的資料元素的引用,具有邊界檢查。
vector<int> v;
for (vector<int>::iterator it = v.begin(); it!=v.end(); ++it/* increment operand is used to move to next element*/) {
cout << *it << endl;
}
vector::迭代器
[edit | edit source]std::vector<T> 提供隨機訪問迭代器;與所有容器一樣,迭代器的主要訪問方式是透過 begin() 和 end() 成員函式。這些函式針對常量和非常量容器進行了過載,分別返回型別為 std::vector<T>::const_iterator 和 std::vector<T>::iterator 的迭代器。
向量示例
[edit | edit source] /* Vector sort example */
#include <iostream>
#include <vector>
int main()
{
using namespace std;
cout << "Sorting STL vector, \"the easier array\"... " << endl;
cout << "Enter numbers, one per line. Press ctrl-D to quit." << endl;
vector<int> vec;
int tmp;
while (cin>>tmp) {
vec.push_back(tmp);
}
cout << "Sorted: " << endl;
sort(vec.begin(), vec.end());
int i = 0;
for (i=0; i<vec.size(); i++) {
cout << vec[i] << endl;;
}
return 0;
}
對sort的呼叫實際上呼叫了函式模板的例項化std::sort,它將在由兩個隨機訪問迭代器指定的任何半開範圍上工作。
如果您想使上面的程式碼更“STLish”,您可以以以下方式編寫此程式。
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main()
{
using namespace std;
cout << "Sorting STL vector, \"the easier array\"... " << endl;
cout << "Enter numbers, one per line. Press ctrl-D to quit." << endl;
istream_iterator<int> first(cin);
istream_iterator<int> last;
vector<int> vec(first, last);
sort(vec.begin(), vec.end());
cout << "Sorted: " << endl;
copy(vec.begin(), vec.end(), ostream_iterator<int>(cout, "\n"));
return 0;
}
連結串列
[edit | edit source]STL 提供了一個名為list(標準名稱空間(std::)的一部分)的類模板,它實現了一個非侵入式雙向連結串列。連結串列可以在常數時間內在中間插入或刪除元素,但沒有隨機訪問。std::list 的一個有用特性是,只要該項保留在列表中,對插入到列表中的專案的引用、指標和迭代器就保持有效。
列表示例
[edit | edit source] /* List example - insertion in a list */
#include <iostream>
#include <algorithm>
#include <iterator>
#include <list>
void print_list(std::list<int> const& a_filled_list)
{
using namespace std;
ostream_iterator<int> out(cout, " ");
copy(a_filled_list.begin(), a_filled_list.end(), out);
}
int main()
{
std::list<int> my_list;
my_list.push_back(1);
my_list.push_back(10);
print_list(my_list); //print : 1 10
std::cout << std::endl;
my_list.push_front(45);
print_list(my_list); //print : 45 1 10
return 0;
}
關聯容器(鍵和值)
[edit | edit source]這種型別的容器使用鍵值指向容器中的每個元素,從而簡化了程式設計師搜尋容器。您不必逐個遍歷陣列或向量的元素來查詢特定的元素,而只需查詢 people["tero"] 即可。與向量和其他容器一樣,關聯容器可以擴充套件以容納任意數量的元素。
對映和多對映
[edit | edit source]map 和 multimap 是關聯容器,它們將鍵/值對管理為元素,如上所示。每個容器的元素將使用實際的鍵作為排序標準自動排序。這兩者之間的區別在於對映不允許重複,而多對映允許重複。
- map - 唯一鍵
- multimap - 可以多次使用相同的鍵
- set - 唯一鍵是值
- multiset - 鍵是值,可以多次使用相同的鍵
/* Map example - character distribution */
#include <iostream>
#include <map>
#include <string>
#include <cctype>
using namespace std;
int main()
{
/* Character counts are stored in a map, so that
* character is the key.
* Count of char a is chars['a']. */
map<char, long> chars;
cout << "chardist - Count character distributions" << endl;
cout << "Type some text. Press ctrl-D to quit." << endl;
char c;
while (cin.get(c)) {
// Upper A and lower a are considered the same
c=tolower(static_cast<unsigned char>(c));
chars[c]=chars[c]+1; // Could be written as ++chars[c];
}
cout << "Character distribution: " << endl;
string alphabet("abcdefghijklmnopqrstuvwxyz");
for (string::iterator letter_index=alphabet.begin(); letter_index != alphabet.end(); letter_index++) {
if (chars[*letter_index] != 0) {
cout << char(toupper(*letter_index))
<< ":" << chars[*letter_index]
<< "\t" << endl;
}
}
return 0;
}
容器介面卡
[edit | edit source]- stack - 後進先出 (LIFO)
- queue - 先進先出 (FIFO)
- 優先佇列
迭代器
[edit | edit source]C++ 的迭代器是 STL 的基礎之一。其他語言中也存在迭代器,但 C++ 使用了一種不同尋常的迭代器形式,具有優缺點。
在 C++ 中,迭代器是一個概念,而不是一個特定型別,它們是指標的泛化,是用於容器的抽象。迭代器根據遍歷屬性等屬性進一步細分。
迭代器的基本思想是提供一種方法來遍歷一些物件集合的概念。
一些(重疊的)迭代器類別是
- 單一迭代器
- 無效迭代器
- 隨機訪問迭代器
- 雙向迭代器
- 前向迭代器
- 輸入迭代器
- 輸出迭代器
- 可變迭代器
一對迭代器[begin, end) 用於定義一個半開區間,其中包含從begin 到end 標識的元素,但不包括由end 標識的元素。作為特殊情況,對於任何有效的迭代器 x,半開區間[x, x) 為空。
C++ 中最原始的迭代器示例(可能是其語法的靈感來源)是內建指標,它們通常用於迭代陣列中的元素。
迭代容器
[edit | edit source]訪問(但不修改)容器中的每個元素組型別C<T>使用迭代器。
for (
typename C<T>::const_iterator iter = group.begin();
iter != group.end();
++iter
)
{
T const &element = *iter;
// access element here
}
請注意 typename 的使用。它告訴編譯器 'const_iterator' 是一個型別,而不是一個靜態成員變數。(它僅在模板程式碼中是必需的,事實上,在 C++98 中,它在普通的非模板程式碼中是無效的。這可能在下一版 C++ 標準中發生變化,以便上述 typename 始終允許。)
修改容器中的每個元素組型別C<T>使用迭代器。
for (
typename C<T>::iterator iter = group.begin();
iter != group.end();
++iter
)
{
T &element = *iter;
// modify element here
}
在迭代容器時修改容器本身,一些容器(如 vector)需要謹慎,以確保迭代器不會失效,並最終指向一個無效的元素。例如,而不是
for (i = v.begin(); i != v.end(); ++i) {
...
if (erase_required) {
v.erase(i);
}
}
做
for (i = v.begin(); i != v.end(); ) {
...
if (erase_required) {
i = v.erase(i);
} else {
++i;
}
}
該erase()成員函式返回下一個有效的迭代器,或者end(),從而結束迴圈。請注意,++i在erase()被呼叫時不執行。
仿函式
[edit | edit source]仿函式或函式物件,是一個具有 operator () 的物件。仿函式的重要性在於它們可以在許多可以使用 C++ 函式的上下文中使用,同時還能夠維護狀態資訊。除了迭代器之外,仿函式是 STL 利用的最基本的概念之一。
STL 提供了許多預構建的仿函式類;例如,std::less 通常用於為需要確定兩個物件中哪個物件在“前面”的演算法指定預設比較函式。
#include <vector>
#include <algorithm>
#include <iostream>
// Define the Functor for AccumulateSquareValues
template<typename T>
struct AccumulateSquareValues
{
AccumulateSquareValues() : sumOfSquares()
{
}
void operator()(const T& value)
{
sumOfSquares += value*value;
}
T Result() const
{
return sumOfSquares;
}
T sumOfSquares;
};
std::vector<int> intVec;
intVec.reserve(10);
for( int idx = 0; idx < 10; ++idx )
{
intVec.push_back(idx);
}
AccumulateSquareValues<int> sumOfSquare = std::for_each(intVec.begin(),
intVec.end(),
AccumulateSquareValues<int>() );
std::cout << "The sum of squares for 1-10 is " << sumOfSquare.Result() << std::endl;
// note: this problem can be solved in another, more clear way:
// int sum_of_squares = std::inner_product(intVec.begin(), intVec.end(), intVec.begin(), 0);
演算法
[edit | edit source]STL 還提供了一些有用的演算法,以模板函式的形式提供,這些函式在迭代器概念的幫助下,可以操作 STL 容器(或派生類)。
STL 演算法不限於 STL 容器,例如
#include <algorithm>
int array[10] = { 2,3,4,5,6,7,1,9,8,0 };
int* begin = &array[0];
int* end = &array[0] + 10;
std::sort(begin, end);// the sort algorithm will work on a C style array
- _if 字尾
- _copy 字尾
- 非修改演算法
- 修改演算法
- 移除演算法
- 變異演算法
- 排序演算法
- 排序範圍演算法
- 數值演算法
排列
[edit | edit source]
排序和相關操作
[edit | edit source]sort
[edit | edit source]stable_sort
[edit | edit source]partial_sort
[edit | edit source]最小值和最大值
[edit | edit source]標準庫提供函式模板 min 和 max,它們分別返回兩個引數的最小值和最大值。每個函式都提供了一個過載,允許您自定義比較值的方式。
template<class T>
const T& min(const T& a, const T& b);
template<class T, class Compare>
const T& min(const T& a, const T& b, Compare c);
template<class T>
const T& max(const T& a, const T& b);
template<class T, class Compare>
const T& max(const T& a, const T& b, Compare c);
如何使用 Compare 型別引數的示例
#include <iostream>
#include <algorithm>
#include <string>
class Account
{
private :
std::string owner_name;
int credit;
int potential_credit_transfer;
public :
Account(){}
Account(std::string name, int initial_credit, int initial_credit_transfer) :
owner_name(name),
credit(initial_credit),
potential_credit_transfer(initial_credit_transfer)
{}
bool operator<(Account const& account) const { return credit < account.credit; }
int potential_credit() const { return credit + potential_credit_transfer; }
std::string const& owner() const { return owner_name; }
};
struct CompareAccountCredit
{
bool operator()(Account const& account1, Account const& account2) const
{ return account1 < account2; }
};
struct CompareAccountPotentialCredit
{
bool operator()(Account const& account1, Account const& account2) const
{ return account1.potential_credit() < account2.potential_credit(); }
};
int main()
{
Account account1("Dennis Ritchie", 1000, 250), account2("Steeve Jobs", 500, 10000),
result_comparison;
result_comparison = std::min(account1, account2, CompareAccountCredit());
std::cout << "min credit of account is : " + result_comparison.owner() << std::endl;
result_comparison = std::min(account1, account2, CompareAccountPotentialCredit());
std::cout << "min potential credit of account is : " + result_comparison.owner() << std::endl;
return 0;
}
分配器
[edit | edit source]分配器由標準 C++ 庫(尤其是 STL)使用,以允許對記憶體分配策略進行引數化。
分配器的話題有點晦澀,大多數應用程式軟體開發人員可以安全地忽略它。所有允許指定分配器的標準庫構造都具有預設分配器,如果使用者沒有指定分配器,則使用該分配器。
如果一段程式碼的記憶體使用方式不尋常,以至於如果使用通用預設分配器會導致效能問題,則自定義分配器可能會有用。在其他情況下,預設分配器也不合適,例如在使用全域性運算子 new 和 delete 的替代品的實現中使用標準容器時。
| 此頁面可能需要更新以反映當前知識。 自從 C++11 之後,auto_ptr 已被棄用,不再使用。您應該使用新的智慧指標來代替 auto_ptr:unique_ptr、weak_ptr 和 shared_ptr。unique_ptr 現在應該是 auto_ptr 的首選替代品。 您可以幫助更新它,討論進度,或請求幫助。 |
智慧指標
[edit | edit source]使用原始指標來儲存分配的資料,然後在解構函式中清理它們,通常被認為是一個非常糟糕的想法,因為它容易出錯。即使將分配的資料臨時儲存在原始指標中,然後在完成操作後將其刪除,也應避免這樣做。例如,如果您的程式碼丟擲異常,則可能難以正確捕獲異常並刪除所有分配的物件。
智慧指標可以透過使用編譯器和語言語義來確保指標內容在指標本身超出作用域時自動釋放,從而減輕這種麻煩。
#include <memory>
class A
{
public:
virtual ~A() {}
virtual char val() = 0;
};
class B : public A
{
public:
virtual char val() { return 'B'; }
};
A* get_a_new_b()
{
return new B();
}
bool some_func()
{
bool rval = true;
std::auto_ptr<A> a( get_a_new_b() );
try {
std::cout << a->val();
} catch(...) {
if( !a.get() ) {
throw "Memory allocation failure!";
}
rval = false;
}
return rval;
}
語義
[edit | edit source]auto_ptr 具有嚴格所有權的語義,這意味著 auto_ptr 例項是負責物件生命週期的唯一實體。如果複製 auto_ptr,源將丟失引用。例如
#include <iostream>
#include <memory>
using namespace std;
int main(int argc, char **arv)
{
int *i = new int;
auto_ptr<int> x(i);
auto_ptr<int> y;
y = x;
cout << x.get() << endl;
cout << y.get() << endl;
}
此程式碼將為第一個 auto_ptr 物件列印一個 NULL 地址,為第二個物件列印一個非 NULL 地址,表明源物件在賦值 (=) 期間丟失了引用。示例中的原始指標 i 不應刪除,因為它將由擁有引用的 auto_ptr 刪除。事實上,new int 可以直接傳遞給 x,從而消除了對 i 的需求。
請注意,由 auto_ptr 指向的物件使用 operator delete 銷燬;這意味著您應該只將 auto_ptr 用於使用 operator new 獲得的指標。這排除了由 malloc()、calloc() 或 realloc() 和 operator new[] 返回的指標。
異常處理
[edit | edit source]異常處理 是一種旨在處理異常發生的構造,即改變程式執行正常流程的特殊情況。在設計程式設計任務(類甚至函式)時,無法始終假設應用程式/任務能夠正常執行或完成(以預期結果退出)。它可能是該給定任務報告錯誤訊息(返回錯誤程式碼)或僅退出不合適的情況。為了處理這些型別的案例,C++ 支援使用語言構造將錯誤處理和報告程式碼與普通程式碼分開,也就是說,可以處理這些異常(錯誤和異常)的構造,因此我們稱這種為程式設計新增一致性的全域性方法為異常處理。
在檢測到某些錯誤或異常情況的地方,據說異常被丟擲。丟擲將導致正常程式流程中止,成為引發異常。異常是程式化的,程式設計師指定丟擲的條件。
在已處理異常中,程式的執行將在指定程式碼塊(稱為catch 塊)處恢復,該程式碼塊在程式執行方面包含丟擲點。catch 塊可以(通常位於)位於與丟擲點不同的函式/方法中。透過這種方式,C++ 支援非區域性錯誤處理。除了改變程式流程之外,丟擲異常還會將一個物件傳遞給 catch 塊。此物件可以提供處理程式碼決定如何對異常做出反應所需的資料。
考慮以下關於try 和catch 塊組合的程式碼示例以供說明
void AFunction()
{
// This function does not return normally,
// instead execution will resume at a catch block.
// The thrown object is in this case of the type char const*,
// i.e. it is a C-style string. More usually, exception
// objects are of class type.
throw "This is an exception!";
}
void AnotherFunction()
{
// To catch exceptions, you first have to introduce
// a try block via " try { ... } ". Then multiple catch
// blocks can follow the try block.
// " try { ... } catch(type 1) { ... } catch(type 2) { ... }"
try
{
AFunction();
// Because the function throws an exception,
// the rest of the code in this block will not
// be executed
}
catch(char const* pch) // This catch block
// will react on exceptions
// of type char const*
{
// Execution will resume here.
// You can handle the exception here.
}
// As can be seen
catch(...) // The ellipsis indicates that this
// block will catch exceptions of any type.
{
// In this example, this block will not be executed,
// because the preceding catch block is chosen to
// handle the exception.
}
}
另一方面,未處理異常將導致函式終止,並且堆疊將被展開(堆疊分配的物件將呼叫解構函式),因為它正在尋找異常處理程式。如果沒有找到,最終將導致程式終止。
從程式設計師的角度來看,引發異常是發出訊號表明例程無法正常執行的一種有用方法。例如,當輸入引數無效(例如,除法中的零分母)或它依賴的資源不可用(例如丟失的檔案或硬碟錯誤)時。在沒有異常的系統中,例程需要返回一些特殊的錯誤程式碼。但是,這有時會因半謂詞問題而變得複雜,在半謂詞問題中,例程的使用者需要編寫額外的程式碼來區分正常的返回值和錯誤的返回值。
由於很難編寫異常安全的程式碼,因此只有在必要時才使用異常——當發生無法處理的錯誤時。不要將異常用於程式的正常流程。
此示例是錯誤的,它演示了要避免的內容
void sum(int iA, int iB)
{
throw iA + iB;
}
int main()
{
int iResult;
try
{
sum(2, 3);
}
catch(int iTmpResult)
{
// Here the exception is used instead of a return value!
// This is wrong!
iResult = iTmpResult;
}
return 0;
}
考慮以下程式碼
void g()
{
throw std::exception();
}
void f()
{
std::string str = "Hello"; // This string is newly allocated
g();
}
int main()
{
try
{
f();
}
catch(...)
{ }
}
程式的流程
main()呼叫f()f()建立一個名為str的區域性變數str建構函式分配一個記憶體塊來儲存字串"Hello"f()呼叫g()g()丟擲異常f()不會捕獲異常。
- 由於異常沒有被捕獲,我們現在需要以乾淨的方式退出
f()。 - 此時,所有在丟擲之前區域性變數的解構函式
- 被呼叫——這被稱為“堆疊展開”。
str的解構函式被呼叫,它釋放了它佔用的記憶體。
- 正如您所見,“堆疊展開”機制對於防止資源洩漏至關重要——如果沒有它,
str將永遠不會被銷燬,它使用的記憶體將一直丟失到程式結束(甚至一直到下一次斷電,或冷啟動取決於作業系統的記憶體管理)。
main()捕獲異常- 程式繼續。
“堆疊展開”保證在離開其作用域時會呼叫區域性變數(堆疊變數)的解構函式。
有幾種方法可以丟擲異常物件。
丟擲指向物件的指標
void foo()
{
throw new MyApplicationException();
}
void bar()
{
try
{
foo();
}
catch(MyApplicationException* e)
{
// Handle exception
}
}
但現在,誰負責刪除異常?處理程式?這使程式碼更難看。一定有更好的方法!
怎麼樣這個
void foo()
{
throw MyApplicationException();
}
void bar()
{
try
{
foo();
}
catch(MyApplicationException e)
{
// Handle exception
}
}
看起來不錯!但現在,捕獲異常的 catch 處理程式按值進行捕獲,這意味著會呼叫複製建構函式。如果捕獲的異常是由於記憶體不足而導致的 bad_alloc,這會導致程式崩潰。在這種情況下,看似安全的程式碼(假定可以處理記憶體分配問題)會導致程式因異常處理程式失敗而崩潰。此外,按值捕獲可能會導致複製由於物件切片而具有不同的行為。
正確的方法是
void foo()
{
throw MyApplicationException();
}
void bar()
{
try
{
foo();
}
catch(MyApplicationException const& e)
{
// Handle exception
}
}
此方法具有所有優點——編譯器負責銷燬物件,並且在捕獲時不會進行復制!
結論是異常應該按值丟擲,並按(通常為 const)引用捕獲。
考慮程式碼片段
try
{
void x()
{
throw m();
}
}
catch n();
{
std:cout << "Exception caught\n";
}
當執行此程式碼時,程式將尋找異常的 catch 塊,但它不存在。因此,它將崩潰,不會繼續。finally 關鍵字允許在崩潰之前執行一些最終程式碼。
finally
{
// residual code that will execute in any case
}
請注意,如果異常被捕獲,則 try-catch 塊後面的行將照常執行。因此,finally 塊只會在沒有匹配的 catch 塊時起作用。
當從建構函式中丟擲異常時,該物件不被認為已例項化,因此其解構函式不會被呼叫。但是,相同主物件的已成功構造的基類和成員物件的解構函式將被呼叫。相同主物件的尚未構造的基類或成員物件的解構函式不會執行。示例
class A : public B, public C
{
public:
D sD;
E sE;
A(void)
:B(), C(), sD(), sE()
{
}
};
假設基類 C 的建構函式丟擲異常。然後執行順序為
BC(丟擲)~B
假設成員物件 sE 的建構函式丟擲異常。然後執行順序為
BCsDsE(丟擲)~sD~C~B
因此,如果執行了某些建構函式,那麼可以依賴於相同主物件之前執行的所有其他建構函式都已成功。這使您可以使用已構造的成員或基物件作為相同主物件的後續成員或基物件的建構函式的引數。
使用new分配此物件時會發生什麼?
- 分配物件的記憶體
- 物件的建構函式丟擲異常
- 由於異常,該物件沒有被例項化
- 刪除物件佔用的記憶體
- 異常被傳播,直到被捕獲
從建構函式中丟擲異常的主要目的是通知程式/使用者物件建立和初始化沒有正確完成。這是一種非常乾淨的方式來提供此重要資訊,因為建構函式不返回包含一些錯誤程式碼的單獨值(如初始化函式可能那樣)。
相反,強烈建議不要在解構函式中丟擲異常。重要的是要注意解構函式被呼叫的時間
- 作為正常釋放的一部分(退出作用域,刪除)
- 作為處理先前丟擲異常的堆疊展開的一部分。
在前一種情況下,在解構函式中丟擲異常可能只會導致由於物件分配錯誤而導致的記憶體洩漏。在後一種情況下,程式碼必須更聰明。如果異常是在由另一個異常引起的堆疊展開過程中丟擲的,則無法選擇首先處理哪個異常。這被解釋為異常處理機制的失敗,並導致程式呼叫 terminate 函式。
為了解決此問題,可以測試解構函式是否作為異常處理過程的一部分被呼叫。為此,應使用標準庫函式 uncaught_exception,如果已丟擲異常但尚未捕獲,則該函式返回 true。在這種情況下執行的所有程式碼都不得丟擲另一個異常。
需要這種仔細編碼的情況非常罕見。如果程式碼以解構函式根本不丟擲異常的方式編寫,則除錯起來會更安全、更輕鬆。
- 異常安全性
如果程式碼中的執行時故障不會產生不良影響(例如記憶體洩漏、儲存資料混亂或輸出無效),則稱該程式碼為異常安全。異常安全的程式碼必須滿足即使發生異常也會對程式碼施加的不變式。異常安全性有多個級別
- 故障透明性,也稱為不丟擲保證:即使在出現異常情況的情況下,操作也能保證成功並滿足所有要求。如果發生異常,它不會將異常進一步丟擲。(最佳級別的異常安全性。)
- 提交或回滾語義,也稱為強異常安全性或無更改保證:操作可能會失敗,但失敗的操作保證不會產生副作用,因此所有資料都保留原始值。
- 基本異常安全性:失敗操作的部分執行可能會導致副作用,但狀態上的不變式將保留。即使資料現在與異常發生之前的值不同,任何儲存的資料都將包含有效值。
- 最小異常安全性也稱為無洩漏保證:失敗操作的部分執行可能會儲存無效資料,但不會導致崩潰,並且不會洩漏任何資源。
- 無異常安全性:不提供任何保證。(最差級別的異常安全性)
考慮以下情況
void g()
{
throw "Exception";
}
void f()
{
int* pI = new int(0);
g();
delete pI;
}
int main()
{
f();
return 0;
}
你能看到這段程式碼的問題嗎?如果 `g()` 丟擲異常,變數 `pI` 永遠不會被刪除,就會造成記憶體洩漏。
為了防止記憶體洩漏,`f()` 必須捕獲異常並刪除 `pI`。但是 `f()` 不能處理異常,因為它不知道如何處理!
那麼解決方案是什麼呢?`f()` 應該捕獲異常,然後重新丟擲異常。
void g()
{
throw "Exception";
}
void f()
{
int* pI = new int(0);
try
{
g();
}
catch (...)
{
delete pI;
throw; // This empty throw re-throws the exception we caught
// An empty throw can only exist in a catch block
}
delete pI;
}
int main()
{
f();
return 0;
}
但是,有一個更好的方法:使用 **RAII** 類來避免使用異常處理。
如果你計劃在你的程式碼中使用異常,你必須始終嘗試以異常安全的方式編寫程式碼。讓我們來看看可能出現的一些問題。
考慮以下程式碼
void g()
{
throw std::exception();
}
void f()
{
int* pI = new int(2);
*pI = 3;
g();
// Oops, if an exception is thrown, pI is never deleted
// and we have a memory leak
delete pI;
}
int main()
{
try
{
f();
}
catch(...)
{ }
return 0;
}
你能看到這段程式碼的問題嗎?當丟擲異常時,我們永遠不會執行刪除 `pI` 的那行程式碼!
解決方案是什麼?之前我們看到了基於 `f()` 的捕獲和重新丟擲異常能力的解決方案。但是,使用“棧展開”機制有一個更簡潔的解決方案。但是“棧展開”僅適用於物件的解構函式,那麼我們如何使用它呢?
我們可以編寫一個簡單的包裝類。
// Note: This type of class is best implemented using templates, discussed in the next chapter.
class IntDeleter {
public:
IntDeleter(int* piValue)
{
m_piValue = piValue;
}
~IntDeleter()
{
delete m_piValue;
}
// operator *, enables us to dereference the object and use it
// like a regular pointer.
int& operator *()
{
return *m_piValue;
}
private:
int* m_piValue;
};
`f()` 的新版本。
void f()
{
IntDeleter pI(new int(2));
*pI = 3;
g();
// No need to delete pI, this will be done in destruction.
// This code is also exception safe.
}
這裡介紹的模式稱為*保護*。保護在其他情況下非常有用,它也可以幫助我們使程式碼更加異常安全。保護模式類似於其他語言中的*finally* 塊。
請注意,C++ 標準庫提供了名為 `unique_ptr` 的模板保護。
你可以丟擲一個物件(比如類或字串)、一個指標(比如 `char*`)或一個基本型別(比如 `int`)作為異常。那麼,你應該選擇哪一個呢?你應該丟擲物件,因為它們使程式設計師更容易處理異常。通常情況下,會建立一個異常類的類層次結構。
- class MyApplicationException {};
- class MathematicalException : public MyApplicationException {};
- class DivisionByZeroException : public MathematicalException {};
- class InvalidArgumentException : public MyApplicationException {};
- class MathematicalException : public MyApplicationException {};
一個例子
float divide(float fNumerator, float fDenominator)
{
if (fDenominator == 0.0)
{
throw DivisionByZeroException();
}
return fNumerator/fDenominator;
}
enum MathOperators {DIVISION, PRODUCT};
float operate(int iAction, float fArgLeft, float fArgRight)
{
if (iAction == DIVISION)
{
return divide(fArgLeft, fArgRight);
}
else if (iAction == PRODUCT))
{
// call the product function
// ...
}
// No match for the action! iAction is an invalid agument
throw InvalidArgumentException();
}
int main(int iArgc, char* a_pchArgv[])
{
try
{
operate(atoi(a_pchArgv[0]), atof(a_pchArgv[1]), atof(a_pchArgv[2]));
}
catch(MathematicalException& )
{
// Handle Error
}
catch(MyApplicationException& )
{
// This will catch in InvalidArgumentException too.
// Display help to the user, and explain about the arguments.
}
return 0;
}
一個函式可以丟擲的異常範圍是該函式公共介面的重要組成部分。沒有這些資訊,你將不得不假設在呼叫任何函式時可能會發生任何異常,因此需要編寫非常防禦性的程式碼。瞭解可以丟擲的異常列表,你可以簡化程式碼,因為它不需要處理每種情況。
這些異常資訊是公共介面的特定部分。類的使用者不需要知道它的實現方式,但他們確實需要知道可以丟擲的異常,就像他們需要知道成員函式的引數數量和型別一樣。向庫的客戶端提供這些資訊的一種方法是透過程式碼文件,但這需要非常仔細地手動更新。錯誤的異常資訊比沒有資訊更糟糕,因為你最終可能會編寫比你預期的異常安全性更低的程式碼。
C++ 提供了另一種記錄異常介面的方法,即透過*異常規範*。異常規範由編譯器解析,編譯器提供一定程度的自動檢查。異常規範可以應用於任何函式,看起來像這樣。
double divide(double dNumerator, double dDenominator) throw (DivideByZeroException);
你可以使用空的異常規範指定函式不能丟擲任何異常。
void safeFunction(int iFoo) throw();
C++ 在編譯時不會以程式設計方式強制執行異常規範。例如,以下程式碼是合法的。
void DubiousFunction(int iFoo) throw()
{
if (iFoo < 0)
{
throw RangeException();
}
}
C++ 不會在編譯時檢查異常規範,而是在執行時檢查它們,這意味著你可能在測試時才意識到你的異常規範不準確,或者如果你運氣不好,程式碼已經投入生產了才發現。
如果在執行時丟擲一個異常,該異常會從一個函式中傳播出去,而該函式在其異常規範中不允許該異常,那麼該異常將不再傳播,而是呼叫函式 `RangeException()`。函式 `RangeException()` 不會返回,但可以丟擲另一個型別的異常,該異常可能會(也可能不會)滿足異常規範並允許異常處理正常繼續。如果仍然無法恢復情況,程式將被終止。
許多人認為在執行時嘗試轉換異常的行為比僅僅允許異常傳播到可能處理它的呼叫者更糟糕。違反異常規範的事實並不意味著呼叫者*不能*處理這種情況,而只是意味著程式碼的作者沒有預料到這種情況。通常,棧上會存在一個 `catch (...)` 塊,它可以處理*任何*異常。
RTTI 指的是系統能夠報告物件的動態型別,並在執行時(而不是編譯時)提供有關該型別的的資訊,並且當一致地使用時,它可以成為一個強大的工具,簡化程式設計師管理資源的工作。
dynamic_cast
[編輯 | 編輯原始碼]考慮你已經瞭解的關於dynamic_cast 關鍵字的知識,假設我們有以下類層次結構。
class Interface
{
public:
virtual void GenericOp() = 0;// pure virtual function
};
class SpecificClass : public Interface
{
public:
virtual void GenericOp();
virtual void SpecificOp();
};
假設我們還有一個型別為 `Interface*` 的指標,如下所示。
Interface* ptr_interface;
假設出現了一種情況,我們被迫假設但沒有保證指標指向一個型別為 `SpecificClass` 的物件,並且我們想要呼叫該類的成員 `SpecificOp()`。為了動態轉換為派生型別,我們可以使用dynamic_cast,如下所示。
SpecificClass* ptr_specific = dynamic_cast<SpecificClass*>(ptr_interface);
if( ptr_specific ){
// our suspicions are confirmed -- it really was a SpecificClass
ptr_specific->SpecificOp();
}else{
// our suspicions were incorrect -- it is definitely not a SpecificClass.
// The ptr_interface points to an instance of some other child class of the base InterfaceClass.
ptr_interface->GenericOp();
};
使用dynamic_cast,程式將基類指標轉換為派生類指標,並允許呼叫派生類成員。但是,要非常小心:如果你嘗試轉換的指標不是正確的型別,那麼dynamic_cast 將返回一個空指標。
我們也可以對引用使用dynamic_cast。
SpecificClass& ref_specific = dynamic_cast<SpecificClass&>(ref_interface);
它的工作原理幾乎與指標相同。但是,如果要轉換的物件的實際型別不正確,那麼dynamic_cast 不會返回空(沒有空引用這樣的東西)。相反,它會丟擲一個 `std::bad_cast` 異常。
- 語法
typeid( object );
`typeid` 運算子用於在執行時確定物件的類。它返回對 `std::type_info` 物件的引用,該物件描述了“物件”,並且一直存在到程式結束。如果“物件”是一個解引用後的空指標,則該操作將丟擲一個 `std::bad_typeid` 異常。
類 `std::bad_typeid` 的物件派生自 `std::exception`,並且由 `typeid` 等丟擲。
在只需要類資訊的情況下,使用typeid通常比dynamic_cast<class_type>更受歡迎,因為typeid在型別或非解除引用的值上應用時是一個恆定時間過程,而dynamic_cast必須在執行時遍歷其引數的類派生網格。但是,您永遠不應該依賴於確切的內容,例如由std::type_info::name()返回的內容,因為這是針對編譯的實現特定的。
通常,僅在指向多型類型別(至少具有一個虛擬成員函式的類)的物件的指標或引用(即typeid(*ptr)或typeid(ref))的解除引用上使用typeid才有用。這是因為這些是與執行時型別資訊相關的唯一表達式。任何其他表示式的型別在編譯時都是靜態已知的。
- 示例
#include <iostream>
#include <typeinfo> //for 'typeid' to work
class Person {
public:
// ... Person members ...
virtual ~Person() {}
};
class Employee : public Person {
// ... Employee members ...
};
int main () {
Person person;
Employee employee;
Person *ptr = &employee;
// The string returned by typeid::name is implementation-defined
std::cout << typeid(person).name() << std::endl; // Person (statically known at compile-time)
std::cout << typeid(employee).name() << std::endl; // Employee (statically known at compile-time)
std::cout << typeid(ptr).name() << std::endl; // Person * (statically known at compile-time)
std::cout << typeid(*ptr).name() << std::endl; // Employee (looked up dynamically at run-time
// because it is the dereference of a
// pointer to a polymorphic class)
}
輸出(確切的輸出因系統而異)
Person
Employee
Person*
Employee
在 RTTI 中,它在這種設定中使用
const std::type_info& info = typeid(object_expression);
有時我們需要知道物件的精確型別。typeid運算子返回對標準類std::type_info的引用,該類包含有關型別的資訊。此類提供一些有用的成員,包括==和!=運算子。最有趣的方法可能是
const char* std::type_info::name() const;
此成員函式返回指向包含物件型別名稱的 C 風格字串的指標。例如,使用我們前面示例中的類
const std::type_info &info = typeid(*ptr_interface);
std::cout << info.name() << std::endl;
此程式將列印類似於[1] SpecificClass的內容,因為這是指標ptr_interface的動態型別。
typeid實際上是一個運算子而不是一個函式,因為它也可以作用於型別
const std::type_info& info = typeid(type);
例如(有點迴圈)
const std::type_info& info = typeid(std::type_info);
將提供一個type_info物件,該物件描述type_info物件。後一種用法不是 RTTI,而是 CTTI(編譯時型別識別)。
限制
[edit | edit source]RTTI 有一些限制。首先,RTTI 只能用於多型型別。這意味著您的類必須至少具有一個虛擬函式,無論是直接的還是透過繼承。其次,由於儲存型別所需的額外資訊,某些編譯器需要一個特殊的開關來啟用 RTTI。
請注意,對指標的引用在 RTTI 下不起作用
void example( int*& refptrTest )
{
std::cout << "What type is *&refptrTest : " << typeid( refptrTest ).name() << std::endl;
}
將報告int*,因為typeid()不支援引用型別。
RTTI 的誤用
[edit | edit source]RTTI 應該在 C++ 程式中謹慎使用。這有幾個原因。最重要的是,其他語言機制(如多型和模板)幾乎總是優於 RTTI。與所有事物一樣,存在例外,但關於 RTTI 的通常規則與goto語句或多或少相同。不要將其用作適當、更健壯設計的捷徑。只有在有充分的理由這樣做時才使用 RTTI,並且只有在知道自己在做什麼的情況下才使用它。
- ↑ (由 std::type_info::name() 返回的確切字串取決於編譯器)。
章節摘要
[edit | edit source]
