2013年10月31日 星期四

Round 8: Exception - fail fast

Backlog中仍有SP2與SP7,這一 round 我們選SP7:建立vector與matrix超限功能與測試。

U步驟:

超限測試是vector::operator [] 與 matrix::at極為重要的面向。以vector::operator []為例,由於當初我選擇以operator [] 取用向量的分量,一開始向量v的第一分量為v[0],後來認錯修正為v[1],這樣的歷程即便是你自己都有可能記錯。於是,你有可能仍用v[0]表達向量v的第一分量 -- 超限了。

簡單的說起來,我們需要在取用向量的分量之前檢查operator []的傳入參數是否超限。如是,則應避免傳值回去。這種情況類似於先前計算innerProduct的函數,


bool innerProduct(double &product, const vector &u, const vector &v);

所以,一種解法是仿照innerProduct的函數的做法,傳回bool並引進一個參數作為回傳結果。

問題來了,由於operator []是C++ 內建運算子,只能接受一個輸入參數,且返回型態在這裡必須是 double:


double & operator [](int i) const;

所以無法套用innerProduct的函數的做法!我們得另想辦法。

幸運的是C++及其他較新的程式語言如Java、Python、Scala等,均提供表達與處理例外狀況的機制(exception handling mechanism)如果將超限視為一種例外狀況,則 operator []可檢查(detect)是否超限,如是則拋出例外(throw an exception),表示超限已發生, 並終止正常函數執行:

double & vector::operator [](int i) const {
if (i <=0 || i > _dim )
throw std::string("Index to vector out of bound!");
return _v[i-1];
}

C++ 提供一個exception class,用來表達例外。我們可以利用這個型態,但是代價為較為複雜些。幸好,C++容許拋出任意型態的例外,例如int、const char* (C string)、string等。這裡,vector::operator[]可拋出的例外型態為string。

呼叫operator []的敘述,則可放置在 try 敘述 (try statement)try block之中,並由緊跟在後的catch block加以捕捉:

try {
double component = u[0];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}

注意 catch敘述是以型態比對的方式捕捉例外。

如超限例外未被捕捉(例如,呼叫operator []的敘述未被放置於try statement或無對應的catch block加以捕捉),則該例外將繼續上傳至前一個做出呼叫的的函數,依此類推,直到到達main。如main未加以捕捉,則C++ runtime將會終結程式。

D步驟:

我們將以string作為operator []超限例外。工作列出如下

T31:寫vector::operator []超限測試。
T32:vector::operator []超限檢查,及拋出例外
T33:決定如何處理vector::operator []超限例外,做必要的程式修改
T34:寫matrix:at超限測試。
T35:matrix:at超限檢查,及拋出例外
T36:決定如何處理matrix:at超限例外,做必要的程式修改

C步驟:

T31:operator []超限測試。

增加兩個超限測試,期望超限發生時,拋出的例外物件為string("Index to vector out of bound!")。

TEST (index_outofbound_low, vector){
vector u(2);
try {
double ccomponent = u[0];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}
}

TEST (index_outofbound_high, vector){
vector u(2);
try {
double ccomponent = u[3];
}
catch (string s) {
CHECK(string("Index to vector out of bound!") == s);
}

}


T32:operator []超限檢查,及拋出例外

double & vector::operator [](int i) const {
if (i <=0 || i > _dim )
throw std::string("Index to vector out of bound!");
return _v[i-1];

}

注意到 throw std::string(...)了嗎?如果只寫 throw string("...")則發生compilation error,原因是vector有一個同名的函數 string(),故以std::string(...)加以區別。另一種做法是將vector::string()改名,例如改為vector::str()。

T33:決定如何處理超限例外,做必要的程式修改
operator []變更,影響innerProduct.exe 與 linearTransform.exe。我們需要決定當vector index超限發生時,該如何處理?

首先,我們先問,使用者會不會引發超限例外?如果會,則超限例外處理的方式,需要顯考慮使用者。

以innerProduct.exe為例,程式並不會讓使用者有直接引發這個例外的機會

接著我們問,使用vector::operator []的programmer會不會所引發超限例外?注意,這裡programmer可能是你,或者其他使用你寫的vector class的programmer。同時,這個超限例外極容易發生:由於後來從1開始的使用方法顯然與operator [] 的從0開始慣用法不合,發生超限的機率就更大了。

