C++

2020-08-12 18:42:05

程式: 用一系列指令描述如何做一件事的原語。
庫: 擁有特定功能的程式碼的集合。

物件,型別和值

型別: 定義了一組可能的值與一組運算(對於一個物件)。
物件: 用來儲存指定型別值的記憶體單元。
值: 根據一個型別來解釋的記憶體中的一組位元。
變數: 一個擁有名字的物件。
宣告: 爲一個物件命名的語句。
定義: 是一種宣告,但同時也分配了記憶體單元。
每一種內建型別的實體大小都一樣,(例如每個int都有固定大小)記憶體中的位元的含義完全取決於存取它的所用的型別,計算機記憶體不知道具體的含義,只負責儲存,當我們決定一塊記憶體如何解釋時,它們纔會有意義,就像12.5的含義是什麼?它可以是12.5米,12.5升一樣,只有擁有單位纔有其含義

  • 型別安全
    如果使用的物件符合他們規定的型別 ,那麼他們是型別安全 。如果沒有初始化就使用,則是不安全的。
    一個值被轉換成一個等價的值,或者是接近的值,則轉換就是安全的(沒有截斷)。
    靜態型別安全:編譯器隱式生成程式碼檢查每一個物件,當違反型別安全則標記它們,這是一種很低效的做法,C++不支援。
  • 不安全轉換
    將一個尺寸較大的物件放入一個較小的物件中,會發生「截斷」現象。
    由於歷史原因,不安全轉換是C++從他的前輩C繼承而來的,如果把一種型別的值拷貝給另外一個物件,沒有發生整數截斷就認爲是安全的。反之,一個物件可以顯式或隱式轉換成另外型別物件,但是兩者的值不相等(被截斷),則是不安全轉換。

計算

有一種觀點認爲,程式就是以計算爲目的,即程式都要有輸入和輸出,同時把能夠執行程式的硬體統稱爲計算機。
廣義上的輸入輸出實質是數據進入離開計算機,也可以引申到程式之間的數據傳遞。一個完整的程式通常含幾個子程式,子程式的輸入通常稱爲參數,輸出稱爲結果
計算就是基於輸入生產生成輸出的過程。
爲了處理輸入,程式需要包含一些數據,這些數據有時被稱爲數據結構狀態
從程式設計的角度來看,最重要的兩類輸入和輸出是

  • 從其他程式輸入或輸出
  • 從同一程式的不同子程式輸入或輸出

下圖展示了在共同作業完成一個大程式時,應該如何合理的設計結構程式,並保證每一個子程式之間都能共用的交換數據。
目標和工具

  • 正確
  • 簡單
  • 高效

程式的組織體現了程式設計師的程式設計思路,目前有兩種手段。

  • 抽象: 將不需要瞭解的程式實現細節隱藏在相應的介面後面。eg:
    • 類,泛型演算法的使用
    • 通過識別符號,泛型容器等使用記憶體。
  • 分治: 把一個大問題細分成多個小問題。eg:建立字典可以分爲:讀取數據,對數據排序,輸出數據這三個小問題。

在考慮細分一個問題的時候,首先要考慮手裏有哪些明確的工具可以用來表示各個子程式及其之間的關係,這就是爲什麼一個提供充分介面和功能的庫能大大簡化程式劃分的困難(不同的庫可以實現相應的功能)。憑空想象出程式劃分是不切實際的,按照功能劃分一個程式的結構是很好的方法。一個好的程式需要利用已有的各種庫從而可以簡化程式的工作量並使得效率最大化。抽象方法的一個例子:iostream庫提供了使用者與計算機的I/O操作,使用者無需理解實現細節只需呼叫特定功能的介面即可。

