要閱讀本文,你要熟悉C++,熟悉類模板和函數模板。本文匯集了大量有關的信息,指引你逐步閱讀。
本文用提問,設計和解決方案引導閱讀。希望你能喜歡。
問題提出:
有二篇文章都含有許多行文字。我們要建立一個程序來找出二者之間的不同之處並將這些不同內容的行顯示出來。程序必須做成可重復使用的組件,就是說,這個組件能夠未經修改地被其他程序使用。
設計:
假設這二個文件非常之大(每個文件都有數千行),我們這樣設計有關解決方案:
將各個文件讀進內存塊,
在內存塊中進行文件內容比較,
將不同之處放進一個新的第三個內存塊。
設計方案還要考慮到各個文件的元素位置可能不同,亦即相同的元素不一定在同一行裡。這意味著,必須在內存中遍歷搜索不相同的術語並將其存放在第三個內存塊中。
考慮到程序的可重用性,我們用類屬編程技術來設計,讓方案能夠適應於存儲介質的變化。
當文件很大時(每個文件有數千行),那麼要把每個文件都存儲進內存可能是不現實的。另外也給執行過程帶來困難。
執行細節:
可以用容器來設計,比如用數組或隊列,將字符數組存儲到容器中。不過這會使得程序的可讀性降低並導致組件的可重用性下降。
本文的解決方案用標准模板庫(Standard Template Library, STL)的容器來管理內存塊。並且用STL的元素來管理將文件讀進內存塊。這樣的設計方案使得程序具有模板容器級水平的可讀性。
為達到互用水准的目的,就要使用C++的類模板和函數模板技術來實現。如果你不熟悉這些模板或要復習一下,可參看文末的鏈接。
方案與指南
你寫的程序是給二部分人看的:最終用戶和程序開發人員。寫給程序員是因為有人可能對你的程序作某些更改。他們必須花時間來理解你的程序。也可能就是你自己在以後的時間裡要對程序作出修改 - 改善它的可讀性而不降低運行效率,或者增加一系列注釋。
舉例來說,讓我們看一下主函數main():
int main(int argc, char* argv[])
{
// 確認得到正確的參數數量
if(argc!=3)
{
cout << "compareFiles - copyright (c) Essam Ahmed 2000" << endl << endl;
cout << "This program compares the conents of two files and prints" << endl
<< "the differences between the files to the screen" << endl << endl;
cout << "Usage: compareFiles <file_name_1> <file_name_2>" << endl << endl;
return 1;
}
// 聲明要使用的容器
typedef vector<string> stringSet;
stringSet s1, s2,s3;
// 將第一篇文章讀進集合
populate_set_from_file(s1,argv[1]);
cout << "Contents of Set 1" << endl << endl;
for_each(s1.begin(),s1.end(),printElement);
// 將第二篇文章讀進集合
populate_set_from_file(s2,argv[2]);
cout << endl << "Contents of Set 2" << endl << endl;
for_each(s2.begin(),s2.end(),printElement);
/// 比較集合,將不同之處存放到s3
Container_Differences< stringSet,string > (s1,s2,s3);
// 顯示結果
cout << endl << "Difference is:" << endl;
for_each(s3.begin(),s3.end(),printElement);
return 0;
}
這裡不過多論述如何讀文件和比較文件內容,這些都是封裝的工作。這裡關心的是函數扮演的角色。在本例中,main()函數扮演發報機的角色,而由其他函數執行真正的工作。
可以看到函數的功能,比如populate_set_from_file()和Container_Differences()函數執行大多數核心工作。for_each()函數則是STL的運算規則。
main()函數的精華在於:
typedef vector<string> stringSet;
它定義了一個向量的容器類型,用於存儲字符串對象。如果不熟悉什麼是向量,可參考文末鏈接有關於向量的指南。字符串集(stringSet)對象是STL數據類型,其中封裝了各個字符串。類型定義typedef使它成為可重復使用的數據類型並使得代碼可讀性很強。
stringSet s1, s2,s3;
聲明了3個容器,指向所含的字符串集合。前2個包含各個輸入的文件內容,後面一個則存放不同的字符串。當然變量名應該描述得更正規些。
populate_set_from_file()函數將文件內容讀進容器。它是個函數模板,可以使用不同類型的參數。它的構成如下:
template<class T>
bool populate_set_from_file(T &s1,const char *file_name)
{
ifstream file_in;
string line_from_file;
file_in.open(file_name);
if(file_in.fail()){
cout << "Error opening file ["
<< file_name << "] - please check file name" << endl;
return false;
}
try{
getline(file_in,line_from_file);
while(file_in.good())
{
addElementToSet(s1,line_from_file);
getline(file_in,line_from_file);
}
}
catch(bad_alloc &e)
{
cout << "Error - Caught Exception: " << e.what() << endl;
throw e;
return false;
}
file_in.close();
return true;
}
這是一個函數模板,它將文件逐行讀進它定義的容器類型裡。函數打開給定的文件,逐行閱讀(回車換行符結尾)並加入到容器(容器可以是模板支持的任何類型)中去。用addElementToSet函數將每行文件加入到容器,這也是個函數模板。
用STL的文件流對象(ifstream)來讀取文件。ifstream支持基本的文件I/O和出錯處理。當文件操作失敗時,它的fail()成員函數返回真(true)。文件全部正常讀取完畢後,成員函數good()返回真。
getline()是STL函數,讀取文件中的每一行字符直至讀到行結束符(行結束符不讀進字符串)。它的參數是源文件流和字符串對象。要注意,它在讀取行字符串時不過濾頭尾的空格字符。
其它是出錯處理過程 - 雖然不是很理想的方式,但本例還是用它。當line_from_file對象中的字符串過長時將拋出bad_alloc出錯信息。
函數的文件名參數file_name是常量(const)參數,表示該參數為只讀,不被修改。使用常量參數讓編譯器產生一個只讀的快速內存映象並使應用程序變得更小些。
addElementToSet也是一個模板函數。容器的使用有時顯得很復雜。有些容器用insert()方法來添加成員,另一些容器卻用push_back()[譯者注:容器的種類很多,隊列(list)用前者而堆棧(stack)用後者]。更為復雜的是映象(map),它用insert()增加成員,帶入的參數卻是pair<>。雖然可以重載容器的函數,但我選擇使用模板。這樣可以更為靈活,甚至可以用於新的或未知的容器。
addElementToSet函數代碼如下:
template<class C,class V >
void addElementToSet(C &c, const V &v)
{
c.insert(v);
}
模板的容器類是C,傳遞的參數是V(V被聲明為常量參數,是只讀的。記住,一個V的拷貝被加入到C)。用insert()函數將V加入到C。這對於支持insert()方法的容器是很方便的,但對其他一些容器就有問題了。
對於這樣的容器,比如向量(vector)使用push_back()來添加成員,模板要進行特例化處理。C++模板支持類屬理念,但類屬執行時仍將優化成某種特定類型。模板的特例化與重載類似。
下面代碼將addElementToSet特例化為向量(vector):
template<> void addElementToSet<vector<string>,string>
(vector<string> &c, const string &v) {
c.push_back(v);
}
注意在"template"關鍵字的後面是一對空的尖括號,這樣聲明了一個類屬的特例化。可以聲明任意多個特例化。
Container_Differences函數模板
在把文件讀進容器之後,就要用Container_Differences函數來進行比較。
這也是用模板寫成的函數,可以用於其他應用。它調用addElementToSet函數模板往容器裡增加不相同的字符串。函數雖然不使用返回值,但容器的內容一直在發生變化。最後,如果容器裡沒有成員,意味著比較的文件是相同的。下面是Container_Differences函數代碼:
template<class container_type,class value_type>
void Container_Differences(const container_type &container1,
const container_type &container2,
container_type &result_grp)
{
container_type temp;
container_type::const_iterator iter_pos_grp, iter_found_at;
if(&container1 != &container2)
{
iter_pos_grp=container1.begin();
while(iter_pos_grp!=container1.end())
{
iter_found_at=find(container2.begin(),
container2.end(),
(*iter_pos_grp));
if(iter_found_at==container2.end())
addElementToSet(temp,
static_cast<value_type>((*iter_pos_grp)));
++iter_pos_grp;
}
iter_pos_grp=container2.begin();
while(iter_pos_grp!=container2.end())
{
iter_found_at=find(container1.begin(),
container1.end(),
(*iter_pos_grp));
if(iter_found_at==container1.end())
addElementToSet(temp,
static_cast<value_type>((*iter_pos_grp)));
++iter_pos_grp;
}
}
temp.swap(result_grp);
}
可以看到文件比較過程是相當簡單的,這是設計出發點。函數只作一件事,而且要做好。
函數在對每個源文件容器的搜索循環裡反復調用begin()和end()函數。end()函數在檢測到零(null)字符(C字符串的結尾)時結束。用STL的find()函數尋找相同字符串,如果沒有找到,說明存在著不同的字符串,就返回end()並將字符串加入到結果容器。
函數的最後一行用swap()函數將臨時容器的內容拷貝到引用參數的結果容器,並釋放臨時容器。
仔細看一下,可以看到迭代器用static_case<>指向值的類型,因為編譯器有時無法處理addElementToSet()所需的數據類型。另外使用static_case<>能使代碼看得更清楚些。
函數的參數中,前二者是常量(const)參數,最後一個是非常量參數,用於寫入結果。這樣可以使程序占用的內存較少。
模板支持的不同容器類型
上面的代碼可以支持這些容器類型:
隊列(list)
集合(set)
向量(vector)
只要在主函數main()裡作一次改動就能輕易地改變使用的容器類型。如果你要將集合類型改為向量類型,將:
typedef set<string> stringSet;
改成:
typedef vector<string> stringSet;
就行了。
當然要重新編譯一下(要確保包含文件中有所需要的容器類型)。
還可以對addEmenetToSet()函數模板進行特例化來支持其他類型的容器,比如映象(map)。只要使用的容器支持迭代操作就能用於這段代碼。如果要在你的應用裡使用Container_Differences函數,要先對函數addEmenetToSet()作類屬特例化處理。
結語
本文涉及內容很多。最主要的是了解如何使用C++模板來創建STL的類屬元素。我們還介紹了如何將應用分拆到幾個專項函數中去,每個函數只作一件事並作得很好。從而使得整個執行過程顯得簡單並易於理解和維護。
這裡所做的工作都是為了建立柔性應用系統,充分利用現有元素的優點,減少設計、開發和測試時間。建議閱讀其他有關STL,C++模板及C++語言特征的資料,將你的應用程序改變得更有生命力。
代碼
文末可供下載(http://www.designs2solutions.com/articles/dev/tcd/d2s_fc.zip)的代碼文件適用於VC++ 6.0。裡面還有一個可執行文件可以直接使用。我還包含了二段隨機語句的文件供作比較。