Programmer引發這個超限例外時,我們可以說這個例外使因為程式的bug造成的。有bug的程式不該給客戶用。所以,這個決定很容易做:提示programmer這個情形後終結程式,讓programmer修復造成超限的code。

決定了超限例外處理方針之後,下一個決定是由哪一個函數來提示programmer這個情形後終結程式被賦予責任處理這個例外的函數,它的code將因增加try statement而變複雜。根據前面關於例外處理的描述,最簡單的作法是不要做任何事:
超限例外發生時,任由C++ runtime 異常終止 main函數。

以下在main中使用者輸入第一個向量之後,故意讓超限發生,程式執行不正常終止的畫面:




我寫的程式提供你參考。畫面中告訴我們程式在拋出 'std::string' 之後異常終結,並請我們連繫開發者。不論對使用者或programmer,以此方式處置一個bug,應屬恰當。

T34、T35、T36的工作請你試試看。

L步驟:

1. vector::operator [] index 由1而非0開始,有違慣例,對programmer而言,vector::operator []的使用已成bug的溫床。請將vector::operator []改名,例如vector::component(int i)或vector::at(int i)。請你試試看。

2. 前述超限例外,如果我們讓使用者輸入特定維度,為它取得向量在該維度分量,則使用者將有直接引發這個例外的機會,如果這種情形發生,程式應適度提示使用者,使他有機會更正此項錯誤,並繼續正常執行。同理,innerProduct程式有哪些地方會有此類使用者引發的錯誤?這些地方都可以改以例外的方式處理。 

© Y C Cheng, 2013. All rights reserved.

2013年10月27日 星期日

Round 7: Testing improves design

U步驟:

在測試議題上,根據Round 6回顧,列出問題如下:
SP6:建立vector與matrix輸出的單元測試。
SP7:建立vector與matrix超限功能與測試。

讓我們先解決SP6。vector物件的輸出函數在實作上讀取u的分量,適當的符號裝飾,直接使用C++ 輸出物件 cout 進行輸出:


void outputVector(vector u){
cout << "[";
for (int i=1; i<u.dim(); ++i)
cout << u[i] << ",";
cout << u[u.dim()] << "]" << endl;
return;
}

我們的問題是如何測試?前次L步驟說不該直接讓cout輸出「污染」測試輸出畫面,所以不能直接在測試中呼叫 outputVector。

結論:outputVector的可測試性(testability)不佳。SP6的目標就是改善outputVector與outputMatrix的可測試性。

D步驟:

如何改善outputVector的testability?解決方法之一,是以間接的方式測試:先讓vector u轉成一個可測試且容易輸出到cout的中間表示型式(intermediate form),再將該型式輸出到cout。假設後者均能以C++內建的方式做到,則能測試vector u轉出的中間表示型式,即能測試outputVector!

有了這個解決方案,接下來就是決定vector u轉出的中間表示型式。由於outputVector輸出是給使用者看的,直覺上字串(string)就是一個適用的中間表示型式。

列出待辦工作如下:

T23 測試 vector轉成字串。
T24 撰寫vector轉成字串的函數。
T25 修改 outputVector。
T26 迴歸測試。
T27 測試 matrix轉成字串。
T28 撰寫 matrix轉成字串的函數。
T29 修改 outputMatrix
T30 迴歸測試。

相信你已經知道如何列出這些工作了,同時這些工作已經難不倒你了!

C步驟:

完成T23、T24、T25的相關程式片段如下。
(學習課題:string, ostringstream)

T23:
TEST (outputVector, vector){

double a[3]={1,2,3};

vector u(3,a);

CHECK(string("[1,2,3]")==u.string()); }

T24:in vector.h

class vector {

...

public:
...
std::string string() const;
};

in vector.cpp

string vector::string() const {

ostringstream oss;

oss << "[";
for (int i=1; i<= dim()-1; ++i)
oss << (*this)[i] << ",";
oss << (*this)[dim()] << "]";
return oss.str();
}

T25:
void outputVector(vector u){
cout << u.string() << endl;
return;
}


T25得到的 outputVector 對當前程式功能似乎夠了,但稍加思考,不難發現它有些限制,例如輸出兩個向量

vector u(...);
vector v(...);
outputVector(u);
outputVector(v);

後二行code看起來有些小題大作。為什麼不寫成

