程式: 用一系列指令描述如何做一件事的原語。
庫: 擁有特定功能的程式碼的集合。
型別: 定義了一組可能的值與一組運算(對於一個物件)。
物件: 用來儲存指定型別值的記憶體單元。
值: 根據一個型別來解釋的記憶體中的一組位元。
變數: 一個擁有名字的物件。
宣告: 爲一個物件命名的語句。
定義: 是一種宣告,但同時也分配了記憶體單元。
每一種內建型別的實體大小都一樣,(例如每個int都有固定大小)記憶體中的位元的含義完全取決於存取它的所用的型別,計算機記憶體不知道具體的含義,只負責儲存,當我們決定一塊記憶體如何解釋時,它們纔會有意義,就像12.5的含義是什麼?它可以是12.5米,12.5升一樣,只有擁有單位纔有其含義
有一種觀點認爲,程式就是以計算爲目的,即程式都要有輸入和輸出,同時把能夠執行程式的硬體統稱爲計算機。
廣義上的輸入和輸出實質是數據進入和離開計算機,也可以引申到程式之間的數據傳遞。一個完整的程式通常含幾個子程式,子程式的輸入通常稱爲參數,輸出稱爲結果。
計算就是基於輸入生產生成輸出的過程。
爲了處理輸入,程式需要包含一些數據,這些數據有時被稱爲數據結構或狀態。
從程式設計的角度來看,最重要的兩類輸入和輸出是
下圖展示了在共同作業完成一個大程式時,應該如何合理的設計結構程式,並保證每一個子程式之間都能共用的交換數據。
目標和工具
程式的組織體現了程式設計師的程式設計思路,目前有兩種手段。
在考慮細分一個問題的時候,首先要考慮手裏有哪些明確的工具可以用來表示各個子程式及其之間的關係,這就是爲什麼一個提供充分介面和功能的庫能大大簡化程式劃分的困難(不同的庫可以實現相應的功能)。憑空想象出程式劃分是不切實際的,按照功能劃分一個程式的結構是很好的方法。一個好的程式需要利用已有的各種庫從而可以簡化程式的工作量並使得效率最大化。抽象方法的一個例子:iostream庫提供了使用者與計算機的I/O操作,使用者無需理解實現細節只需呼叫特定功能的介面即可。
僅通過編寫大量的語句是寫不出好程式的,唯一的方法就是閱讀大量的優質程式碼加以模仿和改進,要注意程式的組織結構。初學者常犯的錯誤是:不仔細分析問題,構建程式的結構,而是想當然的寫程式,最後扎進技術細節中,最後不得不放棄。軟體結構是在開發過程中不可忽視的過程,缺少軟體結構的程式猶如沒有一座沒有打好地基和使用鋼筋混凝土的土坯房,想要維護和擴充套件需要付出高昂的代價,到最後甚至需要回過頭重新構建。
表達式
表達式是組成程式的最基本元素,表達式就是從一些運算元(物件)中計算出一個值,最簡單的表達式是字面值常數,變數名也是一種表達式,是物件識別符號。
物件在賦值運算子左邊被稱爲左值,在右邊被稱爲右值。
常數表達式
除了個別情況,不要使用魔術常數(不能直接識別出其含義的值),應該儘可能使用符號常數(可以知道其含義的常數),符號常數的識別符號含義應該明瞭其含義,不要用模糊不清的名字。
常數表達式: 即表達式中的運算物件都是由常數組成。
型別轉換
表達式中的物件可以是不同類型,所有運算物件必須可以隱式或顯式轉換成同一型別,並且支援將要進行運算的運算子,最後計算出其值。
記號 {} 或 () 可以用來顯式型別轉換, {} 語法可以防止窄化,也相當於用value型別的值用來初始化type型別的變數。
type{value} type(value)
在表達式運算完畢後可能還會執行轉換(由實現定義),用來作爲初始化變數或賦值語句的rvalue。
語句
計算機執行語句順序是按照編寫的順序執行的。
表達式語句是以分號結尾的表達式,主要分爲:
選擇語句
當沒有對回圈變數進行初始化時編譯器會給出本地變數沒有初始化的錯誤,這個錯誤不屬於編譯錯誤。並且使用未初始化的變數其結果是未定義的。
函數宣告
通常也稱爲函數原型,可以讓呼叫者知道其函數的資訊。要呼叫一個實體,必須要讓編譯器知道它是否存在(使用前置宣告),C++提供了定義(實現)與宣告(介面)分離的方法,通常在頭中存放宣告,原始檔中存放其定義,這樣需要呼叫相應實體只需要在原始檔中使用#include指令引入相應的頭便可使用相應的介面,使用者只需使用介面而無需知道實現細節,同時也可以防止使用者在讀原始碼的時候注意力被分散。
計算機不是一種只能完成固定的功能,利用更多的語言特性可以完成任何計算任務,理論上計算機可以完成任何工作。
事實上,對於一個程式設計師來說,如何準確的定義所有的錯誤是不可能的,但是這幾項涵蓋了程式設計時遇到的絕大部分錯誤。
只要符合下面 下麪兩點程式就可以正常工作。
有三種方法可以編寫出可接受的程式,也必須掌握和使用。
精心組織軟體結構來避免錯誤
通過偵錯和測試來消除大部分錯誤
確定餘下的錯誤是不重要的
錯誤來源
所有編譯單元中的實體的定義和宣告必須嚴格一致。
函數只能被定義一次但可以被宣告無數次,因爲定義分配了記憶體單元,宣告沒有,只是在作用域中引入了名字。
異常
把錯誤檢測(被調函數處理)和錯誤處理(主調函數處理)分離開。
當一個函數出現了自身不能處理的錯誤,則拋出一個異常來表示錯誤發生,主調函數接受異常並進行相應的處理。如果沒有catch所有異常預設情況則會出現系統錯誤並終止程式執行。
捕捉異常與呼叫多少函數無關,異常會被「離得最近」的catch所捕獲。
估計
是一種優雅的藝術,將人的常識和一些用來解決非常常見的問題的方法進行結合(通常用筆和紙進行,除了計算量比較低或已經在實踐中獲得了一些經驗可直接得出答案的問題)。瞎估計:將猜測與常識進行結合的方法(在頭腦中簡單的進行思考)。
偵錯
當寫完程式後進行排除錯誤,進行的這項工作被稱爲偵錯。
可以被簡單的描述爲:
偵錯的主要問題就是:
如何知道程式是否正確的執行並可以得到預期(正確)的結果
在編寫程式時要考慮如何偵錯程式,如果編寫後才考慮就太晚了。
可以在在會隱藏錯誤的語句中插入檢查不變式(永遠成立的條件)來查詢錯誤,如果找不到錯誤就說明找錯地方了。陳述一個不變式的語句稱爲斷言。
前置條件: 函數對於自己參數提出的要求。
如果前置條件違反了則
後置條件:
與前置條件類似,檢測返回值是否是預期值。
測試
測試是把一個巨大的,系統篩選出的數據集輸入到程式,把相關結果與預期結果進行比較。基於一組給定數據集輸入的一次程式執行被稱爲測試用例。
程式設計就是問題理解!編寫程式需要不斷細分所實現的功能和表達式。
程式的編寫往往都是從一個問題出發,也就是藉助程式來解決一個實際問題,正確理解問題對程式的實現就顯得至關重要。解決一個理解錯誤的問題則是浪費精力和時間,即使它是完美的。程式設計應該簡單,清晰的解決要處理的問題。
一個好的程式應該有:
對問題的思考
如何開始?大體上說,我們所要做的就是對問題和問題求解方法進行思考。首先考慮程式應該完成聲什麼?人機互動方式是怎樣的?然後,考慮如何進行程式設計並實現這些功能。試着寫出每個解決方案的簡單框架,並檢驗他們的正確性。與別人討論問題是很有效的方法,甚至比寫在紙上還要好,因爲此時只有自己思考的想法,因爲有很多地方可能考慮不到。
策略
有時候用於實驗的小程式稱爲原型。如果第一個程式不能工作或者在此基礎上很難繼續實現可以將其丟棄,直到成功爲止。不要在一棵樹上吊死,否則會走火入魔(越來越亂)。
學習程式設計重要的是思想和和如何用的代碼表達思想而不是單獨的語言特性,就好比一首詩我們主要關注的是其文筆和含義,而不是關注哪些詞用的好。
我們要牢記:
大多數程式設計語言的設計概念是相通的,很多這種概念被流行的程式設計語言所廣泛支援。也就是說可以廣泛的運用到許多的語言,而語言特性卻僅僅侷限於一兩種語言。
宣告
宣告語句用於將名字引入作用域。
宣告可以重複,定義只能定義一次。
定義:給出了實體的完整描述的宣告。
兩者反映出如何使用實體(介面)和這個實體如何完成相應工作(實現)的根本區別。
C++不提供內建型別的預設初始化。全域性變數會被預設初始化爲0。
宣告和定義可以讓一個程式分成幾部分,分開編譯。宣告功能使程式的每一個部分都能保有程式其他部分的一個檢視,而不必關係其實現細節(實現者檢視)。
頭
提供了一個宣告和定義相分離的方法,將實體的宣告(介面)放在頭(標頭檔案),將其定義放在原始檔,則需要某些功能則只需在原始檔中包含相應的標頭檔案,就可以使用其介面。
作用域
是一個程式文字區域,每個名字都定義在作用域中。
函數呼叫和返回
函數爲我們提供了表示操作和計算的途徑,當我們要完成一個特定工作時,我們可以爲這個工作取一個名字,呼叫這個工作就叫函數呼叫,爲了組織這些由原語構成的程式碼,我們需要函數。我們用來爲計算和一些操作命名的語法結構稱爲函數。
函數不能返回區域性變數,原因是函數返回時所有函數體內物件都被銷燬。
傳值
將參數簡單的拷貝給函數實參稱爲傳值。函數參數是區域性變數。
傳常數參照和傳參照
當拷貝代價很高的時候則需要一種將參數高效傳遞給其他物件或函數的一種方法(可以直接使用被參照的物件)。傳常數參照可以防止傳入的物件被修改,參照不是一個物件,它只指向其他物件,相當於給指向的物件取一個外號。
常數參照不是必須參照lvalue,它可以像初始化和傳值方式一樣進行轉化。例如函數形參有一個常數參照,則可以參照字面值常數,編譯器會分配一個臨時變數,令該形參指向它。
參數檢查和轉換
參數傳遞的過程就是用函數呼叫中指定的實際參數初始化函數形式參數的過程
,當形參可以隱式轉換成實參時就沒問題,不然就需要顯式型別轉換。
函數呼叫棧
編譯器會分配一個數據結構,用來儲存呼叫函數的所有參數和區域性變數的拷貝,只有用到的實體纔會被拷貝在棧中,並且棧是從高地址往低地址進行,函數執行完畢後才從下往上銷燬。
constexpr函數
有時希望一些簡單的運算在編譯時完成,從而避免執行時計算造成的資源開銷(如果一個函數被頻繁呼叫就會消耗棧記憶體),函數必須非常簡單使得在編譯時計算出其值,只能有一個返回值並還可以含有簡單的回圈語句,不能修改區域性變數以外的變數。
計算順序
編譯器會按照原始碼的書寫規範逐行按順序執行,並且在一個作用域中,實體的建立順序和銷燬順序是相反的。
...//
{
int i; // J銷燬後才銷燬I
int j;
}
...//
如果一個型別編譯器無需藉助程式設計師在原始碼中的宣告就可以知道如何表示這種型別物件和可以進行什麼運算,則被稱爲內建型別。
UDT包括ISO標準庫型別,它們很大程度上可以和內建型別一樣作爲語言的一部分,但還是將他們看作UDT,因爲它們和我們自己建立的型別使用了同種語言功能和技術,並沒有什麼獨有的語言特性等。
通過型別可以用程式碼直接表達我們的思想,編寫程式碼時,理想情況就是能在程式碼中直接表達思想,例如int和double可以幫助我們如何表示數位以及數學運算,string可以用來實現字串等等。
表示: 一個型別知道如何表示物件中的數據。
運算: 一個型別知道物件可以進行什麼運算。
很多程式設計想法都表現爲將」一些東西「表示它們當前值的一些數據(狀態)和可以對他們執行的一些操作(方法)。操作的結果依賴於當前物件的狀態。
一個類可以直接表達思想,可以指出這個型別物件如果建立(建構函式),進行什麼操作(方法),刪除(解構函式)。如果將某些東西作爲一個單獨的實體,那麼就應該在程式中定義一個類來表示」這個東西「。
類和成員
一個類就是一個UDT,由一些型別以及一些函陣列成。用來定義類的組成部分稱爲成員。
介面和實現
一般將介面和實現稱爲類,即使用者檢視(介面)和實現者檢視(實現)。
結構就是隻有數據的類,也就是狀態可以是任意的數據結構,也不能爲其建立不變式。
建構函式
與類同名的特殊成員函數,用於物件的構造,同時我們也需要多個建構函式,以用來接受不同對象來初始化(顯式型別轉換)。
如果希望不提供初始化器就能定義一個物件,則需要預設建構函式(f()=default),當通過一個有意義,顯然的預設值來建立不變式纔會有意義。
初始化器的含義/使用完全取決建構函式。
對於任意型別T,T{}表示預設值。
通常用建構函式來建立不變式,沒有不變式的建構函式是一種糟糕的數據結構,因爲無法確保數據的狀態是否合法,從而會造成大量錯誤。
用()表示元素數量,{}表示元素列表,需要從形式上區分。
編譯器會將{}中的一個值解釋成一個元素值,並將它作爲一個lnitializer_list的元素傳遞給初始化器列表建構函式,{}初始化器列表前面的=是可選的。如果()建構函式沒有explice限定,則會定義一個型別轉換。
拷貝構造
拷貝建構函式接受一個待拷貝物件的參照作爲參數,初始化式的 = 是可選的,當初始化器和被初始化變數型別相同,且該型別定義了拷貝建構函式,則這兩種語法方式完全相同( = 和 {} ),預設拷貝含義是:逐成員拷貝,指針語意,會造成雙重釋放和記憶體漏失。拷貝構造和拷貝賦值的是值語意,拷貝指針指向的數據。
移動構造
&&是右值參照,用來定義移動操作。並且不支援const限定符,原因在於將被移動物件內的所有元素移動到移動物件內,否則就沒辦法修改被移動物件。同時也隱式實現了函數返回,沒有移動構造返回的是副本,同時也無需爲了獲取資訊而處理指針或參照,例如返回指針物件容易造成記憶體漏失。
拷貝術語
如果一個類需要獲取資源,則需要建構函式,釋放資源則需要虛構函數來釋放。一個明顯的標誌是:它擁有指針語意或參照語意的成員。則需要拷貝和解構操作。爲什麼要拷貝操作?因爲預設拷貝(參照語意)成員會發生錯誤。同時還需要移動操作,如果一個物件獲取了一種資源(並擁有指向資源控制代碼的成員),則進行預設拷貝會發生錯誤,而拷貝代價又很高(一個很大的vector或string)則可以使用移動操作。對於一個基礎類別,則預設需要一些虛擬函式(例如解構函式和建構函式)。
顯式構造
沒有explicit限定的只支援一個參數的建構函式定義了從其參數型別向所屬型別轉換的操作(型別轉換的實現),被explicit限定後的建構函式如果用來轉換會出現錯誤。eg: vector< int > a =10 //不存在vector到int的轉換。
不變式
判定物件的狀態是否合法的規則稱爲不變式。 一個或一組條件,對於一個判斷其狀態是否合法的條件總是爲真。如果不需要不變式則可用struct。
將成員函數放在類內:
列舉
是一種非常簡單的UDT,它指定一些值的集合,用符號常數表示,稱爲列舉量。
作用域列舉列舉
不會污染作用域,呼叫列舉值需要使用全限定名。
平坦列舉
會污染作用域。
運算子過載
可以爲UDT提供運算,一個過載必須至少作用於一個UDT物件。
運算子過載的呼叫有兩種方式:
過載函數也支援const限定符。
使用一個static 來限定一個類物件,使得整個程式中都只有一個拷貝。
拷貝
只要不特別宣告,編譯器會預設進行逐成員拷貝。
一個類內成員在宣告時初始化被稱爲類內初始化。
const成員函數
表明此函數可以在一個常數物件上呼叫。
輔助函數
是一種設計思想,用來完成一些簡單並且頻繁呼叫的工作,或者是大函數中的小函數。通常使用類物件作爲參數,並用一個命名空間來建立一個大規模的類。
陣列
可以定義爲
+,-,+=,-=,++,–可以用來作爲指針運算子。
將陣列名退化成指針傳遞可以避免大量的數據傳遞,定義一個指針不要期望能將它作爲陣列使用。
所有輸入輸出都可以看做是位元組流與字元流的相互轉化,由輸入輸出庫處理。
要進行輸入、輸出需要:
ostream
istream
緩衝區這一數據結構用來儲存提交給I/O流的數據,並通過它與操作系統通訊,緩衝區可以提高I/O效率。
輸出流的主要目標就是生成可供人們閱讀的數據形式,輸出流可以格式化文字來滿足需求,輸入流則可以獲取輸出流的資訊並進行格式化(可選)輸出。
ostream將記憶體中的物件轉換成位元組流並儲存到記憶體。
istream將從記憶體獲取位元組流並轉換成物件存入記憶體。
當一個檔案流離開作用域會被隱式關閉,與之關聯的緩衝區被重新整理,也就是將緩衝區中的內容寫入檔案。
儲存媒介和儲存的數據不會影響到流的操作,這就是檔案和流抽象層的好處。
用來改變流行爲的關鍵字被稱爲操縱符。
C++標準庫和很多UDT都基於作用域程式設計風格,也就是離開作用域資源控制代碼會被隱式解構。
面向二進制和字元I/O時要放棄使用 << 和 >>,因爲這兩個操作符預設將值轉換成字元序列,例如:「ascd」=‘a’,‘s’,‘c’,‘d’ 123=‘1’,‘2’,‘3’
流狀態
static_cast用來獲取一個實體的原始位元組表示(地址).
從一個file讀取的輸入輸出流是fstream
從一個string讀取輸入輸出流是stringstream
硬體能直接支援的只有位元組序列。
計算機的記憶體是一些位元組序列,可以將這些位元組從零開始標記,這個標記就是指針(地址),雖然指針可以列印成整型值,但不意味着指針就是整型值,指針型別有提供一些適用地址的操作,不能將不同類型的指針進行拷貝或賦值,不同類型指針之間只能通過強制型別轉換(reinerpret_case)
編譯器爲程式碼分配的記憶體稱爲程式碼儲存,爲全域性變數分配靜態記憶體,爲函數呼叫預留記憶體(棧記憶體),用new使用自由空間(堆)。
[] 運算子依賴於元素型別大小來計算出到哪裏找到一個元素。
如果對沒有初始化的指針進行存取會出現未定義行爲,因爲不知道會指向哪塊記憶體,例如:如果修改一個只讀記憶體會出現硬體錯誤。
指針的操作直接對映爲機器指令,語言只提供操作的方便,一些情況會用到特殊指針型別:void*
可以將參照看作爲一種自動解除參照的常數指針或直接操作物件的方式。
移動構造隱式用於實現函數返回,因爲函數返回後區域性變數將被銷燬並將區域性變數拷貝給呼叫者,則可以直接將移出而非拷貝。
通常使用建構函式來建立不變式。
改變它的有兩方面:
使用容器的方法就是儲存數據可以誰是改變大小和操作容器內部。這種可變性是非常有用的特性,程式中包含各種數據型別元素的容器。
記憶體管理的最底層,記憶體中的所有物件都是固定大小且沒有型別。
在模板中可以用typename或class,兩者沒有區別。
當參數化是類時,我們將得到一個類別範本。參數化是一個函數時,得到一個函數模,通常也稱爲演算法,泛型程式設計也被稱爲面向演算法的的程式設計。設計重點在於如何處理數據而不是如何表示。
依賴於顯式模板參數的多型被稱爲參數化多型。從類層次與虛擬函式獲得的多型被稱爲即時多型(物件導向)。每種型別都依賴於程式設計師通過一個單一的介面表示一個概唸的多個版本。多型在希臘語中的含義是:多種形狀。就像一個函數模板(介面)可以處理任何型別實體,只要此型別滿足模板的要求。
泛型在編譯時才確定數據型別,物件導向則是在執行時。不同程式設計範式可以進行自由組合。
泛型程式設計: 由模板支撐,依賴編譯時解析。
物件導向程式設計: 由類層次和虛擬函式支撐,依賴執行時解析。
泛型的缺點就是介面和實現不能很好的分離。當編譯器使用模板程式碼時,編譯器會探查模板內部以及模板實參,這樣做是爲了獲得生成優化程式碼所需的資訊,所以都要求使用模板的地方存在模板的完整定義。包括所有呼叫的成員函數的模板。因此把模板編寫在頭中可以解決這個問題。
array<int,1024> buf;
template<typename T,int N>void fill (array<T,N> &b, cosnt T& value);
void f()
{
fill(buf,'x'); //T是int,N是char
}
泛化
我們作爲模板的實現者就需要考慮這些問題,爲了處理這些問題,可以設定一個使用者選項,以便我們需要一個預設值時能夠指定使用什麼值。
template<typename T>void vector<T>::resize(int newsize,T def = T());
除非使用者指定了型別,否則使用預設值T( )。
vector<double> a;
a.resize(10); //新增100個double();
struct No_define {
No_define(int);
};
vector<No_define> a(100,No_define(1)); //新增100個No_define(1);
設計上的考慮
相容性: 人們總是希望在很久以前編寫的程式碼能夠依舊在未來執行,例如:C++98中一些程式設計師實現了類似於STL中的VECTOR,它沒有範圍檢查操作機制 機製但是維護人員爲這些程式碼找出了所有的錯誤,如果需要在這些程式碼中使用C++11中的vector會是一個很複雜的作業。
效率: 有一些操作雖然可以保證程式碼的安全性,但是資源的消耗會很大,應該」見機行事「,在不影響效率的前提下使用。
約束: 在一些環境中一些機制 機製或功能特性不能被使用。
可選性: 一些功能並沒強制要求使用可以依情況使用。
資源
基本規則:如果我們獲取了資源,那麼也要考慮如何歸還。
本質上:資源的使用者必須向資源管理者歸還資源,並由資源管理者賦值回收。對於一個負責釋放資源的物件稱爲控制代碼。
RAII
通過物件建構函式獲取資源,並通過其解構函式釋放資源。
基本保證: try…catch語句在拋出錯誤時不會造成資源泄露。
強保證: 如果一個函數提供了基本保證,還具有----在拋出異常後不會破壞其他數據。
無拋出保證: 除非可以保證所寫程式碼不會拋出異常併產生錯誤,否則不會寫出滿足前面兩點的程式碼。
C++標準庫爲處理數據序列提供了一個框架,稱爲STL。STL是標準模板庫的簡稱。STL是標準的一部分,它提供了容器和通用演算法,因此我們可以稱vector這類物件爲STL一部分,標準庫其他部分,如IOSTREAM,c標準字元處理常式等不屬於STL。
計算過程主要是這兩個方面:
當我們想知道如何處理數據(演算法)時,並沒有提到數據是如何儲存的。很顯然我們需要輸入流,向量等數據結構來儲存數據,但是我們不必瞭解實現細節。重點是這些值或物件,我們可以對他們進行什麼操作或如何存取。
STL不僅可以看作一個功能強大的庫,更應該看作一個兼顧靈活性與效能的函數庫設計典範。
我們主要遇到的問題是:
通過泛化程式碼可以解決上述問題,例如有如下的解決策略:
序列和迭代器
序列是STL核心概念,從STL角度來看,數據集合就是一個序列。迭代器可以標識序列中的元素的物件。
迭代器:
是一種可以標示序列中元素物件,我們可以按照如下方式來看待一個序列:
迭代器不僅僅可以是指針,比如可以定義一個邊界檢查迭代器,當試圖指向[begin,end)之外,則會拋出一個異常。把一個迭代器作爲一個類可以帶給我們很大的靈活性。
我們可以利用迭代器實現演算法與數據的連線,程式設計師瞭解迭代器的使用方法(不需要知道實際如何儲存數據),數據提供方向使用者提供相應迭代器而不是數據儲存的資訊。這樣的數據結構使得演算法和容器之間保持了很好的獨立性,但他們都知道由一對迭代器定義的序列。
將迭代器作爲一個嵌入類的原因在於:沒有任何理由將迭代器的型別實現爲全域性類,每一種容器都有相應的迭代器,這樣就可以同時使用兩個以上的容器。
介面庫只是在類的實現細節中使用了FLTK介面,而沒有將它暴露給使用者,任何使用者都無需瞭解如何呼叫或使用庫函數,這樣當需要其他的庫只需修改相應的介面,而無需修改使用者程式碼。
以GUI介面庫爲例
Shape與其他例子不一樣,它是個純抽象,通用的概念。我們無法實現一個普通的圖形,我們只能看到四邊形,五邊形,六邊形等具體特例,正如爲一個抽象類建立一個物件,編譯器會阻止你,只能建立它的派生類(特例)。
圖形介面類構成一個庫,這些類(Shape,Line,shape)可以被組合在一起使用,這些類也可以作爲基本構件,供你來構造描述其他複雜形狀的類。我們並不是定義了一組無關的類,所以不能孤立的設計每一個類。我們不能對它的完整性有所期望,沒有任何類庫可以直接對該領域的所有方面進行建模,我們的目標是簡潔性和擴充套件性。
我們必須確定對於我們來說什麼是最重要的。對於GUI設計,就是要決定我們要如何做好GUI。試圖做好所有的事情通常會走向失敗,正如術業有專攻,好的庫會從一個特定的角度,清晰的對其領域有所建模,強調某些方面,對於其他方面則不會關注。
使用者可在類庫的基礎之上進行擴充套件,如果需要更多特性,可以瞭解如何直接使用庫,建議還是用介面庫,因爲可以減少很多工作量。
爲GUI庫提供了很多小類和少的操作,而不是提供帶有很多參數和方法的大類,能通過參數指定一個物件是什麼型別,甚至將這種型別轉換成另外一種派生型別,這種思路的極致就是提供一個基礎類別,所有情況都可以歸爲一種,如果提供了一個包含很多型別的大類,則會使得程式碼變得混亂難以理解。
我們希望所有類的介面都有一致的風格,例如不同類型物件執行的同種操作,都有相同的函數簽名。
draw_retangle(point{100,200},300,400);//良好的風格
draw_retangle(100,200,300,400);//可讀性差的風格
第一個定義我們很清楚該傳遞什麼參數,下面 下麪的就讓人琢磨不透,同時在需要同樣參數的介面中順序應保持一致,就像座標的定義(x,y)一樣,這樣就不容易寫出問題程式碼。邏輯上,擁有同種邏輯操作的函數名稱應該一樣,有時候,這些一致性甚至可以允許我們編寫出用於很多不同類型的程式碼,只要型別滿足函數的運算,這樣的程式碼被稱爲泛型。
命名
邏輯上,不同的操作應該有不同的名字,思考一個問題:Shape類的attach()和add()都是新增到某物,但是它們卻根本不是同一種概念。思考爲什麼將Shape"J新增「到WINDOW中而把Line」加入「到Shape中,這兩種相似的操作不應該擁有相同的操作?
Shape::Open_line ops;
ops.add(point ti);//傳值一份拷貝儲存在基礎類別
win.attach(ops);//將ops與window關聯起來,儲存一個參照。在使用win時不可以讓ops離開作用域
可變性
當設計一個類的時候,要考慮」誰可以修改「和」如何修改「,如何讀取物件的狀態,保證類只能被自身成員修改用public/private可以實現。
基礎類別(Shape)
是一個一般性的概念,在GUI中可以提供圖形和WINDOW的關聯。
Shape提供圖形和WINDOW的關聯,而WINDOW提供了與操作系統的關聯,操作系統與物理螢幕關聯
Shape是一個類,可以處理與所有圖形的有關操作,各種圖形的資訊都存放在Shape中
Shape統一實現其派生類的實現以及修改
我們實現了這一思想:不能顯示一個」普通的圖形「但是可以顯示一個」特殊的圖形「(四邊形,線條等),他們所含有的成員基礎類別(shape)都含有,將基礎類別的建構函式或其他操作設定爲Virtual,則其他圖形(派生類)可以爲自己定義一個建構函式或其它操作,然後將數據轉發給Shape儲存。
表示一個宏觀概唸的類被稱爲」抽象類「,定義抽象類的方法被稱爲」純虛擬函式「,在其函數分號前新增「=0」。
存取控制
一般將類的狀態設爲「不可見」,只有類介面可以存取它們,數據成員的初始化不依賴於建構函式的的參數,所以我們在數據成員的宣告中定義它們,如果一些數據成員擁有預設函數則可以不用定義。
通常將需要讀寫X的函數名字設爲X(),set_X(),但是美中不足的是不能將狀態名字與函數名一致,否則會有名字衝突,一般將狀態名新增一些額外字元,雖然不美觀但是因爲使用者根本用不上這些。在函數簽名末尾新增const限定符指明這個函數不會改變物件狀態。Shape中保持了一個vector< point >,還有一個add(),用來爲vector新增點,Shape只負責維護(儲存)這些Point
void Shape::add(point &a)
{
points.push_back(a);
}
Shape本身是不理解這些點的含義的,只負責儲存,只有其派生類才理解(Cicle,polygon),派生類要控制如何新增點,例如: