目錄html
C++筆記主要參考侯捷老師的課程,這是一份是C++面向對象編程(Object Oriented Programming)的part1部分,這一部分講述的是以良好的習慣構造C++類,基於對象(object based)講述了兩個c++類的經典實例——complex類和string類。看這份筆記須要有c++和c語言的基礎,有一些很基礎的不會解釋。c++
markdown文件能夠從GitHub中下載,連接:https://github.com/FangYang970206/Cpp-Notes, 推薦使用typora打開。git
轉發請註明github和原文地址,謝謝~github
談到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語言中,數據和函數是分開的,構造出的都是一個變量,函數經過變量進行操做,而在c++中,生成的是對象,數據和函數都包在對象中,數據和函數都是對象的成員,這是說得通,一個對象所具備的屬性和數據應該放在一塊,而不是分開,而且C++類一般都是經過暴露接口隱藏數據的形式,讓使用者能夠調用,更加安全與便捷。設計模式
下圖爲part1兩個類的數據和函數分佈,能夠看看:數組
基於對象(Object Based):面對的是單一class的設計安全
面向對象(Object Oriented):面對的是多重classes 的設計,classes 和classes 之間的關係。markdown
顯然,要寫好面向對象的程序,先基於對象寫出單個class是比不可少的。cookie
一個是沒有指針的類,好比將要寫的complex類,只有實部和虛部,另外一個就是帶有指針的類,好比將要寫的另外一個類string,數據內部只有一個指針,採用動態分配內存,該指針就指向動態分配的內存。
從這開始介紹complex類,首先是防衛式聲明,與c語言同樣,防止頭文件重複包含,上面是經典寫法,還有一個# pragma once
的寫法,二者的區別能夠參考這篇博客。
首先是防衛式聲明,而後是前置聲明(聲明要構建的類,這個例子中還有友元函數),類聲明中主要寫出這個類的成員數據以及成員函數,類定義部分則是將類聲明中的成員函數進行實現。
這裏的complex類是侯捷老師從c++標準庫中截取的一段代碼,足夠說明問題,complex類主體分爲public和private兩部分,public放置的是類的初始化,以及複數實虛部訪問和運算操做等等。private中主要防止類的數據,目的就是要隱藏數據,只暴露public中的接口,private中有double類型的實虛部,以及一個友元函數,這個友元函數實現的是複數的相加,將用於public中的+=操做符重載中,在public中,有四個函數,第一個是構造函數,目的是初始化複數,實虛部默認值爲0,當傳入實虛部時,後面的列表初始化會對private中的數據進行初始化,很是推薦使用列表初始化數據。第二個是重載複數的+=操做符,應該系統內部沒有定義複數運算操做符,因此須要本身重載定義。第三個和第四個是分別訪問複數的實部和虛部,能夠看到在第一個大括號前面有一個const,這個緣由將在後面講述(加粗提醒本身),只要不改變成員數據的函數,都須要加上const,這是規範寫法。
因爲咱們不光是想建立double類型的複數,還想建立int類型的複數,愚蠢的想法是在實現一遍int類的complex,這時候類模板派出用場了,模板是一個很大的話題,侯捷老師有一個專門課程講模板,筆記也會更新到那裏。模板能夠只寫一份模板代碼,須要生成不一樣類型的class,編譯器會自動生成,具體作法是在類定義最上方加入template
內聯函數和普通函數的區別在於:當編譯器處理調用內聯函數的語句時,不會將該語句編譯成函數調用的指令,而是直接將整個函數體的代碼插人調用語句處,就像整個函數體在調用處被重寫了一遍同樣。是一種空間換取時間的作法,當函數的行數只有幾行的時候,應該將函數設置爲內聯,提升程序總體的運行效率。更加詳細的說明能夠參考這篇文章. (補充:在類的內部實現的函數編譯器會自動變爲inline,好像如今新的編譯器能夠自動對函數進行inline,無需加inline,即便加了編譯器也未必真的會把函數變爲inline,看編譯器的判斷)
這裏上面說過,private內部的函數和成員變量是不能被對象調用的,能夠經過public提供的接口對數據進行訪問。
c++中容許「函數名」相同,但函數參數須要不一樣(參數後面修飾函數的const也算是參數的一部分),這樣能夠知足不一樣類型參數的應用。上述中就有不一樣的real,沒必要擔憂它們名字相同而反正調用混亂,相同函數名和不一樣參數,編譯器編譯後的實際名稱會不同,實際調用名並不同,因此在開始的函數名打了引號。另外,寫相同函數名仍是要注意一下,好比上面有兩個構造函數,當使用complex c1初始化對象時,編譯器不知道調用哪個構造函數,由於兩個構造函數均可以不用參數,這就發生衝突了,第二個構造函數是不須要的。
通常狀況下,構造函數都放在public裏面,否則外界沒法初始化對象,不過也有例外的,有一種單例設計模式,就將構造函數放入在private裏面,經過public靜態(static)函數進行生成對象,這個類只能建立一份對象,因此叫單例設計模式
參數傳遞分爲兩種:pass-by-value和pass-by-reference
一條很是考驗你是否受過良好c++訓練就是看你是否是用pass-by-reference。傳值會分配局部變量,而後將傳入的值拷貝到變量中,這既要花費時間又要花費內存,傳引用就是傳指針,4個字節,要快好多,若是擔憂傳入的值被改變,在引用前加const,若是函數試圖改變,就會報錯。
與參數傳遞同樣,返回值傳引用速度也會很快,但有一點是不能傳引用的,若是你想返回的是函數內的局部變量,傳引用後,函數所分配的內存清空,引用所指的局部變量也清空了,空指針出現了,這就很危險了。(引用本質上就是指針,主要用在參數傳遞和返回值傳遞)
友元函數是類的朋友,被設定爲友元的函數能夠訪問朋友的私有成員,這個函數(do assignment plus)用來作複數加法的具體實現。第一個參數是複數的指針,這個會在this一節中進行說明。
另外還有一種狀況頗有意思,以下圖所示,複數c2能夠訪問c1的數據,這個也是能夠的,這可能讓人感到奇怪,侯捷老師說了緣由:相同類的各個對象互為友元。因此能夠c2能夠訪問c1的數據。
上面介紹的__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
這樣的程序。
因爲使用者可能有多種複數的加法,因此要設計不一樣的函數知足使用者的要求,因爲帶有其餘類型的參數,因此沒有放入complex類中,放在外面定義,這裏的有一個很是有趣的使用,返回的直接是complex( xx, xx),沒見過呢,這個語法是建立一個臨時對象,這個臨時對象在下一行就消亡了,不過不要緊,咱們已經把臨時對象的值傳到返回值了。因爲是臨時對象,因此返回值不能是引用,必須是值。
好了,complex的相關細節寫得差很少,有些沒寫,上面都提到了,還有些操做符重載,與加法相似,不重複寫了。具體參考complex.h
,下面進入string類的實現。
與complex同樣,string類的整個實現分佈如上圖,右邊的是測試的程序。
下面來看看string的縮小版實現:
因爲字符串不像複數那樣固定大小,而是可大可小,因此在實現string類的時候,私有數據是一個指針,指向動態分配的char數組,這樣就能夠實現相似動態字符串大小。這個小章節叫big three,這裏的big three分別是,拷貝構造(String(const String & str) ),拷貝賦值(String& operator=(const String& str)),以及析構函數( ~String()) 。爲何要有big three,這個立刻就會介紹。
在構造函數中,若是沒有傳入字符串,則string申請動態分配一個char[1], 指向的就是'\0'
,也就是空字符,若是傳入的是「hello」
, 則動態分配「hello」
的長度再加一(一表明結束標識符'\0'),都是用string內部的指針指向動態分佈的內存的頭部。爲何多了一個析構函數呢?在complex類爲啥沒有呢?這是由於complex中沒有進行動態分配內存,在複數死亡後,它所佔用的內存所有釋放,徹底ok,但string類動態分配了內存,這分內存在對象的外部,不釋放內存的話,在對象死亡後依然存在,這就形成內存泄漏,因此須要構建一個析構函數,在對象死亡釋放動態分配的內存。動態分配使用的時new命令,返回的是分配出來的內存的首地址,釋放動態分配內存使用delete命令,若是分配的是數組對象,則須要在delete後加上[],若是是單個,直接delete指向的指針便可。上面就有兩種狀況的實例。
complex類其實內部存在c++語言自身提供的拷貝構造和拷貝賦值,不須要本身寫,由於沒有指針的類的數據賦值無非就是值傳遞,沒有變化。但string類不同,上面的圖是很好的例子,由於使用的是動態分配內存,對象a和對象b都指向外面的一塊內存,若是直接使用默認的拷貝構造或者拷貝賦值(例如將b = a),則是將b的指針指向a所指的區域,也就是a的動態分配內存的首地址,原來b所指向的內存就懸空了,因而發生內存泄漏,並且兩個指針指向同一塊內存,也是一個危險行爲。因此帶有指針的類是不能使用默認的拷貝構造和拷貝賦值的,須要本身寫。下面看看怎麼寫的。
首先是拷貝構造,因爲是構造函數一種,跟以前的構造函數同樣,須要分配一塊內存,大小爲要拷貝的string的長度+1,而後使用C語言自帶的strcpy進行逐個賦值。
上面這個拷貝賦值,首先檢查是否是自我賦值,只要有這種狀況發生,就要考慮,自我賦值則直接返回this所指的對象就能夠了,若是不是自我賦值,則刪除分配的內存,從新分配內存,長度爲傳入字符串的長度+1,同理使用strcpy函數進行逐個賦值。
自我賦值的檢查很重要,沒有自我檢查,就會發生上面的狀況,一運行程序的第一句話,內存就釋放了,指針就又懸空了,不肯定行爲產生。
string剩餘一點放到這裏面,打印直接調用get_c_str成員函數就能夠,返回指針,os會遍歷它所指向的內存,打印出字符串,遇到'\0'
終止。
c1 即是所謂stack object,其生命在做用域(scope) 結束之際結束。這種做用域內的object,又稱爲auto object,由於它會被「自動」清理。p所指的即是heap object,其生命在它被deleted 之際結束,因此要在指針生命結束以前對堆內存進行釋放。
上面的c2和c3分別是靜態對象和全局對象,做用域爲整個程序。如下是它們四個的內存分佈,更具體的細節能夠參考這篇文章。
能夠到使用new命令動態分配內存,主要有如下三步,首先分配要構建對象的內存,返回的是一個空指針,而後對空指針進行轉型,轉成要生成對象類型初始化給指針,而後指針調用構造函數初始化對象。
能夠看到delete操做能夠分爲兩步,首先經過析構函數釋放分配的內存,而後經過操做符delete(內部調用free函數)釋放對象內存。
上圖中就是vc建立complex類以及string類的內存塊圖,左邊兩個是complex類,長的那個是調試(debug)模式下的內存塊分佈,短的那個是執行(release)模式下的內存塊分佈,複數有兩個double,因此內存佔用8個字節,vc調試模式下,調試的信息部份內存佔用是上面灰色塊的32個字節以及下面灰色塊的4個字節,紅色的表明內存塊的頭和尾(叫cookie),佔用八個字節,合在一塊兒是52個字節,vc會以16個字節對齊,因此會填充12字節,對應的是pad的部分,另外,爲了凸顯這是分配出去的內存,因此在頭尾部分,用最後一位爲1表明該內存分配出去了,爲0就是收回來了。執行模式下沒有調試信息。string類相似分析。
上面是動態分配內存,生成complex類的數組以及string類的數組的內存塊圖,與上面相似,不過這裏多了一個長度的字節,都爲3,標記對象的個數。
上面說明的是,若是分配的是動態對象數組,就必定要在delete後面加上[]符號,否則就沒法徹底釋放動態分配的內存。array new必定要搭配array delete。
part1到此結束。