僅通過編寫大量的語句是寫不出好程式的,唯一的方法就是閱讀大量的優質程式碼加以模仿改進,要注意程式的組織結構。初學者常犯的錯誤是:不仔細分析問題,構建程式的結構,而是想當然的寫程式,最後扎進技術細節中,最後不得不放棄。軟體結構是在開發過程中不可忽視的過程,缺少軟體結構的程式猶如沒有一座沒有打好地基和使用鋼筋混凝土的土坯房,想要維護和擴充套件需要付出高昂的代價,到最後甚至需要回過頭重新構建。

  • 表達式
    表達式是組成程式的最基本元素,表達式就是從一些運算元(物件)中計算出一個值最簡單的表達式是字面值常數,變數名也是一種表達式,是物件識別符號。
    物件在賦值運算子左邊被稱爲左值,在右邊被稱爲右值。

  • 常數表達式
    除了個別情況,不要使用魔術常數(不能直接識別出其含義的值),應該儘可能使用符號常數(可以知道其含義的常數),符號常數的識別符號含義應該明瞭其含義,不要用模糊不清的名字。

  • 常數表達式: 即表達式中的運算物件都是由常數組成。

    • 一個constexpr表達式其值必須要在編譯時求值。
    • const表達式是執行時求值
    • 宏替換將宏名替換成對應表達式,並在編譯時或執行時求出其值。
  • 型別轉換
    表達式中的物件可以是不同類型,所有運算物件必須可以隱式或顯式轉換成同一型別,並且支援將要進行運算的運算子,最後計算出其值。
    記號 {}() 可以用來顯式型別轉換, {} 語法可以防止窄化,也相當於用value型別的值用來初始化type型別的變數。
    type{value} type(value)
    在表達式運算完畢後可能還會執行轉換(由實現定義),用來作爲初始化變數或賦值語句的rvalue。

  • 語句
    計算機執行語句順序是按照編寫的順序執行的。
    表達式語句是以分號結尾的表達式,主要分爲:

    • 賦值語句
    • I/O語句
    • 函數呼叫
    • 字面值常數
  • 選擇語句

    • if語句
      if 關鍵字括號裡必須是表達式,其中else if()不是else-if語句,C++也沒有實現這種語句。
    • switch語句
      用於與多個常數進行比較,如果沒有與之匹配的case分支則執行default分支,因爲沒有任何人可以預料到所有可能發生的情況,程式設計可以讓人知道世界上沒有絕對的事情。switch的 () 中必須是常數表達式字元型列舉型,不可以是使用者自定義型別或string(可以用map)。case語句中必須是常數表達式。(並且編譯器會優化生成程式碼,比if語句更高效。)
    • 回圈語句
      提供當要對一些操作執行多次時的方法,在對一系列數據進行同樣操作時稱爲迭代
    • will語句
      通常用在無限回圈
    • 複合語句
      通常也稱爲程式塊,是一種特殊語句,可以將花括號內的語句作爲一條語句來執行。
    • for語句
      跟will類似,只是將回圈變數的初始化和改變放在一起便於理解,通常回圈變數的改變不會發生在函數體內。

當沒有對回圈變數進行初始化時編譯器會給出本地變數沒有初始化的錯誤,這個錯誤不屬於編譯錯誤。並且使用未初始化的變數其結果是未定義的。

  • 函數
    是一個具名的語句序列,通常能夠返回指定型別的計算結果,返回型別void是一種僞返回型別,函數體是實現函數功能的複合語句。
  • 函數的意義:
    根據需求將一部分計算任務分離並出獨立實現。
    • 實現計算邏輯的分離
    • 使用函數呼叫使得程式的結構更加清晰
    • 利用函數,可以避免重複的勞動,每個函數實現不同的功能,則只需呼叫相應的介面
    • 減少程式的偵錯,使得程式碼之間的耦合性降低