cout << u << "\n" << v << endl; // u v 間有換行
或者
cout << u << " " << v << endl; // u v 間無換行

這看起來是個好想法,而且並不難做到,我只要 overload operator << 即可:

ostream & operator << (ostream & os, const vector & u){
return os << u.string();
}

同意嗎?matrix相關工作請自己試試看,可參考我寫的程式

L步驟:

此round我們出發時,面對的是一個測試的問題。不容易測試的函數,通常隱含著其他的問題。工作T24完成後,outputVector函數變得可測;T25完成後,你可以將 vector透過標準的operator << 輸出到 cout以外的輸出串流(output stream),如檔案(使用ofstream)、字串(使用ostringstream)等。此時outputVector即無存在的必要!故有本篇的title: 

Testing improves design.

此外,我可以說

Design to enable testing. 

© Y C Cheng, 2013. All rights reserved.

2013年10月11日 星期五

Round 6 continued: Member function or ordinary function?

接下來實作T20,線性轉換的unit test。我們需要決定線性轉換函數為matrix的member function

matrix m(...);
vector v(...);
vector u = m.transform(v);

或者一般函數

matrix m(...);
vector v(...);
vector u = transform(m, v);

功能上二者完全一樣,何者較佳?

  • 如採member function,則matrix 認得(knows) vector :vector v被當成參數傳入transform,且transform需傳回一型態為vector的值。當vector有變動時,vector test專案與matrix test專案均需被編譯、執行。
  • 如採一般函數,則matrix與vector維持獨立(independent)當vector有變動時,matrix test專案不需被編譯、執行。

下圖顯示這兩種相依關係(dependency)。左手邊的作法增加matrix對vector的耦合度(coupling),右邊則無。因此,我們決定transform將以一般函數型式存在。套用物件導向設計的說法,左手邊的作法將transform的責任(responsibility)指派(assign)給matrix,右邊則無。由此可知,

     是否增加class耦合度乃為責任指派的重要考慮因素。




接下來的問題是將transform函數放置在哪一個檔案?如與matrix同置(collocate),需在matrix.h 引入vector.h

#include "vector.h"

經過前一項不增加耦合度的設計決定後,這看起來不是一個好的安排。所以:將transform函數安置放在另二個新的檔案 linearAlgebra.h 與 linearAlgebra.cpp,建立 linearAlgebra test專案。這個專案在vector或matrix有變動時,需被編譯、執行以確保正確。

接著實作T19,含transform與rowVector兩個一般函數。transform的演算法如下(設輸入矩陣為M向量為v,輸出向量為u):

for i in 1 to M.row()
  get ith row vector of M
  u[i] = inner product(ith row vector,  v)

這是根據線性代數教科書的所寫的算法。設M為3x2矩陣,則它有三個列向量:第1、2、3列向量。u的第1個分量為第1列向量與v的內積,etc.

你發現問題了嗎?先前vector u 的第一個分量為u[0]而非u[1],或許當時受了使用index operator []的影響,並不覺得是一個問題,現在做線性轉換,卻有些出入。如何解決?有兩個方法:
  • 實作transform的演算法如下
    for i in 0 to M.row()-1
      get (i+1)th row vector of M
      u[i] = inner product((i+1)th row vector,  v)
  • 承認過去的設計有誤,修改vector::operator [],使第一分量為u[1],第二分量為u[2], 依此類推。
選擇第一個,則演算法不易被看懂(特別是對使用你的程式的工程師們),選擇第二個,則對現有使用到vector::operator[]的code有直接的衝擊。如何選擇?

我選擇承認過去的設計有誤,修改vector::operator [],雖然衝擊較大,但更正了一項意外的錯誤,並使得transform演算法的實作易懂。T19可細分為下列工作:

1. 修改vector test中直接關於operator []的測試。修改後,這些測試應會fail。
2. 修改operator [],直到operator []的測試通過。
3. 修改其他使用到operator []的測試。
4. 修改被測函數、成員函數,直至所有測試通過。
5. 測試、修正innerProduct專案,直至通過。
6. 實作transform與rowVector函數,直至linearAlgebra test通過。

最後完成T21, 建立 linear transformation project,實作主程式。

