Bo2SS

Bo2SS

1 類和物件

課程內容#

類型和變數#

可理解為類和物件的別名

  • image-20210723211429904
  • C++ 中只規定了類型的最小位數,所以有的編譯器可以實現更多位數的

類型#

= 類型數據 + 類型操作

  • 例如:int=4 字節大小的數據 + 基本操作(+-*/%),可聯想數據結構,int、double 類型本質都是數據結構
  • 類型數據 + 類型操作 ➡️成員屬性 + 成員方法
    • 可理解為加強版的 C 語言 struct(可以放屬性,但不能放方法)

訪問權限

先區分類內和類外的概念

訪問權限在類內設置,它控制的是類外能不能訪問類內

  • public:公共
    • 類內和類外都可以訪問它
  • private:私有
    • 只有類內的方法可以訪問它
  • protected:受保護
    • 除了類內,繼承的類內也可以訪問它
  • friendly:友元
    • 它修飾的函數可以訪問類內的私有成員和受保護成員

構造函數和析構函數#

物件的生命週期:構造→使用→析構

三種基本的構造函數#

聯想局部變數的初始化,物件也需要初始化

構造函數類型使用方式⚠️注意
默認構造函數People bob;
原型:People ();
1、零參構造函數
2、編譯器自動生成的
轉換構造函數People bob("DoubleLLL");
原型:People (string name);
1、一個參數的有參構造函數
2、該參數傳遞給類的成員變量,且不是本類的 const 引用
[PS] 有點像隱式的類型轉換
拷貝構造函數People bob(hug);
原型:People (const People &a);
1、特殊的有參構造函數,傳入的是本類的物件
2、與賦值運算符 "=" 不等價
[PS] 處理為 const & 更方便

析構函數#

銷毀物件

原型:~People ();

⚠️注意:

1、沒有參數,也沒有返回值

2、資源回收時使用

小結#

都沒有返回值,函數名與類名一致

  • 在工程開發中,構造函數和析構函數的功能會設計得非常簡單
    • ❓為什麼不在構造函數中進行大量的資源申請?
    • 原因:構造函數的 Bug,編譯器較難察覺
    • 解決方式:偽構造函數、偽析構函數;工廠設計模式
  • [+] 移動構造函數(另一個關鍵構造函數,後續學習)
    • 來自 C++ 11 標準 ——C++ 重新歸回神壇的標準
    • 在此之前,STL 的性能低下,因為 C++ 的語言特性不好,它沒有區分左值、右值概念,使得 STL 使用過程中發生的大量拷貝操作,尤其是深拷貝操作,會大大影響性能
    • 在此之後,有了右值的概念,引入了移動構造函數

返回值優化(RVO)#

編譯器默認開啟了 RVO

引入#

物件a通過fun()返回值進行構造

image-20210723211452133

輸出結果:

image-20210723211500226
  • 理論上:應該輸出 1 次 transform 和 2 次 copy
    • 詳見具體分析👇
  • 實際上:沒有輸出 copy,且 a.x 的值為物件 temp 中的 x 值,即局部變數 temp 的地址直接使用了物件 a 的地址(先開辟的物件 a 的數據區,見下)
    • temp 更像是一個引用
    • 存在編譯器優化,即返回值優化 RVO🌟

具體分析#

物件初始化過程:1、開辟物件數據區➡️2、匹配構造函數➡️3、完成構造

  • 了解物件初始化過程後,再分析上面代碼中的構造過程:
    • image-20210723211510540
    • 首先,開辟物件 a 的數據區
    • 然後,進入 func 函數,開辟物件 temp 的數據區,通過「轉換構造」初始化局部變數 temp——A temp (69);
    • 再者,通過「拷貝構造」將 temp 拷貝給臨時匿名物件,並銷毀物件 temp——return temp;
    • 最後,通過「拷貝構造」將臨時匿名物件拷貝給 a,銷毀臨時匿名物件 ——Aa= func ();
    • 👉由此可見,過程包含 1 次轉換構造、2 次拷貝構造
  • 編譯器優化
    • 第 1 次優化,取消第 1 次「拷貝構造」,直接將 temp 拷貝給 a(Windows 下的優化)
    • 第 2 次優化,取消第 2 次「拷貝構造」,直接是 temp 指向 a(Mac、Linux 下的優化)

關閉 RVO 後#

通過g++ -fno-elide-constructors編譯源文件,即可關閉RVO

image-20210723211519691
  • 出現了兩次額外的「拷貝構造」,具體分析見上

注意點#

  • 編譯器本質上是通過替換 this 指針實現 RVO 的
  • 因為編譯器一般會對拷貝構造進行優化,所以在工程開發中,不要改變拷貝構造的語義
    • 即:拷貝構造中只做拷貝操作,而不要做其它操作
    • 如:拷貝構造中,對拷貝的屬性加 1,就不符合拷貝構造的語義,編譯器把拷貝構造優化掉後,結果與優化前不一致

+ 賦值運算的優化#