函數宣告
通常也稱爲函數原型,可以讓呼叫者知道其函數的資訊。要呼叫一個實體,必須要讓編譯器知道它是否存在(使用前置宣告),C++提供了定義(實現)與宣告(介面)分離的方法,通常在頭中存放宣告,原始檔中存放其定義,這樣需要呼叫相應實體只需要在原始檔中使用#include指令引入相應的頭便可使用相應的介面,使用者只需使用介面而無需知道實現細節,同時也可以防止使用者在讀原始碼的時候注意力被分散。

計算機不是一種只能完成固定的功能,利用更多的語言特性可以完成任何計算任務,理論上計算機可以完成任何工作。

錯誤

事實上,對於一個程式設計師來說,如何準確的定義所有的錯誤是不可能的,但是這幾項涵蓋了程式設計時遇到的絕大部分錯誤。

  • 編譯時錯誤編譯器檢查原始碼是否存在語法錯誤和型別錯誤。
    • 語法錯誤
    • 型別錯誤
    • 窄化錯誤當將一個很大的值賦給小物件,則會發生」截斷「,顯式轉化就不會引發窄化錯誤。
  • 執行時錯誤程式執行時導致的錯誤。
    • 由庫檢測出的錯誤
    • 由計算機(硬體,計算機操作系統)檢測出的錯誤
    • 由使用者程式碼檢測出的錯誤
  • 邏輯錯誤(程式設計師檢測出使結果出錯的錯誤)
  • 鏈接錯誤 使用定義的原始檔必須與含有其宣告的檔案鏈接起來(使用了其實體而沒發現其定義,編譯器會報錯

只要符合下面 下麪兩點程式就可以正常工作。

  • 輸入正確的值會得到正確的結果
  • 輸入錯誤的值會得到錯誤的結果

有三種方法可以編寫出可接受的程式,也必須掌握和使用。

  • 精心組織軟體結構來避免錯誤

  • 通過偵錯和測試來消除大部分錯誤

  • 確定餘下的錯誤是不重要的

  • 錯誤來源

    • 缺少規劃: 如果沒有事先計劃好程式要做什麼,則不可能充分檢測程式或確認所有輸入結果的輸出情況都能被正確的處理。
    • 不完備的程式: 在軟件開發過程中不可能充分解決所有情況。
    • 意外的參數: 輸入沒有被正確的處理。
    • 意外的狀態: 多數程式都保留有很多系統各個部分所使用的數據(狀態),如果數據錯誤應該被正確處理。
  • 所有編譯單元中的實體的定義和宣告必須嚴格一致。

  • 函數只能被定義一次但可以被宣告無數次,因爲定義分配了記憶體單元,宣告沒有,只是在作用域中引入了名字。

  • 異常
    把錯誤檢測(被調函數處理)和錯誤處理(主調函數處理)分離開。
    當一個函數出現了自身不能處理的錯誤,則拋出一個異常來表示錯誤發生,主調函數接受異常並進行相應的處理。如果沒有catch所有異常預設情況則會出現系統錯誤並終止程式執行。
    捕捉異常與呼叫多少函數無關,異常會被「離得最近」的catch所捕獲。

  • 估計
    是一種優雅的藝術,將人的常識和一些用來解決非常常見的問題的方法進行結合(通常用筆和紙進行,除了計算量比較低或已經在實踐中獲得了一些經驗可直接得出答案的問題)。瞎估計:將猜測與常識進行結合的方法(在頭腦中簡單的進行思考)。

  • 偵錯
    當寫完程式後進行排除錯誤,進行的這項工作被稱爲偵錯。
    可以被簡單的描述爲:

    • 讓程式編譯通過
    • 讓程式正確鏈接
    • 讓程式正確執行預期操作

偵錯的主要問題就是:
如何知道程式是否正確的執行並可以得到預期(正確)的結果

在編寫程式時要考慮如何偵錯程式,如果編寫後才考慮就太晚了。
可以在在會隱藏錯誤的語句中插入檢查不變式永遠成立的條件)來查詢錯誤,如果找不到錯誤就說明找錯地方了。陳述一個不變式的語句稱爲斷言

  • 前置條件: 函數對於自己參數提出的要求。
    如果前置條件違反了則

    • 忽略它(就認爲呼叫者會使用正確參數)
    • 檢查它(並拋出錯誤)
  • 後置條件:
    與前置條件類似,檢測返回值是否是預期值。

  • 測試
    測試是把一個巨大的,系統篩選出的數據集輸入到程式,把相關結果與預期結果進行比較。基於一組給定數據集輸入的一次程式執行被稱爲測試用例