現在我們用了 vectorTest、matrixTest、linearAlgebraTest、inner product與 linearTransform等五個專案。這五個專案在DEV C++各自存在,但共用一些檔案。例如在專案vectorTest下改變了vector.cpp,則需將其他使用vector.cpp的專案,分別重新編譯、執行;這將會造成一定程度的不便,例如執行迴歸測試。我們需要的是在vector.cpp有所改變時,所有使用vector.cpp的程式自動被重建。

著眼於減少工作上的不便,追加一個工作:

T22:將五個合併專案為一個專案,建立makefile,讓Dev C++使用這個makefile而不每次重新產生 makefile。

我的做法是使用在C/C++程式開發中最常用的建置工具make。 make 讓你將程式建置的指令以目標(target)與相依關係(dependency)編寫在於一個建置腳本(build script),按照慣例,這腳本放在一個名稱為makefile的檔案中。

事實上,之前的五個專案,Dev C++根據你提供的專案資訊自動為你產生一個檔名為 Makefile.win的建置腳本。我提供makefile 即是參考這個檔案修改所得。

有了makefile以後,建立一個新專案 LinearAlgebra,加入所有延伸檔名為 .h 與 .cpp的所有檔案,並在Project options-> Makefile選項下勾選 "use custom makefile"並設定建置腳本檔名(即 makefile)即可。

你可以試著修改vector.cpp檔案。按下Dev C++ compile 鍵,所有依賴vector.cpp的建置目標vectorTest.exe、linearAlgebraTest.exe、innerProduct.exe與linearTransform.exe將會自動被重新產生。相對的,matrixTest.exe則與vector.cpp無關而不會被重新產生。

最後,記得移除原先的五個專案檔 (即延伸檔名為.dev的案),T22即告完成。

(學習課題:make)

L步驟:雖然功能不多,但程式越來越複雜(專案多、檔案多)。我們努力的控制dependency,讓變動發生時受影響的範圍盡量縮小。我們也順利地重複使用vector及inner product函數。儘管如此,我們仍需要認真地檢視有無可改善之處。目前,我們的程式具備容易測的架構,讓我們來檢視單元測試的議題。

 1. 單元測試須獨立運作。但我們測試vectorToFile 時,使用了vectorFromFile。雖然vectorFromFile被測過,但並不能保證它完全正確。E W Dijkstra的關於測試的名言:

Program testing can be used to show the presence of bugs, but never to show their absence!

測試只能用來證明bug的存在,而不能證明bug的不存在!

2. 函數outputVector與outputMatrix(m)仍未被測試,事關輸出,如果真的在console輸出,我們的單元測試 Silent unless broken的特性豈不被破壞了?

3. vector::operator []() 與 matrix::at()都未有超限 (out of bound)測試,而其實作亦未偵測。

© Y C Cheng, 2013. All rights reserved.

2013年10月10日 星期四

Round 6: Enters matrix

目前為止,我的例子只有一個class,vector。接下來,我將讓例子裡有兩個class。我仍取材自簡單的線性代數運算。



問題P2:寫一個程式,提示使用者輸入兩個分別存放一個矩陣M與一個向量v的檔案的名稱,其中矩陣M代表一個線性轉換。請計算對向量v施以該轉換所得之向量u。

u= Mv

並將u輸出到第三個使用者指定名稱的檔案。提示使用者可重複執行或結束程式。

限制:你必須儘量使用為inner product所開發的vector類別及相關函數,並在必要時增加相關函數。

U步驟:

問題P2要求的檔案輸入輸出均有C++ library可供使用,且牽涉的計算不複雜,所以我暫時將不分解成數個更小的問題。當然,如同前5輪U-D-C-L,我們後續仍有可能提出改善的sub-problem。

你可能要問我,為什麼不在一開始(round 1)的時候就把問題P2一併提出來?你的疑問很合理,我的確可以在一開始就一併提出向量內積與線性轉換等二個問題。即便如此,我並不需要一次面對個不同的問題;因為這個問題彼此間的邊界(boundary)相當清楚,分開來解是合理的做法。同時,因為解線性轉換問題時須使用向量內積,故這個解題順序仍屬合理。

除此之外,現實世界中,客戶追加需求是相當常見的情形。追加的需求,當然需要站在既有的基礎上,正像前述問題限制我們必須儘量重複使用(reuse)為inner product所開發的vector類別及相關函數。問題P2中,線性轉換u = Mv的算法,u的分量為M相同位置的列向量(row vector)與v的內積。


