“呵呵,你總結(jié)的很不錯啊。”老C稱贊道,“這樣你就知道自己的road map了,學(xué)習(xí)起來就會更有一些提綱挈領(lǐng)的感覺。”
“那是那是,我的學(xué)習(xí)那個還是不錯的……”小P審慎的謙虛道。
“嗯……通過這幅圖你應(yīng)當(dāng)可以明白UML中動態(tài)模型和靜態(tài)模型的概念上的區(qū)分啦。”老C補充道,“嗯……你的領(lǐng)悟能力出乎我的意料啊……”
“……”小P囧,不知道這是表揚還是批評,“呵呵,我還總結(jié)了我接觸過的UML概念,分為形而上和形而下的對偶。”說著小P又打開了一個文檔。
形而上
|
形而下
|
Class
|
Object
|
Association
|
Link
|
Operation
|
Method
|
“哦……”老C有些驚訝了,“你總結(jié)的有些道理。雖然現(xiàn)在你可能無法分辨Operation 和 Method 的具體區(qū)別,但是在腦海中保持這種概念上的區(qū)別還是很重要的。”他稱贊道,“照這樣的速度發(fā)展下去,過不了一兩年,你就可以教我了……”
“呵呵呵呵,你太謙虛了……”小P有些得意,“我還有很多東西需要你多和我聊聊呢。”
“哈哈,那你可要有所表示哩……”老C打趣。
“……好!為了迎接新的學(xué)習(xí)生活,我決定……在南門請你再吃一回刀削面……大碗也可以噢。”小P痛下決心。
“……寥勝于無……”老C郁悶道,“也行……不過還要再加一瓶冰峰……”
“呵呵,好的好的。”小P很痛快的答應(yīng)。
“嗯……請5天,時間我定。”老C得寸進尺,“包括中午和晚上。”
“……”小P扳著指頭算了算,“可以!不過不能在一個星期內(nèi)連續(xù)吃5次……”
“……誰會連續(xù)5天全部吃刀削面啊!”老C囧。
“呵呵,呵呵,我只是提醒……提醒一下而已。”小P笑道。
“嗯,趁著我心情大好,再給你講講線性鏈表的一些習(xí)慣用法和常用的設(shè)計吧。”老C道,“反正離吃飯還有一段時間。”
“噢!好啊。”小P道,然后很自覺的從角落中拉出白板。
“嗯……”老C覺得這個孩子還是很有前途的,“一個基本問題,就你所知,什么是線性表?”
“哦,叫我想想。”小P眨眨眼睛,“好像和遍歷這些數(shù)據(jù)結(jié)構(gòu)花費的時間有關(guān)系,如果我遍歷一遍這些數(shù)據(jù)結(jié)構(gòu)的所有元素所花費的時間是元素個數(shù)的線性函數(shù),
那么這個數(shù)據(jù)結(jié)構(gòu)就是線性表,哦……時間復(fù)雜度就是n啦。”小P又想想,“好像就是這樣,我所接觸的線性表包括array, linked list,
stack, queues,而queues可能有各種奇怪的queue,比如循環(huán)的,優(yōu)先級的什么的……”
“你的記憶力不錯啊。”老C稱贊道,“嗯,基本上線性表就是這么回事啦。”他點點頭,“我再來問一個貌似題外話的問題,你知道在C語言中,有哪4類指針嗎?”
“槑……”小P搖頭,飛快。
“在C語言中,有4種指針,分別是一般的指針,空指針、0指針和past the last one指針。”老C道。
“槑……”小P道,“一般的指針就是指向數(shù)據(jù)和函數(shù)的指針吧,空指針應(yīng)當(dāng)就是void*,0指針應(yīng)當(dāng)就是無法dereference的那種指針,那么什么是past the last one指針?”他不解的問。
“很簡單,我們舉例說明吧。”老C說著在白板上寫下幾行代碼。
const int MAX_SIZE = 10;
int main()
{
int array[MAX_SIZE];
for (int* p = &array[0]; p != &array[MAX_SIZE]; ++p)
{
*p = 0;
}
return 0;
}
“看,”老C指著代碼,“在這里我們將數(shù)組中所有的元素初始化為0,但是在這里我們使用了一種看似更間接的方法,我們沒有使用類似array[i]這種下
標(biāo)的形式,而是使用了指針。”他用筆在代碼下面標(biāo)出著重符號,“請注意這句, p !=
&array[MAX_SIZE],在這里就表示那個past the last
one的指針。”看到小P還是沒有反應(yīng),老C接著解釋,“其實&array[MAX_SIZE]指針是我們之前沒有定義的,因為其實
array[MAX_SIZE]已經(jīng)越過了array數(shù)組的界限。對于一個越過界限的元素,取它的地址,按道理來說應(yīng)當(dāng)是沒有意義的,但是為了我們可以方
便的表示一個數(shù)組的結(jié)束,C語言特地為我們提供了一定的方便——C語言規(guī)定這個對越過數(shù)組界限一個元素取地址是合法的,而且它就是數(shù)組最后一個元素地址的
下一個地址。當(dāng)然,對這個指針dereference和對其指向的內(nèi)容賦值,都是沒有意義的行為——它是且僅僅是被用來作為一個結(jié)束的標(biāo)識符而已。”他擦
了擦頭上的汗,“總之你就認(rèn)為past the last
one指針就是表示一個數(shù)組結(jié)束用的,對其賦值和解引用是沒有意義的。這個指針的出現(xiàn)完全是為了人們使用的方便而已。”
“唔,就是說C語言幫我們一個忙,就是了?”小P問,“那么和使用下標(biāo)的方法比,這樣有什么好處啊?”
“嗯,使得我們減少一些記憶力上的負(fù)擔(dān),如果你按照某種格式書寫代碼,那么就不用擔(dān)心 ‘過一’
問題。”老C說道。“比如在幼兒園,一個老師問,孩子們,我們現(xiàn)在有幾個小板凳啊?一個孩子數(shù)道,0,1,2,3,4,他回答,我們有5個小板凳!那么他
就是天才并且使用了C編程界在線性表中采用的習(xí)慣用法——使用past the last one指針來表示線性表的結(jié)束!”
“哈哈,”小P笑道,“我還沒有見過哪個孩子這樣數(shù)的。”
“總之就是一種很好的方法幫我們解決過一問題的。”老C說道,“因為C語言的數(shù)組采用以0開始的計數(shù)方式,這樣我們在進行指針運算的時候會產(chǎn)生某種便利
性,比如讓你有某兩個指向array元素的指針,比如p1和p2,且p1 <=
p2,那么計算p1和p2之間的元素個數(shù),很簡單的p2-p1就可以了。這只是一個例子,還有很多其它類似的指針運算,也會顯得很方便。但是這樣又會造成
過一問題,因為一般人在計數(shù)的時候很難適應(yīng)從0開始計數(shù)——在簡化了指針運算的同時,又引入了過一問題——反正世界上沒有完美的東東。”
“哦?那么引入這兒past the last one指針會給我們帶來什么便利呢?”小P問。
“呵呵,這樣我們可以以統(tǒng)一的方式來表示數(shù)組的結(jié)束。”老C說,“這樣大家在編碼時在計算數(shù)組是否應(yīng)當(dāng)已經(jīng)遍歷結(jié)束的時候,智力負(fù)擔(dān)會小很多。”他想想,在白板上寫下一個數(shù)學(xué)表達式。
[first, last)
‘“看,這樣我們就可以用一個左閉右開的區(qū)間表示我們的數(shù)組啦。”老C說道,“對于這個區(qū)間,你可以很容易的用last -
first來計算出區(qū)間中元素的個數(shù);如果你知道了區(qū)間中元素的個數(shù)——比如說SIZE——那么可以很簡單的用 first + SIZE =
last
的公式來計算last數(shù)據(jù)的地址,從而知道了如果在遍歷這個數(shù)組的時候,應(yīng)當(dāng)在那里結(jié)束。”他想了想,又說道,“這樣我們遍歷數(shù)據(jù)結(jié)構(gòu)的代碼可以統(tǒng)一成為
如下的形式。”說罷又在白板上寫下如下代碼。
for (iter = first; iter != last; ++iter)
{
}
“如果我們在代碼中堅持使用這種遍歷方式和左閉右開的區(qū)間表示方法,那么數(shù)組的過一問題就會遠遠的離開我們,而且不會再回來……”老C說道,“而且一般我
們在聲明的時候總是知道數(shù)組的大小的,比如MAX_SIZE,這樣我就很方便的知道了array[MAX_SIZE]就是我定義的數(shù)組區(qū)間的結(jié)束標(biāo)識。”
“嗯……”小P道,“好像真的有些用處……但是為什么for()循環(huán)中要用 != 來表示數(shù)組沒有結(jié)束而不用 < 來表示呢?”他問道。
“因為統(tǒng)一……”老C解釋,“線性表的表示方法不只有使用數(shù)組一種,我們還可以使用linked list,雖然在linked
list中,每個節(jié)點表示的邏輯意義是線性的,但是它們的地址并不是線性的,不能說前面的節(jié)點的地址恰好比后一個節(jié)點的小。我們無法比較兩個指針的大小,
只能比較它們是否相等。”說罷他又在白板上寫下另一行代碼。
for (Node* iter = firstNodeAddr; iter != lastNodeAddr; iter = iter->next)
{
}
“看,這樣在如何表示線性表遍歷結(jié)束方面,兩者保持了適度的統(tǒng)一。”老C說道,“而統(tǒng)一是我們編程人員最喜歡的,這樣即有利于減少編碼者的智力負(fù)擔(dān),又有利于維護者對代碼的閱讀。”他又補充,“總之是一種行話,約定俗成的習(xí)慣。”
“哦,我突然想起來了,我們在實現(xiàn)linked list的時候,也是采用[first, last)的方法的。”小P道,“這樣說的話我倒是馬上明白為什么要將list的head作為表示這個雙向循環(huán)鏈表的結(jié)束了。”
“沒錯!”老C說道,“你可以再回頭看看我們對linked list的實現(xiàn),體會一下[first,
last)區(qū)間表示線性表的好處。”老C接著說道,“因為我們有了基本統(tǒng)一的表示方法,通過運算符重載這一利器,可以將對節(jié)點的操作從數(shù)據(jù)結(jié)構(gòu)中提取出來
——你不是在寫linked
list的時候嫌index這個東東比較煩人嗎——那么我們就可以將這種index操作從線性表中提取出來,從而簡化我們對數(shù)據(jù)結(jié)構(gòu)的操作。”
“哦?怎么運用運算符的重載來簡化這種操作呢?”小P問道。
“一個簡單的例子,如果我們認(rèn)為iter是一個類的,那么我們可以重載iter類的++運算符,從而使得對數(shù)組的遍歷與對linked list的遍歷統(tǒng)一起來。”老C說道,然后在白板上寫下一個示例代碼。
for (Iterator iter = first; iter != last; ++iter)
{
...
}
++iter其實實現(xiàn) iter = iter->next的動作
“看,這樣我們就可以保持代碼形式上的統(tǒng)一。”老C說道,“如果代碼在形式上統(tǒng)一了,那么我們就很容易會想到用一個基類來表示統(tǒng)一的操作,而用子類和虛函
數(shù)對不同的操作進行區(qū)分。”他揉揉手,“這就是有名的iterator模式,我想我們可以從一個最簡單的對數(shù)組遍歷的例子引申開……”
“是么是么?”小P好奇的問,“那么應(yīng)當(dāng)怎么做呢?”
“嗯……那要看今天的面香不香……”老C打岔。
“囧……那我們?nèi)タ纯窗桑绻阌X得勉強還可以,那就回來接著說吧。”小P道。
“是啊是啊,吃飯不積極,思想有問題。我們的思想可沒有什么問題,走啦走啊啦。”老C到隔壁叫了其它人,大家一起浩浩蕩蕩殺出門外。
(老C會怎么樣解釋iterator模式呢)