編寫一個程式

程式設計就是問題理解!編寫程式需要不斷細分所實現的功能和表達式。
程式的編寫往往都是從一個問題出發,也就是藉助程式來解決一個實際問題,正確理解問題對程式的實現就顯得至關重要。解決一個理解錯誤的問題則是浪費精力和時間,即使它是完美的。程式設計應該簡單,清晰的解決要處理的問題。
一個好的程式應該有:

  • 闡明設計和程式設計技術
  • 易於探究程式設計師做出的各種決策和相關考慮
  • 不需要很多的語言結構
  • 對設計的考慮足夠全面
  • 易於對解決方案進行改變
  • 解決一個易於理解的問題
  • 解決一個有價值的問題
  • 具有一個足夠小,從而可完整實現,徹底理解的求解方案

對問題的思考
如何開始?大體上說,我們所要做的就是對問題問題求解方法進行思考。首先考慮程式應該完成聲什麼?人機互動方式是怎樣的?然後,考慮如何進行程式設計並實現這些功能。試着寫出每個解決方案的簡單框架,並檢驗他們的正確性。與別人討論問題是很有效的方法,甚至比寫在紙上還要好,因爲此時只有自己思考的想法,因爲有很多地方可能考慮不到。

  • 程式設計的幾個階段
  • 分析: 判斷應該做什麼並且給出對當前問題理解的描述,稱爲需求集合或者規範(產品經理)。問題的規模越大,規範就越重要。還有一點,當你不知道如何實現一個功能點的時候,我們要學會借鑑!
  • 設計: 給出系統的整體結構圖,並確定具體實現內容以及它們之間的相互聯繫。作爲系統設計的重要方面,要考慮哪些工具(各種各樣的庫或呼叫其他語言輔助實現)有助於實現程式的結構。
  • 實現: 編寫程式碼並測試程式,確保程式能夠正確執行。

策略

  • 要解決的問題是什麼?首先要做的事情就是將要完成目標具體化,包括建立問題的描述或者分析已有描述的真實意圖。這個時候應該站在使用者角度來看待程式。應該實現什麼功能,而不是如何實現。
  • 必須弄清楚所要解決的問題是什麼,另外一個易犯的錯誤是容易把問題複雜化,在描述一個要處理的問題時容易變得貪心,想讓程式實現過多的功能。最好是將問題簡單化,使得程式易於定義理解實現使用。一旦程式實現了預期功能,則可以基於已有的經驗實現第二個版本。
  • 將程式劃分爲可分別處理的多個部分 爲了解決一個問題,再小的問題都可以進一步細分。
    • 知道有哪些工具,庫或者其他可藉助的東西
    • 尋找可獨立描述的部分解決方案(可以用在本程式或其他程式多個地方),發現這樣的方案需要經驗
  • 實現一個小的,有限的程式來解決問題的關鍵部分。 當我們開始程式設計時,對要求解的問題並不瞭解。有時候會認爲自己很瞭解,只有充分思考並實驗之後才能 纔能深入理解要求解的問題,才能 纔能編寫出一個好的程式。實現一個小的,有限的程式:
    • 引出我們對問題的理解,思考和工具中存在的問題。
    • 看看能否改變問題描述的一些細節使其更加容易處理。當我們分析問題並給出初步設計時,預先估計出所有問題幾乎不可能。必須充分利用程式碼編寫和測試過程中的反饋資訊。
  • 實現一個完整的解決方案,最好是能夠運用最初版本的元件,理想情況是逐步構建來來構造程式的整體結構,而不是一下子寫出所有的程式碼。除非你是天才中的鬼才,可以將沒有經驗功能一步到位。

