C++物件導向程式設計_Part1

FANG_YANG發表於2019-05-28

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

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

轉發請註明github和原文地址,謝謝~

C++歷史

談到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++的組成

1557455342813

C++ 與 C 的資料和函式區別

1557455426279

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

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

1557455877512

基於物件與物件導向的區別

基於物件(Object Based):面對的是單一class的設計

物件導向(Object Oriented):面對的是多重classes 的設計,classes 和classes 之間的關係。

顯然,要寫好物件導向的程式,先基於物件寫出單個class是比不可少的。

C++類的兩個經典分類

一個是沒有指標的類,比如將要寫的complex類,只有實部和虛部,另一個就是帶有指標的類,比如將要寫的另一個類string,資料內部只有一個指標,採用動態分配記憶體,該指標就指向動態分配的記憶體。

標頭檔案防衛式宣告

1557456561799

從這開始介紹complex類,首先是防衛式宣告,與c語言一樣,防止標頭檔案重複包含,上面是經典寫法,還有一個# pragma once的寫法,兩者的區別可以參考這篇部落格

標頭檔案的佈局

1557457075841

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

類的宣告

1557457377946

這裡的complex類是侯捷老師從c++標準庫中擷取的一段程式碼,足夠說明問題,complex類主體分為public和private兩部分,public放置的是類的初始化,以及複數實虛部訪問和運算操作等等。private中主要防止類的資料,目的就是要隱藏資料,只暴露public中的介面,private中有double型別的實虛部,以及一個友元函式,這個友元函式實現的是複數的相加,將用於public中的+=操作符過載中,在public中,有四個函式,第一個是建構函式,目的是初始化複數,實虛部預設值為0,當傳入實虛部時,後面的列表初始化會對private中的資料進行初始化,非常推薦使用列表初始化資料。第二個是過載複數的+=操作符,應該系統內部沒有定義複數運算操作符,所以需要自己過載定義。第三個和第四個是分別訪問複數的實部和虛部,可以看到在第一個大括號前面有一個const,這個原因將在後面講述(加粗提醒自己),只要不改變成員資料的函式,都需要加上const,這是規範寫法。

類别範本簡介

1557458654337

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

內聯(inline)函式

1557459096287

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

訪問級別

1557463294773

這裡上面說過,private內部的函式和成員變數是不能被物件呼叫的,可以通過public提供的介面對資料進行訪問。

函式過載

1557463840516

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

建構函式的位置

1557469726240

一般情況下,建構函式都放在public裡面,不然外界無法初始化物件,不過也有例外的,有一種單例設計模式,就將建構函式放入在private裡面,通過public靜態(static)函式進行生成物件,這個類只能建立一份物件,所以叫單例設計模式

1557470130720

引數傳遞

1557470685958

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

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

返回值傳遞

1557471481557

與引數傳遞一樣,返回值傳引用速度也會很快,但有一點是不能傳引用的,如果你想返回的是函式內的區域性變數,傳引用後,函式所分配的記憶體清空,引用所指的區域性變數也清空了,空指標出現了,這就很危險了。(引用本質上就是指標,主要用在引數傳遞和返回值傳遞)

友元

1557472309252

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

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

1557473149205

操作符過載(一),this, cout

1557473358698

上面介紹的__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這樣的程式。

1557473691604

操作符過載(二)非成員函式,無this,臨時物件

1557474432766

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

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

Big Three ---string class begin

1557477552802

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

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

1557477761856

由於字串不像複數那樣固定大小,而是可大可小,所以在實現string類的時候,私有資料是一個指標,指向動態分配的char陣列,這樣就可以實現類似動態字串大小。這個小章節叫big three,這裡的big three分別是,拷貝構造(String(const String & str) ),拷貝賦值(String& operator=(const String& str)),以及解構函式( ~String()) 。為什麼要有big three,這個馬上就會介紹。

建構函式與解構函式

1557478475362

在建構函式中,如果沒有傳入字串,則string申請動態分配一個char[1], 指向的就是'\0',也就是空字元,如果傳入的是“hello”, 則動態分配“hello”的長度再加一(一代表結束識別符號'\0'),都是用string內部的指標指向動態分佈的記憶體的頭部。為什麼多了一個解構函式呢?在complex類為啥沒有呢?這是因為complex中沒有進行動態分配記憶體,在複數死亡後,它所佔用的記憶體全部釋放,完全ok,但string類動態分配了記憶體,這份記憶體在物件的外部,不釋放記憶體的話,在物件死亡後依然存在,這就造成記憶體洩漏,所以需要構建一個解構函式,在物件死亡釋放動態分配的記憶體。動態分配使用的時new命令,返回的是分配出來的記憶體的首地址,釋放動態分配記憶體使用delete命令,如果分配的是陣列物件,則需要在delete後加上[],如果是單個,直接delete指向的指標即可。上面就有兩種情況的例項。

拷貝構造與拷貝賦值

1557479620018

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

1557480221092

首先是拷貝構造,由於是建構函式一種,跟之前的建構函式一樣,需要分配一塊記憶體,大小為要拷貝的string的長度+1,然後使用C語言自帶的strcpy進行逐個賦值。

1557481099332

上面這個拷貝賦值,首先檢查是不是自我賦值,只要有這種情況發生,就要考慮,自我賦值則直接返回this所指的物件就可以了,如果不是自我賦值,則刪除分配的記憶體,重新分配記憶體,長度為傳入字串的長度+1,同理使用strcpy函式進行逐個賦值。

1557481941108

自我賦值的檢查很重要,沒有自我檢查,就會發生上面的情況,一執行程式的第一句話,記憶體就釋放了,指標就又懸空了,不確定行為產生。

1557482417743

string剩餘一點放到這裡面,列印直接呼叫get_c_str成員函式就可以,返回指標,os會遍歷它所指向的記憶體,列印出字串,遇到'\0'終止。

生命期——堆,棧,靜態,全域性

1557487075666

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

1557487606126

1557487638928

上面的c2和c3分別是靜態物件和全域性物件,作用域為整個程式。以下是它們四個的記憶體分佈,更具體的細節可以參考這篇文章

1557487724501

重探new與delete

1557492888491

1557492926893

可以到使用new命令動態分配記憶體,主要有以下三步,首先分配要構建物件的記憶體,返回的是一個空指標,然後對空指標進行轉型,轉成要生成物件型別初始化給指標,然後指標呼叫建構函式初始化物件。

1557493242994

1557493258187

可以看到delete操作可以分為兩步,首先通過解構函式釋放分配的記憶體,然後通過操作符delete(內部呼叫free函式)釋放物件記憶體。

探究動態分配過程的記憶體塊

1557493966655

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

動態分配array需要注意的問題

1557494889736

上面是動態分配記憶體,生成complex類的陣列以及string類的陣列的記憶體塊圖,與上面類似,不過這裡多了一個長度的位元組,都為3,標記物件的個數。

1557495133404

上面說明的是,如果分配的是動態物件陣列,就一定要在delete後面加上[]符號,不然就無法完全釋放動態分配的記憶體。array new一定要搭配array delete

part1到此結束。

相關文章