天天看點

C++面向對象程式設計_Part1

這篇文章是侯捷老師的C++面向對象進階程式設計的學習筆記的part1部分,主要介紹基于對象的設計,講述兩個經典的類complex類和string類。

C++筆記主要參考侯捷老師的課程,這是一份是C++面向對象程式設計(Object Oriented Programming)的part1部分,這一部分講述的是以良好的習慣構造C++類,基于對象(object based)講述了兩個c++類的經典執行個體——complex類和string類。看這份筆記需要有c++和c語言的基礎,有一些很基礎的不會解釋。

圖檔無法檢視請到GitHub上浏覽,點選連結

markdown檔案可以從GitHub中下載下傳,連結:https://github.com/FangYang970206/Cpp-Notes, 推薦使用typora打開。

轉發請注明github和原文位址,謝謝~

目錄

  • C++曆史
  • C++的組成
  • C++ 與 C 的資料和函數差別
  • 基于對象與面向對象的差別
  • C++類的兩個經典分類
  • 頭檔案防衛式聲明
  • 頭檔案的布局
  • 類的聲明
  • 類模闆簡介
  • 内聯(inline)函數
  • 通路級别
  • 函數重載
  • 構造函數的位置
  • 參數傳遞
  • 傳回值傳遞
  • 友元
  • 操作符重載(一),this, cout
  • 操作符重載(二)非成員函數,無this,臨時對象
  • Big Three ---string class begin
  • 構造函數與析構函數
  • 拷貝構造與拷貝指派
  • 生命期——堆,棧,靜态,全局
  • 重探new與delete
  • 探究動态配置設定過程的記憶體塊
  • 動态配置設定array需要注意的問題

談到c++,課程首先過了一周遊史,c++是建立在c語言之上,最早期叫c++ with class,後來在1983年正式命名為c++,在1998年,c++98标志c++1.0誕生,c++03是c++的一次科技報告,加了一些新東西,c++11加入了更多新的東西,标志着c++2.0的誕生,然後後面接着出現c++14,c++17,到現在的c++20。

C++面向對象程式設計_Part1

C++面向對象程式設計_Part1

在c語言中,資料和函數是分開的,構造出的都是一個變量,函數通過變量進行操作,而在c++中,生成的是對象,資料和函數都包在對象中,資料和函數都是對象的成員,這是說得通,一個對象所具有的屬性和資料應該放在一塊,而不是分開,并且C++類通常都是通過暴露接口隐藏資料的形式,讓使用者可以調用,更加安全與便捷。

下圖為part1兩個類的資料和函數分布,可以看看:

C++面向對象程式設計_Part1

基于對象(Object Based):面對的是單一class的設計

面向對象(Object Oriented):面對的是多重classes 的設計,classes 和classes 之間的關系。

顯然,要寫好面向對象的程式,先基于對象寫出單個class是比不可少的。

一個是沒有指針的類,比如将要寫的complex類,隻有實部和虛部,另一個就是帶有指針的類,比如将要寫的另一個類string,資料内部隻有一個指針,采用動态配置設定記憶體,該指針就指向動态配置設定的記憶體。

C++面向對象程式設計_Part1

從這開始介紹complex類,首先是防衛式聲明,與c語言一樣,防止頭檔案重複包含,上面是經典寫法,還有一個

# pragma once

的寫法,兩者的差別可以參考這篇部落格。

C++面向對象程式設計_Part1

首先是防衛式聲明,然後是前置聲明(聲明要建構的類,這個例子中還有友元函數),類聲明中主要寫出這個類的成員資料以及成員函數,類定義部分則是将類聲明中的成員函數進行實作。

C++面向對象程式設計_Part1