有時候用於實驗的小程式稱爲原型。如果第一個程式不能工作或者在此基礎上很難繼續實現可以將其丟棄,直到成功爲止。不要在一棵樹上吊死,否則會走火入魔(越來越亂)。

函數

學習程式設計重要的是思想和和如何用的代碼表達思想而不是單獨的語言特性,就好比一首詩我們主要關注的是其文筆和含義,而不是關注哪些詞用的好。
我們要牢記:

  • 我們學習的是程式設計
    • 我們生產的是程式或操作系統
    • 程式設計語言只是工具

大多數程式設計語言的設計概念是相通的,很多這種概念被流行的程式設計語言所廣泛支援。也就是說可以廣泛的運用到許多的語言,而語言特性卻僅僅侷限於一兩種語言。
宣告
宣告語句用於將名字引入作用域

  • 爲命名實體(變數,函數)指定一個型別
  • (可選)進行初始化(如爲變數指定一個初始值,或爲函數指定函數體)

宣告可以重複,定義只能定義一次。
定義:給出了實體的完整描述的宣告。
兩者反映出如何使用
實體
(介面)和這個實體如何完成相應工作(實現)的根本區別。
C++不提供內建型別的預設初始化。全域性變數會被預設初始化爲0。
宣告和定義可以讓一個程式分成幾部分,分開編譯。宣告功能使程式的每一個部分都能保有程式其他部分的一個檢視,而不必關係其實現細節(實現者檢視)。


  • 提供了一個宣告和定義相分離的方法,將實體的宣告(介面)放在頭(標頭檔案),將其定義放在原始檔,則需要某些功能則只需在原始檔中包含相應的標頭檔案,就可以使用其介面。

  • 作用域
    是一個程式文字區域,每個名字都定義在作用域中。

  • 函數呼叫和返回
    函數爲我們提供了表示操作和計算的途徑,當我們要完成一個特定工作時,我們可以爲這個工作取一個名字,呼叫這個工作就叫函數呼叫,爲了組織這些由原語構成的程式碼,我們需要函數。我們用來爲計算和一些操作命名的語法結構稱爲函數
    函數不能返回區域性變數,原因是函數返回時所有函數體內物件都被銷燬。

  • 傳值
    將參數簡單的拷貝給函數實參稱爲傳值。函數參數是區域性變數。

  • 傳常數參照和傳參照
    當拷貝代價很高的時候則需要一種將參數高效傳遞給其他物件或函數的一種方法(可以直接使用被參照的物件)。傳常數參照可以防止傳入的物件被修改,參照不是一個物件,它只指向其他物件,相當於給指向的物件取一個外號。
    常數參照不是必須參照lvalue,它可以像初始化和傳值方式一樣進行轉化。例如函數形參有一個常數參照,則可以參照字面值常數,編譯器會分配一個臨時變數,令該形參指向它。

  • 參數檢查和轉換
    參數傳遞的過程就是用函數呼叫中指定的實際參數初始化函數形式參數的過程
    ,當形參可以隱式轉換成實參時就沒問題,不然就需要顯式型別轉換。

  • 函數呼叫棧
    編譯器會分配一個數據結構,用來儲存呼叫函數的所有參數和區域性變數的拷貝,只有用到的實體纔會被拷貝在棧中,並且棧是從高地址往低地址進行,函數執行完畢後才從下往上銷燬。

  • constexpr函數
    有時希望一些簡單的運算在編譯時完成,從而避免執行時計算造成的資源開銷(如果一個函數被頻繁呼叫就會消耗棧記憶體),函數必須非常簡單使得在編譯時計算出其值,只能有一個返回值並還可以含有簡單的回圈語句,不能修改區域性變數以外的變數。

  • 計算順序
    編譯器會按照原始碼的書寫規範逐行按順序執行,並且在一個作用域中,實體的建立順序和銷燬順序是相反的

