第一部分ios |
預備知識算法 |
第1章 C + +程序設計 編程
你們好!如今咱們將要開始一個穿越" 數據結構、算法和程序" 這個抽象世界的特殊旅程,以解決現實生活中的許多難題。在程序開發過程當中一般須要作到以下兩點:一是高效地描述數據;二是設計一個好的算法,該算法最終可用程序來實現。要想高效地描述數據,必須具有數據結構領域的專門知識;而要想設計一個好的算法,則須要算法設計領域的專門知識。 數組
在着手研究數據結構和算法設計方法以前,須要你可以熟練地運用 C + +編程並分析程序,這些基本的技能一般是從C + +課程以及其餘分散的課程中學到的。本書的前兩章旨在幫助你回顧一下這些技能,其中的許多內容你可能已經很熟悉了。 數據結構
本章咱們將回顧C++ 的一些特性。由於不是針對C++ 新手,所以沒有介紹諸如賦值語句、 if 語句和循環語句(如for 和w h i l e)等基本結構,而是主要介紹一些可能已經被你忽略的 C + + 特性: 數據結構和算法
本章中沒有涉及的其餘C + +特性將在後續章節中在須要的時候加以介紹。本章還給出了以下應用程序的代碼: ide
此外,本章還給出瞭如何測試和調試程序的一些技巧。 函數
目錄: 測試
1.1 引言 4 this
1.4.6 增長#ifndef, #define和# e n d i f語句 4
在檢查程序的時候咱們應該問一問:
咱們的目標是教會你如何開發正確的、精緻的、高效的程序。
考察函數A b c(見程序1 - 1)。該函數用來計算表達式 a+b+b*c+ (a+b-c) / (a+b) + 4,其中a,b 和c 是整數,結果也是一個整數。
程序1-1 計算一個整數表達式
int Abc(int a, int b, int c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
在程序1 - 1中,a,b和c 是函數Abc 的形式參數(formal parameter),類型均爲整型。若是在以下語句中調用函數A b c:
z = Abc(2,x,y)
那麼,2,x 和y 分別是對應於a,b 和c 的實際參數(actual parameter)。當A bc( 2 ,x,y) 被執行時,a 被賦值爲2;b 被賦值爲x;c 被賦值爲y。若是x 和 / 或y 不是int 類型,那麼在把它們的值賦給b 和c 以前,將首先對它們進行類型轉換。例如,若是x 是float 類型,其值爲3 . 8,那麼b 將被賦值爲3。在程序1 - 1中,形式參數a,b 和c 都是傳值參數(value parameter)。
運行時,與傳值形式參數相對應的實際參數的值將在函數執行以前被複制給形式參數,複製過程是由該形式參數所屬數據類型的複製構造函數( copy constructor)完成的。若是實際參數與形式參數的數據類型不一樣,必須進行類型轉換,從實際參數的類型轉換爲形式參數的類型,固然,假定這樣的類型轉換是容許的。
當函數運行結束時,形式參數所屬數據類型的析構函數(d e s t r u c t o r)負責釋放該形式參數。當一個函數返回時,形式參數的值不會被複制到對應的實際參數中。所以,函數調用不會修改實際參數的值。
假定咱們但願編寫另一個函數來計算與程序 1 - 1相同的表達式,不過此次 a,b和c是f l o a t 類型,結果也是f l o a t類型。程序1 - 2中給出了具體的代碼。程序1 - 1和1 - 2的區別僅在於形式參數以及函數返回值的數據類型。
程序1-2 計算一個浮點數表達式
float Abc(float a, float b, float c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
實際上沒必要對每一種可能的形式參數的類型都從新編寫一個相應的函數。能夠編寫一段通用的代碼,將參數的數據類型做爲一個變量,它的值由編譯器來肯定。程序 1 - 3中給出了這樣一段使用t e m p l a t e語句編寫的通用代碼。
程序1-3 利用模板函數計算一個表達式
template<class T>
T Abc(T a, T b, T c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
利用這段通用代碼,經過把T替換爲i n t,編譯器能夠當即構造出程序1 - 1,把T 替換爲f l o a t 又能夠當即構造出程序 1 - 2。事實上,經過把T替換爲d o u b l e或l o n g,編譯器又能夠構造出函數 A b c的雙精度型版本和長整型版本。把函數 Abc 編寫成模板函數可讓咱們沒必要了解形式參數的數據類型。
程序1 - 3中形式參數的用法會增長程序的運行開銷。例如,咱們來考察一下函數被調用以及返回時所涉及的操做。假定a,b 和c 是傳值參數,在函數被調用時,類型T 的複製構造函數把相應的實際參數分別複製到形式參數a,b 和c 之中,以供函數使用;而在函數返回時,類型
T的析構函數會被喚醒,以便釋放形式參數a,b 和c。
假定數據類型爲用戶自定義的 M a t r i x,那麼它的複製構造函數將負責複製其全部元素,而析構函數則負責逐個釋放每一個元素(假定 Matrix 已經定義了操做符+,*和 /)。若是咱們用具備1 0 0 0個元素的Matrix 做爲實際參數來調用函數 A b c,那麼複製三個實際參數給 a,b 和c 將須要3 0 0 0次操做。當函數 A b c返回時,其析構函數又須要花費額外的 3 0 0 0次操做來釋放 a, b和c。
在程序 1 - 4所示的代碼中, a,b 和c 是引用參數( reference parameter)。若是用語句 A bc (x,y, z) 來調用函數A b c,其中x、y 和z 是相同的數據類型,那麼這些實際參數將被分別賦予名稱a,b 和c,所以,在函數Abc 執行期間,x、y 和z 被用來替換對應的a,b 和c。與傳值參數的狀況不一樣,在函數被調用時,本程序並無複製實際參數的值,在函數返回時也沒有調用析構函數。
程序1-4 利用引用參數計算一個表達式
template<class T>
T Abc(T& a, T& b, T& c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
咱們能夠考察一下當a,b 和c 所對應的實際參數x,y 和z 分別是具備 0 0 0個元素的矩陣時的情形。因爲不須要把x,y 和z 的值複製給對應的形式參數,所以咱們能夠節省採用傳值參數進行參數複製時所須要的3 0 0 0次操做。
C + +還提供了另一種參數傳遞方式——常量引用( const reference),這種模式指出函數不得修改引用參數。例如,在程序 1 - 4中,a,b 和c 的值不能被修改,所以咱們能夠重寫這段代碼,見程序1 - 5。
程序1-5 利用常量引用參數計算一個表達式
template<class T>
T Abc(const T& a, const T& b, const T& c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
使用關鍵字const 來指明函數不能夠修改引用參數的值,這在軟件工程方面具備重要的意義。這將當即告訴用戶該函數並不會修改實際參數。
對於諸如i n t、float 和char 的簡單數據類型,當函數不會修改實際參數值的時候咱們能夠採用傳值參數;對於全部其餘的數據類型(包括模板類型),當函數不會修改實際參數值的時候能夠採用常量引用參數。
採用程序1 - 6的語法,咱們能夠獲得程序 1 - 5的一個更通用的版本。在新的版本中,每一個形式參數可能屬於不一樣的數據類型,函數返回值的類型與第一個參數的類型相同。
程序1-6 程序1 - 5的一個更通用的版本
template<class Ta, class Tb, class Tc >
Ta Abc (const Ta& a, const Tb& b, const Tc& c)
{
return a+b+b*c+(a+b-c)/(a+b)+4;
}
將被釋放,其值固然也再也不有效。爲了不丟失這個值,在釋放臨時變量以及傳值形式參數的空間以前,必須把這個值從臨時變量複製到調用該函數的環境中去。若是須要返回一個引用,能夠爲返回類型添加一個前綴 &。如:
T& X(int i, T& z)
定義了一個函數X,它返回一個引用參數Z。可使用下面的語句返回z:
return z;
這種返回形式不會把z 的值複製到返回環境中。當函數X返回時,傳值形式參數i 以及全部局部變量所佔用的空間都將被釋放。因爲z 是對一個實際參數的引用,所以,它不會受影響。
若是在函數名以前添加關鍵字c o n s t,那麼函數將返回一個常量引用,例如:
const T& X (int i, T& z)
除了返回的結果是一個不變化的對象以外,返回一個常量引用與返回一個引用是相同的。
遞歸函數(recursive function)是一個本身調用本身的函數。遞歸函數包括兩種:直接遞歸(direct recursion)和間接遞歸(indirect recursion)。直接遞歸是指函數F的代碼中直接包含了調用F的語句,而間接遞歸是指函數F調用了函數G,G又調用了H,如此進行下去,直到F又被調用。在深刻探討C + +遞歸函數以前,咱們來考察一下來自數學的兩個相關概念——數學函數的遞歸定義以及概括證實。
在數學中常常用一個函數自己來定義該函數。例如階乘函數 f (n) =n!的定義以下:
其中n 爲整數。
從該定義中能夠看到,當n 小於或等於1時,f (n)的值爲1,如 f (-3) = f (0) = f (1) =1。當n 大於1時,f (n)由遞歸形式來定義,在定義的右側也出現了 f 。在右側使用f 並不會致使循環定義,由於右側f 的參數小於左側f 的參數。例如,從公式(1 - 1)中能夠獲得f ( 2 ) = 2f ( 1 ),因爲咱們已經知道f ( 1 ) = 1,所以 f ( 2 ) = 2 * 1 = 2。以此類推,f ( 3 ) = 3f ( 2 ) = 3 * 2 = 6。
對於函數f (n) 的一個遞歸定義(假定是直接遞歸),要想使它成爲一個完整的定義,必須知足以下條件:
在公式(1 - 1)中,基本部分是:當n≤ 1時f (n) = 1;遞歸部分是f (n) = nf (n- 1 ),其中右側f 的參數爲n- 1,比n 要小。重複應用遞歸部分可把f (n- 1 )變換成對f (n- 2 ),f (n- 3 ),⋯ ,直到f ( 1 ) 的調用。例如:
f (5) = 5f (4) = 20f (3) = 60f (2) = 120f ( 1 )
注意每次應用遞歸部分的結果是更趨近於基本部分,最後,根據基本部分的定義能夠獲得 f (5) = 120。從這個例子中能夠看出,對於n≥ 1有 f (n) = n (n-1) (n- 2 )⋯ 1。
做爲遞歸定義的另一個例子,咱們來考察一下斐波那契數列的定義:
F0 = 0,F1 = 1,Fn =Fn - 1+Fn -2 (n> 1) (1 - 2)在這個定義中,F0 = 0和F1 =1 構成了定義的基本部分,Fn =Fn - 1+Fn -2 是定義的遞歸部分。右側函數的參數都小於 n。爲使公式(1 - 2)成爲F 的一個完整的遞歸定義,對於任何 n> 1的斐波那契數,對遞歸部分的重複應用應能把右側出現的全部 F 變換成基本部分的形式。由於對一個 n> 1的整數重複減去 1或2會獲得0或1,所以右側 F 的出現總能夠被變換成基本定義。好比,
F =F +F =F +F +F +F = 3F + 2F = 3。
4 3 2 2 1 1 0 1 0
如今咱們把注意力轉向與計算機遞歸函數相關的第二個概念——概括證實。在概括證實中,能夠按照以下步驟來證實一個命題的正確性,好比證實以下公式:
首先咱們能夠驗證,對於n 的一個或多個基本的值(如n = 0或n = 0 , 1)該公式是成立的;而後假定當n∈ ( 0 ~m)時公式是成立的,其中m 是一個任意整數(大於等於驗證時所取的最大值),若是可以證實對於n 的下一個值(如m+ 1)公式也是成立的,那麼就能夠肯定該公式是成立的。
這種證實方法能夠概括爲三個部分——概括初值( induction base),概括假設( i n d u c t i o n h y p o t h e s i s)和概括步證實(induction step)。
下面經過對n 進行概括來證實公式( 1 - 3)。在概括初值部分,取 n= 0來進行驗證,因爲公
式的左邊 i= 0,公式的右邊也爲 0,因此當n= 0時公式(1 - 3)是成立的。在概括假設部分假
i =1
定當n≤ m 時公式是成立的,其中m 是任意大於等於0的整數(對於接下來的概括證實,只需假定n = m 時公式是成立的便可)。在概括證實中須要證實當n =m +1 時公式(1 - 3)是成立的。對
於n=m+ 1,公式左邊爲 i,從概括假設中能夠知道 i=m (m+ 1 ) / 2,因此當n =m + 1
i =1 i =1 t=1
時左邊變成m+ 1 +m (m+1)/2 =(m+1) (m+ 2 ) / 2,正好與公式右邊相等。
乍看起來,概括證實好象是一個循環證實——由於咱們創建了一個假設爲正確的結論。不過,概括證實並非循環證實,就像遞歸定義並非循環定義同樣。每一個正確的概括證實都會有一個基本值驗證部分,它與遞歸定義的基本部分相相似,在概括證實時咱們利用了比 n 值小時結論的正確性來證實取值爲n 時結論的正確性。重複應用概括證實,能夠減小對基本值驗證的應用。
C + +容許咱們編寫遞歸函數。一個正確的遞歸函數必須包含一個基本部分。函數中遞歸調用部分所使用的參數值應比函數的參數值要小,以便函數的重複調用能最終得到基本部分所提供的值。
例1-1 [階乘] 程序1 - 7給出了一個利用公式(1 - 1)計算n! 的C + +函數。函數的基本部分包含了
n≤ 1的情形。考慮調用F a c t o r i a l ( 2 )。爲了計算e l s e語句中的2* Factorial(1),須要掛起F a c t o r i a l ( 2 ),而後進入調用F a c t o r i a l ( 1 )。當Factorial(2) 被掛起時,程序的狀態(如局部變量、傳值形式參數的值、引用形式參數的值以及代碼的執行位置等)被保留在遞歸棧中,在執行完 F a c t o r i a l ( 1 )時這些程序狀態又當即恢復。調用Factorial(1) 所獲得的返回值爲1,以後,F a c t o r i a l ( 2 )恢復運行,計算表達式2 * 1,並將結果返回。
程序1-7 計算n!的遞歸函數
int Factorial (int n)
{ / /計算n!
if (n<=1) return 1; else return n * Factorial(n-1 ) ;
}
在計算F a c t o r i a l ( 3 )時,當到達e l s e語句時,計算過程被掛起以便先計算出 F a c t o r i a l ( 2 )。我
們已經看到F a c t o r i a l ( 2 )是怎樣得到最終結果2的。當F a c t o r i a l ( 2 )返回時,F a c t o r i a l ( 3 )繼續運行,計算出最後的結果3 * 2。
鑑於程序1 - 7的代碼與公式(1 - 1)的類似性,該程序的正確性與公式( 1 - 1)的正確性是等
價的。
例1-2 模板函數S u m (見程序1-8) 統計元素a [ 0 ]至a[n-1] 的和(簡記爲a [ 0 : n - 1 ])。從代碼中咱們能夠獲得這樣的遞歸公式:當 n = 0時,和爲0;當n > 0時,n個元素的和是前面n - 1個元素的和加上最後一個元素。見程序1 - 9。
程序1-8 累加a [ 0:n - 1 ]
template<class T> T Sum(T a[], int n)
{ / /計算a[0: n-1]的和 T tsum=0;
f or (int i = 0; i < n; i++) tsum += a[i];
return tsum;
}
程序1-9 遞歸計算a [ 0:n - 1 ]
template<class T> T Rsum(T a[], int n)
{ / /計算a[0: n-1]的和 if (n > 0) return Rsum(a, n-1) + a[n-1];
return 0;
}
例1-3 [排列] 一般咱們但願檢查n 個不一樣元素的全部排列方式以肯定一個最佳的排列。好比, a,b 和c 的排列方式有:a b c, a c b, b a c, b c a, cab 和c b a。n 個元素的排列方式共有 n !種。
因爲採用非遞歸的C + +函數來輸出n 個元素的全部排列方式很困難,因此能夠開發一個遞歸函數來實現。令E= {e , ..., e }表示n 個元素的集合,咱們的目標是生成該集合的全部排列方 1 n
式。令Ei 爲E中移去元素i 之後所得到的集合,perm (X) 表示集合X 中元素的排列方式,ei . p e r m
(X)表示在 perm (X) 中的每一個排列方式的前面均加上 ei 之後所獲得的排列方式。例如,若是
E= {a, b, c},那麼E1= {b, c},perm (E1 ) = (b c, c b),e1 .perm (E1) = (a b c, a c b)。
對於遞歸的基本部分,採用 n = 1。當只有一個元素時,只可能產生一種排列方式,因此
perm (E) = (e),其中 e 是E 中的惟一元素。當 n > 1時,perm (E) = e .perm (E ) +e .p e r m
1 1 2
(E2 ) +e3.perm (E3) + ⋯ +en .perm (En )。這種遞歸定義形式是採用n 個perm (X) 來定義perm (E), 其中每一個X 包含n- 1個元素。至此,一個完整的遞歸定義所須要的基本部分和遞歸部分都已完成。當n= 3而且E=(a, b, c)時,按照前面的遞歸定義可得perm (E) =a.perm ( {b, c} ) +b.perm ( {a,
c} ) +c.perm ( {a, b} )。一樣,按照遞歸定義有 perm ( {b, c} ) =b.perm ( {c} ) +c.perm ( {b}), 因此 a.perm ( {b, c} ) = ab.perm ( {c} ) + ac.perm ( {b}) = a b . c + ac.b = (a b c, a c b)。同理可得
b.perm ( {a, c}) = ba.perm ( {c}) + bc.perm ( {a}) = b a . c + b c . a = (b a c, b c a),c.perm ( {a, b}) = ca.perm ( {b}) + cb.perm ( {a}) = c a . b + c b . a = (c a b, c b a)。因此perm (E) = (a b c, a c b, b a c, b c a, c a b, c b a)。
注意a.perm ( {b, c} )實際上包含兩個排列方式:abc 和a c b,a 是它們的前綴,perm ( {b, c} )
是它們的後綴。一樣地,ac.perm ( {b}) 表示前綴爲a c、後綴爲perm ( {b}) 的排列方式。
程序1 - 1 0把上述perm (E) 的遞歸定義轉變成一個C++ 函數,這段代碼輸出全部前綴爲l i s t [ 0:
k-1], 後綴爲l i s t [ k:m] 的排列方式。調用Perm(list, 0, n-1) 將獲得list[0: n-1] 的全部n! 個排列方式,在該調用中,k=0, m= n - 1,所以排列方式的前綴爲空,後綴爲list[0: n-1] 產生的全部排列方式。當k =m 時,僅有一個後綴l i s t [ m ],所以list[0: m] 便是所要產生的輸出。當k<m時,先用list[k] 與l i s t [ k:m] 中的每一個元素進行交換,而後產生list[k+1: m] 的全部排列方式,並用它做爲list[0: k] 的後綴。S w a p是一個inline 函數,它被用來交換兩個變量的值,其定義見程序 1 11。P e r m的正確性可用概括法來證實。
程序1-10 使用遞歸函數生成排列
template<class T>
void Perm(T list[], int k, int m)
{ / /生成list [k:m ]的全部排列方式 int i;
if (k == m) {//輸出一個排列方式
for (i = 0; i <= m; i++)
cout << list [i];
cout << endl;
}
else // list[k:m ]有多個排列方式
// 遞歸地產生這些排列方式 for (i=k; i <= m; i++) {
Swap (list[k], list[i]);
Perm (list, k+1, m);
Swap (list [k], list [i]);
}
}
程序1 - 11 交換兩個值
template <class T> inline void Swap(T& a, T& b)
{// 交換a和b
T temp = a; a = b; b = temp;
}
輸入成功時,函數應返回true, 不然返回f a l s e。上機測試該函數。
n
C + +操做符n e w可用來進行動態存儲分配,該操做符返回一個指向所分配空間的指針。例如,爲了給一個整數動態分配存儲空間,可使用下面的語句來講明一個整型指針變量:
int *y;
當程序須要使用該整數時,可使用以下語法來爲它分配存儲空間:
y = new int;
操做符n e w分配了一塊能存儲一個整數的空間,並將指向該空間的指針返回給 y,y是對整數指針的引用,而* y則是對整數自己的引用。爲了在剛分配的空間中存儲一個整數值,好比 1 0,可使用以下語法:
*y = 10;
咱們能夠把上述三步(說明y, 分配存儲空間,爲*y 賦值)進行適當的合併,以下例所示:
int *y = new int;
*y = 10; 或
int *y = new int (10); 或
int *y;
y = new int (10);
在本書的許多示例程序中都使用了一維或二維數組,這些數組的大小在編譯時多是未知的,事實上,它們可能隨着函數調用的變化而變化。所以,對於這些數組必須進行動態存儲分配。
爲了在運行時建立一個一維浮點數組 x,首先必須把x說明成一個指向f l o a t的指針,而後爲數組分配足夠的空間。例如,一個大小爲n 的一維浮點數組能夠按以下方式來建立:
float *x = new float [n];
操做符n e w分配n 個浮點數所須要的空間,並返回指向第一個浮點數的指針。可使用以下語法來訪問每一個數組元素:x[0], x[1], ..., x[n-1]。
在執行語句
float *x = new float [n];
時,若是計算機不能分配足夠的空間怎麼辦?在這種狀況下, new 不可以分配所需數量的存儲空間,將會引起一個異常(e x c e p t i o n)。在Borland C++中,當new 不能分配足夠的空間時,它會引起(t h r o w)一個異常xalloc (在except.h 中定義)。能夠採用try - catch 結構來捕獲(c a t c h) new 所引起的異常:
float *x;
try {x = new float [n];}
catch (xalloc) { // 僅當new 失敗時纔會進入 cerr << "Out of Memory" << endl; e x i t ( 1 ) ; }
當一個異常出現時,程序進入與該異常相對應的c a t c h語句塊中執行。在上面的例子中,只有在執行try 語句時產生了xalloc 異常,纔會進入catch (xalloc) 語句塊,由語句exit (1) 終止程序的運行。(exit () 在stdlib.h 中定義。)
在C++ 程序中處理錯誤條件的經常使用方法就是每當檢測到這樣的條件時就引起一個異常。當一個異常被引起時,必須指明它的類型(如前面的 x a l l o c)。咱們把可能會產生異常的程序代碼放入try 塊中,在try 塊以後放上一個或多個catch 塊,每一個catch 塊用來處理一個特定類型的異常。例如catch (xalloc) 塊僅處理xalloc 異常。語法catch (...) 定義了一個能捕獲全部異常的c a t c h 塊。當一個異常被引起時,檢查在程序執行過程當中所遇到的最接近的 try-catch 代碼,若是其中的一個catch 塊可以處理所產生的異常,程序將從這個catch 塊中繼續執行,不然,繼續檢查直到找到與異常相匹配的塊。若是找不到能處理該異常的 c a t c h塊,程序將顯示信息" A b n o r m a l program termination(異常程序終結)" 並終止運行。若是一個try 塊沒有引起異常,程序的執行將越過與該t r y塊相對應的c a t c h塊。
動態分配的存儲空間再也不須要時應該被釋放,所釋放的空間可從新用來動態建立新的結構。
可使用C + +操做符d e l e t e來釋放由操做符n e w所分配的空間。下面的語句能夠釋放分配給 * y的空間以及一維數組x:
delete y;
delete [ ] x;
雖然C + +提供了多種機制用來講明二維數組,但其中的多數機制都要求在編譯時明確地知道每一維的大小。並且,在使用這些機制時,很難編寫出一個容許形式參數是一個第二維大小未知的二維數組的函數。之因此如此,是由於當形式參數是一個二維數組時,必須指定其第二維的大小。例如,a[ ][10]是一個合法的形式參數,而a[ ][ ] 不是。
克服這種限制的一條有效途徑就是對於全部的二維數組使用動態存儲分配。本書從頭到尾使用的都是動態分配的二維數組。
當一個二維數組每一維的大小在編譯時都是已知時,能夠採用相似於建立一維數組的語法來建立二維數組。例如,一個類型爲c h a r的7× 5數組可用以下語法來定義:
char c[7][5];
若是在編譯時至少有一維是未知的,必須在運行時使用操做符 n e w來建立該數組。一個二維字符型數組,假定在編譯時已知其列數爲5,可採用以下語法來分配存儲空間:
char (*c)[5];
try { c = new char [n][5];}
catch (xalloc) {//僅當n e w失敗時纔會進入 cerr << "Out of Memory" << endl; exit (1);}
在運行時,數組的行數 n要麼經過計算來肯定,要麼由用戶來指定。若是在編譯時數組的列數也是未知的,那麼不可能調用一次 n e w就能建立該數組(即便數組的行數是已知的)。構造二維數組時,能夠把它當作是由若干行組合起來的,每一行都是一個一維數組,能夠按照前面討論的方式用n e w來建立,指向每一行的指針能夠保存在另一個一維數組之中。圖 1 - 1給出了創建一個3× 5數組所須要的結構。
x[0], x[1], x[2]分別指向第0行,第1行和第2行的第一個元素。因此,若是x是一個字符數組,那麼x [ 0 : 2 ]是指向字符的指針,而x自己是一個指向指針的指針。可用以下語法來講明 x :
char **x;
爲了建立如圖1 - 1所示的存儲結構,可使用程序 1 - 1 2中的代碼,該程序建立一個類型爲 T 的二維數組,這個數組有r o w s行和c o l s列。程序首先爲指針x [ 0 ] , . . . , x [ r o w s - 1 ]申請空間,而後爲數組的每一行申請空間。在程序中操做符 n e w被調用了r o w s + 1次。若是n e w的某一次調用引起了一個異常,程序控制將轉移到c a t c h塊中,並返回f a l s e。若是沒有出現異常,數組將被成功建立,函數M a k e 2 D A r r a y返回t r u e。對於所建立的數組x中的元素,可使用標準的用法來引用,如x [ i ] [ j ] ,其中0≤ i<r o w s , 0≤ j<c o l s。
程序1-12 爲一個二維數組分配存儲空間
template <class T> bool Make2DArray ( T ** &x, int rows, int cols)
{// 建立一個二維數組
t r y {
/ /建立行指針
x = new T * [rows];
/ /爲每一行分配空間
for (int i = 0 ; i < rows; i++) x[i] = new int [cols];
return true;
}
catch (xalloc) {return false;}
}
在程序1 - 1 2中,函數經過返回布爾值false 把n e w所產生的異常(若是有的話)告訴調用者。固然,M a k e 2 D A r r a y失敗時也能夠什麼都不作,這樣也能使調用者知道產生了異常。若是使用程序1 - 1 3中的代碼,調用者能夠捕獲由n e w所產生的任何異常。
程序1-13 建立一個二維數組但不處理異常
template <class T> void Make2DArray( T ** &x, int rows, int cols)
{// 建立一個二維數組
// 不捕獲異常
/ /建立行指針
x = new T * [rows];
/ /爲每一行分配空間
for (int i = 0 ; i<rows; i++) x[i] = new int [cols];
}
當M a k e 2 D A r r a y按程序1 - 1 3定義時,可使用以下代碼來肯定存儲分配是否成功:
try { Make2DArray (x, r, c);} catch (xalloc) {cerr<< "Could bot create x" << endl; e x i t ( 1 ) ; }
在M a k e 2 D A r r a y中不捕獲異常不只簡化了函數的代碼設計,並且可使用戶在一個更合適的地方捕獲異常,以便更好地報告出錯誤的明確含義或進行錯誤恢復。
能夠按以下兩步來釋放程序1 - 1 2中爲二維數組所分配的空間。首先釋放在 f o r循環中爲每一
行所分配的空間,而後釋放爲行指針所分配的空間,具體實現見程序 1 - 1 4。注意在程序1 - 1 4中 x被置爲0,以便阻止用戶繼續訪問已被釋放的空間。
程序1-14 釋放由M a k e 2 D A r r a y所分配的空間
template <class T> void Delete2DArray( T ** &x, int rows)
{// 刪除二維數組x
/ /釋放爲每一行所分配的空間 for (int i = 0 ; i < rows ; i++)
delete [ ] x[i];
/ /刪除行指針
delete [] x; x = 0;
}
C + +語言支持諸如 i n t , f l o a t和c h a r之類的數據類型,在本書所提供的許多應用中還使用了
C + +語言不直接支持的數據類型。用C + +來定義自有數據類型最靈活的方式就是使用類( c l a s s)結構。假定你想處理類型 C u r r e n c y的對象,其實例擁有三個成員:符號(+或-),美圓和美分。舉兩個例子,如$2.35 (符號是+,2美圓,3 5美分)和- $ 6 . 0 5 (符號是-,6美圓,5美分)。對這種類型的對象咱們想要執行的操做以下:
假定用無符號長整型變量d o l l a r s、無符號整型變量c e n t s和s i g n類型的變量s g n來描述貨幣對象,其中s i g n類型的定義以下:
enum sign { plus, minus};
可使用程序1 - 1 5中的語法來定義C + +類C u r r e n c y。第一行簡單地說明一個名爲C u r r e n c y的類,而後在一對括號({ })之間給出類描述。類描述被分紅兩個部分: public 和p r i v a t e。public 部分用於定義一些函數(又稱方法),這些函數可對C u r r e n c y類對象(或實例)進行操做,它們對於C u r r e n c y類的用戶是可見的,是用戶與 C u r r e n c y對象進行交互的惟一手段。 p r i v a t e部分用於定義函數和數據成員(如簡單變量,數組及其餘可賦值的結構),這些函數和數據成員對於用戶來講是不可見的。藉助於 p u b l i c部分和p r i v a t e部分,咱們可使用戶只看到他(或她)須要看到的部分,同時把其他信息隱藏起來。儘管 C + +語法容許在p u b l i c部分定義數據成員,但在軟件工程實踐中不鼓勵這種作法。
程序1-15 定義C u r r e n c y類
class Currency { p u b l i c :
// 構造函數
Currency(sign s = plus, unsigned long d = 0, unsigned int c = 0);
// 析構函數
~Currency() {} bool Set(sign s, unsigned long d, unsigned int c); bool Set(float a); sign Sign() const {return sgn;} unsigned long Dollars() const {return dollars;} unsigned int Cents() const {return cents;} Currency Add(const Currency& x) const; Currency& Increment(const Currency& x); void Output() const; p r i v a t e :
sign sgn; unsigned long dollars; unsigned int cents;
} ;
p u b l i c部分的第一個函數與 C u r r e n c y類同名,這種函數稱之爲構造函數。構造函數指明如何建立一個給定類型的對象,它不能夠有返回值。在本例中,構造函數有三個參數,其缺省值
分別是plus, 0和0,構造函數的具體實如今本節稍後給出。在建立一個 C u r r e n c y類對象時,構造函數被自動喚醒。能夠採用以下兩種方式來建立 C u r r e n c y類對象:
Currency f, g (plus, 3,45), h (minus, 10);
Currency *m = new Currency ( plus, 8, 12);
第一行定義了三個C u r r e n c y類變量(f,g 和h),其中f 被初始化爲缺省值plus, 0和0,而g被初始化爲$ 3 . 4 5,h 被初始化爲-$ 1 0 . 0 0。注意初始值從左至右分別對應構造函數的每一個參數。若是初始值的個數少於構造函數參數的個數,剩下的參數將取缺省值。在第二行, m被定義爲指向一個C u r r e n c y對象的指針。咱們調用 n e w函數來建立一個C u r r e n c y對象,並把對象的指針存儲在m中。所建立的對象被初始化爲$ 8 . 1 2。
下一個函數爲~ C u r r e n c y,與類名相比多了一個前綴( ~),這個函數被稱爲析構函數。每當一個C u r r e n c y對象超出做用域時將自動調用析構函數。這個函數用來刪除對象。在本例中析構函數被定義爲空函數( { })。對於其餘類,因爲類的構造函數可能會建立一些動態數組,那麼當對象超出做用域時,析構函數須要釋放這些空間。與構造函數同樣,析構函數也不能夠有返回值。
接下來的兩個函數容許用戶爲 C u r r e n c y類成員賦值。其中第一個函數要求用戶提供三個參
數,而第二個函數須要一個浮點數做爲參數。若是成功,兩個函數均返回 true, 不然返回f a l s e。這兩個函數的具體實如今本節稍後給出。請注意,這兩個函數具備相同的名字,但編譯器和用戶都很容易區分它們,由於它們具備不一樣的參數集合。 C + +容許函數名的重用,只要它們的參數表不一樣。還須要注意的是,沒有指定欲賦值(符號,美圓,美分)對象的名稱,這是由於調用類成員函數的語法以下:
g . S e t ( m i n u s , 3 3 , 0 ) ; h . S e t ( 2 0 . 5 2 ) ;
其中g 和h 是Currency 類變量。在第一個句子中,g 是喚醒Set 的對象,而在第二個句子中h是喚醒Set 的對象。在爲函數S e t編寫代碼時,咱們有辦法訪問調用本函數的對象,所以,不須要把對象的名稱放入參數表中。
函數S i g n,D o l l a r s和C e n t s返回對象的相應數據成員,關鍵字 c o n s t指出這些函數不會修改數據成員。咱們把這種類型的函數稱之爲常元函數( constant function)。
函數S u m把當前對象的貨幣數量與對象x的貨幣數量相加,而後返回所得結果,所以A d d函數不會修改當前對象,是一個常元函數。函數 I n c r e m e n t把對象x 的貨幣數量添加到當前對象上,這個函數修改了當前對象,所以不是一個常元函數。最後一個函數是 O u t p u t,它顯示當前對象的貨幣數量。函數Output 不會修改當前對象,所以是一個常元函數。
儘管A d d和I n c r e m e n t都返回C u r r e n c y類對象,但A d d返回的是值,而I n c r e m e n t返回的是引用。如1 . 2 . 5節所提到的,返回值和返回引用分別與傳值參數和引用參數有相同的做用。在返回一個值的狀況下,返回的對象被複制到所返回的環境,而返回引用則避免了這種複製,在返回的環境中能夠直接使用該對象。返回引用比返回值要快,由於省去了複製過程。從 A d d的代碼中能夠看出,它返回了一個局部對象,在函數終止時該對象將被刪除,所以, r e t u r n語句必須複製該對象。而I n c r e m e n t返回的是一個全局對象,於是不須要複製。
複製構造函數被用來執行返回值的複製及傳值參數的複製。程序 1 - 1 5中沒有給出複製構造函數,因此C + +將使用缺省的複製構造函數,它僅可進行數據成員的複製。對於類 C u r r e n c y來講,使用省缺的複製構造函數已經足夠。後面還將看到許多類,對於這些類缺省的複製構造函數已難以勝任它們的複製工做。
在p r i v a t e部分,定義了三個數據成員,它們對於一個 C u r r e n c y對象來講是必須的。每個
C u r r e n c y對象都擁有本身的這三個數據成員。
因爲在類定義的內部沒有給出函數的具體實現,所以必須在其餘地方給出。在具體實現時,必須在每一個函數名的前面加上 C u r r e n c y::,以指明該函數是 C u r r e n c y類的成員函數。因此
C u r r e n c y::C u r r e n c y表示該函數是C u r r e n c y類的構造函數,而C u r r e n c y::O u t p u t表示該函數是
C u r r e n c y類的O u t p u t函數。程序1 - 1 6給出了C u r r e n c y類的構造函數。
程序1-16 Currency類的構造函數
Currency::Currency(sign s, unsigned long d, unsigned int c)
{// 建立一個C u r r e n c y對象
if(c > 99)
{ / /美分數目過多
cerr << "Cents should be < 100" << endl; e x i t ( 1 ) ; }
sgn = s; dollars = d; cents = c;
}
構造函數在初始化當前對象的 sgn, dollars 和cents 數據成員以前須要驗證參數的合法性。若是參數值出現錯誤,構造函數將輸出一個錯誤信息,而後調用函數 e x i t ( )終止程序的運行。在本例中,僅須要驗證c 的值。
程序1 - 1 7給出了兩個S e t函數的代碼。第一個函數首先驗證參數的合法性,若是參數合法,則用它們來設置p r i v a t e成員變量。第二個函數不執行參數合法性驗證,它僅使用小數點後面的
頭兩個數字。形如 d1.d2d3 的數可能沒有一個精確的計算機表示,例如,用計算機所描述的數
5 . 2 9實際上要比真正的5 . 2 9稍微小一點。當用以下語句
cents = (a - dollars) * 100
抽取cents 成員時,這種描述方法可能會帶來一個錯誤,由於 (a - dollars) * 100稍微小於2 9,當程序把(a - dollars) * 100轉換成一個整數時,c e n t s獲得的將是2 8而不是2 9。只要d .d d 的計
1 2 3
算機表示與實際值相比很多於 0 . 0 0 1或很少於0 . 0 0 9,就能夠採用爲a 加上0 . 0 0 1來解決咱們的問題。例如,若是5 . 2 9的計算機表示是5 . 2 8 9 9 9,那麼加上0 . 0 0 1將獲得5 . 2 9 0 9 9 ,由此所計算出的 c e n t s就是2 9。
程序1-17 設置p r i v a t e數據成員
bool Currency::Set(sign s, unsigned long d, unsigned int c)
{// 取值
if (c > 99) return false; sgn = s; dollars = d; cents = c; return true;
}
bool Currency::Set(float a)
{// 取值
if (a < 0) {sgn = minus; a = -a;} else sgn = plus; dollars = a; // 抽取整數部分
// 獲取兩個小數位
cents = (a + 0.005 - dollars) * 100; return true;
}
程序1 - 1 8給出了函數 A d d的代碼,該函數首先把要累加的兩個貨幣數量轉換成整數,如 $2.32 變成整數2 3 2,-$ 4 . 7 5變成整數-4 7 5。請注意引用當前對象的數據成員與引用參數 x的數據成員在語法上有所區別。 x . d o l l a r s指定x 的數據成員dollars ,而當前對象使用d o l l a r s時能夠直接引用d o l l a r s而沒必要在它的前面加上對象名。當函數 A d d終止時,局部變量 a 1 , a 2 , a 3和a n s 被l o n g數據類型的析構函數刪除,這些變量所佔用的空間也將被釋放。因爲 C u r r e n c y對象a n s將被做爲調用結果返回,所以必須把它複製到調用者的環境中,因此 A d d返回的是值。
程序1-18 累加兩個C u r r e n c y
Currency Currency::Add(const Currency& x) const {// 把 x累加到 * t h i s . long a1, a2, a3;
Currency ans;
// 把當前對象轉換成帶符號的整數 a1 = dollars * 100 + cents; if (sgn == minus) a1 = -a1;
// 把x轉換成帶符號的整數
a2 = x.dollars * 100 + x.cents; if (x.sgn == minus) a2 = -a 2 ;
a3 = a1 + a2;
// 轉換成 currency 形式
if (a3 < 0) {ans.sgn = minus; a3 = -a 3 ; } else ans.sgn = plus; ans.dollars = a3 / 100;
ans.cents = a3 - ans.dollars * 100;
return ans;
}
程序1 - 1 9給出了函數I n c r e m e n t和O u t p u t的代碼。在C + +中,保留關鍵字t h i s用於指向當前對
象,*this 表明對象自己。看一下調用g . I n c r e m e n t ( h )。函數I n c r e m e n t的第一行調用了p u b l i c成員
函數A d d,它把x (這裏是h) 加到當前對象上(這裏是 g),所得結果被返回,並被賦給 * t h i s,
* t h i s就是當前對象。因爲該對象不是函數I n c r e m e n t的局部對象,所以當函數結束時,該對象不會自動被刪除。因此能夠返回一個引用。
程序1-19 Increment與O u t p u t
Currency& Currency::Increment(const Currency& x) {// 增長量 x .
*this = Add(x); return *this;
}
void Currency::Output () const
{// 輸出currency 的值
if (sgn == minus) cout << '-'; cout << '$' << dollars << '.'; if (cents < 10) cout << "0"; cout << cents;
}
經過把C u r r e n c y類的成員變成私有(p r i v a t e),咱們能夠拒絕用戶訪問這些成員,因此用戶不能使用以下的語句來改變這些成員的值:
h.cents = 20;
h.dollars = 100;
h.sgn = plus;
利用成員函數來設置數據成員的值能夠確保數據成員擁有合法的值。構造函數和 S e t函數已經作到了這一點,其餘函數固然也應該保證數據成員的合法性。所以,在諸如 A d d和O u t p u t 函數的代碼中沒必要驗證c e n t s是否介於0到1 0 0之間。若是數據成員被聲明爲 p u b l i c成員,它們的合法性將難以保證。用戶可能會錯誤地把 c e n t s設置成3 0 5,於是將致使一些函數(如 O u t p u t函數)產生錯誤結果,因此,全部的函數在處理任務以前都必須驗證數據的合法性。這種驗證將會下降代碼的執行速度,同時也使代碼不夠優雅。程序1 - 2 0給出了類C u r r e n c y的應用示例。這段代碼假定類定義及實現都在文件 c u r r 1 . h之中。
咱們通常把類定義和類實現分放在不一樣的文件中,然而,這種分開放置的方法可能會對後續章節中大量使用模板函數和模板類帶來困難。
函數m a i n的第一行定義了四個C u r r e n c y類變量:g, h, i 和j。除h 具備初值$ 3 . 5 0外,構造函數把它們都初始化爲$ 0 . 0 0。在接下來的兩行中, g 和i 分別被設置成-$ 2 . 2 5和-$ 6 . 4 5,以後調用函數A d d把g 和h 加在一塊兒,並把所返回的對象(值爲$ 1 . 2 5)賦給j。爲此,需使用缺省的賦值過程把右側對象的各數據成員分別複製到左側對象相應的數據成員之中,複製的結果是使 j 具備值$ 1 . 2 5,這個值在下一行被輸出。
下兩行語句把i 累加到h 上,並輸出i的新值-$ 2 . 9 5。接下來的一行首先把 i和g加在一塊兒,
而後返回一個臨時對象(其值爲- $ 5 . 2 0),此後,把h 加到這個臨時對象上並返回一個新的臨時對象,其值爲-$ 1 . 7 0。新的臨時對象被複制到 j 中,而後輸出j 的值(爲-$ 1 . 7 0)。注意' .' 序列的處理順序是從左到右。
接下來的一行語句首先使用I n c r e m e n t爲i累加g,它返回一個引用給i。A d d把i和h的和返回給j,最後輸出j 的結果爲-$ 1 . 7 0,i 的結果爲-$ 5 . 2 0。
程序1-20 Currency類應用示例
#include <iostream.h> #include "curr1.h"
void main (void)
{
Currency g, h(plus, 3, 50), i, j; g.Set(minus, 2, 25); i . S e t ( - 6 . 4 5 ) ;
j = h.Add(g);
j.Output(); cout << endl; i . I n c r e m e n t ( h ) ;
i.Output(); cout << endl; j = i.Add(g).Add(h);
j.Output(); cout << endl; j = i.Increment(g).Add(h); j.Output(); cout << endl;
i.Output(); cout << endl;
}
假定已經有許多應用採用了程序 1 - 1 5中所定義的C u r r e n c y類,如今咱們想要對 C u r r e n c y類的描述進行修改,使其應用頻率最高的兩個函數 A d d和I n c r e m e n t能夠運行得更快,從而提升應用程序的執行速度。因爲用戶僅能經過 p u b l i c部分所提供的接口與 C u r r e n c y類進行交互,所以對p r i v a t e部分的修改並不會影響應用代碼的正確性。因此能夠修改 p r i v a t e部分而不會使應用發生變化。
在C u r r e n c y對象新的描述中,僅有一個私有數據成員,其類型爲 l o n g。數1 3 2表明$ 1 . 3 2 , 而-2 0表明-$ 0 . 2 0。程序1-21, 1-22, 1-23中給出了C u r r e n c y類的新的描述方法以及各成員函數的具體實現。
注意,若是把新代碼放在文件c u r r 1 . h中,則能夠運行程序1 - 2 0中的代碼而不須要作任何修改。對用戶隱藏實現細節的一個重大好處在於能夠用新的、更高效的描述來取代之前的描述而不須要改變應用代碼。
程序1-21 Currency類的新定義
class Currency { p u b l i c :
// 構造函數
Currency(sign s = plus, unsigned long d = 0, unsigned int c = 0);
// 析構函數
~Currency() {}
bool Set(sign s, unsigned long d, unsigned int c); bool Set(float a); sign Sign() const
{if (amount < 0) return minus; else return plus;}
unsigned long Dollars() const {if (amount < 0) return (-amount) / 100; else return amount / 100;}
unsigned int Cents() const
{if (amount < 0) return -amount - Dollars() * 100;
else return amount - Dollars() * 100;}
Currency Add(const Currency& x) const;
Currency& Increment(const Currency& x)
{amount += x.amount; return *this;} void Output() const; p r i v a t e :
long amount;
} ;
程序1-22 新的構造函數及S e t函數
Currency::Currency(sign s, unsigned long d, unsigned int c)
{// 建立Currency 對象 if (c > 99)
{// 美分數目過多
cerr << "Cents should be < 100" << endl;
e x i t ( 1 ) ; }
amount = d * 100 + c; if (s == minus) amount = -amount;
}
bool Currency::Set(sign s, unsigned long d,
{// 取值
if (c > 99) return false;
amount = d * 100 + c; if (s == minus) amount = -amount; return true;
}
bool Currency::Set(float a)
{// 取值
sign sgn; if (a < 0) {sgn = minus; a = -a;} else sgn = plus; amount = (a + 0.001) * 100; if (sgn == minus) amount = -amount; return true;
}
程序1-23 函數A d d和O u t p u t的新代碼
Currency Currency::Add(const Currency& x) const {// 把x 累加至 * t h i s .
Currency y;
y.amount = amount + x.amount; return y;
}
void Currency::Output() const
{ / /輸出currency 的值 long a = amount;
if (a < 0) {cout << '-' ; a = -a;} long d = a / 100; // 美圓 cout << '$' << d << '.'; int c = a-d * 100; // 美分 if (c < 10) cout << "0"; cout << c;
}
C u r r e n c y類包含了幾個與 C + +標準操做符相相似的成員函數,例如, A d d進行+操做,
I n c r e m e n t進行 + =操做。直接使用這些標準的 C + +操做符比另外定義新的函數(如 A d d, I n c r e m e n t)要天然得多。能夠藉助於操做符重載( operator overloading)的過程來使用+和+ =。操做符重載容許擴充現有C + +操做符的功能,以便把它們直接應用到新的數據類型或類。
程序1 - 2 4給出了把A d d和I n c r e m e n t分別替換爲+和+ =的類描述。O u t p u t函數採用一個輸出流的名字做爲參數。這些變化僅需修改 A d d和O u t p u t的代碼(見程序1 - 2 3)。程序1 - 2 5給出了修改後的代碼。在這個程序還給出了重載C + +流插入操做符< <的代碼。
注意在C u r r e n c y類中重載了流插入操做符,但沒有定義相應的成員函數,而重載 +和+ =時則把它們定義爲類成員。一樣,也能夠重載流抽取操做符 > >而不須要把它定義爲類成員。請留意,是函數O u t p u t支持了< <的重載。因爲C u r r e n c y對象的p r i v a t e成員對於非類成員函數來講不可訪問(被重載的 < <不是類成員,而 +是),因此,重載 < <的代碼不能引用對象 x的私有成員(在< <操做中x將被插入到輸出流中)。特別地,下面的代碼是錯誤的,由於成員 a m o u n t是不可訪問的。
// 重載< < ostream& operator<< ( ostream& out, const Currency& x)
{ out << x.amount; return out; }
程序1-24 使用操做符重載的類定義
class Currency { p u b l i c :
// 構造函數
Currency(sign s = plus, unsigned long d = 0, unsigned int c = 0);
// 析構函數
~Currency() {} bool Set(sign s, unsigned long d, unsigned int c); bool Set(float a); sign Sign() const
{if (amount < 0) return minus; else return plus;}
unsigned long Dollars() const
{if (amount < 0) return (-amount) / 100; else return amount / 100;}
unsigned int Cents() const
{if (amount < 0) return -amount - Dollars() * 100;
else return amount - Dollars() * 100;}
Currency operator+(const Currency& x) const;
Currency& operator+=(const Currency& x)
{amount += x.amount; return *this;} void Output(ostream& out) const; p r i v a t e :
long amount;
} ;
程序1-25 +,O u t p u t和< <的代碼
Currency Currency::operator+(const Currency& x) const {// 把 x 累加至* t h i s .
Currency y;
y.amount = amount + x.amount; return y;
}
void Currency::Output(ostream& out) const
{// 將currency 的值插入到輸出流 long a = amount;
if (a < 0) {out << '-' ; a = -a ; } long d = a / 100; // 美圓 out << '$' << d << '.' ; int c = a - d * 100; // 美分 if (c < 10) out << "0"; out << c;
}
// 重載< < ostream& operator<<(ostream& out, const Currency& x) {x.Output(out); return out;}
程序1 - 2 6是程序1 - 2 0的另外一個版本,它假定操做符都已經被重載,程序 1 - 2 4和1 - 2 5的代碼位於文件c u r r 3 . h之中。
程序1-26 操做符重載的應用
#include <iostream.h> #include "curr3.h"
void main(void)
{
Currency g, h(plus, 3, 50), i, j; g.Set(minus, 2, 25); i . S e t (-6 . 4 5 ) ;
j = h + g; cout << j << endl; i += h; cout << i << endl; j = i + g + h; cout << j << endl; j = (i+=g) + h; cout << j << endl; cout << i << endl;
}
諸如構造函數和S e t函數這樣的類成員在執行預約的任務時有可能會失敗。在構造函數中處理錯誤條件的方法是退出程序,而在 S e t中則返回一個失敗信號( f a l s e)給調用者。實際上能夠經過引起異常來處理這些錯誤,以便在程序最合適的地方捕獲異常並進行處理。爲了引起異常,必須首先定義一個異常類,好比B a d I n i t i a l i z e r s (見程序1 - 2 7 )。
程序1-27 異常類B a d I n i t i a l i z e r s
// 初始化失敗
class BadInitializers { p u b l i c :
BadInitializers() {}
} ;
咱們能夠修改程序1 - 2 1中S e t函數的描述,使其返回v o i d類型。也能夠修改構造函數的代碼以及程序1 - 2 8中定義的第一個S e t函數的代碼。其餘的代碼不作修改。
程序1-28 引起異常
Currency::Currency(sign s, unsigned long d, unsigned int c)
{// 建立一個C u r r e n c y對象 if (c > 99) throw BadInitializers();
amount = d * 100 + c;
if (s == minus) amount = -amount;
}
void Currency::Set(sign s, unsigned long d, unsigned int c)
{// 取值
if (c > 99) throw BadInitializers();
amount = d * 100 + c; if (s == minus) amount = -amount;
}
正如前面所指出的那樣,一個類的 p r i v a t e成員僅對於類的成員函數是可見的。在有些應用中,必須把對這些p r i v a t e成員的訪問權授予其餘的類和函數,作法是把這些類和函數定義爲友元(f r i e n d)。在C u r r e n c y類例子中(見程序1 - 2 4),定義了一個成員函數O u t p u t以便於對操做符< <的重載。定義這個函數是必要的,由於以下函數:
ostream& operator <<(ostream& out, const Currency& x)
class Currency { friend ostream& operator<< (ostream&, const Currency&); p u b l i c :
有了這個友元,就可使用程序 1 - 2 9中的代碼來重載操做符< <。當C u r r e n c y的p r i v a t e成員發生變化時,必須檢查它的友元以便作出相應的變化。
程序1-29 重載友元< <
// 重載 < < ostream& operator<<(ostream& out, const Currency& x)
{// 把currency 的值插入到輸出流 long a = x.amount;
if (a < 0) {out << '-' ; a = -a;} long d = a / 100; // 美圓 out << '$' << d << '.' ; int c = a - d * 100; // 美分 if (c < 10) out << "0"; out << c; return out;
}
稍後咱們將看到如何從一個類B派生出另一個類A,此時類A被稱爲派生類(drived class),
類B被稱爲基類(base class)。派生類須要訪問基類的部分或全部數據成員。爲了便於傳遞這
些訪問權,C + +提供了第三類成員——保護類成員( p r o t e c t e d)。保護類成員相似於私有成員,區別在於派生類能夠訪問保護類成員。
用戶應用程序能夠訪問的類成員應被聲明爲 p u b l i c成員,數據成員儘可能不要定義爲這種類型,其餘成員應分紅p r i v a t e和p r o t e c t e d兩部分。軟件工程實踐告訴咱們,數據成員應儘可能保持爲p r i v a t e成員。經過增長保護類成員來訪問和修改數據成員的值,派生類能夠間接訪問基類的數據成員。同時,能夠修改基類的實現細節而不會影響派生類。
文件c u r r 1 . h (或c u r r 3 . h )的所有內容包含了C u r r e n c y類的描述及實現細節。在文件頭,必須放上以下語句:
#ifndef Currency_
#define Currency_
而在文件尾須要放上語句:
# e n d i f
這些語句確保C u r r e n c y的代碼僅被程序包含(i n c l u d e)和編譯一次。建議你爲本書中所提供的其餘類定義也加上相應的語句。
9. 1) 採用程序1 - 1 5中的描述,所能表示的最大和最小貨幣值分別是多少?假定用四個字節
表示一個l o n g型數據,用兩個字節表示一個i n t型數據,則一個unsigned long數介於0~23 2- 1之間,一個unsigned int數介於0~6 5 5 3 5之間。
10. 試擴充程序1 - 1 5中的C u r r e n c y類,爲該類添加以下的p u b l i c成員函數:
2) 利用重載賦值操做符 =來替換兩個S e t函數。把一個整數賦值給一個 C u r r e n c y對象可用 operator=(int x) 來表示,它可用來替換第一個S e t函數,其中x表示一個包含符號、美圓和美分的整數。一樣,operator=(float x)可用來替換第二個S e t函數。
如1 . 1節所示,正確性是一個程序最重要的屬性。因爲採用嚴格的數學證實方法來證實一個程序的正確性是很是困難的(哪怕是一個很小的程序),因此咱們想轉而求助於程序測試
(program test)過程來實施這項工做。所謂程序測試是指在目標計算機上利用輸入數據,也稱之爲測試數據( test data)來實際運行該程序,把程序的實際行爲與所指望的行爲進行比較。若是兩種行爲不一樣,就可斷定程序中有問題存在。然而,不幸的是,即便兩種行爲相同,也不可以判定程序就是正確的,由於對於其餘的測試數據,兩種行爲又可能不同。若是使用了許多組測試數據都可以看到這兩種行爲是同樣的,咱們能夠增長對程序正確性的信心。經過使用所用可能的測試數據,能夠驗證一個程序是否正確。然而,對於大多數實際的程序,可能的測試數據的數量太大了,不可能進行窮盡測試,實際用來測試的輸入數據空間的子集稱之爲測試集(test set)。
例1-4 [二次方程求解] 一個關於變量x 的二次函數形式以下:
a x2 + b x +c
其中a, b, c 的值是已知的,且a≠ 0。3x2-2x+4, -9x2-7x, 3.5x2+ 4以及5 . 8x2+ 3 . 2x+ 5都是二次函數的實例。5x+ 3不是二次函數。
二次函數的根是指使函數的值爲 0的那些x。例如,函數 f (x) =x2-5x+ 6的根爲2和3,由於 f ( 2) = f (3) =0。每一個二次函數都會有兩個根,這兩個根可用以下公式給出:
對於函數f (x) = x2-5x+6, a=1, b=-5, c= 6 ,把a, b, c 代入以上公式,可得:
因此f (x) 的根是x = 3和x = 2。
當d=b2-4ac =0 時,所獲得的兩個根是同樣的;當 d >0 時,兩個根不一樣且是實數;當 d <0 時,兩個根也不相同且爲複數,此時,每一個根都有一個實部( r e a l)和一個虛部(i m a g i n a r y), 實部爲-b/ 2a,虛部爲 −d 。複數根爲" 實部+虛部*i " 和" 實部-虛部*i",其中i = −1。
函數O u t p u t R o o t s(見程序1 - 3 0)計算並輸出一個二次方程的根。咱們不去試圖對該函數的正確性進行形式化證實,而是但願經過測試來驗證其正確性。對於該程序來講,全部可能的輸入數據的數目實際上就是全部不一樣的三元組(a, b, c)的數目,其中a≠ 0。即便a, b和c 都被限制爲整數,全部可能的三元組的數目也是很是巨大,要想測試全部的三元組是不可能的。若整數的長度爲1 6位,b 和c 都有216 種不一樣取值,a 有21 6-1種不一樣取值(由於a 不能爲0),全部不一樣三元組的數目將達到23 2(21 6-1)。若是目標計算機能按每秒鐘1 000 000個三元組的速率進行測試,那麼至少須要9年才能完成!若是使用一個更快的計算機,按每秒測試 1 000 000 000 個三元組的速度,也至少須要三天才能完成。因此一個實際使用的測試集僅是整個測試數據空間中的一個子集。
程序1-30 計算並輸出一個二次方程的根
template<class T> void OutputRoots(T a, T b, T c)
{// 計算並輸出一個二次方程的根
T d = b*b-4 * a * c ;
if (d > 0) {// 兩個實數根
float sqrtd = sqrt(d);
cout << "There are two real roots "
<< (-b+sqrtd)/(2*a) << " and "
<< (-b-sqrtd)/(2*a)
<< endl;} else if (d == 0)
// 兩個根相同
cout << "There is only one distinct root "
<< -b/(2*a)
<< endl; else // 複數根
cout << "The roots are complex"
<< endl
<< "The real part is "
<< -b/(2*a) << endl
<< "The imaginary part is "
<< sqrt(-d)/(2*a) << endl;
}
若是使用數據(a, b, c)=(1, -5, 6)來進行測試,程序將輸出2和3,程序的行爲與指望的行爲是一致的,所以能夠推斷對於該輸入數據,程序是正確的。然而,使用一個適當的測試數據子集來驗證所觀察行爲與所指望行爲的一致性並不能證實對於全部的輸入數據,程序都可以正確工做。
因爲能夠提供給一個程序的不一樣輸入數據的數目通常都很是巨大,因此測試一般都被限制在一個很小的子集中進行。使用子集所完成的測試不能徹底保證程序的正確性。因此,測試的目的不是去創建正確性認證,而是要暴露程序中的錯誤!必須選擇能暴露程序中所存在錯誤的測試數據,不一樣的測試數據能夠暴露程序中不一樣的錯誤。
例1-5 測試數據(a, b, c)=(1, -5, 6) 可使函數OutputRoots 執行產生兩個實數根的代碼,若是輸出了2和3,能夠有一些信心地認爲在本次測試中所執行的代碼是正確的。注意,一段錯誤的代碼也可能給出正確的結果。例如,若是在關於 d的表達式中忽略a,將其錯誤地寫成:
T d = b * b - 4 * c;
d的值與所測試的結果相同,由於 a = 1。因爲使用測試數據( 1,-5,6)未能執行完代碼中的全部語句,故咱們對還沒有執行的語句尚未多大的信心。
測試集{(1, -5, 6), (1, 3, 2), (2, 5, 2)} 僅可用來暴露OutputRoots 前7行語句中存在的錯誤,由於這個測試集中的每一個三元組僅須要執行代碼的前 7行語句。然而,測試集{(1, -5, 6), (1, -8 ,
16), (1, 2, 5)} 可以使OutputRoots 中的每行語句都獲得執行,因此該測試集將能夠暴露較多的錯誤。
在設計測試數據的時候,應當牢記:測試的目標是去披露錯誤。若是用來尋找錯誤的測試數據找不到錯誤,咱們就能夠有信心相信程序的正確性。爲了弄清楚對於一個給定的測試數據,程序是否存在錯誤,首先必須知道對於該測試數據,程序的正確結果應是什麼。
例1-6 對於二次方程求解的例子,能夠用以下兩種方法之一來給定任意測試數據時程序的正確輸出。第一種方法是,計算出所測試二次方程的根。例如,係數( a, b, c)=(1, -5, 6)的二次方程的根爲2和3。對於測試數據(1, -5, 6),能夠把程序所輸出的根與2和3進行比較,以驗證程序1 - 3 0的正確性。第二種可行的方法是把程序所產生的根代入二次函數以驗證函數的值是否真爲0。因此,若是程序輸出的是2和3,能夠計算出f (2) = 22-5*2 + 6=0, f ( 3) = 3 2-5 * 3 + 6 = 0。能夠把這種驗證方法用計算機程序來實現。對於第一種方法,測試程序輸入三元組( a, b, c)和指望的根,而後把程序計算出的根與指望的根進行比較。對於第二種方法,能夠編寫代碼來計算對於程序輸出的根,二次函數的相應函數值,而後驗證這個值是否爲 0。
能夠採用下面的條件來計算任何候選的測試數據:
設計測試數據的技術分爲兩類:黑盒法(black box method)和白盒法(white box method)。在黑盒法中,考慮的是程序的功能,而不是實際的代碼。在白盒法中,經過檢查程序代碼來設計測試數據,以便使測試數據的執行結果能很好地覆蓋程序的語句以及執行路徑。
最流行的黑盒法是I/O 分類及因果圖,本節僅探討I / O分類。在這種方法中,輸入數據和 / 或輸出數據空間被分紅若干類,不一樣類中的數據會使程序所表現出的行爲有質的不一樣,而相同類中的數據則使程序表現出本質上相似的行爲。二次方程求解的例子中有三種本質上不一樣的行爲:產生複數根,產生實數根且不一樣,產生實數根且相同。能夠根據這三種行爲把輸入空間分爲三類。第一類中的數據將產生第一種行爲;第二類中的數據將產生第二種行爲;而第三類中的數據將產生第三種行爲。一個測試集應至少從每一類中抽取一個輸入數據。
白盒法基於對代碼的考察來設計測試數據。對一個測試集最起碼的要求就是使程序中的每一條語句都至少執行一次。這種要求被稱爲語句覆蓋( statement coverage)。對於二次方程求解的例子,測試集{(1, -5, 6),(1,-8,1 6),(1,2,5)} 將使程序中的每一條語句都得以執行,而測試集 {(1,-5,6),(1,3,2),(2,5,2)} 則不能提供語句覆蓋。
在分支覆蓋(decision coverage)中要求測試集要可以使程序中的每個條件都分別能出現t r u e和f a l s e兩種狀況。程序1 - 3 0中的代碼有兩個條件:d > 0和d = = 0。在進行分支覆蓋測試時,要求測試集至少能使條件d > 0和d = = 0分別出現一次爲t r u e、一次爲f a l s e的狀況。
例1-7 [求最大元素] 程序1 - 3 1用於返回數組a [ 0 : n-1 ]中最大元素所在的位置。它依次掃描a [ 0 ]到 a [ n-1 ],並用變量p o s來保存到目前爲止所能找到的最大元素的位置。數據集 a [ 0 : 4 ] = [ 2 , 4 , 6 , 8 , 9 ] 可以提供語句覆蓋,但不能提供分支覆蓋,由於條件a [ p o s ] < a [ i ]不會變成f a l s e。數據集[ 4,2,6,
8,9 ]既能提供語句覆蓋也能提供分支覆蓋。
程序1-31 尋找最大元素
template<class T> int Max(T a[], int n)
{// 尋找 a [ 0 : n - 1 ]中的最大元素 int pos = 0;
for (int i = 1; i < n; i++) if (a[pos] < a[i]) pos = i;
return pos;
}
能夠進一步增強分支覆蓋的條件,要求每一個條件中的每一個從句( c l a u s e)既能出現t r u e也能出現f a l s e的狀況,這種增強的條件被稱之爲從句覆蓋(clause coverage)。一個從句在形式上被定義成一個不包含布爾操做符(如 & & , | | , !)的布爾表達式。表達式 x > y,x + y < y * z以及c ( c是一個布爾類型)都是從句的例子。考察以下語句:
if((C1 && C2) || (C3 && C4)) S1; else S2;
其中C 1,C 2,C 3和C 4是從句,S 1和S 2是語句。在分支覆蓋方式下,須要使用一個能使 ( ( C 1 && C2) || (C3 && C4))爲t r u e的測試數據以及一個能使該條件爲 f a l s e的測試數據。而從句覆蓋則要求測試數據能使四個從句 C 1 , C 2 , C 3和C 4都分別至少取一次t r u e值和至少取一次f a l s e值。還能夠繼續增強從句覆蓋的條件,要求測試各從句值的全部可能組合。對於上面的條件
((C1 && C2) || (C3 && C4)),增強後的從句覆蓋要求使用1 6個測試數據集:每一個測試集對應於四個從句值組合後的情形。不過,其中有些組合是不可能的。
若是按照某個測試數據集來排列程序語句的執行次序,能夠獲得一條執行路徑( e x e c u t i o n p a t h)。不一樣的測試數據可能會獲得不一樣的執行路徑。程序 1 - 3 0僅存在三條執行路徑——第1行至第7行,第1、2、8~1 2行,第1、2、8、1 3~1 9行。而程序1 - 3 1中的執行路徑則隨着n的增長而增長。當n= 1時,僅有一條執行路徑——1、2、5行;當n= 2時,有兩條路徑——1、2、3、2、
5和1、2、3、4、2、5行;當n = 3時,有四條路徑——1、2、3、2、3、2、5行, 1、2、3、4、
2、3、2、5行,1、2、3、2、3、4、2、5行,1、2、3、4、2、3、4、5行。執行路徑覆蓋要求測試數據集能使每條執行路徑都得以執行。對於二次方程求解程序,語句覆蓋、分支覆蓋、從句覆蓋以及執行路徑覆蓋都是等價的,但對於程序 1 - 3 1,語句覆蓋、分支覆蓋、和執行路徑覆蓋是不一樣的,而分支覆蓋和從句覆蓋是等價的。
在這些白盒測試方法中,通常要求實現執行路徑覆蓋。一個能實現所有執行路徑覆蓋的測試數據一樣能實現語句覆蓋和分支覆蓋,然而,它可能沒法實現從句覆蓋。所有執行路徑覆蓋一般會須要無數的測試數據或至少是很是可觀的測試數據,因此在實踐中通常不可能進行所有執行路徑覆蓋。
本書中的許多練習都要求你測試所編代碼的正確性。你所使用的測試數據應至少提供語句覆蓋。此外,你必須測試那些可能會使你的程序出錯的特定情形。例如,對於一個用來對 n≥ 0 個元素進行排序的程序,除了測試n 的正常取值外,還必須測試n= 0 , 1這兩種特殊情形。若是該程序使用數組a[0:99], 還須要測試n= 1 0 0的情形。n= 0 , 1和1 0 0分別表示邊界條件爲空,單值和全數組的情形。
測試可以發現程序中的錯誤。一旦測試過程當中產生的結果與所指望的結果不一樣,就能夠了解到程序中存在錯誤。肯定並糾正程序錯誤的過程被稱爲調試( d e b u g)。儘管透徹地研究程序調試的方法超出了本書的範圍,但咱們仍是提供一些好的建議給你們:
這種策略被稱爲增量測試與調試( incremental test and debug)。在使用這種策略時,能夠有理由認爲產生錯誤的語句位於剛剛引入的函數之中。練習