這裡的complex類是侯捷老師從c++标準庫中截取的一段代碼,足夠說明問題,complex類主體分為public和private兩部分,public放置的是類的初始化,以及複數實虛部通路和運算操作等等。private中主要防止類的資料,目的就是要隐藏資料,隻暴露public中的接口,private中有double類型的實虛部,以及一個友元函數,這個友元函數實作的是複數的相加,将用于public中的+=操作符重載中,在public中,有四個函數,第一個是構造函數,目的是初始化複數,實虛部預設值為0,當傳入實虛部時,後面的清單初始化會對private中的資料進行初始化,非常推薦使用清單初始化資料。第二個是重載複數的+=操作符,應該系統内部沒有定義複數運算操作符,是以需要自己重載定義。第三個和第四個是分别通路複數的實部和虛部,可以看到在第一個大括号前面有一個const,這個原因将在後面講述(加粗提醒自己),隻要不改變成員資料的函數,都需要加上const,這是規範寫法。

C++面向對象程式設計_Part1

由于我們不光是想建立double類型的複數,還想建立int類型的複數,愚蠢的想法是在實作一遍int類的complex,這時候類模闆派出用場了,模闆是一個很大的話題,侯捷老師有一個專門課程講模闆,筆記也會更新到那裡。模闆可以隻寫一份模闆代碼,需要生成不同類型的class,編譯器會自動生成,具體做法是在類定義最上方加入template ,然後講所有的double都換成T即可,在初始化的時候,在類的後面使用尖括号,尖括号中放入你想要生成的類型即可。

C++面向對象程式設計_Part1

内聯函數和普通函數的差別在于:當編譯器處理調用内聯函數的語句時,不會将該語句編譯成函數調用的指令,而是直接将整個函數體的代碼插人調用語句處,就像整個函數體在調用處被重寫了一遍一樣。是一種空間換取時間的做法,當函數的行數隻有幾行的時候,應該将函數設定為内聯,提高程式整體的運作效率。更加詳細的說明可以參考這篇文章. (補充:在類的内部實作的函數編譯器會自動變為inline,好像現在新的編譯器可以自動對函數進行inline,無需加inline,即使加了編譯器也未必真的會把函數變為inline,看編譯器的判斷)

C++面向對象程式設計_Part1

這裡上面說過,private内部的函數和成員變量是不能被對象調用的,可以通過public提供的接口對資料進行通路。

C++面向對象程式設計_Part1

c++中允許“函數名”相同,但函數參數需要不同(參數後面修飾函數的const也算是參數的一部分),這樣可以滿足不同類型參數的應用。上述中就有不同的real,不必擔心它們名字相同而反正調用混亂,相同函數名和不同參數,編譯器編譯後的實際名稱會不一樣,實際調用名并不一樣,是以在開始的函數名打了引号。另外,寫相同函數名還是要注意一下,比如上面有兩個構造函數,當使用complex c1初始化對象時,編譯器不知道調用哪一個構造函數,因為兩個構造函數都可以不用參數,這就發生沖突了,第二個構造函數是不需要的。

C++面向對象程式設計_Part1

一般情況下,構造函數都放在public裡面,不然外界無法初始化對象,不過也有例外的,有一種單例設計模式,就将構造函數放入在private裡面,通過public靜态(static)函數進行生成對象,這個類隻能建立一份對象,是以叫單例設計模式

C++面向對象程式設計_Part1

C++面向對象程式設計_Part1

參數傳遞分為兩種:pass-by-value和pass-by-reference

一條非常考驗你是否受過良好c++訓練就是看你是不是用pass-by-reference。傳值會配置設定局部變量,然後将傳入的值拷貝到變量中,這既要花費時間又要花費記憶體,傳引用就是傳指針,4個位元組,要快好多,如果擔心傳入的值被改變,在引用前加const,如果函數試圖改變,就會報錯。

C++面向對象程式設計_Part1

與參數傳遞一樣,傳回值傳引用速度也會很快,但有一點是不能傳引用的,如果你想傳回的是函數内的局部變量,傳引用後,函數所配置設定的記憶體清空,引用所指的局部變量也清空了,空指針出現了,這就很危險了。(引用本質上就是指針,主要用在參數傳遞和傳回值傳遞)

C++面向對象程式設計_Part1

友元函數是類的朋友,被設定為友元的函數可以通路朋友的私有成員,這個函數(do assignment plus)用來做複數加法的具體實作。第一個參數是複數的指針,這個會在this一節中進行說明。