...//
{
	int i; // J銷燬後才銷燬I
	int j;
}
...//
  • 表達式計算
    完整表達式中子表達式計算順序所遵循的規則是按照優化編譯器的需求而設計。所以運算順序是由實現定義,如果表達式中含有臨時變數則要在整個完整表達式運算完畢才銷燬。
  • 全域性初始化
    在同一個命名空間中或原始檔全域性作用域中定義的變數,按順序初始化。應避免全域性變數,因爲程式設計師無法得知哪個變數被程式的哪個部分讀寫了,在不同編譯單元中的全域性變數的初始化順序是不確定的。可實現一個輔助函數來返回需要的變數,如果不需要變化並且頻繁使用可以用static限定返回值。一個static變數只有在函數首次呼叫時纔會被建立
  • 全限定名
    由一個類名或命名空間和一個成員名組成的名字。
  • USING指令
    可以看作是typedef的泛化。

UDT

如果一個型別編譯器無需藉助程式設計師在原始碼中的宣告就可以知道如何表示這種型別物件和可以進行什麼運算,則被稱爲內建型別
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

將成員函數放在類內:

  • 函數將成爲INLINE的: 即呼叫該函數不會直接呼叫而是直接生成程式碼,將其嵌入到呼叫者中。
  • 無需重新編譯:類介面改變時才需要重新編譯,而修改函數體則不需重新編譯。
  • 類定義變大: 當定義在類內會使得程式碼變得很多。
    如果需要將函數定義爲行內函式,則只需用inline限定符即可。

列舉
是一種非常簡單的UDT,它指定一些值的集合,用符號常數表示,稱爲列舉量

作用域列舉列舉
不會污染作用域,呼叫列舉值需要使用全限定名

平坦列舉
會污染作用域。

運算子過載
可以爲UDT提供運算,一個過載必須至少作用於一個UDT物件
運算子過載的呼叫有兩種方式:

  • cin >> a >>b;
  • a.operator(b.operator(cin));

過載函數也支援const限定符。

  • 類介面
  • 保持介面的完整性
  • 保持介面的最小化
  • 提供建構函式
  • 支援(禁止)拷貝
  • 使用型別來提供完善的參數檢查
  • 識別不可修改的成員函數
  • 在解構函式中釋放所有資源

使用一個static 來限定一個類物件,使得整個程式中都只有一個拷貝。

拷貝
只要不特別宣告,編譯器會預設進行逐成員拷貝
一個類內成員在宣告時初始化被稱爲類內初始化

const成員函數
表明此函數可以在一個常數物件上呼叫。

輔助函數
是一種設計思想,用來完成一些簡單並且頻繁呼叫的工作,或者是大函數中的小函數。通常使用類物件作爲參數,並用一個命名空間來建立一個大規模的類。

陣列
可以定義爲

  • 全域性變數(通常是糟糕的決定)
  • 區域性變數(有侷限性)
  • 函數參數(陣列不知道自身大小)
  • 類成員(很難初始化)
  • 一個具名陣列在編譯時必須設定大小。
  • 沒有範圍檢查。

+,-,+=,-=,++,–可以用來作爲指針運算子。
將陣列名退化成指針傳遞可以避免大量的數據傳遞,定義一個指針不要期望能將它作爲陣列使用。

輸入輸出流

  • 輸入和輸出
    大多數現代操作系統都將I/O處理細節放在驅動中,通過一個I/O庫存取驅動進行輸入和輸出操作。

所有輸入輸出都可以看做是位元組流與字元流的相互轉化,由輸入輸出庫處理。

要進行輸入、輸出需要:

  • 建立一個指向恰當數據源和數據目的的I/O流
  • 從這些流讀取數據或寫入數據