D步驟:


我們需要針對matrix運算與測試、線性轉換主程式建立相關專案。

由於問題要求重複使用且我們也可能需要修改或新增inner product所開發的code,在專案的安排上,我們需要繼續讓
project inner product、 project vector test維持在開發中的狀態。雖然問題P2並未要求與 inner product有關係,由於vector可能有所變動,所以project inner product需要連帶被編譯、執行以確保其正確性。

列出工作如下:


T13 設計 vector在檔案中的儲存格式,編寫讀/寫檔案函數。
T14 測試vector讀/寫檔案函數。
T15 設計matrix在檔案中的儲存格式,編寫讀/寫檔案函數。
T16 測試matrix讀/寫檔案函數。
T17 編寫 matrix類別,含data members 與 member functions。
T18 測試matrix類別。
T19 編寫線性轉換函數。
T20 測試線性轉換函數。
T21 編寫main函數。

C步驟:
先做哪個task?基於round 5結果,我們有vector test專案,因此可以先顯選擇範圍限制在處理T13(先寫 vector讀寫檔函數)或T14(先寫 vector讀寫檔函數單元測試)。 

我採取 測試先行(test first)策略,先選擇T14。好處有二:

  • 當unit test code完成時,已初步完成vector讀寫檔函數的函數原型設計。
  • unit test執行無誤時,你知道已初步完成vector讀寫檔函數的函數的實作。

T14與T13完成後,編譯、執行inner product project,確認無誤。這個動作為inner product project進行迴歸測試(Regression testing)原有的inner product 執行已達預期狀況,但它使用的class vector與函數變動了,我們需重新加以確認。

(學習課題:ifstream, ofstream, fstream)

接著,完成matrix相關測試、類別與函數,仍採測試先行,依序執行T18、T17、T16、T15。在此之前,我們仿照先前vector的做法,建立matrix test專案。

下篇繼續。

© Y C Cheng, 2013. All rights reserved.

2013年10月3日 星期四

Round 5: Unify when you can; separate when you must

U步驟:第四輪結束時,所有class與function均存放在同一個file main.cpp,如下:

main函數是一個劇烈變動點,經常透過編輯code的方式主功能(production code)測試(test code)間切換。將此問題具體敘述如下:

SP5:分割程式碼,建立測試專案,與production專案分立。

D步驟:要解決SP5,必須要使test code與product code不共用 main function。具體的做法是自innerProduct專案拆出另一個測試專案,暫時稱為 vectorTest專案,並設法讓兩個專案共用 vector, inputVector, outputVector 與 innerProduct,示意如下:


建立待辦工作如下:
T11 分割test code、vector與相關函數
T12 建立test project, 修訂inner product project。

C步驟:

T11 分割test code、vector與相關函數

1. 將main.cc中vector與相關的函數搬到二個檔案,vector.h與vector.cpp,在main.cc中引用 vector.h。編譯,修正錯誤。

2. main.cc中test code搬移至另一個檔案,vectorTest.cpp
vectorTest.cpp中引用 vector.h,編譯,修正錯誤。

