天天看點

《C++面向對象高效程式設計(第2版)》——1.1 背景

本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第1章,第1.1節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

c++面向對象高效程式設計(第2版)

程式員開發軟體的曆史已有數十年,他們使用各種不同的程式設計語言,如algol、cobol、lisp、c、pascal等,來實作各種規模的軟體開發——從簡單的小程式到複雜的大型系統。[我們把編寫諸如漢諾塔的解決方法、紙牌遊戲、簡單的快速排序等小程式,僅作為課程學習的作業。雖然這些小程式沒有任何商業價值,但能幫助我們了解新的概念和新的語言。相比之下,大型系統(如庫存控制、字處理、醫院患者管理、天氣預報、個人資金管理等軟體系統)涉及許多重大問題,這樣的軟體系統,需要由設計者和程式員組成的小組協同工作才能實作,然後由公司出售,賺取利潤。我們從設計和實作小程式中學到的知識和經驗,對解決大型問題會有很大幫助。] 日常生活中,我們使用的系統都由這些語言實作。在開發和使用它們的過程中,我們已獲得許多知識和經驗,為什麼還要學習新的程式設計範式?繼續閱讀以下内容,答案就其中。

如果給定一個問題(即問題的口頭或書面說明),如何使用某種語言(如c)設計和實作解決這個問題?首先,我們将該問題分解為多個便于處理的部分,這些部分稱為子產品。然後,設計出許多資料結構儲存資料,并實作一些函數(也稱為過程、例程)來操作這些資料。函數可用于修改資料結構,将它們儲存到檔案中,或列印資料。在面向過程程式設計系統中,解決問題的辦法就是将待解決之事轉換成一組函數。也就是說,我們把注意力都集中在函數身上,沒有函數就無法完成任何操作。這種以函數為主的程式設計方法稱為面向過程程式設計(procedure-oriented programming)。之是以稱為面向過程,是因為它的重點在過程。這種程式設計方法從函數的角度來思考問題,是以也稱為問題的功能分解。

注意:

在c和c++中,過程、函數、子程式和例程這些術語之間沒有差别。然而,在pascal、modula-2和eiffel中,函數是指傳回計算值的例程,而過程是指接收某些參數并執行一項操作,但不向主調函數傳回任何值的例程。在本書中,過程、函數和例程将互換使用,它們的含義相同。algol、fortran、pascal和c等程式設計語言都稱為過程語言(procedural language)。

然而,經過更深入地研究面向過程的實作發現,資料結構更重要。我們最感興趣的是儲存在資料結構中的值,而非過程本身。過程隻是修改資料結構的簡單工具,沒有資料結構,過程什麼也做不了。而且,在程式的運作過程中,資料結構中的資料不斷改變,而過程的代碼絲毫未變。從這個角度而言,過程是靜态的。我們之前費盡心思設計這些過程,殊不知重點根本不在這裡。舉個簡單的例子,假設一個銀行系統,允許客戶有不同類型的銀行賬戶(如存款賬戶、支票賬戶和貸款賬戶),允許客戶存款、取款以及在賬戶之間轉賬。如果該系統用c實作,可以看到以下一組過程1:

// 這是一個極其普通的銀行賬戶檔案

struct account {

      char name;  / 賬戶名 */

      accountnum accountid; 

      float  balance;

      float  interestytd; / 資料利息的年份/

      char  accounttype; / 存款,支票,貸款等/

