【期末复习】转眼到了C++的复习时间(更新中)
時間過的很快。似乎昨天才剛剛開學,轉眼間已經到了期末復習的時間了。距離C++期末考試還有一個月的時間,我正式開始了C++的復習計劃,以期讓我在后期可以更加從容、淡定,能夠擁有更多的時間去投入到自己想去做的事情上去。
使用指南
1.里面的語言表達和課堂上的PPT以及一些教材很不一樣,本著對讀者友好的原則,盡量寫得通俗易懂,就像在講故事一樣,而不是在那里羅列名詞。當然,這樣文字會略有冗長,可以自己總結,也可以參考PPT或書籍。
2.我在閱讀書本和博客的時候經常會看到很多對讀者不友好的東西,尤其是一些工科教材,其內容完全不適合于自學,不得不說這是現在的一個通病。我希望我能夠整合一些資源,把內容寫的更容易理解,避免“啃”那些質量上實在不敢恭維的所謂的教科書。
3.可能一開始閱讀的時候發現不太像之前所學的東西,別著急,靜下心來慢慢讀,假定自己是個零基礎的,什么都不會,忘掉之前所有的C++知識,咱們一起從頭開始,建立整套體系,突然在某一時刻你會發現自己能夠巧妙地將這些東西和自己之前所學建立聯系,甚至有了更深刻的理解,那么,我所寫的這些就是有用的。
4.里面會出現一些英文,是很常用的那些。了解這些常用的英語,對平時的學習很有幫助。大家遇到這些英文的時候可以多注意一下。
聲明
本文初衷是自己的復習筆記,但是覺得可以作為博客等進行發布,同時和大家一起學習進步。
本文內容的原型是翁愷老師在網易云課堂上的C++相關課程,大家可以去聽一聽,當然如果覺得完全觀看視頻太費時間可以閱讀本文。
本文全部為手敲。本文是免費、開源的,但轉載請注明出處。
序言
????本次復習以翁愷老師在網易云課堂上的慕課和老師的PPT為主,加上一些自己踩過的坑和對于一些問題的求解思路。期望在文章中加上自己對程序語言的理解,體會OOP的思想。
????本次復習的大致提綱如下:首先是類和對象,之后自然地過渡到繼承和派生。因為這兩者之間關系緊密,所以有很多交叉的內容。之后就是運算符重載、模板、異常、STL、文件,一些C++中比C語言多出來的東西。
讀者應具備的知識
閱讀本文,我們假定你已經學習了一些基本的C語言的知識,懂得如何定義變量,寫函數,知道循環、條件、順序結構,知道指針,能夠編寫出一些基本的程序。如果沒有這些知識,建議你先把C語言打下一些基礎。
目錄
- 類和對象
- 繼承和派生
- 運算符重載
- 模板
- 文件
- 異常
- STL工具庫
- 附錄 更新日志
目前完結進展
截至2019.6.22,已經完成的內聯函數筆記。
1 類和對象
什么是對象
????首先咱們明確一下咱們所討論的東西——C++語言,它是“面向對象”的語言,其英文是Object Oriented Programming,簡寫就是OOP。對象就是實現OOP的方法。
????所以咱們先來看一看對于C++中很重要的東西。在C++中,我們要學習“面向對象”的編程方法,所以,到底什么是對象?
可以這樣去理解:對象就是“東西”。面向呢?其實Oriented指的是在對象這個層面去思考問題。對象可能是可見的,比如你正在學習的那張桌子;也可以是不可見的,比如我說了一句話。但是這句話可以被記錄、加工、處理,所以這也是對象。
其實,在之前C語言里,我們就已經一直在接觸對象了,只不過從來沒有那么去說。我們寫過int i;這個變量i就是一個對象。沒錯的,變量就是對象。變量用來存儲數據,而變量的類型決定能夠存放什么樣的數據。同樣地,對象也是會有類型的。
對象=屬性+服務
看一看上面這個形似雞蛋的圖,有沒有什么想法?
蛋黃是里面的數據,比如一盞燈的功率、現在是否發光、能否充電;蛋清是一些操作,比如一個開關,控制通電與否。所以,這個就是對象的模型。
我們進行對蛋黃的所有操作必須建立在對蛋清的控制之上,這也是經常出現的一個問題——很多時候人們可能會越過蛋清直接操作蛋黃,而這就違背了OOP(面向對象)的思想。
我們可以通過教室上課這個例子比較一下面向過程和面向對象的區別。
現在看這樣一個例子:一個三維點坐標。其實C++里面的class和C里面的structure很類似,那么,在實際操作的時候有什么不同呢?
明顯,C++的操作和數據都放在一起,就像那個雞蛋圖;而C中數據和操作是分開的。
所以,我們可以回答什么是對象了:對象是一種方法,能用來組織設計、實現(從問題思路到代碼實現)。我們關注的問題是對象,是“東西”!
面向對象基本原理
我發送消息,通過傳輸以后對方接收到消息。他自己決定要不要執行我給他的指令。也就是,我的消息一定要明確,比如我按下了電燈的開關。通過導線,燈泡知道了我想讓他亮。然而,燈泡發現自己的燈絲壞了,因此他決定不亮燈。再比如,我現在讓屏幕面前的你大叫一聲。你現在叫了嗎?或許沒有。但我已經傳達了明確的指令,可你知道我不過是在舉例子,因此你決定不叫。我們程序猿就是發指令的人,至于指令能不能執行,看接收者的決定。我們做的事情僅限于發指令,不要強迫他做。
這個消息,可能導致接收者狀態改變,比如燈亮了;也可能返回結果,比如我讀取到了燈的狀態是開著的。
類
Birds of a feather flock together.——亞里士多德
物以類聚,這是OOP的哲♂學。
類就是對對象的歸納總結。類定義了對象,對象屬于類。
比如我說,蘋果類。里面有紅的、青的各種,但是它們都長成這個樣子,是這樣的生理結構,所以我手中有一個紅富士蘋果、一個黃香蕉蘋果,它們都是屬于蘋果類的。
類是抽象出來的一個概念
為什么會出現類?
拿蘋果來說。先是因為有了一個又一個的蘋果,它們都符合這些特點,能夠顯著跟別的東西區分出來,所以我們把各種各樣的蘋果統稱為蘋果。說這是個蘋果,你就知道這不是西瓜,也不是桌子,也不是別的什么玩意。
在生活中,人們經驗總結總結出了類。
而計算機科學與技術發展卻是先出現了類的概念,才有了對象。因為計算機科學屬于人類創造的科學而非自然科學,必須需要先有統一的規定才能有后續的發展。但是,和咱們的認知相同,類也是一種抽象出來的東西。我說蘋果,你腦子里可能出現了各種各樣的蘋果,但你不知道我說的是哪一個。而對象就是一個具體的蘋果,比如我手里拿著的這個(當然隔著屏幕你們看不到)紅富士蘋果,你們都很確定我說的是哪一個——我手里的這個蘋果。
這就是類和對象的區別。類是抽象出來的,便于我們認知這個世界,但你沒法說這個類指的是哪一個;對象可以代表這個類——它有類應有的特征,眾多對象組成在一起并抽象出來,求出共同點,形成了類。
OOP的原則
1.萬物皆對象
2.程序是一堆能夠互相告訴別人"What to do"的對象的總和
3.每個對象有由其他對象組成的內存空間
4.每種對象都有類型
5.所有特定類型對象都能接收相同的消息
對象有接口
對象提供的那個操作就是接口(interface)。
想一想,接口。生活中用過吧?比如USB接口,我可以插U盤,可以插數據線,可以插充電寶的充電線……總之,you name it,只要符合這個接口的條件的都可以被插進來。
你做的東西遵循這個接口,那么這個東西是可以替換的。比如燈泡的螺紋,你可以插上去各種型號的燈。
接口的好處:可以去通信交流、保護。這樣,我們的程序可以拆換,各部分的耦合程度會松散。比如把燈泡焊在墻上,和把燈泡使用插線板連接,前者耦合程度非常緊密,就不太可以拆卸。
所以,我們需要隱藏一些東西。比如燈絲,我們需要放在玻璃罩里面;蛋黃被蛋殼和蛋白包圍。寫出class的程序猿就需要讓調用這些class的程序猿的手遠離她們不該碰的東西。
為此我們要進行encapsulation,即封裝,把操作放在外面,把數據放在里面。
試著建立一個類
咱們試著建立一個屬于自己的類和對象吧~
以上面的點的坐標為例,咱們試著建立這樣的一個類。
class Point{ private:int x;int y;int z; public:void printPoint();void makePoint(int a,int b,int c);void doublePoint(); };這個類,里面有什么?
第一,三個點,是Pirvate的,也就是私有的。這個跟咱么提到過的“保護”很像,其實這就是把我不想讓別人直接操控的東西保護起來的辦法。把數據放到別人碰不到的位置,這就是雞蛋模型里面的蛋黃部分。
第二,那么保護起來了以后呢?需要提供接口。上文提到過這個詞,再想一想?對,就是電燈的那個開關,是我向里面的數據發指令的途徑。在這個程序里,就是public的幾個函數啦。通過這幾個函數,我們間接地改變三個坐標,正如同打開電燈的開關讓燈間接地亮起來,而不是我把燈絲摳出來給它兩端接上電,想想,如果這樣多危險!
好了,類建立好了,咱們接下來怎么做呢?把相關函數和main寫一下。
#include <iostream> using namespace std;void Point::printPoint(){cout<<"("<<x<<","<<y<<","<<z<<")"<<endl; }void Point::setPoint(int a,int b,int c){x=a;y=b;z=c; }void Point::doublePoint(){x=2*x;y=2*y;z=2*z; }int main(){Point pt;pt.setPoint(1,2,3);pt.printPoint();pt.doublePoint();pt.printPoint();return 0; }我們做了什么?第一,建立了一個對象。想一想類和對象的區別。其次我們設置了它們的值,輸出一次,然后把每個點坐標翻倍,再次輸出。當然這些函數都很好理解。
輸出結果是:
(1,2,3)
(2,4,6)
至此,我們已經自己建立了一個類和對象。
域解析符
::叫做resolver,也就是域解析符。
我們可以看到void Point::doublePoint()這句話里面,在說doublePoint這個函數是有歸屬的,它的家是Point這個類。
當然,域解析符還可以直接拿出來用,比如
::doublePoint(); ::a;此時,前面沒有任何東西,直接使用,它表示全局的函數或者全局的變量。
每個類和對象分別對應一個.h和.cpp文件(選讀)
說明:在我們的期末考試中這是用不到的,但是在現實的開發中可能會經常用到。因此這一部分可以選擇性閱讀,了解即可。以后標注“選讀”的章節亦如此。
大家再區分一下定義和聲明的區別吧。在C語言中,extern int i; 是變量的聲明,告訴編譯器有這樣的一個東西; i=3; 是變量的定義,我告訴編譯器這個變量的值是多少。在類里面也是這樣子。
每個類,當然是有數據(蛋黃)和操作(蛋清),也就是變量和函數。變量和函數都有聲明和定義的區別,在類中也是一樣的。
對于每一個類的聲明,我們都應該把所有的變量和函數的名字寫出來;而在類的定義中我們應當寫出它們的值和具體的實現。
我們回顧一下上面的例子:
比如,一個point.h文件中如下:
class Point{ private:int x;int y;int z; public:void printPoint();void makePoint(int a,int b,int c);void doublePoint(); };而對應的point.cpp文件中如下:
#include “point.h”void Point::printPoint(){cout<<"("<<x<<","<<y<<","<<z<<")"<<endl; }void Point::setPoint(int a,int b,int c){x=a;y=b;z=c; }void Point::doublePoint(){x=2*x;y=2*y;z=2*z; }在使用到這個類的地方我們都需要include上這個.h文件,這可以算是一種“合同”,我作為類的使用者,我使用你的頭文件,我就遵守這個合同,然后我使用合同里面規定的東西。
此時,頭文件是這個接口,于是可以進行使用。
多說一些——頭文件里允許出現的東西
多說一些吧,可能有一些比較底層的東西了。
編譯器在進行編譯的時候每次只針對一個.cpp文件獨立地進行編譯,此時每個文件我們叫做“編譯單元”。此時里面出現多個.cpp文件之間的一些沖突問題都不會檢查出來,而是在最后鏈接在一起的時候會有叫做ld的程序來檢查。所以,在.h文件中,不要出現不正確的聲明!否則,多個.cpp文件共用一個.h的時候,會出現問題。
所以,在.h文件中只允許出現以下聲明:
- extern的變量,比如extern int i;
- 函數的原型
- 類和結構體的聲明
#include的作用,就是文本的替換。把頭文件的全文放到需要被插入的地方。
一個歷史小故事,為什么#include <iostream>沒有.h?其實在早期的版本中有<iostream.h>這個頭文件,在后來C++進行改版的時候保留了之前的.h文件,那么新的文件名字怎么解決呢?于是就使用了新的作為替換。注意,文件名是一樣的,就是.h沒有了,但是這不妨礙它成為我們的頭文件。iostream.h和iostream有一些區別,比如用了<iostream.h>后不用再加using namespace std;,其中的輸入輸出也會有些不同。其實,文件后綴并不是那么重要,這個和UNIX的歷史有一些小故事。
標準頭文件結構
所以,我們應當讓頭文件盡可能地標準化。我們給出了標準頭文件結構:
#ifndef HEADER_FLAG #define HEADER_FLAG//declaration here#endif怎么理解?ifndef就是if undefined,如果沒有被定義過,那么我們就定義這個HEADER_FLAG宏。有什么用呢?比如里面有一個類的聲明,這個頭文件被兩個.cpp都include了,如果沒有這三句,會造成這個類的重復聲明,這是不允許的。但是有了這個就不一樣了:第一次用到.h,還沒有HEADER_FLAG,所以我們定義這個宏,并把里面的內容復制過來;第二次再次用到的時候.h,已經有了先前的HEADER_FLAG,所以不會再去把中間的內容復制,避免了多次聲明。
因此,我們在寫.h的時候總是要遵循著標準頭文件結構。
試著一起設計一個類
我們想做一個時鐘類,有小時和分鐘。當然這在C語言里面很好實現,因為兩個for循環就可以了嘛。不過在OOP中,我們嘗試去看里面有什么“東西”,把它劃分。
抽象
這是一個思想:有意地去忽略一些細節,只在乎對我這個問題有用的。
比如看到了一位同學,我們會說他是誰,多高,今天精神面貌怎么樣,但是一般不會去想他的心臟現在是舒張還是收縮吧?因為這不是我們要研究的問題。
分析問題
我們可以想見,一個時鐘要怎樣去劃分里面的“東西”。首先,有兩個顯示時間的地方;其次,每個地方(小時、分鐘)可以+1,或者到頭了返回0并且告訴別人自己返回了。
雖然小時不會返回給日期(因為我們這個程序沒有要求),但是我們應當留下這個接口,以便以后去進行再次探索開發。在工作量不太大的情況下,我們應當從長遠去考慮,給未來留下一些接口。
因此,我們可以把類設計成這樣:
成員變量
下面我們來談談成員變量。
我們首先回顧一下本地變量。在C語言中可是學過的喲~記不記得那個交換兩個數的程序,如果不用指針或引用值是交換不了的。這就是因為本地變量在函數執行結束后立刻被“干掉”了。這就是我們所說的“生存期”和“作用域”的問題。在函數里面的聲明出來的變量都是本地變量。
還有個小細節。如果全局變量和函數里面的變量恰巧同名,那么會最終使用誰呢?在C語言中也有所涉及,“就近”選擇本地變量;而全局變量就被屏蔽掉了。對于類里面的變量,亦是如此。
C++中有三類變量
在C++中,比C語言多了類,因此變量也多了這樣的類型。
- 成員變量 Field
- 參數 Parameter
- 本地變量 Local Variable
一句話說清后兩者的關系,那就是參數和本地變量一模一樣,相同的性質,相同的生存期,相同的作用域——都離不開函數。(注:當然是現階段的理解,再深入到內存中會有一些不同)
成員變量在類里面,所以,類活多久,這個變量就活多久。就像是身體的一個器官,你活多久,它與你一樣。
可是,成員變量到底在哪里存在呢?上文提到,類是抽象出來的概念,抽象的東西并不存在于客觀世界中。可是我們還在說“成員變量”的生存期和作用域,說明它是可以存在的。其實這正好比“蘋果的果肉”和“我手里這個蘋果的果肉”之間的區別,前者是個概念,可一旦給你一個符合這個概念的東西,我們就知道它在哪兒存在了。
成員變量的歸宿是對象
來看這樣一個類吧,很簡單的一個類:
class A{ private:int i; public:void f(); };void A::f(){int j=10;i=10; }int main(){A a;a.f();return 0; }寫完class A以后,我們在里面聲明了int i,但是它還并不存在。它在什么時候開始存在的呢?是main函數里A a;這句。A是蘋果這個東西,int i是里面的果肉,那么A a;就相當于我買了一個蘋果,它的果肉就是a.i(a里面的i)了。當然,由于i是Private的,我們不能直接這么用。所以,我們得到了答案,成員變量存在于哪里?我們的對象里。
沒錯的,編譯器就是這樣做事情:你告訴他什么他都會相信。比如類里面的int i;我們只是告訴了編譯器一定會存在這樣的一個i,然后它就相信了,于是進行了編譯。至于i在什么位置,編譯器并不關心,因為咱們已經告訴他“一定會存在變量i”。但是如果你在后面不給出i的歸宿(對象)就直接用,鏈接器是會報錯的,盡管二進制代碼已經出來了。
函數的歸宿是類
為了說明一些東西,咱們先讓它們都是Public好了。
class A{ public:int i;void f(); };void A::f(){i=20;cout<<i<<endl; }int main(){A a;A b;cout<<a.i<<endl;a.f();b.f();return 0; }假定吃蘋果就是函數f()。
如果每次咱們建立新的蘋果,吃蘋果的機制要每次復制一次,確實很麻煩了。畢竟函數(吃蘋果)本身就是相同的方法,方法本身就是抽象的,比如洗蘋果,咬一口,再咬一口(循環),直到吃干凈。基本上吃所有的蘋果都是這個流程(不要用削皮或者別的方法來懟我orz),所以咱們所有吃蘋果的人共用這一套方法(函數)好了。
所以,大家會吃蘋果了吧,更明白類里面的函數歸宿就是類本身了吧~
深入一些——函數共用,不會用混嗎(選讀)
剛剛那段代碼,里面有a.f()和b.f(),可是它怎么知道誰是誰,這個i到底是a的還是b的?
在早期C++沒有編譯器,只有翻譯器,翻譯成C語言的源代碼,因此C++的全部功能可以通過C語言來實現。我們能不能想想,如果自己是C++的設計者,我們會怎么做,讓函數直到這個i是a.i還是b.i?
可以想象,在結構體的一些函數中(咱們肯定都用過,C語言大作業),我們可以把結構體的地址傳進去,對結構體進行操作。所以一個思路是把對象的地址傳進去。我們可以驗證一下:
把上文代碼的相關部分改成下面這樣:
void A::f(){printf("A::f()--&i=%p",&i);printf("this=%p",&i); //這句話可以在看完下一部分的時候加上測試一下 }int main(){A a;A b;printf("&a=%p",&a);a.f();printf("&b=%p",&b);b.f();return 0; }有興趣的可以在自己電腦上試一試,你會發現得到的結果是前兩個值相同,后兩個值相同,證明了C++的成員函數有能力實現這個事情。所以,到底是什么樣的方式呢?下面就是我們的結論:
this指針
所有的成員函數,都有一個藏起來了的參數,它就是我們選讀部分所說的那個指針。
所有的成員函數,
void A::f();都可以被看作
void A::f(A *p);所以,上面那個函數和下面這樣是一樣的:
void A::f(){this->i=20;cout<<this->i<<endl; }也即,this->i就是i。
我們有時候會直接用到this,當然this是關鍵字,咱們不能去定義。
構造和析構
關于“燙燙燙”——電腦太熱了?
在使用Visual Studio的Debug模式下,沒有初始化的對象,他會幫助你調試:編譯器會默認給變量賦初始值0xcd。兩個0xcd連在一起就是漢字國標碼的“燙”。所以以后寫程序看到輸出了“燙”,不是電腦溫度太高了,是變量沒有初始化!
C++不會默認進行初始化
在其他的一些OOP語言中,比如Java,你建立了一個變量,它會默認給賦予一個初值;但是在C++中,他不會這樣去做。因為他更看重的是效率。我在內存里面給你找到了能夠放下你需要的東西的一間“屋子”,你就應當去進行這間屋子的打掃工作。所以,我們應當自己去想辦法去打掃這個屋子。
嚇得我把上文的Point類趕緊加了個Init(初始化)函數:
class Point{ private:int x;int y;int z; public:void printPoint();void makePoint(int a,int b,int c);void doublePoint();void Init(int a,int b,int c); };這樣,每次我建立新的對象,同時調用一次Init函數,就解決了這個問題。
可是,建立對象的那個程序猿,真的能每次記得都主動調用Init()函數嗎?如果忘了,可能就會有問題。
所以,我們需要這樣的機制,每次建立新的對象,自動就進行初始化。
構造函數(Constructor)
那個能夠進行初始化并且每次都會自動被調用的函數就是“構造函數”。
它長這個樣子:
class X{int i; public:X(); }里面這個和X類同名而且沒有任何返回類型的函數就是構造函數。它沒有返回類型,和類同名。
只要做了對象,它立刻會被調用。
構造函數可以有參數
構造函數也是成員函數,所以成員函數的很多性質它也有。比如前文的this指針,或者是帶上參數。還是剛剛那個,不過把Init()改成了構造函數:
class Point{ private:int x;int y;int z; public:void printPoint();void makePoint(int a,int b,int c);void doublePoint();Point(int x,int y,int z);//constructor };析構函數(Destructor)
對象可以被創建。當然,它也有需要被消滅的時候。類似于構造函數,我們可以使用析構函數把對象給刪掉。比如,一個不想吃的蘋果,用析構直接扔掉。
析構函數和構造函數一樣,和類同名,沒有返回值類型。區別在于析構函數名字前面加一個波浪號(~,tilde)。
析構是毀滅者,不需要其他的參數,因為它只做一件事——刪掉所有的東西,然后讓這個對象不復存在。中間是不需要引入其他參數來干預的。
什么時候去調用呢?當這個對象走到了自己生命的尾聲,比如main函數的結束,所在的{}之間的結束……
使用域解析符調用構造和析構是這樣的:
Point::Point(int a,int b,int c){//... }Point::~Point(){//... }缺省構造函數(default constructor)
看到這個名詞,先顧名思義一下。default,有默認的意思,那么它什么意思?
我沒寫構造函數,系統給我分配了一個?
不完全是。我們寫的構造函數是可以帶參數的,比如我們可以去定義一個Point類,還是用的之前的那個例子:
Point p(1,2,3); Point p1;對比一下兩者,有什么區別?好,一個有參數,一個沒有參數。有參數那個就知道了,我是要x=1,y=2,z=3。但是第二個沒有參數呢?就應該去尋找那個沒有參數的構造函數了。如果此時沒有,系統會給分配,叫做auto default constructor。(這個可以不用記憶)。
如果我要是寫了一個不帶參數的構造函數,是不是就實現了每次都可以給Point p1這樣的東西自動賦一個默認值?
比如我這么寫:
Point::Point(){x=0;y=0;z=0; }那么每次忘了加參數的,它的值默認就可以得到(0,0,0),解決了之前沒有構造函數的“燙燙燙”的問題。
new和delete
這是一個不得不提的地方。在每次C++實驗課中使用new和delete的題目可能AC Rate都會低一些。所以,為什么動態分配內存就會對大家造成一定的傷害呢?
兩個運算符
我們可以使用new和delete進行動態的分配對象:我需要建立一個新的對象,new一個;如果要收回,delete一個。(要是對象真的new一個就有了該多好……這樣我就有小哥哥了(╯‵□′)╯︵┻━┻)
new int; new Point; new int[10];delete p; delete[] p;每次new一個對象之后,根據上文,構造函數是一定會被調用的。作為運算符,它會有結果。結果是什么呢?新建的東西的地址。
再來看delete,有兩個樣子。咱們大概可以看出來用new []新建的咱們就用delete[]來回收。
當然大家可以想見,使用delete,標志著一個對象的終結。而一個對象在結束的時候,通常會去調用析構函數。
一個細節值得注意一下。看下面的小栗子:
Point * i = new Point [10];delete[] i;Point類還是咱們上文的那個。我們建立了一個10個元素的數組,然后給它回收。此時應當是這樣的:先分配10個Point的空間,然后對于每一個執行構造函數。執行10次構造以后到了delete,先執行這10個的析構函數,然后回收空間。
如果我用的是delete i;會有什么不同?只有第一個的析構被調用,然后10個的空間被回收。
因此,我們注意這樣一個事情就好了,用new []新建的咱們就用delete[]來回收。
怎么知道delete[]會刪除多少個(選讀)
在內存里面其實有一個我們看不見的東西,在調用new之后一些東西會被存放到一個表中。這個表記錄了所分配空間的大小和地址。因此,假設我new了一個int,比如int*p=new int;這個表中會記錄[4,p]。4是int的大小,4個字節;p是它的地址。
所以我們執行Point* p=new Point[10];之后,表中會存儲[120,p]。(我們假定一個Point大小是12字節)所以在進行delete之后,根據120/12=10,就知道了我們需要執行10次析構函數。
new和delete的一些注意問題
- delete去刪除new出來的東西,不要刪除malloc之類分配的空間。
- 不要delete一個地方兩次
- new[]配delete[]
- new配delete
- 刪除一個空指針,是安全的
- new完了一定要去delete:申請的空間記得去釋放——否則可能會一直占用著空間,越用越多,直至程序崩潰。
訪問限制
在前文,我們已經建立了OOP的一些思想。這時候,我打算再把這個圖放出來:
數據要被保護起來,別人能夠操控的只有外部的一些接口。所以,我們怎么去實現這個東西呢?
想必在前面的代碼中,我沒有說,但是大家應該很好理解:pirvate什么意思,public什么意思,就搞定啦。
訪問控制的類型
- public
- private
- protected
public——共有,誰都可以訪問
private——私有,只有自己可以訪問
“自己”是誰——這個類的成員函數。
private是對類來說的。在A的a和b兩個對象中,我可以在a中去訪問b的東西,只要傳參,把b傳給a的函數,就可以~
“咱們是一家人,我錢包里的錢就是你的錢;你錢包里的錢也是我的錢。”
protected——保護,子子孫孫可以訪問
Friends 友元函數——你是我的好朋友
好朋友,“我錢包里的錢就是你的錢;你錢包里的錢不是我的錢。”
畢竟,我說小葩同學是我的朋友,我的錢包里的錢他可以動。這個還相對合理。但是如果我說“我是小葩同學的朋友,所以我可以動他的錢包”……我怎么這么壞呢?
我們試著寫一個:
class A{private:int i;public:A();friend void doubleI(A*);friend void plusI(A*,int);friend class Z; }void doubleI(A*a){a->i=a->i*2; }void plusI(A*a,int b){a->i=a->i+b; }同時,Z這個類的所有對象可以隨心所欲地訪問A類中的東西。
class和struct的小區別(選讀)
class和struct都是可以聲明類的。它們的區別在于:如果不自己加上private,public,protected之類的東西,那么會有默認的屬性。
class默認里面是private,struct默認里面是public。
在C++里,我們首選class。
Initializer List 初始化列表
我們對類初始化的時候,除了在函數里面賦值,還可以更為直接一些:
還是之前的Point類。
class Point{ private:int x;int y;int z; public:Point(int a,int b,int c);void printPoint();void makePoint(int a,int b,int c);void doublePoint(); };對于構造函數,咱們之前是這么寫的:
Point::Point(int a,int b,int c){x=a;y=b;z=c; }現在也可以這樣去寫:
Point::Point(int a,int b,int c):x(a),y(b),z(c){}剛剛后面一個冒號加上x(a),y(b),z?這樣的語句就是初始化列表。這和構造函數有什么區別呢?雖然效果是一樣的,但是深入一些,初始化列表的執行早于構造函數。
它可以去初始化任何類型的變量。
為什么有時用初始化列表會更好(選讀)
對比一下:
Student::Student(string s){name=s; }和
Student::Student(string s):name(s){}前者是進行了賦值運算。在此之前,需要先調用一個默認構造函數。如果里面有一個對象是另一個類的對象,那么必須存在它的默認構造函數,否則會出錯。而后者,是直接進行了初始化。
來探究一下必須存在默認構造函數的事情:
class B{ public:int i;B(int c){i=c;} }class A{ public:A(){b=0;cout<<"A::A()<<endl;} private:B b; }這樣會報錯,說沒有找到B::B()。這就是因為進行賦值運算必須要先調用默認構造函數。
一個小的建議,以后初始化的操作咱們都寫進初始化列表。
寫了帶有參數的構造函數后一定要跟隨一個無參的
根據上面選讀部分的結論,大家一定要時刻謹記:
如果我寫了帶參的構造函數,一定要跟一個無參構造函數!
2 繼承和派生
代碼重用
大家了解過sort函數吧?就是C++提供的幫你完成排序的函數,不管里面是int,還是其他數據類型,都使用同樣的代碼。這些代碼是大佬們很久以前就寫好的,你不用自己想著怎么實現排序,你用這個函數就可以了。
這樣,任何時候,不同數據類型都可以用sort,而sort是相同的代碼,這就實現了反復利用代碼,也就是“代碼重用”。
OOP的三大特性是封裝、繼承、多態性。我們其實在上一章里面著重介紹了封裝:那個雞蛋一樣的圖片。而大家可以去顧名思義一下,繼承和多態有什么含義。繼承就是我用了你已經有的東西,實現“重復利用”。多態就是我利用的方式多種多樣。
而它們其實都是對代碼重用這個問題的回答:我們要通過多次反復利用已經寫好的東西來實現我要做的事情。
代碼重用是一個很久以來的夢想:人們從計算機軟件誕生的那一天起,就一直夢想著去尋找方式,能夠使用我以前的一些東西來往下開發。
誕生出OOP以后,人們找到了繼承這樣的方式。但是值得一提的是,這只是一個解決辦法,不是唯一的方法,也不一定是最好的辦法。
對象組合 Composition
組合就是像建造汽車,汽車里有引擎,有輪胎。
實現的方式
有兩種方法可以實現:直接裝進來、引用。前者就是讓對象作為成員變量,后者就是用指針。
什么時候用哪個?看情況。比如一個人,他的心臟就要直接裝進身體內部;而他的書包就要用指針:我可以找到我的書包并且操控他,但他不是人身體的一部分。
對象依舊邊界清晰
比如我們把兩個對象放進了一個大的對象里面去(當然都是先通過類實現的)。
class Person{//個人信息的類string name;string address; };class Currency{//錢數的類int money; };class Account{//組合成為賬戶后的類 private://對象組合Person per;Currency cur; public:Account(string a,string b,int c);//構造~Account();//析構void print();//輸出內容 };//實現構造函數,使用Initializer List(初始化列表,上面說過的) Account::Account(string a,string b,int c):per(a,b),cur(c){ }觀察一下這個構造函數。我們在大的構造里面分別調用兩個小的構造,可以說明一些問題吧?對象還是那個對象,構造都需要構造的。
使用初始化列表可以不提供默認構造函數。
private or public
我們可以把對象設置為public從而可以讓內部函數得到訪問,但是這不是OOP所喜歡的:因為這讓雞蛋模型里面的數據對外公開了,就相當于你把心臟放到體外,讓別人進行隨意操作。
所以我們應當把對象設置為private。
鋪墊了這一節,想要說明代碼重用不只是繼承,也可以對象組合。那么繼承是怎么用的,歡迎看下一節。
繼承 Inheritance
繼承:對已有類的改造
繼承就是把一個已經存在的類拿過來,我們做一些改造,加一些新的東西,從而就變成了新的類。因此,我們可以在新的類中使用先前存在的類的相關東西:數據成員、成員函數,當然包括public的函數和變量,也就是接口。這樣,我們就可以使用以前的類的功能進行新的類的設計。
這是C++的一項核心技術。
繼承就是我們使用一個類來定義全新的類的一種辦法。
Student繼承了Person,Person顯然是等級更高的;而Student的內容是更豐富的。顯然,人們都具有男女的區分、身高、體重等特征,學生也有;而學生有年級、就讀學校、專業等信息,這些是未必所有人都具有的。因此,學生類是人這個類的超集。
人是基類、超類、父類,學生是派生類、副類(一般不這么說,要不讀音就重了,是不是)、子類。
class A { public:A():i(0){};~A(){};void print(){cout<<i<<endl;} protected:void set(int x){ i=x; }; private:int i; };class B:public A { public:B(){};~B(){};void f(int x){set(x);} };int main(){B b;b.print();//b.set(20);//這句話就是錯誤的b.f(20);b.print();return 0; }看上面的例子,里面B類就是繼承的語法。分析一下這段代碼,為什么b.set(20);這句話是錯誤的?因為這是protected的,只有B內部才能夠使用。
所以,子類能使用父類的public和protected,其他東西只能用父類的public。
子類和父類關系(選讀)
當然是先有父親再有孩子,因此我們知道在進行構造前,先去執行父類的構造函數,然后再來構造自己。析構的時候先析構自己,再去析構父類。
還可以提一句“名字隱藏”。如果父類有函數重載,恰巧子類中有同名函數,那么父類中的所有同名的函數會全部被隱藏掉,因此只剩下子類中的這些函數。
在其他OOP語言里都不是這樣子的,它們會存在overide,但是C++中還規定了子類和父類中的同名函數無關,所以必須隱藏才能保證函數不會出現錯亂。
函數重載 Function Overload 默認參數 Defualt Argument
函數重載就是說如果兩個函數的參數表不同,那么就可以去定義這兩個同名的函數。
為什么可以出現函數重載呢?我們回顧一下,之前說過所有C++最終都可以翻譯成C語言的程序,但C不支持函數重載。如果你是設計者,你會怎么實現呢?
一個可行的方法就是將函數名字換掉,換掉函數名加參數類型名,這樣就可以的得到若干完全不同的函數,也就解決了C語言不支持函數重載的問題。
默認參數是可以在函數定義的時候就給出它預先的值。當然,默認參數一定要從最右邊從左邊寫過來,否則語法不正確。
void set(int i,int initSize=0);在這樣的函數里如果我們用set(5),那么initSize會自動變成0;而如果使用set(10,10),那么initSize會變成10。
void set(int i=0,int j,int k=3);//錯誤這樣寫是錯誤的:必須從右往左進行,不然,這就容易亂了套了。
一定要警覺注意的是,這個只能寫在函數聲明里面,定義里面絕對不能出現!比如,下面這樣寫就是錯誤的:
void set(int i,int j=0){//... }一定要切記默認參數出現在什么樣的位置。
再深入一點,defalut argument是在編譯時刻的事情,只不過是編譯器看到了原型聲明他中的default argument,記住了這個函數可以有這樣的default的值,因此它就這樣去記住了,但是函數的參數還是那些參數。
雖說考試會有這類的考察,但是在軟件工程中,我們一般不會去這樣用。因為這不但可能造成閱讀上對于參數個數的誤解,還有可能使得值被篡改,不符合設計者的意圖,這樣就是不安全的了。
內聯函數 Inline Function
函數調用的額外開銷 Overhead(選讀)
每個程序都會有一個自己獨立的堆棧,其中包含本地變量和返回地址。當調用函數的時候,函數的返回地址是會放到堆棧里面去的。函數的參數和本地變量是一樣的,它們都會放到堆棧里面去。當調用函數的時候,我們會做兩件事情,把返回地址和臨時變量入棧,寄存器棧頂指針上移,同時跳轉到所需的地址,將這個地址入棧。經過計算以后,pop掉所有的臨時變量,再返回原來的地址,pop掉這個地址,把寄存器ax的結果傳遞給接收函數值的變量。
上面就是簡單調用一個函數的過程,甚至是最簡單的不需要任何參數的函數中也會經歷如此復雜的過程。
我們在調用一個函數的時候,我們可能會產生如下額外的開銷:將參數入棧、將返回地址入棧、準備返回值、將所有不再需要的東西出棧。
因此,我們可以使用一種開銷比較小的方式來完成這些比較簡單的事情。
內聯函數 Inline Functions
如果我使用了內聯函數,我就不會真正去做上面選讀部分說的一大堆事情,而是直接把函數的代碼嵌入在了調用的代碼塊,但仍然保持函數的獨立性。
(圖源自翁愷老師的慕課)
如果使用內聯,那么最終代碼里面是不會出現這樣的一個函數的。
如何使用內聯函數呢?我們一定要在聲明和定義的時候都要寫上inline的關鍵字。
inline int f(int i);inline int f(int i){return 2*i; }如果想要把內聯函數放進去,直接放進.h文件就可以了,而不需要經過.cpp文件。其實inline的definition就是它的declaration。
使用內聯函數是一種“以空間換時間”的策略:這樣確實會加長代碼的長度,但是也能夠減少程序的一些開銷。
雖說C語言中使用宏也實現了相關的功能,但是#define是不能夠進行類型檢查的。
當然,如果有了遞歸或者非常大的函數,編譯器可能就會拒絕。其實成員函數在類內寫的時候都是inline的。
咱們在類里面寫inline的時候,為了保持類的清爽,可以這樣寫:
class A{ private:int x,y,z; public:A(int a,int b,int c);~A(); };inline A::A(int a,int b,int c):x(a),y(b),z(c){}這種書寫方式可以保證類的聲明是比較干凈的。
建議把比較小的函數或者頻繁調用(循環內部)的函數設置成Inline的。
Const
一直會見到const這樣的東西。這小家伙,真別致,不是嗎?
const int本質還是變量(選讀)
我說const int i = 1;,就真的說i是一個常量了嗎?看上去是這樣的,但本質上它還是變量,只不過編譯器采取了一些措施,讓我們不能夠直接去修改這樣的值。
const帶來的麻煩——指針還是變量?(選讀)
char * const q = "abc";const char * p = "abc";這就很麻煩了:上面那個說q是常量,也就是地址不能變。也就是q只能指向固定的一塊區域。但是,
*q='A';是完全正確的,但就不可以q++。而對于下面那個,在說目前p所指向的那個字符不能通過*p改變。因此,
*p='B';就是不正確的。可是p所指向的區域可以變化。不是說p指到哪里去,哪里就是const,而是你只能通過*p去訪問,不能通過*p去修改。
下面,對象是const還是指針是const?
const A * a = &a1; A const * a = &a1; A * const a = &a1;我們區別的標記是位于*前面還是后面。前兩個,對象是const,后面那個,指針是const。
不要試圖對const int i中的i取地址并且傳遞給int *p,這樣是非法的。
const和字符串(選讀)
我們來看這樣一小段代碼:
int main(){char *s = "Hello world!";cout<<s<<endl;s[0]='B';cout<<s<<endl;return 0; }首先給了一個warning,之后正常輸出Hello world!,之后程序異常退出。
5:12: warning: deprecated conversion from string constant to ‘char*’
[-Wwrite-strings]
char *s = “Hello world!”;
Hello world!
[Finished in 7.3s with exit code 3221225477]
我們來分析一下上面這些結果是什么意思:
下節預告
const
附錄 更新日志
- 2019.5.26 完成了MOOC1-4節的整理。
- 2019.6.1 對前一部分進行修訂,完成了MOOC第五節的整理。
- 2019.6.2 完成了MOOC6-8節的整理。
- 2019.6.6 完成了MOOC9-10節的整理。高考加油!
- 2019.6.8 完成了MOOC11-13節的整理。高考加油!第一部分至此完成。
總結
以上是生活随笔為你收集整理的【期末复习】转眼到了C++的复习时间(更新中)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql string长度限制_Str
- 下一篇: 都才40出头,近一个月已有至少5名优秀青