Essential C++ 學習筆記
1 C++ 程式設計基礎
1.1 如何撰寫 C++ 程式
1.2 物件的定義與初始化
物件初始化有兩種方法
int num_tries = 0; // 使用 assignment 運算子(=)進行初始化
int num_right(0); // 建構函式初始化
對比:
-
使用 assignment 初始化只能用於內建型別單一值初始化,而如果初始化只能用多個值的話,必須使用建構函式初始化
#include <complex> complex<double> purei(0, 7);
-
當“內建資料型別”與“程式設計師自行定義的 class 型別”具備不同初始化語法時,無法編寫出一個 template 使它同時支援“內建型別”與“class型別”。讓語法統一,可以簡化 template 的設計。
1.3 撰寫表示式
1.3.1 算術運算子
+ 加法運算
- 減法運算
* 乘法運算
/ 除法運算
% 取餘數
兩個整數相除會產生另一個整數(商)。小數點後的部分被捨棄,沒有四捨五入。
1.3.2 三目運算子
expr ? expr為true則執行這裡 : expr為false執行這裡
1.3.3 複合賦值
+=, -=, *=, /=, %= 等
1.3.4 自增 自減
前置方式:用前加/減,比如 ++cnt,--cnt
後置方式:用後加/減,比如 cnt++,cnt--
1.3.5 關係運算符
任何關係運算符的求值結果為 bool 值 (true/false)
== 相等
!= 不等
< 小於
> 大於
<= 小於等於
>= 大於等於
1.3.6 邏輯運算子
&& AND邏輯運算 左右兩個表示式結果皆為true時其求值結果為true
|| OR邏輯運算 左右兩個表示式有一個為true時其求值結果為true
! NOT邏輯運算 如果表示式結果為true,其求值結果為false,反之同理
AND邏輯運算子和 OR 邏輯運算子都是短路運算子,如果通過左邊的表示式的求值結果已經能確定整個表示式的求值結果,則右邊的表示式不會被求值。
1.3.7 運算子的優先順序
邏輯運算子 NOT
算術運算子 (*, /, %)
算術運算子 (+, -)
關係運算符 (>, <, <=, >=)
關係運算符 (==, !=)
邏輯運算子 (AND)
邏輯運算子 (OR)
賦值運算子
例:判斷偶數 ! ival % 2
實際上為 (!ival) % 2
,應該寫成 !(ival % 2)
1.4 條件語句和迴圈語句
前置知識:一個表示式求值的真假:
-
表示式返回的值是 bool 值 (true/false)
-
其他,C++ 會認為所有非0的值為 true,0值為false
- 數值型別的0值為
0
- 指標型別的0值為
nullptr / NULL
- 常量字串型別的0值為
""
補充:C++ 中賦值語句表示式的求值結果是被賦的值,比如a=3
的求值結果為3。
- 數值型別的0值為
1.4.1 條件語句
1.4.1.1 if 語句
if 後括號內的條件表示式如果求值為 true,則之後的語句/語句塊便會被執行
if () {
}
if () {
} else {
}
if () {
} else if {
} else if {
} else {
}
if 語句也是可以巢狀的
1.4.1.2 switch 語句
switch (expr) {
case 'a':
cout << 'a' << endl;
break;
case 'b':
cout << 'b' << endl;
break;
default:
cout << 'c' << endl;
break;
}
如果不加 break 的話,則其後的語句都會被執行,如果不匹配任何 case 則會執行 default 後的語句。
1.4.2 迴圈語句
1.4.2.1 while 迴圈
while (cin >> word) { // 如果讀取失敗則結果為 false
}
1.4.2.2 do-while 迴圈
do {
} while (expr);
1.4.2.3 for 迴圈
for (init-statement; condition; expression)
statement
其中 init-statement, condition, expression 都可以被省略,但是分號要保留。
1.5 array 和 vector
array 和 vector 都是連續存放變數的容器 (container) 型別
,可以通過變數在容器中的位置也就是索引來獲取變數
array(陣列) 是內建的資料型別,而 vector 則是由標準庫提供。
array 示例:
const int seq_size = 18;
int pell_seq[seq_size]; // 陣列的大小必須是常量表達式,也就是一個不需要再執行時求值的表示式
vector 示例:
#include <vector>
const int seq_size = 18;
vector<int> pell_seq(seq_size);
無論 array 還是 vector 我們都可以指定容器中的某個位置,進而訪問該位置上的元素。索引操作 (indexing) 是通過下標運算子 ([]
) 達成的。
注意:容器的第一個元素的索引為0
1.6 指標
指標中存放的是變數的記憶體中的地址,所以指標就是地址
int * p;
int
表明該地址下所存變數的型別*
表明該變數是指標p
變數的名稱
一個未指向任何物件的指標,其地址為 0 。有時候會被稱為 NULL
指標。
1.6.1 取址
獲取一個變數的記憶體地址用取址運算子(&
)
int *pi = &ival;
1.6.2 提領 / 解引用(dereference)
其實就是取得“位於該指標所指記憶體地址上”的物件,方法是在指標前使用 *
int val = *pi;
因為 NULL 其值為 0,沒有指向任何物件,所以不能提領,所以在對物件進行提領時,最好判斷指標的地址是否為0
if (pi && *pi != 1024)
*pi = 1024;
1.7 檔案的讀寫
要對檔案進行讀寫操作,需要包含 fstream 標頭檔案
#include <fstream>
1.7.1 寫檔案
開啟一個檔案用於輸出
// 檔案不存在會新建,存在則會覆蓋
ofstream outfile("seq_data.txt");
以追加模式開啟
ofstream outfile("seq_data.txt", ios_base:app);
判斷是否開啟成功
// 如果檔案未能成功開啟,則 ofstream 物件的求值結果為 false
if (!outfile)
cerr << "Can not open the file!\n";
1.7.2 讀檔案
以讀取模式 (input mode) 開啟 infile
ifstream infile("seq_data.txt");
判斷是否開啟成功
// 如果檔案未能開啟成功,則 ifstream 物件的求值結果為 false
if (!infile)
cerr << "Can not open the file!\n";
讀取檔案示例
ifstream infile("seq_data.txt");
if (!infile) {
cerr << "Can not open the file!\n";
} else {
string name;
while (infile >> name) {
// do something
}
}
infile >> name
的返回值是從 infile
讀取到的 class object。一旦讀到檔案末尾,對讀入 class object 的求值結果就會是 false
1.7.3 讀寫同一個檔案
fstream iofile("seq_data.txt", ios_base::in|ios_base::app);
if (!iofile) {
cerr << "Can not open the file!\n";
}
else {
iofile.seekg(0);
}
注意:
- 如果以追加模式開啟文件,檔案位置會位於末尾,
seekg()
將iofile
重新定位至檔案起始處。 - 由於此檔案是以追加模式開啟,任何寫入操作都會將資料新增在檔案末尾。
2 面向過程的程式設計風格
2.1 編寫函式
函式的四個部分
- 返回型別
- 函式名
- 引數列表
- 函式體
函式的宣告要包括 1~3, 引數列表可以只寫型別,目的是讓編譯器知道這個函式的存在
bool fibon_elem(int, int&);
函式的定義要包括 1~4
bool fibon_elem(int pos, int &elem) {
// implementation
}
2.2 呼叫函式
呼叫函式時,要向函式傳遞使用的引數,引數傳遞有兩種方式
- 傳值 (by value)
- 傳引用 (by reference)
傳值時,會將傳遞的物件複製一份,函式內對引數的修改不會影響傳入物件的值。
傳引用時,函式中對該物件進行的任何操作,都相當於是對傳入的物件進行間接操作,也就是說會改變傳入物件的值。
如果我們想在函式內部改變傳入物件的值,可以使用傳引用,也可以傳值的方式傳入物件的地址也就是指標,然後通過地址對物件進行操作
引用
引用型別,用起來感覺比較像給變數起別名
pointer 和 reference 的區別:
- pointer 可能(也可能不)指向某個實際物件,當提領 pointer 時,一定要先去頂其值不為0
- reference 必定會代表某個物件,所以不需要做此檢查
作用域及範圍
相關概念
- 儲存期 (storage duration)/範圍 (extent): 物件的存活時期
- 作用域 (scope): 物件在程式內的存活區域,物件在其作用於外不可見
-
file scope: 宣告在所有函式外物件所具有的作用域,具備 static extent(該物件的記憶體在
main()
開始執行之前便已分配好,可以一直存在至程式結束)。 - local scope: 宣告在函式內的變數具有的作用域
除了 static 物件,函式內定義的物件,只存在於函式執行期間。
內建型別的物件,如果定義在 file scope 內,必定被初始化為 0。但如果被定義於 local scope,那麼除非指定其初值,否則不會被初始化。
動態記憶體管理
local scope 和 file scope 中的物件都由作業系統自動管理。第三種儲存期形式為 dynamic extent(動態範圍)。其記憶體由系統的空閒空間(heap memory)分配,必須由程式設計師自動管理:
- 分配 new
- 釋放 delete
預設情況下,由 heap 分配而來的物件,皆未經過初始化,可以手動對單個物件進行初始化,但是C++ 沒有提供任何語法讓我們得以從 heap 分配陣列的同時為其元素設定初值。
從 heap 分配的物件具有 dynamic extent,因為它們是在執行通過 new 分配,因此可以持續存活,直到 delete 釋放。
int *pi = new int(1024); // 初始化為 1024
int *pia = new int[24]; // 分配陣列,無法設定初值
delete pi; // 無需檢測 pi 是否非0,編譯器會自動檢查
delete [] pia;
2.3 預設引數
預設引數的兩個規則
- 預設值的解析操作由最右邊開始進行。如果我們為某個引數提供了預設值,那麼這一引數右側的所有引數都必須也具有預設值才行。
- 預設值只能指定一次,可以在函式宣告處,也可以在函式定義處,但不能在兩個地方都指定。
關於第二點,為了更高的可見性,預設值一般放在函式的宣告處,而非定義處。
2.4 區域性靜態變數
區域性靜態變數使用 static
關鍵字
區域性靜態變數,在函式呼叫結束後,不會被釋放。
2.5 inline 函式
將函式宣告為 inline,表示編譯器在每個函式呼叫點上,將函式的內容展開,省去了呼叫函式的開銷。
inline 常用於程式碼短,所從事計算不復雜,常被呼叫的函式,一般不用於遞迴函式。
2.6 函式過載
函式過載 (function overloading): 引數列表不相同(引數型別不同或引數個數不同)的兩個或多個函式可以擁有相同函式名稱。
編譯器無法通過返回值型別來區分兩個具有相同名稱的函式。
為什麼需要函式過載:
將一組實現程式碼不同但工作內容相似的函式加以過載,可以讓使用者更容易使用這些函式。如果沒有過載機制,我們就得為每個函式提供不同的名稱。
2.7 函式模板
函式示例:
template<typename T>
T max(T a, T b) {
return a > b ? a : b;
}
好處是,我們不用為每一種型別單獨寫一個函數了。
一般而言,如果函式具備多種實現方式,我們可將它過載(overload),其實每份提供的是相同的通用服務。如果我們希望讓程式程式碼主體不變,僅僅改變其中用到的資料型別,可以通過 function template 達到目的。
2.8 函式指標
函式指標,必須指明其所指函式的範圍型別及引數列表,比如
cont vector<int>* *seq_ptr(int); // 幾乎是對的
為了讓 seq_ptr 被視為一個指標,必須以小括號改變運算優先順序
cont vector<int>* (*seq_ptr)(int);
由函式指標指向的函式,其呼叫方式和一般函式相同
獲取函式地址只需要提供函式名即可:
seq_ptr = pell_seq; // 將 pell_seq() 的地址賦值給 seq_ptr
2.9 設定標頭檔案
使用標頭檔案的原因:
在呼叫
seq_elem()
之前,必須先宣告以使程式知道它的存在。如果它被五個程式檔案呼叫,就必須進行五次宣告操作。為了不用分別在五個檔案中宣告seq_elem()
,我們把函式宣告放在標頭檔案中,並在每個程式程式碼檔案內 include 這些函式宣告。
標頭檔案副檔名:
標頭檔案的副檔名,習慣上是
.h
。標準庫例外,它們沒有副檔名。
為什麼不能把函式的定義放在標頭檔案裡:
函式的定義只能有一份,但是宣告可以有多份。因為同一個程式的多個程式碼檔案可能都會包含這個標頭檔案,所以不能把函式的定義放在標頭檔案裡。
“只能定義一份”的規則有個例外:inline 函式的定義
為了能夠展開 inline 函式的內容,在每個回撥用點上,編譯器都得取得其定義,所以 inline 函式的定義必須放在標頭檔案中
在 file scope 內定義的物件,如果可能被多個檔案訪問,就應該被聲明於標頭檔案中。但是下面的宣告是不對的:
const int seq_cnt = 6;
const vector<int>* (*seq_array[seq_cnt])(int);
因為這是定義而非宣告,可以用 extern
關鍵字表示是宣告變數。
const int seq_cnt = 6;
extern const vector<int>* (*seq_array[seq_cnt])(int);
為什麼 seq_cnt
不需要加上關鍵字 extern
:
const object 就和 inline 函式一樣,是“一次定義”規則下的例外。const object 的定義只要一出文件之外便不可見。所以我們可以在多個程式程式碼檔案中加以定義,不會導致任何錯誤。
總的來說,標頭檔案是在“一次定義規則”下用來宣告變數/函式的,可以跨 file scope 訪問變數/函式。在“一次定義規則”下有兩個例外 inline 函式和 const object
3 泛型程式設計風格
3.0 STL 的兩種元件
STL 主要由兩種元件構成:
- 容器,包括 vector、list、set、map等;
- 用以操作這些容器的所謂泛型演算法(generic algorithm),包括
find()
、sort()
、replace()
、merge()
等。
容器有兩類
- 順序性容器,比如 vector, list
- 關聯容器,比如 map, set
泛型演算法的性質
- 與操作元素的型別無關
- 與容器型別無關
泛型演算法通過 function template 技術,達到“與操作物件的型別相互獨立”的目的。而實現與容器無關的訣竅,就是不要直接在容器身上操作,而是藉由一對 iterator(first 和 last),標示我們要進行迭代的元素範圍。
3.1 指標的算數運算
指標的下標操作就是將指標的地址加上索引,產生出某個索引,然後再被提領,返回元素值。
在指標的算數運算中,會把“指標所指型別的大小”考慮進去
比如 int* p = 1000; 則 p + 2 = 1008,因為 int 型別佔四個位元組
int ia[8] = {1, 1, 2, 3, 5, 8, 13, 21};
int *pi = find(ia, ia + 8, ia[3]);
我們傳入第二個地址,表示出陣列最後一個元素的下一個地址。這合法嗎?
是的,不過倘若我們企圖對此地址進行讀取或寫入,那就不合法。如果我們僅僅是將該地址拿來和其他元素的地址進行比較,那就完全不會有任何問題。
3.2 Iterator(泛型指標)
3.2.1 獲取 iterator
-
begin()
獲取指向第一個元素的 iterator -
end()
獲取指向最後一個元素的 iterator
vector<string> svec;
vector<string>::iterator iter = svec.begin();
// const iterator 不允許修改 iterator 指向的元素
vector<string> cs_vec;
vector<string>::const_iterator iter = cs_vec.begin();
3.2.2 iterator 的基本操作
iterator 的操作與指標類似。
遍歷容器
for (auto iter = container.begin(); iter != container.end(); ++iter) {
cout << *iter << endl;
}
通過 iterator 取得元素值:提領
auto elm = *iter;
通過 iterator 呼叫元素所提供的操作
int n = iter->size();
3.3 所有容器的共通操作
下列為所有容器類(以及 string 類)的共通操作
- equality(
==
)和 inequality(!=
),返回 true 或 false - assignment(
=
)運算子,將某個容器複製給另一容器 -
empty()
會在容器無任何元素時返回 true,否則返回 false -
size()
返回容器內目前持有的元素個數 -
clear()
刪除所有元素 -
begin()
返回一個 iterator,指向容器的第一個元素 -
end()
返回一個 iterator,指向容器的最後一個元素的下一位置 -
insert()
將單一或某個範圍內的元素插入容器 -
erase()
將容器內單一元素或某個範圍內元素刪除
insert()
和 erase()
的行為視容器本身為順序性(sequential)容器或關聯(associative)容器而有所不同。
3.4 順序性容器
順序性容器用來維護一組排列有序、型別相同的元素。
順序性容器主要有
- vector
- list
- deque
其對應的標頭檔案分別為 #include <vector>
#include <list>
#include <deque>
有五種方法定義順序性容器:
1 產生空的容器
list<string> slist;
vector<int> ivec;
2 產生特定大小的容器。每個元素都以其預設值作為初值。
list<int> ilist(1024);
vector<string> svec(32);
3 產生特定大小的容器,併為每個元素指定初值。
vector<int> ivec(10, -1);
list<string> slist(16, "unassigned");
4 通過一對 iterator 產生容器。
int ia[8] = {1, 1, 2, 3, 5, 8, 13, 21};
vector<int> fib(ia, ia+8);
5 根據某個容器產生出新容器。複製原容器內的元素,作為新容器的初值。
list<string> slist;
// 填充 slist...
list<string> slist2(slist); // 將 slist 複製給slist2
順序性容器的通用操作:push_back, pop_back(),front(), back()
除此之外,list 和 deque(但不包括 vector)還提供了 push_front() 和 pop_front()
pop_back() 和 pop_front() 不會返回被刪除的元素
通用插入 insert()
iterator insert(iterator position, elemType value)
void insert(iterator position, int count, elemType value)
void insert(itertor1 position, iterator2 first, iterator2 last)
iterator insert(iterator position)
通用刪除 erase()
iterator erase(iterator position)
iterator erase(iterator first, iterator last)
list 型別不支援 iterator 的偏移運算,下面的寫法是錯誤的
slist.erase(it1, it1+num_tries); // 錯誤
3.5 泛型演算法
要使用泛型演算法,首先得包含對應的 algorithm 標頭檔案
#include <algorithm>
四種可能用到的泛型搜尋演算法
-
find()
遍歷查詢 -
binary_search
二分查詢,要求有序 count()
-
search()
對比某個容器內是否存在某個子序列
獲取容器內的最大值:max_element()
3.6 設計泛型演算法
Function Object
這類 class 對 function call 運算子做了過載操作,如此一來可是 function object 被當成一般函式來使用。
P85
Function Object Adaptor
P86
3.7 Map
標頭檔案:#include <map>
map<string, int> words_count;
string word;
while (cin >> word) {
++words_count[word];
}
其中 words_count[word]
,如果 word 不在 map 內,它變為因此放到map內,並獲得預設的0值。
一般查詢一個值是否在 map 內不用索引的方式,因為查詢後會把值放在 map 內,更常用的兩種方式
if(words_count.find(word) != words_count.end())
-
if(words_count.count(word)
, 任何一個 key 在 map 中最多隻有一份
3.8 Set
標頭檔案:#include <set>
判斷一個元素是否在 set 中的兩種方式
- find() 返回值不為 end() iterator
- count() 值不為 0
插入元素 -
insert(ival)
插入單個元素 -
insert(vec.begin(), vec.end())
插入多個元素
泛型演算法中與 set 有關的演算法
set_intersection(), set_union(), set_difference(), set_symmetric_difference()。
3.9 Iterator Inserter
下面的函式將 vec 複製了一份
vector<int> temp(vec.size());
copy(vec.begin(), vec.end(), temp.begin());
copy()
接受兩個 iterator,標示出複製範圍。第三個 iteraotr 指向複製行為的目的地(也是個容器)的第一個元素,後續元素會被一次填入。確保“目標容器”擁有足夠空間以放置每個即將到來的元素,這是程式設計師的責任。如果我們不確定這件事,可以使用 inserter
,以插入模式取代預設的 asssignment 行為。
inserter 標頭檔案:#include <iterator>
-
back_inserter()
會以容器的push_back()
函式取代 assignment 運算子。 -
inserter()
會以容器的insert()
函式取代 assignment 運算子。 -
front_inserter()
會以容器的push_front()
函式取代 assignment 運算子。
注意: 這些 adapter 不能用在 array 上。
3.10 iostream Iterator
標頭檔案:#include <iterator>
ifstream in_file("input_file.txt");
ofstream out_file("output_file.txt");
if (!in_file || !out_file) {
return -1;
}
istream_iterator<string> is(in_file);
istream_iterator<string> eof; // 空iterator 代表 eof
vector<string> text;
copy(is, eof, back_inserter(text));
sort(text.begin(), text.end());
ostream_iterator<string> os(out_file, " ");
copy(text.begin(), text.end(), os);
4 基於物件的程式設計風格
一般而言,class 由兩部分組成:一組公開的(public)操作函式和運算子,以及一組私有的(private)實現細節。
- 這些操作符和運算子稱為 class 的 member function(成員函式),並代表這個 class 的公開介面。身為 class 的使用者,只能訪問其公開介面。
- class 的 private 實現細節可由 member function 的定義以及與此 class 相關的任何資料組成
4.1 實現一個 Class
class 的宣告以關鍵字 class
開始,其後接一個 class 名稱
class 的定義由兩部分組成:
-
class 宣告
-
緊跟在聲明後的主體,主體由一對大括號括住,並以分號結尾。
-
主體內的兩個關鍵字
public
和private
,用來標識每個塊的“member 訪問許可權”。- public member 可以在程式的任何地方被訪問
- private member 只能在 member function 或是 class friend 內被訪問
-
class Stack {
public:
bool push(const string&);
bool pop(string &elem);
bool peek(string &elem);
bool empty();
bool full();
int size() { return _stack.size(); }
private:
vector<string> _stack;
}
所有 member function 都必須在 class 主體內進行宣告。至於是否要同時進行定義,可自由決定。
- 如果要在 class 主體內定義,這個member function 會被自動視為 inline 函式。
- 如果要在 class 主體外定義,必須使用特殊語法,用來分辨該函式所屬的 class。如果系統該函式為 inline,應該在最前面指定關鍵字 inline。
inline bool Stack::empty() {
return _stack.empty();
}
bool Stack::pop(string &elem) {
if (empty()) return false;
elem = _stack.back();
_stack.pop_back();
return true;
}
其中 Stack::empty()
表明 empty()
是 Stack
class 的一個 member。
class scope resolution(類作用域解析)運算子: class 名稱後的兩個冒號(::
)
- 對於 inline 函式而言,無論在函式體內定義還是在函式體外定義,都要被放入標頭檔案中;
- non-inline member function 應該放在程式程式碼檔案中定義,該檔案通常和 class 同名,其後接著副檔名 .c、.cc、.cpp 或 .cxx。
4.2 建構函式和解構函式
4.2.1 constructor
建構函式是一種特別的初始化函式,會在 class object 定義的時候呼叫。
Constructor 的函式名必須與 class 名稱相同。語法規定,constructor 不應指定返回型別,也不用返回任何值,可以被過載。
通過 constuctor 進行初始化
class Triangular {
public:
Triangular(); // default constructors
Triangular(int len);
Triangular(int len, int beg_pos);
}
Triangular t; // 會呼叫無參建構函式
Triangular t2(10, 3); // 會呼叫第三個建構函式
Triangular t3 = 8; // 會呼叫第二個建構函式
注意: Triangular t3 = 8;
不會呼叫 assignment operator!
呼叫無引數的建構函式不能使用下面的方法:
Triangular t5();
因為會將 t5
定義為一個引數列表為空的函式,返回型別是 Triangular
。
最簡單的 constructor 是所謂的 default constructor。它不需要任何引數。這意味著兩種情況
- 第一,它不接受任何引數
- 第二,它為每個引數都提供了預設值,這種情況更常見。
如果兩種情況同時存在會報錯,因為編譯器不知道要呼叫哪個建構函式。
Member Initialization List(成員初始化列表)
Triangular::Triangular(const Triangular &rhs)
: _length(rhs._length), _beg_pos(rhs._beg_pos), _next(rhs.beg_pos-1)
{ }
通過這種方法初始化,會呼叫 member class object 的 constructor,而第一種方法是通過 assignment 的形式。
copy constructor
當我們以某個 class object 作為另一個 object 的初值,例如:
Triangular tri1(8);
Triangular tri2 = tri1; // 會呼叫 constructor 而不是 assignment!!!
class data member 會被一次複製
但是如果 class member 中有指標型別,就不能簡單的把指標給複製過來,這樣的話,兩個 class 的 指標 member 會指向同一片地址,正確的做法是應該對指標所指的資料進行復制,比如下面這個 Matrix 類(Matrix 類的具體定義見 4.2.2)
Matrix::Matrix(const Matrix &rhs) : _row(rhs._row), _col(rhs._col) {
int elem_cnt = _row * _col;
_pmat = new double[elem_cnt];
for (int ix = 0; ix < elem_cnt; ++ix)
_pmat[ix] = rhs._pmat[ix];
}
4.2.2 destructor
如果一個 class object 有 destructor,則其 object 結束生命的時候,便會自動呼叫 destructor。
destructor 主要用來釋放在 constructor 中或物件生命週期中分配的資源。
destructor 的名稱為:class 名稱再加上 ~
字首。它絕不會有任何返回值,也沒有任何引數。由於其引數列表是空的,所以也不可能被過載。
class Matrix {
public:
Matrix(int row, int col) : _row(row), _col(col) {
_pmat = new double[row * cols];
}
~Matrix() {
delete [] _pmat;
}
private:
int _row, _col;
double *_pmat;
}
4.3 mutable 和 const
4.3.1 const
如果我們希望一個 member function 不會改變 class object 的內容,就必須在 member function 身上標註 const
,在定義和宣告中都要指定 const。
class Triangular {
public:
// const member function
int length() const {return _length;}
int beg_pos() const {return _beg_pos;}
int elem(int pos) const;
// non-const member function
bool next(int &val);
void next_reset() {_next = _beg_pos - 1;}
private:
int _length;
int _beg_pos;
int _next;
static vector<int> _elems;
}
4.3.2 mutable
如果一個 class object 中的變數被宣告為 mutable,則 const member function 就可以對其進行修改。
class Triangular {
public:
// 對 mutalbe 變數 _next 的修改不會影響到 const
void next_reset() const { _next = _beg_pos - 1;}
private:
mutable int _next;
}
4.4 this 指標
this 指標在 member function 內用來指向其呼叫者。
比如下面這個複製函式,返回的是呼叫者本身。
Triangular& Triangular::copy(const Triangular &rhs) {
if (this != &rhs) {
_length = rhs._length;
_beg_pos = rhs._beg_pos;
_next = rhs._beg_pos - 1;
}
return *this;
}
要以一個物件複製出另一個物件,先確定兩個物件是否相同是個好習慣。
4.5 靜態 類成員
4.5.1 Static Data Member
static(靜態) data member 用來表示唯一的、可共享的 member。它可以在同一類的所有物件中訪問。
下面的例子,宣告了_elems
是 Triangular class 的一個 static data member:
class Triangular {
pubic:
// ...
private:
static vector<int> _elems;
}
對於 class 而言,static data member 只有唯一的一份實體,因此我們必須在程式程式碼檔案中提供其清楚的定義。
// 以下程式碼放在程式程式碼檔案中,例如 Triangular.cpp
vector<int> Triangular::_elems;
4.5.2 Static Member Function(靜態成員函式)
一般情況下,member function 必須通過其類的某個物件來呼叫。這個物件會被繫結至該 member function 的 this
指標。通過儲存與每個物件的 this 指標, member function 才能夠訪問每個物件中的 non-static data memer。
member function 只有在不訪問任何 non-static member 的條件下才能夠被宣告為 static。
當我們在 class 主體外部進行 member function 的定義時,無須重複加上關鍵字 static(這個規則也適用於 static data member)。
呼叫靜態成員變數時,使用 class scope 運算子即可
4.6 運算子函式
iterator class 要定義 !=
, *
, ++
等運算子,可通過運算子函式來實現。
運算子函式的名稱是 operator
後面跟上運算子
class Triangular_iterator {
public:
Triangular_iterator(int index) :_index(index - 1) {}
bool operator==(const Triangular_iterator&) const;
bool operator!=(const Triangular_iterator&) const;
int operator*() const;
Triangular_iterator& operator++(); // 前置版
Triangular_iterator operator++(int); // 後置版
}
後置版本的引數列表原本也應該是空的,然後過載規則要求,引數列表必須獨一無二。因此C++語言想出一個變通方法,要求後置版得有一個 int 引數。編譯器會自動為後置版產生一個 int 引數(其值必為0)。
任何一個運算子如果和另一個運算子性質相反,我們通常會以後者實現出前者
inline bool Triangular_iterator::
operator!=(const Triangular_iterator &rhs) const {
return !(*this == rhs);
}
運算子過載的規則見:P120
Non-member 運算子的引數列表中,一定會比相應的 member 運算子多出一個引數,也就是 this 指標。對於 member 運算子而言,這個 this 指標隱式代表做運算元。
巢狀型別
typedef
可以為某個型別設定另一個不同的名稱。其通用形式為
typedef exsiting_type new_name
4.7 friend
如果一個function/class B 要訪問另一個 class A的 private member,則必須在 A 中宣告 B 為 friend
class A {
friend class B; // 宣告 clss B 為 A 的 friend
friend int C(); // 宣告函式 C 為 A 的 friend
friend void D::E(); // 宣告 class D 中的函式 E 為 A 的 friend
}
但是,我們一般不會直接訪問一個類的 private member,而是讓類提供具有 public 訪問許可權的 inline 函式來進行相關操作。
4.8 copy assignment operator
default memberwise copy(預設的成員逐一複製)
和拷貝建構函式一樣,我們要考慮指標的問題。
4.9 function object
當編譯器在編譯的過程中遇到函式呼叫,例如:
lt(ival);
- lt 可能是函式名
- lt 可能是函式指標
- lt 可能是一個提供了 funciton call 運算子的 function object
如果 lt 是一個 class object,編譯器會在內部將此語句轉換為
lt.operator(ival);
對比於 subscript 運算子僅能接受一個引數,function call 能接受人一個是的引數。
實現一個 function object只需要定義function call運算子函式(operator()
)即可,例如:
inline bool LessThan::operator()(int value) const {
return value < _val;
}
4.10 過載 iostream 運算子
ostream& operator<<(ostream &os, const Triangular &rhs) {
// something
return os;
}
istream& operator>>(istream &is, const Trangular &rhs) {
// something
return is;
}
4.11 指標,指向 Class Member Function
pointer to member function 於 pointer to non-member function 類似,不過在宣告時候要指定指向哪一個 class。例如:
void (num_sequence::*pm)(int) = 0;
聲明瞭一個名為 pm 的指標,指向 num_sequence 的 member function,後者的返回型別必須是 void, 引數型別為 int。pm 的初始值為 0,表示目前並不指向任何一個 member function。
如果每次都這樣寫,有點複雜,可以用 typedef 進行簡化
typedef void (num_sequence::*PtrType)(int);
PtrType pm = 0;
不同於普通 function,為了取得某個 member function 的地址,我們對函式名稱應用 address-of(取址)運算子,且函式名稱前必須先以 class scope 加以限定。
PtrType pm = &num_sequence::fibonacci;
Pointer to member funtion 和 pointer to funtion 的一個不同點是,前者必須通過同一類物件加以呼叫,而該物件便是此 member function 內的 this
指標所指的物件。
num_sequence ns;
num_sequence *pns = &ns;
PtrType pm = &num_sequence::fibonacci;
通過 ns 呼叫函式
// 與 ns.fibonacci(pos) 效果相同
(ns.*pm)(pos);
其中 .*
是個 pointer to member selection 運算子,必須為它加上外圍的小括號,才能正常工作。
也可以通過 pns 呼叫
// 與 pns->fibonacci(pos) 效果相同
(pns->*pm)(pos);
針對 pointer to clss object 工作的 pointer to member selection 運算子是 ->*
。
5 面向物件程式設計風格
5.1 面向物件程式設計概念
面向物件程式設計概念的兩項最主要特質是:繼承(inheritance)和多型(polymorphism)。
- 繼承使我們得以將一群相關的類組織起來,並讓我們得以分享其間的共通資料和操作行為
- 多型讓我們在這些類之上進行程式設計時,可以如同操控單一個體,而非相互獨立的類,並賦予我們更多彈性來加入或移除任何特定類。
5.1.1 繼承
繼承機制定義了父子關係。父類定義了所有子類共通的共有介面和私有實現。每個子類都可以增加或覆蓋繼承而來的東西,以實現其自身獨特行為。
相關概念:
- 基類(base class):父類
- 派生類(derived class):子類
- 繼承體系(inheritance hierarchy):父類和子類之間的關係
5.1.2 多型
在面向物件應用程式中,我們會間接利用指向抽象基類的 pointer 或 reference 來作業系統中的各物件,而不直接操作各個實際物件。這讓我們得以在不變動舊有程式的前提下,加入或移除任何一個派生類。
多型:讓基類的 pointer 或 reference 得以十分透明地指向任何一個派生類物件。
動態繫結(dynamic binding):只有在執行的時候我們才能知道一個 pointer 或 reference 指向哪個派生類。
5.2 面向物件程式設計思維
預設情況下,member function 的解析(resolution)皆在編譯的靜態地進行。若要令其在執行時動態進行,我們就得在它的宣告前加上關鍵字
virtual
。
5.3 不帶繼承的多型
這樣做極費工夫,尤其事後的維護更是工程浩大。
5.4 定義一個抽象基類
-
定義抽象類的第一個步驟就是找出所有子類共通的操作行為。
-
設計抽象類的下一步,便是設法找出哪些操作行為與型別相關(type-dependent)——也就是說,有哪些操作行為必須根據不同的派生類而有不同的實現方式。這些操作行為應該成為整個類繼承體系中的虛擬函式(virtual function)。
-
設計抽象基類的第三步,便是試著找出每個操作行為的訪問層級(access level)。
- 如果某個操作行為應該讓一般程式皆能訪問,我們應該將它宣告為 public
- 如果某個操作行為在基類之外不需要被用到,我們就將他宣告為 private。即使是該基類的派生類也無法訪問基類中的 priavte member。
- 第三種訪問層級,是所謂的 protected。這種層級可以讓派生類方位,卻不允許一般程式使用。
純虛擬函式:
每個虛擬函式,要麼得有其定義,要麼可設為“純”虛擬函式(pure virtual function)——如果對於該類而言,這個虛擬函式並無實質意義的話,將虛擬函式賦值為 0,意思便是令它為一個純虛擬函式。
virtual void gen_elems(int pos) = 0;
任何類如果宣告有一個(或多個)純虛擬函式,那麼由於其介面的不完整性(純虛擬函式沒有函式定義),程式無法為他產生任何物件。這種類只能作為派生類的子物件使用,而且前提是這些派生類必須為所有虛擬函式提供確切的定義。
根據一般規則,凡基類定義有一個(或多個)虛擬函式,應該要將其 destructor 宣告為
virtual
。但不建議將其設為純虛擬函式
5.5 定義一個派生類
派生類有兩部分組成:
- 一是基類構成的子物件(subobject),由基類的 non-static data member組成
- 二是派生類的部分,由派生類的 non-static data member 組成。
class Fibonacci: public num_sequence {
public:
// ...
};
派生類的名稱之後緊跟這冒號、關鍵字 public,以及基類名稱。
在類之外對虛擬函式進行定義時,不必指明關鍵字 virtual
對於 non-virua如果要在派生類中使用繼承來的那份member而不是派生類本身的member,可以使用 class scope 運算子加以限定。
如果在基類的 print 函式前加上 virutal,在執行時會動態判斷呼叫哪個函式,所以會輸出 "this is derive class",如果沒加,則會輸出"this is base class"。
#include <iostream>
using namespace std;
class Base {
public:
virtual void print() {
cout << "this is base class" << endl;
}
};
class Derive : public Base {
public:
void print() {
cout << "this is derive class" << endl;
}
};
int main() {
Base* ptr = new Derive;
ptr->print();
}
也就是說,在基類和派生類中提供同名的 non-virtual 函式,則在通過多型呼叫函式(通過基類的指標或引用)時,總是會呼叫基類的函式,這並不是我們想要的。
書中關於 check_integrity() 的設計:
當派生類欲檢查其自身狀態的完整性時,已實現完成的基類缺乏足夠的知識。而我們知道,根據不完整資訊所完成的實現,可能也是不完整的。這和“宣稱實現與型別相關,因而必須將它宣告為 virtual”的情況並不相同。
5.6 運用繼承體系
使用基類的指標或引用,再根據指向的派生類去執行相關的操作。
5.7 基類應該多麼抽象
5.8 初始化、析構、複製
5.9 在派生類中定義一個虛擬函式
如果我們決定覆蓋基類所提供的虛擬函式,那麼派生類提供的新定義,其函式原型必須完全符合基類所宣告的函式原型,包括:引數列表,返回型別,常量性。
“返回型別必須完全符合”這一規則有個例外——當基類的虛擬函式返回某個基類形式(通常是 pointer 或 refernece)時,派生類中的同名函式便可以返回該基類所派生出來的型別。
虛擬函式的靜態解析
有兩種情況,虛擬函式機制不會出現預期行為:
- (1) 基類的 constructor 內和 desctructor 內
- (2) 當我們使用的是基類的物件,而非基類物件的 pointer 或 reference 時
在派生類中,為了覆蓋基類的某個虛擬函式,而進行宣告操作時,不一定得加上 virtual 關鍵字。編譯器會根據兩個函式的原型宣告,決定某個函式是否會覆蓋其基類中的同名函式。
5.10 執行時的型別鑑定機制
每個類都有一份 what_am_i()
函式,另一種設計方法,便是隻提供一份 what_am_i()
函式,令各派生類通過繼承機制加以複用。
一種可能的做法是為 num_sequence
增加一個 string
member,並令每個派生類的 constructor 都將自己的類名作為引數。
另一種實現便是利用所謂的 typeid
運算子,這是所謂的執行時型別鑑定機制(Run-Time Type Identification,RTTI)的一部分,由程式語言支援。
#include <typeinfo>
inline const char* num_seqence::
what_am_i() const {
return typeid(*this).name();
}
dynamic_cast 也是一個 RTTI 運算子,他會進行執行時檢驗操作,如果失敗則返回0。
6 以 template 進行程式設計
6.1 被引數化的型別
template 機制幫助我們將類定義中“與型別相關”和“獨立於型別之外”的兩部分分離開來。
需要使用 template parameter list 限定 class template 的場景:除了class template 及其 member 的定義中的其他場合。
6.2 Class Template 的定義
template <typename elemType>
inline BinaryTree<elemType>:: // 在 class 定義範圍之外(所以需要限定型別)
BinaryTree() : _root(0) // 在 class 定義範圍之內
{}
6.3 Template 型別引數的處理
實際運用中,不論內建型別或 class 型別,都可能被指定為 class template 的實際型別。我建議,將所有的 template 型別引數視為“class 型別”來處理。這意味著我們會把它宣告為一個
const
reference,而非以 by value 方式傳送。
推薦在 constructor 的 member initialization list 內為每個型別引數進行初始化操作:
template<typename valType>
inline BTnode<valType>::BTnode(const valType &val) : _val(val) {
_cnt = 1;
_lchild = rchild = 0;
}
而不選擇在 constructor 函式體內進行:
template<typename valType>
inline BTnode<valType>::BTnode(const valType &val) {
_val = val; // 不建議這樣,因為它可能是 class 型別
_cnt = 1;
_lchild = rchild = 0;
}
6.4 實現一個 Class Template
6.5 一個以 Function Template 完成的 Output 運算子
6.6 常量表達式與預設引數值
以表示式作為 template 引數。這種 template 引數在 C++ Primer 一書中稱為“非型別引數”。
Template 引數並不是非得某種型別(type)不可,也可以用常量表達式(constant expression)作為 template 引數。
template <int len>
class num_sequence {
public:
num_sequence(int beg_pos=1);
// ...
};
template <int len>
class Fibonacci : public num_sequence<len> {
public:
Fibonacci(int beg_pos=1) : num_sequence<len>(beg_pos) {}
// ...
};
當我們產生 Fibonacci 物件,像這樣:
Fibonacci<16> fib1;
Fibonacci<16> fib2(17);
也可以提供預設值
template <int len, int beg_pos=1>
output 運算子的 function template 定義
template <int len, int beg_pos>
ostream & operator<<(ostream &os, const num_sequence<int len, int beg_pos> &ns) {
return ns.print(os);
}
6.7 以 Template 引數作為一種設計策略
template <typename elemType>
class LessThan {
public:
LessThan(const elemType &val) : _val(val) {}
bool operator()(const elemType &val) const {
return val < _val;
}
void val(const elemType &newval) { _val = newval; }
elemType val() const { return _val;}
private:
elemType _val;
};
上述情況沒有考慮到使用者所提供的型別是否有 <
運算子的定義,解決方法是提供一個 less-than 運算子,其預設值是 less<elemType>
template <typename elemType, typename BinaryComp = less<elemType> >
6.8 Member Template Function
可以將 member function 定義成 template 形式,不過不用指定 template的型別。
class PrintIt {
public:
PrintIt(ostream &os) : _os(os) {}
template <typename elemType>
void print(const elemType &elem, char delimiter = '\n') {
_os << elem << delimiter;
}
private:
ostream &_os;
};
int main() {
PrintIt to_standard_out(cout);
to_standard_out.print("Hello");
to_standard_out.print(1024);
string my_string("i and a string");
to_standard_out.print(my_string);
return 0;
}
7 異常處理
7.1 丟擲異常
C++ 通過 throw
關鍵字丟擲異常,異常是某種物件。
throw 42;
throw "panic: no buffer!";
throw iterator_overflow; // iterator_overflow 是個類
7.2 捕獲異常
用 try
關鍵字來執行可能丟擲異常的語句,然後可以利用單條或一連串的 catch
子句來捕獲被丟擲的異常,如果異常的型別相符,則會執行 catch 中的語句。
同一個型別的異常不能出現兩次,因為會被第一個捕獲。
catch 前必須有 try。
try {
// 可能會丟擲異常
}
catch (int erro) { // int 型別的異常
// 處理異常
}
catch (string err) { // string 型別的異常
}
捕獲任何型別的異常,使用 ...
try {
//...
}
catch (...) {
// 處理異常
}
7.3 提煉異常
在遇到異常時,如果該異常沒有被 catch 則當前函式會被停止,然後會沿著函式呼叫鏈一路回溯,直到異常被 catch。如果一直回到了 main() 函式還沒有被 catch,便會呼叫標準庫提供的 terminate()
——中斷整個程式的執行。
7.4 區域性資源管理
下面的程式碼無法保證資源最終一定會被釋放掉(process 可能會出現異常)
extern Mutex m;
void f() {
int *p = new int;
m.acquire();
process(p);
m.release();
delete p;
}
一個容易想到的解決方法: try catch,但是資源釋放的程式碼需要在 try 和 catch 中都寫一遍。且,捕獲異常、釋放資源、重新丟擲異常,這些操作會使異常處理程式的搜尋時間進一步延長。此外,程式程式碼本身也變得更復雜了。
另一種有效的方法:
Resource Acquisition Is Initialization (RAII):在初始化階段即進行資源請求,或資源請求即初始化。
對物件而言,初始化操作發生於 constructor 內,資源的請求也應該在 constructor 內完成。資源釋放則應該在 destructor 內完成。
#include <memory>
void f() {
auto_ptr<int> p(new int);
MutexLock ml(m);
process(p);
// p 和 ml 的 destructor 會在此處被自動呼叫
}
class MutexLock {
public:
MutexLock(Mutex m) : _lock(m) {
lock.acquire();
}
~MutexLock() { lock.release() }
private:
Mutex &_lock;
};
- 如果 process 執行無誤,則區域性變數 p 和 ml 的 destructor 則會在函式結束前被自動呼叫。
- 如果 process 丟擲異常,C++會保證在異常處理機制終結某個函式前,呼叫函式中所有區域性變數的 destructor
ps: auto_ptr
是標準庫提供的 class template,它的 destructor 會呼叫 delete 釋放記憶體。使用時需要 #include <memory>
。
7.5 標準異常
如果 new 表示式無法分配足夠的空間,會返回 bad_alloc(是一個類),如果我們想要操作 bad_alloc 異常物件,它提供了哪些操作呢?
標準庫定義了一套異常體系(exception class hierarchy),其根部是名為 exception 的抽象基類。
exception 的標頭檔案:#include <exception>
exception 宣告有一個 what()
虛擬函式,返回一個 const char *