也存在RVO——優化了1次拷貝構造,即將局部變數拷貝給臨時匿名物件

image-20210723211527733
  • 添加了紅框部分代碼
  • 優化前結果:
    • image-20210723211535203
    • 1 次「轉換構造」 + 1 次「拷貝構造」
  • 優化後結果:
    • image-20210723211540883
    • 1 次「拷貝構造」

+ 拷貝構造函數的調用分析#

多種寫法下,拷貝構造函數是如何調用的?

場景:類 A 中包含一個自定義類 Data 的物件 d,紅框為添加的代碼

image-20210723211549347

「主要關注第 29 行;關閉 RVO 編譯,否則會跳過拷貝構造函數」

  1. 自定義拷貝構造函數,並且對每個成員屬性都進行了顯式拷貝,即第 29 行不變
  • image-20210723211557942
  • 在構造物件 d 時,會調用 Data 類的拷貝構造函數
  1. 自定義拷貝構造函數,不對每個成員屬性進行顯式拷貝,即刪去 ",d (a.d)"
  • image-20210723211603337
  • 在構造物件 d 時,會調用 Data 類的默認構造函數(自定義時沒顯式拷貝,則匹配默認構造)
  1. 不自定義拷貝構造函數,編譯器自動為其添加默認的拷貝構造函數,即刪去第 29~31 行
  • image-20210723211610555
  • 在構造物件 d 時,會調用 Data 類的拷貝構造函數(編譯器默認的)

結論

❗️要想達到預期的結果,在自定義拷貝構造函數時,應該對每個成員屬性進行顯式拷貝

  • 如果自定義的拷貝構造函數什麼都不寫,那該函數就什麼都不會做

其它知識點#

引用#

引用就是其綁定物件的別名

  • ❗️引用在定義的時候就需要初始化,即綁定物件,如:
    • People a;
    • 定義時就初始化:People &b = a;
    • 否則:People &b; b = c; 會發生歧義 —— 綁定物件還是賦值?

類屬性與方法#

加有 static 關鍵字
區別於成員屬性(每一個物件特有的)、成員方法(this 指針指向當前物件)

  • 類屬性:該類所有物件統一的屬性
    • 全局唯一、共享
    • 例如:全人類的數量 —— 人類中所有物件的數量
  • 類方法:不單獨屬於某個物件的方法
    • 不和物件綁定,無法訪問 this 指針
    • 例如:測試某個高度是否為合法身高

const 方法#

不改變物件的成員屬性,不可調用非 const 成員函數

  • 提供給 const 物件使用(其任何屬性都不能被改變)

⚠️:

mutable:可變的,其修飾的變數在 const 方法中可變

default 和 delete#

默認函數的控制,C++ 11

  • default:顯式地使用編譯器默認提供的規則
    • 僅適用於類的特殊成員函數(默認構造函數、析構函數、拷貝構造函數、拷貝賦值運算符),且該特殊成員函數沒有默認參數
    • ❗️該特性沒有功能上的意義,這是 C++ 的設計哲學:關注可讀性、可維護性等
      • 理解這種理念,可以提高自己在 C++ 上的審美標準
  • delete:顯式地禁用某個函數

struct 和 class#

  • struct
    • 默認訪問權限:public(黑名單機制,需要顯式定義 private 成員)
    • 也是用來定義類的,有成員屬性和成員方法,要與 C 中區分開來
  • class
    • 默認訪問權限:private(白名單機制:需要顯式定義 public 成員)

❓:C++ 為什麼要保留 struct 關鍵字?默認權限為什麼是 public?

  • 都是為了兼容 C 語言,可以減小推廣難度

PS:前端語言 JavaScript 為了減小推廣難度,從名稱蹭 Java 的熱度,本質與 Java 無關

代碼演示#

類的示例#

image-20210723211620159
  • 類中屬性和方法的聲明和定義,建議分開
  • this 指針只在成員方法中使用,它指向當前物件的地址

簡單實現 cout#

image-20210723211627367
  • cout 是一個物件,是一個高級變數
    • 返回其自身引用可以實現連續 cout,使用引用的原因後續理解
    • 字符串需要使用 const 類型變數接收,否則會警告,因為是字符串是字面量
  • 命名空間的精髓:相同的物件名可以存在不同的命名空間中

構造函數和析構函數#

image-20210723211634762 image-20210723211642463

運行結果:

image-20210723211648649

1)析構順序的探討#

  • 圖片
  • 構造順序:物件 a,物件 b
  • 析構順序:物件 b,物件 a
  • ❓為什麼析構函數的調用順序是反過來的?是因為編譯器產生的特例,還是一個正常的語言特性?👉語言特性
    • 物件 b 的構造可能依賴物件 a 的信息➡️在析構的時候物件 b 也可能會用到物件 a 的信息➡️物件 b 要先於物件 a 析構
    • ❗️誰先構造,它就後析構
    • PS
      • 這與物件放在堆空間還是棧空間無關,實驗表明,析構順序都是反過來的
      • 可以認為是一種拓撲序