        / 其他細節 /

};<code>`</code>

可通過下面的函數建立一個account結構的執行個體,為客戶建立一個賬戶。

<code>accountnum createnewaccount(const char name[], char typeofaccount);</code>

建立賬戶的函數将傳回新賬戶的賬号。我們在研究這種解決方案時發現,客戶将銀行賬戶視為他們血汗錢的安全天堂,他們感興趣的是賬戶中的資金和獲得的利息,而非存取款功能。事實上,客戶并不關心在銀行系統中存款或取款的過程如何實作,他們隻需要一種簡單友善的方法完成操作。但是,作為程式員,我們卻冥思苦想如何編寫makedeposit函數(以及其他函數),如何建立一個小型資料結構管理資料。換句話說,我們把注意力放在了客戶毫不關心的問題上。正因如此,我們設計出來的銀行系統導緻客戶與其銀行賬戶間毫無關系,客戶僅僅被看做是一系列的字元和數字。這樣的系統甚至根本無需考慮核對賬戶持有人和賬戶中的内容,就可直接操控資料來操作賬戶。從函數角度看,銀行賬戶僅僅是一串數字——賬号。

進一步分析可以注意到,任何人(或者其他程式或者程式員)都可以建立一個賬戶,并操作它。因為賬戶僅作為一段資料儲存,任何可以通路銀行賬戶檔案的人都可以修改它(甚至非法的)并取款。這是賬戶被當做一系列字元和整數的後果,儲存在客戶銀行賬戶中的值沒有任何保護措施。而且,也沒有任何條款規定銀行賬戶必須隻能由可信任的銀行職員修改,即使有,又由誰來執行?語言(如c或者pascal)不能做到這一點,因為它們并不知道銀行賬戶和普通整數的差別。

如果要列印客戶的賬戶,需要添加一個新函數:

<code>printaccount(account thisaccount);</code>

該函數将執行列印功能。但是,函數必須知道正在列印何種類型的賬戶(支票賬戶還是存款賬戶?)。這很容易,隻需檢視accounttype中的值即可。假設開始時我們有三種賬戶(支票賬戶、存款賬戶和貸款賬戶),printaccount()函數了解這些類型,它将對其中的代碼進行寫死(hard coded)。到目前為止,一切運作正常。現在再添加一個新的賬戶類型——retirement_account。如果傳遞retirement_account給printaccount函數會出現什麼情況?函數将無法正常工作。我們會看見以下的錯誤資訊:

<code>“unknown acount type- cannot print”(“未知賬戶類型-無法列印”)</code>

或者更糟:

<code>“illegal account type – call supervisor”(“非法賬戶類型-聯系主管”)</code>

之是以會出現這種情況,是因為在該系統中對賬戶的類型采用了寫死方式,除非修改源代碼,重新編譯并再次連結,否則無法通過編譯。是以,如果添加一個新賬戶類型,我們需要修改與該資訊有關的所有函數,并重新進行編譯—連結—測試過程。這些過程都十分冗長,而且極易出錯。怎麼會出現這樣的問題?答案是:函數和資料結構被當做是彼此脫節的實體,是以函數難以了解資料結構中的改動。也就是說,對現有的實作進行改進非常困難。這樣看來,我們隻能以另一種方式建立銀行系統,使得該系統在添加新賬戶類型不會影響其他賬戶類型,而且不會引起代碼的重新編譯。

以上談到的問題都源于在最初的解決方案中誤入歧途。我們将重點錯誤地放在自認為很重要的函數上,徹底忽略了對客戶(即銀行客戶)而言更為重要的資料。換言之,我們之前全神貫注于如何做,其實應該将重點放在做什麼上。這就是面向對象程式設計和面向過程程式設計的差別。

如果用面向對象程式設計(object-oriented programming)技術解決此銀行賬戶的問題,我們會把注意力放在銀行賬戶上。首先分析客戶想用這個賬戶進行什麼操作,對他們而言什麼最重要,諸如此類的問題。簡而言之,在面向對象程式設計中,重點是正在被操作的資料,而不是實作這些操作的過程。我們應該先找出使用者(該例中是銀行客戶)想用資料(即銀行賬戶)進行什麼操作,然後再為其提供必要的操作。而且,資料和操作不像以前那樣被當作彼此孤立的實體,現在它們被看做是一個整體。資料帶有一組操作,允許使用者對資料執行一些有意義的操作。同時,任何外部程式或過程無法直接通路資料本身。修改銀行賬戶内資料唯一的方法,就是使用為修改資料而提供的操作,這些操作為使用者提供修改銀行賬戶的行為。現在,我們可以說,銀行賬戶是一個類(class),我們可以建立任意數量的銀行賬戶執行個體,每個執行個體都是銀行賬戶類的對象(object)。是以,面向對象程式設計是一種由合作對象(cooperating object)(就是類的執行個體)構成程式的程式設計方法。而且,多個類之間可通過繼承關系互相關聯2 。

在面向對象程式設計中,關鍵要了解類和對象的概念。類是一個實體(entity),它擁有一組對象的共同屬性(特性)。對象是類的執行個體(instance),類的所有對象都具有相同的結構和行為。可以把類看做是切甜餅的工具,而甜餅就是切甜餅的工具所建立的執行個體。這個比喻可能有些粗淺,但類似于類建立對象的過程。切甜餅的工具決定了甜餅的大小和形狀(不是味道)。與此類似,類決定了建立對象的大小和行為。在面向對象的解決方案中,一切皆為類的對象3。這樣看來,在面向對象程式設計中,我們隻需考慮類和對象、類之間的關系,以及對象之間的關系4。shalaer-mellor 5 以另一種方式解釋了面向過程程式設計和oop(面向對象程式設計)之間的差別——功能分解按順序提出了系統設計中的三要素(算法、控制流和資料描述),而在oop中,此三要素的順序完全相反。

現在,重新回到銀行賬戶的問題上。我們重點關注的是銀行賬号,将它設計成一個類。在c++中,bankaccount類的架構如下所示。這裡再次提醒讀者,請不要過多擔心c++的文法。

 // bankaccount類其中一項操作的實作

void bankaccount::makedeposit(float amount)

{

  if (amount &gt; 0.0)

    balance = balance + amount;

}<code>`</code>

隻有在bankaccount類内部聲明的操作,才能通路私有成員balance(和其他私有成員)。類的外部無法使用私有成員。我們将這種私有資料稱為被封裝的資料,以這種方式隐藏在類的内部稱為資料封裝(data encapsulation)。我們将在第2章中詳細介紹資料封裝。

《C++面向對象高效程式設計(第2版)》——1.1 背景

1無需過多關注文法,隻需注意過程和參數的概念。不同的語言都通過不同的文法進行聲明。

2繼承将在第5章中詳細介紹。

3在c++中,如果将main()程式看做是根對象(root object),的确可以說一切皆為類對象。

4在某些語言中,對象可以建立其他類。

5譯者注:shalaer-mellor軟體設計方法,由sally shlaer和stephen mellor共同研發,是衆多面向對象分析(ooa)/ 面向對象設計(ood)方法之一。

繼續閱讀