另外還有一種情況很有意思,如下圖所示,複數c2可以通路c1的資料,這個也是可以的,這可能讓人感到奇怪,侯捷老師說了原因:相同類的各個對象互為友元。是以可以c2可以通路c1的資料。

C++面向對象程式設計_Part1

C++面向對象程式設計_Part1

上面介紹的

__doapl

函數将在操作符重載中進行調用,可以看到第一個參數是this,對于成員函數來說,都有一個隐藏參數,那就是this,this是一個指針,指向調用這個函數的對象,而操作符重載一定是作用在左邊的對象,是以+=的操作符作用在c2上,是以this指向的是c2這個對象,然後在

__doapl

函數中修改this指向c2的值。

另外,還記得上面說過

<<

運算符重載嘛,它作用的不是複數,而是ostream,這是處于使用者習慣的考量,作用複數的話将形成

complex<<cout

的用法,這樣很不習慣,用于ostream就跟平常使用的cout一樣,另外,下面這個函數傳回的引用,那麼就可以構成

cout << c2 << c1

這種連串列印的程式(與平常的習慣,

cout << c2

傳回的依然是cout的引用,又可以調用

<<

重載函數,如果不是引用,則會報錯,侯捷老師講到這,真感覺标準庫的設計真是厲害。另外,每次向os傳入值列印時,os的狀态會發生改變,是以os不能加const。上面複數的加法由于傳回的是引用,也可以構成

c3 += c2 += c1

這樣的程式。

C++面向對象程式設計_Part1

C++面向對象程式設計_Part1

由于使用者可能有多種複數的加法,是以要設計不同的函數滿足使用者的要求,由于帶有其他類型的參數,是以沒有放入complex類中,放在外面定義,這裡的有一個非常有趣的使用,傳回的直接是complex( xx, xx),沒見過呢,這個文法是建立一個臨時對象,這個臨時對象在下一行就消亡了,不過沒關系,我們已經把臨時對象的值傳到傳回值了。由于是臨時對象,是以傳回值不能是引用,必須是值。

好了,complex的相關細節寫得差不多,有些沒寫,上面都提到了,還有些操作符重載,與加法類似,不重複寫了。具體參考

complex.h

,下面進入string類的實作。

C++面向對象程式設計_Part1

與complex一樣,string類的整個實作分布如上圖,右邊的是測試的程式。

下面來看看string的縮小版實作:

C++面向對象程式設計_Part1

由于字元串不像複數那樣固定大小,而是可大可小,是以在實作string類的時候,私有資料是一個指針,指向動态配置設定的char數組,這樣就可以實作類似動态字元串大小。這個小章節叫big three,這裡的big three分别是,拷貝構造(String(const String & str) ),拷貝指派(String& operator=(const String& str)),以及析構函數( ~String()) 。為什麼要有big three,這個馬上就會介紹。

C++面向對象程式設計_Part1

在構造函數中,如果沒有傳入字元串,則string申請動态配置設定一個char[1], 指向的就是

'\0'

,也就是空字元,如果傳入的是

“hello”

, 則動态配置設定

“hello”

的長度再加一(一代表結束辨別符'\0'),都是用string内部的指針指向動态分布的記憶體的頭部。為什麼多了一個析構函數呢?在complex類為啥沒有呢?這是因為complex中沒有進行動态配置設定記憶體,在複數死亡後,它所占用的記憶體全部釋放,完全ok,但string類動态配置設定了記憶體,這份記憶體在對象的外部,不釋放記憶體的話,在對象死亡後依然存在,這就造成記憶體洩漏,是以需要建構一個析構函數,在對象死亡釋放動态配置設定的記憶體。動态配置設定使用的時new指令,傳回的是配置設定出來的記憶體的首位址,釋放動态配置設定記憶體使用delete指令,如果配置設定的是數組對象,則需要在delete後加上[],如果是單個,直接delete指向的指針即可。上面就有兩種情況的執行個體。

C++面向對象程式設計_Part1

complex類其實内部存在c++語言自身提供的拷貝構造和拷貝指派,不需要自己寫,因為沒有指針的類的資料指派無非就是值傳遞,沒有變化。但string類不一樣,上面的圖是很好的例子,因為使用的是動态配置設定記憶體,對象a和對象b都指向外面的一塊記憶體,如果直接使用預設的拷貝構造或者拷貝指派(例如将b = a),則是将b的指針指向a所指的區域,也就是a的動态配置設定記憶體的首位址,原來b所指向的記憶體就懸空了,于是發生記憶體洩漏,而且兩個指針指向同一塊記憶體,也是一個危險行為。是以帶有指針的類是不能使用預設的拷貝構造和拷貝指派的,需要自己寫。下面看看怎麼寫的。

C++面向對象程式設計_Part1

首先是拷貝構造,由于是構造函數一種,跟之前的構造函數一樣,需要配置設定一塊記憶體,大小為要拷貝的string的長度+1,然後使用C語言自帶的strcpy進行逐個指派。

C++面向對象程式設計_Part1

上面這個拷貝指派,首先檢查是不是自我指派,隻要有這種情況發生,就要考慮,自我指派則直接傳回this所指的對象就可以了,如果不是自我指派,則删除配置設定的記憶體,重新配置設定記憶體,長度為傳入字元串的長度+1,同理使用strcpy函數進行逐個指派。

C++面向對象程式設計_Part1

自我指派的檢查很重要,沒有自我檢查,就會發生上面的情況,一運作程式的第一句話,記憶體就釋放了,指針就又懸空了,不确定行為産生。

C++面向對象程式設計_Part1

string剩餘一點放到這裡面,列印直接調用get_c_str成員函數就可以,傳回指針,os會周遊它所指向的記憶體,列印出字元串,遇到

'\0'

終止。

C++面向對象程式設計_Part1

c1 便是所謂stack object,其生命在作用域(scope) 結束之際結束。這種作用域內的object,又稱為auto object,因為它會被「自動」清理。p所指的便是heap object,其生命在它被deleted 之際結束,是以要在指針生命結束之前對堆記憶體進行釋放。

C++面向對象程式設計_Part1
C++面向對象程式設計_Part1

上面的c2和c3分别是靜态對象和全局對象,作用域為整個程式。以下是它們四個的記憶體分布,更具體的細節可以參考這篇文章。

C++面向對象程式設計_Part1

C++面向對象程式設計_Part1
C++面向對象程式設計_Part1

可以到使用new指令動态配置設定記憶體,主要有以下三步,首先配置設定要建構對象的記憶體,傳回的是一個空指針,然後對空指針進行轉型,轉成要生成對象類型初始化給指針,然後指針調用構造函數初始化對象。

C++面向對象程式設計_Part1
C++面向對象程式設計_Part1

可以看到delete操作可以分為兩步,首先通過析構函數釋放配置設定的記憶體,然後通過操作符delete(内部調用free函數)釋放對象記憶體。

C++面向對象程式設計_Part1

上圖中就是vc建立complex類以及string類的記憶體塊圖,左邊兩個是complex類,長的那個是調試(debug)模式下的記憶體塊分布,短的那個是執行(release)模式下的記憶體塊分布,複數有兩個double,是以記憶體占用8個位元組,vc調試模式下,調試的資訊部分記憶體占用是上面灰色塊的32個位元組以及下面灰色塊的4個位元組,紅色的代表記憶體塊的頭和尾(叫cookie),占用八個位元組,合在一起是52個位元組,vc會以16個位元組對齊,是以會填充12位元組,對應的是pad的部分,另外,為了凸顯這是配置設定出去的記憶體,是以在頭尾部分,用最後一位為1代表該記憶體配置設定出去了,為0就是收回來了。執行模式下沒有調試資訊。string類類似分析。

C++面向對象程式設計_Part1

上面是動态配置設定記憶體,生成complex類的數組以及string類的數組的記憶體塊圖,與上面類似,不過這裡多了一個長度的位元組,都為3,标記對象的個數。

C++面向對象程式設計_Part1

上面說明的是,如果配置設定的是動态對象數組,就一定要在delete後面加上[]符号,不然就無法完全釋放動态配置設定的記憶體。array new一定要搭配array delete。

part1到此結束。