2)轉換構造函數#

  • ❓為什麼叫做轉換構造函數(單一參數的構造函數)
    • 將一個變數轉化成了該類型的物件
  • 🌟a=123 涉及運算符重載:隱式類型轉換➡️賦值➡️析構,詳解見代碼

3)拷貝構造函數#

1、加引用:防止無限調用拷貝構造

  • ❗️如果拷貝構造函數為:A (A a) {},則 A b = a; 時,
  • 因為 [形參 a] 不是引用(值傳遞),所以需要先將 [物件 a] 拷貝到 [形參 a] 中生成臨時物件
  • 此時,又會發生拷貝構造,而這個拷貝構造同樣會經歷上述過程
  • 從而無限遞歸
  • PS:引用不產生任何拷貝行為,比指針更方便

2、加 const:防止 const 類型物件使用非 const 類型的拷貝構造,報錯

  • 也防止被拷貝的物件被修改

注:

  • 在定義物件時,"=" 調用的是拷貝構造函數,而不是賦值運算,例如,A b = a

4)思考#

  • 物件是什麼時候完成了構造?
    • 「參考代碼,以默認構造函數為例」
    • 功能上的構造 [邏輯上]:到第 46 行,構造函數表面上執行完成了
    • 編譯器的構造[實際上]:到第 39 行,就已經可以調用物件成員了❗️🌟
  • 通過思考部分的代碼可以理解:
    • 場景:在類 Data 中添加有參構造函數,使編譯器幫其添加的默認構造函數被刪除
    • 過程:在生成類 A 物件時,成員屬性 Data 類物件 p、q 需要已經完成構造,而此時 Data 類的默認構造已經被刪除
    • 結果
      • 如果不使用初始化列表初始化 p、q,則報錯
      • 如果使用初始化列表,則可行,初始化列表屬於編譯器所謂的構造
    • ❗️這說明編譯器的構造是在函數聲明後(第 39 行)就完成了
  • ⚠️:
    • 編譯器會默認添加默認構造函數和拷貝構造函數
    • 構造行為都應該放在編譯器所謂的構造裡,如使用初始化列表

+)左值引用#

  • 後續學習

+)友元函數#

  • 在類內聲明(同時保證是類的管理者批准的)
  • 在類外實現,本質是類外的函數,但可以訪問類內的私有成員屬性

深拷貝、淺拷貝#

拷貝物件:數組

image-20210723211710169
  • 編譯器默認添加的拷貝構造為淺拷貝
    • 對於指針,只拷貝地址,所以對拷貝後的物件的修改,會修改原物件
  • 自定義深拷貝版的拷貝構造函數
    • 對於指針,拷貝其指向地址的值
    • PS:構造函數只有在初始化時才被調用,不存在自己拷貝自己的行為

⚠️:

  • 為了適配重載 "<<" 時的 const 參數,需要實現 const 版的 "[]" 重載
  • Array 類裡的 end 和數組結尾數據沒關係,他只是用來監控數組越界的情況

new、malloc 差別分析#

image-20210723211717190

運行結果:

image-20210723211724784
  • malloc 和 new 都可以申請空間,分別對應 free 和 delete(如果是數組,則為 delete [])銷毀空間
  • new 會自動調用構造函數,對應的 delete 會自動調用析構函數;而 malloc 和 free 都不會
  • malloc + new 可以實現原地構造,常用於深拷貝,其中,new 還可以對應不同類的構造函數

類屬性與方法、const、mutable#

image-20210723211734657
  • 類屬性:帶 static 在類內聲明,不帶 static 在類外初始化
  • 類方法:可以通過物件或類名兩種方式調用它
    • 因此,物件調用的方法不一定是成員方法,還可能是類方法
  • const:const 物件只能調用 const 方法,const 方法內只能調用 const 方法
  • mutable:其修飾的變數可在 const 方法中改變

default、delete#

功能需求:使某個類下的物件,不能被拷貝

image-20210723211742795
  • 禁用拷貝構造函數和賦值運算符
    • 拷貝構造函數:設置為 = delete,或者,將其放在 private 權限中
    • 賦值運算符重載函數:同樣設置為 = delete(永遠不能使用賦值運算),或者,將其放在 private 權限中(只有類內方法可以使用賦值運算)
    • PS:賦值運算符要同時考慮 const 版和非 const 版
  • ❓要真正實現物件不能被拷貝,應該將拷貝構造函數和賦值運算符重載都設置為 = delete,否則類內方法還是可以拷貝該物件

附加知識點#

  • 構造函數後的花括號
    • 加花括號,表示是一個函數實現
    • 不加花括號,則只是一个函數聲明,還需要寫一個專門的實現

思考點#

  • 要關注編譯器默認做的很多事情,這是 C++ 複雜的地方

Tips#

  • C++
    • 學習方法:按照編程範式分門別類地學習
    • 學習重點:程序的處理流程 [遠比 C 複雜]

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。