本文適合初級讀者
Chuck Allison 是鹽湖城聖 Latter Day 教堂總部下耶稣教堂家族歷史研究處的軟件體系設計師。他擁有數學學士和數學碩士學位。他從1975年起開始編程,從1984年起他開始從事c語言的教學和開發。他目前的興趣是面向對象的技術及其教育。他是X3J16,ANSI C ++標准化委員會的一員。發送e-mail 到 [email protected],或者撥打電話到 (801)240-4510 均可以與他取得聯系。
在上個月的封裝中我提出了一個簡單的C++日期類的雛形。為了提供一個能夠計算兩個日期的間隔的函數,這個類舉例說明了C++的下列特征:
內聯函數
引用
構造函數
對私有數據成員的訪問控制
在這個月的部分裡我將增加相關的運算符、輸入/輸出操作和得到當前日期的能力。它們示范了下列特征:
運算符重載
流
友元函數
靜態成員
當使用日期的時候你經常需要確定某一日期是否在另一日期之前。我將為日期類增加下面這個成員函數(參見 Listing 1):
int compare(const Date& d2) const;
Date::compare 類似於strcmp-如果當前對象(*this)在d2之前,它返回一個負整數;如果這兩個日期相同,則返回0;否則返回一個正整數(參見 Listing 2 中的函數實現和 Listing 3 中的示例程序)。就像你們都很熟悉的C標准庫中的qsort一樣,你也可以使用Date::compare來對日期進行排序,就好像你使用strcmp對字符串進行排序一樣。下面是一個可傳遞給qsort的比較函數(下個月的代碼封裝將包括qsort):#include "date.h"
int datecmp(const void *p1, const void *p2)
{
const Date
*d1p = (const Date *) p1,
*d2p = (const Date *) p2;
return d1p->compare(*d2p);
}
運算符重載
大多數時候,擁有相關的運算符是更方便的,例如:
if (d1 < d2)
// do something appropriate..
使用Date::compare來添加一個"小於"運算符是非常容易的--只要在類的定義裡插入下面這個內聯成員函數就可以了:
int operator<(const Date& d2) const
{return compare(d2) < 0};
每一個表達式:d1 < d2出現的地方,都會被編譯器翻譯成函數調用的形式:
d1.operator<(d2)
Listing 4 中類的定義中擁有六個相關的操作符,Listing 5中展示了更新之後的示范程序。 既然函數Date::interval 的功能類似減法(它給出兩個日期的差),把它重命名為Date::operator-就是件很自然的事情了。在做這個事情之前,我們仔細研究一下下列語句的語音:
a = b - c;
無論變量是什麼類型,下述語句總是成立的: a 是一個由減法產生的明確的對象,並且 b - c == - (c - b) 我們使用下列約定俗成的習慣,即一個正的日期對象的所有數據成員都是正的,反之亦然(不允許符號的混合)。在 Listing 7 中我用 Date::operator- (const Date&)代替了Date::interval,前者為每一個數據成員增加了正確的符號並且返回重新構造過的類的對象。
Listing 6 中重新定義的類中還包括了一個一元的"-"運算符函數,它的名字還是 Date::operator-,但是沒有任何參數。編譯器將把下列的語句
1 - d2;
-d1;
分別替換為:
d1.operator-(d2); // Calls Date::operator-(const Date&)
d1.operator-(); // Calls Date::operator-()
Listing 8 中有一個使用了新的成員函數的簡單示例程序。
輸入輸出流
正如我以前所說的一樣,一個日期類的對象應該具有和系統內建類型一致的外觀和感覺--輸入/輸出支持。C++提供了能夠處理標准類型的的輸入輸出操作的流的對象。例如下列程序 :
#include <iostream.h>
main()
{
int i;
cout << "Enter an integer: ";
cin >> i;
cout << "You typed " << i << endl;
return 0;
}
的輸出結果為:
Enter an integer: 5
You typed 5
cout 是 C++ 流庫中提供的 output 流(類ostreom)而cin是C++流庫中提供的input流(類istreom),它們分別與標准輸出和標准輸入相關。當編譯器看到下面的表達式:
cout << "Enter an integer: "
它將用如下語句代替:
cout.operator<<("Enter an integer: ")
上述語句調用了成員函數ostream::operator<<(const char *)。同樣的,表達式
cout << i
調用了函數
ostream::-operator<<(int)。
endl 是一個特殊的流指示,它輸出一個換行符並清空輸出緩沖區。Output行可以連在一起:
cout << "You typed " << i
因為 ostream::operator<< 返回一個到 stream 自身的引用。上述語句變成了
(cout.operator<<("You typed ")).operator<<(i)
為了適應 Date 對象的輸出,你需要一個全局函數,該函數將要輸出的內容發送給一個給定的輸出流,並且返回到那個流的引用:ostream& operator<<(ostream& os, const Date& d)
{
os << d.get_month() << ''/''
<< d.get_day() << ''/''
<< d.get_year();
return os;
}
這當然不能是一個成員函數,因為流(並非正在被輸出的對象)總是出現在流插入符號的左邊。
友元
為了提高效率,通常會賦予 operator<< 進入到一個對象的私有數據成員的權限(大多數的類的實現都提供了相關的I/O操作符,因此在這種情況下打破封裝的邊界似乎是比較安全的)。為了穿破對私有數據成員訪問的限制,你需要在類的聲明中加入如下語句,把operator<< 聲明為 Date 的友元:
friend ostream& operator<<(ostream&, const Date&);
Listing 9中展示了新的類的聲明,並且包括了輸入函數operator>>的聲明。Listing 10中展示了這些函數的實現,Listing 11中有一個簡單的示例程序。
靜態成員
C++中的類定義了一個作用域。這就是為什麼函數Date::compare不會和一個叫做compare的全局函數發生沖突的原因(即使它們參數和返回值的類型都相同)。現在考慮實現文件中的一個數組dtab[]。dtab的靜態存儲類型使它對文件來說是private的。但是它實際上屬於整個類,而不是這個文件。如果我想要傳遞Date的成員函數到多個文件中,我不得不將需要訪問dtab的函數傳遞到同一個文件中。一個更好的辦法是使dtab成為類的靜態成員。靜態成員屬於整個類,而不是一個單獨的對象。這意味著只有一個dtab的拷貝存在,它被所有類的對象所共享。使函數isleap成為static則允許你不需要和一個對象相關就能調用它,比如,你只需要這樣寫:isleap(y);
而不需要這樣寫:
d.isleap(y);
要想使isleap對任何調用者都可用,使它為 public,用如下方式調用:
Date::isleap(y);
最後,我將重新定義缺省構造函數,用當前日期初始化類的對象。最後的類的定義、實現和示例程序分別參見 Listing 12 - Listing 14。
總結
在上面兩個部分中,我試圖說明C++是如何支持數據抽象--使用者的產物--自定義的數據類型。構造函數使得當你聲明一個對象的時候能夠自動對它進行初始化。你可以通過聲明類的成員為private來保護它們不受到無意中的訪問。重載公用的運算符可以使得你的對象看起來跟系統內建的數據類型很相似--這增加了可讀性和可維護性。