ostream

  • 從記憶體中將不同類型的值轉換成字元序列存入緩衝區中等待重新整理
  • 將這些字元發送到輸出源

istream

  • 從輸入源將字元序列載入緩衝區,重新整理後將通過I/O庫轉換成對應的值儲存在記憶體中
  • 從輸入源獲取字元

緩衝區這一數據結構用來儲存提交給I/O流的數據,並通過它與操作系統通訊,緩衝區可以提高I/O效率。

輸出流的主要目標就是生成可供人們閱讀的數據形式,輸出流可以格式化文字來滿足需求,輸入流則可以獲取輸出流的資訊並進行格式化(可選)輸出。

ostream將記憶體中的物件轉換成位元組流並儲存到記憶體。

istream將從記憶體獲取位元組流並轉換成物件存入記憶體。

當一個檔案流離開作用域會被隱式關閉,與之關聯的緩衝區被重新整理,也就是將緩衝區中的內容寫入檔案。

儲存媒介和儲存的數據不會影響到流的操作,這就是檔案和流抽象層的好處。
用來改變流行爲的關鍵字被稱爲操縱符。

C++標準庫和很多UDT都基於作用域程式設計風格,也就是離開作用域資源控制代碼會被隱式解構。

面向二進制和字元I/O時要放棄使用 <<>>,因爲這兩個操作符預設將值轉換成字元序列,例如:「ascd」=‘a’,‘s’,‘c’,‘d’ 123=‘1’,‘2’,‘3’

流狀態

  • good()
  • eof()
  • fail()
  • bad()

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
}

泛化

  • 如果型別T沒有預設值,我們如何處理X< T >
  • 當元素使用完畢,我們如何確保被銷燬?

我們作爲模板的實現者就需要考慮這些問題,爲了處理這些問題,可以設定一個使用者選項,以便我們需要一個預設值時能夠指定使用什麼值。

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語句在拋出錯誤時不會造成資源泄露。

  • 強保證: 如果一個函數提供了基本保證,還具有----在拋出異常後不會破壞其他數據。

  • 無拋出保證: 除非可以保證所寫程式碼不會拋出異常併產生錯誤,否則不會寫出滿足前面兩點的程式碼。

STL

C++標準庫爲處理數據序列提供了一個框架,稱爲STL。STL是標準模板庫的簡稱。STL是標準的一部分,它提供了容器和通用演算法,因此我們可以稱vector這類物件爲STL一部分,標準庫其他部分,如IOSTREAM,c標準字元處理常式等不屬於STL。
計算過程主要是這兩個方面:

  • 計算
  • 數據
    要完成真正的任務兩要同時兼顧兩者。如果對大量數據不進行處理,則雜亂的數據是沒有意義的。相反,我們可以隨時進行計算,但是隻有與實際數據相關聯後,才能 纔能避免枯燥的無意義計算,而且「計算部分」要優雅的與」數據部分「進行互動。

當我們想知道如何處理數據(演算法)時,並沒有提到數據是如何儲存的。很顯然我們需要輸入流,向量等數據結構來儲存數據,但是我們不必瞭解實現細節。重點是這些值或物件,我們可以對他們進行什麼操作或如何存取。

STL不僅可以看作一個功能強大的庫,更應該看作一個兼顧靈活性與效能的函數庫設計典範。
我們主要遇到的問題是:

  • 數據的型別
  • 數據的儲存方式
  • 對數據如何處理

通過泛化程式碼可以解決上述問題,例如有如下的解決策略:

  • 收集數據並裝入容器
  • 組織數據
  • 提取數據
  • 修改容器
  • 進行簡單的數值運算
    我們在完成上述問題時要避免陷入各種細節:各種容器之間的差別,各種數據的存取方法以及各種數據型別差別。
    泛化程式碼後可以實現這些:
    使用Int和使用double沒有區別
    使用vector< int > 和 list < double > 沒有差別
    我們希望能夠將任何操作作用於任何容器,只有當需要新功能才編寫其他程式碼,不必考慮新容器。