(學習課題:為class與函數分別建立 header file(宣告)與 implementation file(定義),亦即 interface in .h file, implementation in .cpp file)
(學習課題:#ifndef, #define, #endif的使用)

T12 建立test project。

1. 建立 test project:vectorTest.cpp code併入 main function所在的mainTest.cpp;設定CppUnitLite 路徑;在test project中加入 vector.h、 vector.cpp;編譯,修正錯誤。

2. 移除原 innerProduct專案之CppUnitLite 路徑設定,移除並刪除vectorTest.cpp。

L步驟:現在,你已擁有 a production project and a test project各一個!接下來將能更容易的為vector及相關函數做更多測試。再度檢視待辦工作,我們發現只剩SP2了。解或不解?請你自己決定。inner product問題,將暫告一個段落。





© Y C Cheng, 2013. All rights reserved.

2013年9月26日 星期四

Round 4: Forget main; unit testing is better

第三輪結束時,我們找到一個議題:讓test code脫離 main函數存在。按照round 3的做法,test code寄生在main之下,則innerProduct函數的test code可如下:




第四輪將分離這些test code。

U步驟:檢視test code,可了解做的是針對innerProduct函數的局部測試,分別為可計算內積與不可計算內積的兩種情形。我們採取的方式是將結果輸出到console,然後自行判讀執行結果是否符合期望。用同樣的方法做更多局部測試,將出現下面的現象:
  • 當我們做的局部測試更多時,console輸出將會越來越多,不易判讀結果是否符合期望。
  • 為了省寫些test code,你可能會重複使用某些變數,例如前列中的向量u出現在兩個測試裡。你必須很確定u第一次被使用後,仍然有原來的值,否則第二次測試的test code本身就很難說是對的了。
根據測試分類,我們所做的測試屬於單元測試(unit testing),每一個目的不同的測試則稱為一個test case,所以前述test code含兩個 test case。

執行一個單元測試的test case,具備下列四步驟:
  1. 建立測試用資料
  2. 呼叫待測函數,得到結果值
  3. 比對預期值與結果值
  4. 撤掉測試用資料
C++程式單元測試框架如CppUnit、CppUnitLite等提供對test case依上述四步驟自動執行機制必要的library(例如第三步驟的比對),並提供組織test case的具體方法。使用後,將能一舉解決這些問題。 我將使用簡單易用的CppUnitLite

SP4:以CppUnitLite重作test case。

SP2與SP4二選一,我們將先解SP4。


D步驟:將SP4分割成兩個task:

T9:安裝CppUnitLite。
T10:搬移(與新增)test cases。

C步驟
依序進行T9(參考資料)與T10。完成T10後,原先在main的test code改變如下:

...
#include "C:\Program Files (x86)\Dev-Cpp\MinGW64\include\cppunitlite\TestHarness.h"
...

int main(int argc, char** argv) {
TestResult tr;
TestRegistry::runAllTests(tr);
/* 
... what main should do here
*/
return 0;
}

TEST (computable, innerProduct){
  double a[2]={0,1};
  double b[2]={1,0};
  vector u(2,a);
  vector v(2,b);
  double prod;
  CHECK_EQUAL(true,innerProduct(prod,u,v));
  DOUBLES_EQUAL(0,prod,0.00001);
}

TEST (dimension_error, innerProduct){
  double a[2]={0,1};
  double c[3]={1,2,3};
  vector u(2,a);
  vector w(3,c);
  double prod;
  CHECK_EQUAL (true,innerProduct(prod,u,w))
}

main 中兩行紅色的code使用CppUnitLite中的API,執行所有test case(這裡只有兩個)。

蓄意讓第二個test case驗證失敗: 向量u的 dimension為2,向量w的 dimension為3,所以innerProduct還傳的實際值(return value)為false,但我將期望值設為true執行後,console畫面如下:



看畫面第一列,我們的得到驗證失敗的原因(Failure: "expected true but was false")、發生的地方(line 123 in main.cpp)。值得注意的是第一個test case驗證通過,故無任何訊息。最後總計失敗的次數,共計一次

比較原test code執行後console畫面,好壞立見:



console上每一列輸出,都需要靠你自己判讀是否正確。如果判定為有錯,你得自己尋找錯誤發生在哪裡。想像你跑了100個測試,這個判讀/尋找的任務看起來還挺辛苦且易錯的!

L步驟:OK,test code與main函數已分開再次以programmer的觀點進行回顧。現在,這個功能簡單的程式達到120 Lines的規模。在裡面進行修改、新增code,經常需滾動IDE編輯視窗,programmer犯錯的機率升高。此外,啟動unit testing的API仍然與佔據main函數,需與main該做的事互相切換。

© Y C Cheng, 2013. All rights reserved.

2013年9月21日 星期六

Round 3: All bundled -- vector as object

Round 2回顧中找到的議題向量及其維度應能確保一致。我們將以C++的class加以解決。進入第三輪。

U步驟:我們需要一個策略由於我們的目的是減少programmer犯錯的機會,因此採取策略:檢視program code,找出容易導致programmer犯錯的code。

先看一個程式片段:



上面main函數讀起來,可以說是以u這個變數表示向量;和向量u的定義、使用相關的敘述有 
  • line 51: int dim_u; (向量的維度) 
  • line 52: double * u; (double 指標,指到向量記憶體中的位置)
  • line 55: u = new double[dim_u];(配置記憶體,佔用連續的dim_u個double,u為這塊記憶體的beginning address) 
  • line 71: delete [] u; (釋放u佔用的記憶體)
  • line 36 (and other lines): p += u[i]*v[i];(取用第i個分量)

簡單以下圖表示它們之間的關係:



我們發現programmer需自行關聯double vector[]、int dim;兩者也與 double * 宣告、new 運算子、delete運算子、與index 運算子 [] 等有關係,這些關係都得靠你自己記住;C幫不上忙! 「C是個相對低階的語言」這種說法,果然不是隨便說說的。

好消息:C++可以幫上忙!C++的class可以讓我們把這些構成向量的變數、變數間的關係變數與運算子間的關係做一個統合

壞消息:你得學會如何將散落各處的資料、函數打包(好吧,這算不上是一個壞消息 :) 

列出新的子問題如下:

SP3: 統合向量相關變數、運算、函數,以物件表達,修改相關函數,使其接受並使用向量物件。

當此問題解決後,programmer使用物件時不需自行關聯獨立的變數。

現在未解的問題有SP3 及 SP2,讓我們先解 SP3。
D步驟:如何列出工作?

T7 宣告新的向量型態,決定它打包的資料函數
T8 修改相關函數以使用新的向量型態

更新待辦工作如下:



C步驟先做T7。將新的類別型態(class type)稱為vector,經過U步驟地分析得知,
  • vector類別型態打包兩項資料double []與int (學習課題:class, data member, member functions, private, public)。
  • 定義vector類別型態變數的建構元(constructor) (學習課題:constructor, initialization list, dynamic memory allocation, zeroing)
  • 回收被vector類別型態變數佔用的記憶體用的解構元(destructor) (學習課題:scope and lifetime of a variable, destructor, memory deallocation)
  • 向量分量取用函數(accessor function)(學習課題:operator overloading, return by reference) 
  • 向量維度的讀取函數(getter function) (學習課題:const member function)
括弧中為C++ 相關的學習課題,你可以找任何一本C++書或任何C++ web site,直接閱讀與學習課題相關的頁數/內容,直到你可了解下面的code即可:

class vector {
private:
double * _v;
int _dim;
public:
vector(int dim):_dim(dim){
_v = new double[_dim];
for (int i=0; i<_dim; ++i)
_v[i]=0;
}
~vector() {
delete [] _v;
}
double & operator [](int i) const {
return _v[i];
}
int dim() const {
return _dim;
}
};

畫張類別圖(class diagram)來表示vector打包的資料與函數:





有了 vector class 型態,接著處理T8。

T8的學習課題:model of a variable in memory, runtime memory model of a program, call by reference vs call by value, copy constructor, deep copy vs shallow copy。

原 main函數片段



可修改如下:

新程式line 70

    vector u(dim_u);

呼叫建構元,定義vector型態的物件u,它的維度為dim_u;line 70 取代原程式line 51, line 52及line 55。

新程式line 71

    inputVector(u);

取代舊程式line 56,

    inputVector(u, dim_u);

呼叫inputVector時只需傳入vector型態的物件u,而不傳入它的維度dim_u。

完成的程式請點連結下載。

L步驟:請檢查round 3的程式碼是否優於前一版,讓programmer犯錯的機會減少。

除此之外,讓我們再次以programmer的觀點進行回顧。這次,回想自round 1起,我們為求方便,每當完成一個工作時,(例如round 1中的innerProduct函數)經常借用main函數來測試這個工作完成的code。(例如,準備好兩個向量,將作為參數他們傳給innerProduct函數做計算,然後將結果輸出到console:見round 3程式)

/*
double a[2]={0,1};
double b[2]={1,0};
double c[3]={1,2,3};
vector u(2,a);
vector v(2,b);
vector w(3,c);

double prod;
if (innerProduct(prod,u,v))
cout << "inner product is " << prod << endl;
else
cout << "Dimension error!" << endl;

if (innerProduct(prod,u,w))
cout << "inner product is " << prod << endl;
else

cout << "Dimension error!" << endl;
*/

注意到這些code被註解掉了嗎?因為後來我們需要 main 函數做它該做的事!在HTSI的U-D-C-L迴授圈中的C步驟,這些測試碼 (test code)擔負著驗證工作是否完成的責任,但是它們卻「寄生」在main函數之下。

我們找到一個議題:讓test code脫離 main函數存在

© Y C Cheng, 2013. All rights reserved.