序列和迭代器
序列是STL核心概念,從STL角度來看,數據集合就是一個序列。迭代器可以標識序列中的元素的物件。

迭代器:
是一種可以標示序列中元素物件,我們可以按照如下方式來看待一個序列:

  • 可以指向序列中的某個元素(或序列末端元素之後的位置)
  • 可以使用運算子計算
  • 可以直接操作迭代器指向的物件
  • 可以利用操作符移動迭代器

迭代器不僅僅可以是指針,比如可以定義一個邊界檢查迭代器,當試圖指向[begin,end)之外,則會拋出一個異常。把一個迭代器作爲一個類可以帶給我們很大的靈活性。
我們可以利用迭代器實現演算法與數據的連線,程式設計師瞭解迭代器的使用方法(不需要知道實際如何儲存數據),數據提供方向使用者提供相應迭代器而不是數據儲存的資訊。這樣的數據結構使得演算法和容器之間保持了很好的獨立性,但他們都知道由一對迭代器定義的序列。

將迭代器作爲一個嵌入類的原因在於:沒有任何理由將迭代器的型別實現爲全域性類,每一種容器都有相應的迭代器,這樣就可以同時使用兩個以上的容器。

  • 介面庫
    • 減少庫的使用複雜度
    • 經過修改可以將原始碼運用於同種型別的不同庫(使用FLTK編寫的程式碼要想直接移植到QT上只需要修改介面庫的實現,不需要修改原始碼)
    • 可以很方便的進行擴充套件和新增新特性(可以使用條件編譯實現)
      用來隱藏庫介面和其狀態的實現細節,例如:
      FLTK中有表示顏色的Fl_Color,介面庫可以實現一個Color類,我們隱藏了FLTK將int值表示顏色的細節,將Fl_Color(一個enum)的值對映到Color(enum),則需要使用顏色可以直接使用Color而使用者無需瞭解FLTK庫的Color,使用red相當於使用Fl_Color::red。

介面庫只是在類的實現細節中使用了FLTK介面,而沒有將它暴露給使用者,任何使用者都無需瞭解如何呼叫或使用庫函數,這樣當需要其他的庫只需修改相應的介面,而無需修改使用者程式碼。

如何設計一個類

以GUI介面庫爲例

  • 設計原則
    我們的GUI介面類設計原則是什麼?
    首先考慮這是一個什麼類別的問題?
    什麼是」設計原則「?爲什麼要考慮這些設計原則?
    爲什麼要考慮這些而不是直接實現圖形這個重要的問題?
  • 型別
    我們所關注的是如何爲(像我們一樣的程式)提供一組基本的應用程式概念或工具。程式設計理想是用程式碼直接描述應用領域概念,如果你理解應用領域就能理解程式碼,反之亦然。例如:
  • WINDOW-------一個由操作系統負責管理的視窗
  • LINE-----一條畫在螢幕上的線
  • Shape所有形狀的統稱(以我們的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),派生類要控制如何新增點,例如:

  • 一些派生類不允許新增任何point,因爲新增沒有任何意義。例如一個三角形新增一個點意味着什麼?
  • Lines只允許新增成對的點
  • Open_polyline允許新增任意個
    將add()設定爲protedcted,即只有派生類可以新增,而其他任何人都無法新增。
    增加存取說明符有什麼價值呢?基本作用是保護類的描述不會被設計者以不可預見的方式進行更改,從而可以寫出更好的類,這就是所謂的「不變式」。
    如果我們需要拷貝不同類型的物件,而預設拷貝被禁止了,則需要顯式的函數來完成這項工作,通常實現clone()這個函數。只有當成員讀取函數能充分表達夠好函數需要什麼內容時,纔可以編寫出clone()。