C++ 類
作者:AceTan,轉(zhuǎn)載請標明出處疙渣!
0x00 面向?qū)ο笈c面向過程##
討論類之前分尸,我們有必要先探討一下面向?qū)ο?/strong>和面向過程攻礼。
面向?qū)ο螅?Object Oriented,簡稱OO睡互。面向?qū)ο笫前褬?gòu)成問題的事物分解成各個對象肠套,建立對象的目的不是為了完成一個個步驟舰涌,而是為了描述某個事物在解決整個問題的過程中所發(fā)生的行為。
面向過程: Procedure Priented,簡稱PO糠排。面向過程是分析解決問題的步驟舵稠,然后把這些步驟一步一步的實現(xiàn),然后在使用的時候一一調(diào)用。
面向?qū)ο蠛兔嫦蜻^程代表了兩種不同的編程思想哺徊。其本身并沒有誰好誰壞之分室琢,書本中其實有過分夸大面向?qū)ο笞饔弥印5锹渥罚谶@個OO大行其道的年代盈滴,面向?qū)ο筮@種編程范式可能更符合實際需求,尤其是涉及GUI編程和龐大的系統(tǒng)規(guī)模時轿钠〕驳觯總的來說,面向?qū)ο蟊让嫦蜻^程多了一種對問題的抽象疗垛,它有時是一種更為有效的思路症汹,打開了新世界的大門。
0x01 類
類(class) 是C++中提供的自定義數(shù)據(jù)類型的機制贷腕。類可以包含數(shù)據(jù)背镇、函數(shù)和類型成員。一個類定義一種新的類型和一個新的作用域泽裳。我們用“類”來描述“對象”瞒斩,而“對象”是指現(xiàn)實世界中的一切事物。類可以看做是對相似事物的抽象涮总。
類的基本思想就是數(shù)據(jù)抽象(data abstraction)和封裝(encapsulation)胸囱。數(shù)據(jù)抽象是一種依賴于接口(interface)和實現(xiàn)(implementation)分離的編程技術(shù)。類的接口包括用戶所能執(zhí)行的操作瀑梗;類的實現(xiàn)則包括類的數(shù)據(jù)成員烹笔、負責接口實現(xiàn)的函數(shù)體以及定義類所需要的各種私有函數(shù)。
封裝實現(xiàn)了類的接口和實現(xiàn)的分離夺克。封裝后的類隱藏了它的實現(xiàn)細節(jié)箕宙。
類機制是C++最重要的特性之一。
有些人會混淆類和對象铺纽。簡單來說柬帕,類和對象之間是抽象和具體的關(guān)系。類是對具有相同數(shù)據(jù)結(jié)構(gòu)和相同操作的一類對象的描述狡门。對象是描述其屬性的數(shù)據(jù)和對這些數(shù)據(jù)的操作陷寝。
0x02 定義和使用類
C++使用class這個關(guān)鍵字來定義一個類。我們定義一個三角形類其馏。
class Triangle
{
// 公有方法
public:
// 獲取三角形周長
double GetCircumference();
// 獲取三角形面積
double GetArea();
// 保護成員凤跑,只有其子類才能訪問
protected:
double a, b, c; // 三角形的三個邊長
};
以上的類進行了聲明,并沒有實現(xiàn)叛复。
我們也可以用struct這個關(guān)鍵字來定義一個類仔引。class關(guān)鍵字和struct關(guān)鍵字的唯一區(qū)別是:struct和class的默認訪問權(quán)限不一樣扔仓。class關(guān)鍵字定義的成員默認是private的,struct關(guān)鍵字定義的成員默認是public的(public和private下面會有介紹)咖耘。
0x03 面向?qū)ο蟮娜齻€基本特征##
面向?qū)ο蟮娜齻€基本特征是:封裝翘簇、繼承、多態(tài)儿倒。
- 封裝版保、繼承、多態(tài)
- 封裝夫否、繼承彻犁、多態(tài)
- 封裝、繼承凰慈、多態(tài)
聽說重要的事情要說三遍汞幢。因為不僅考試要考,面試的筆試題也會經(jīng)常見到溉瓶。下面著重介紹一下這三個面向?qū)ο蟮幕咎卣鳌?/p>
封裝
封裝就是把客觀的事物封裝成抽象的類急鳄,并且類可以把自己的數(shù)據(jù)和方法只讓可信的類或者對象操作,對不可信的信息隱藏堰酿。比如上面的三角形類,我們把具體的三角形抽象成一個三角形類张足,對外提供求周長和面積的方法(操作),我們對三角形的三條邊長信息進行隱藏触创,只讓繼承它的子類訪問。
C++中使用訪問說明符(access specifiers) 來加強對類的封裝为牍。訪問說明符具體有三個哼绑。
public : 定義在public說明符之后的成員在整個程序內(nèi)可被訪問,public成員定義類的接口碉咆。
protected : 定義在protected說明符之后的成員在這個類以及其子類中可以訪問抖韩。
private : 定義在private說明符之后的成員可以被類的成員函數(shù)訪問,但不能被使用該類的代碼訪問疫铜,private封裝了類的實現(xiàn)細節(jié)茂浮。
繼承
在一個已存在的類的基礎(chǔ)上建立一個新的類,新的類具有它所繼承的類的全部特性壳咕,且可以增加一些新的特性席揽。繼承可以說是面向?qū)ο蟮某绦蛟O(shè)計最重要的特點。它實現(xiàn)了軟件的可重用性(reuseability)谓厘。
通過繼承創(chuàng)建的新類稱為“子類”或“派生類”幌羞。
被繼承的類稱為“基類”、“父類”或“超類”竟稳。
繼承的過程属桦,就是從一般到特殊的過程熊痴。
在C++語言中,基類將類型相關(guān)的函數(shù)與派生類不做改變直接繼承的函數(shù)區(qū)別對待聂宾。對于某些函數(shù)愁拭,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數(shù)聲明成虛函數(shù)(virtual function)亏吝。 例如岭埠,我們可以把上面的三角形類修改成這樣。
class Triangle
{
// 公有方法
public:
// 獲取三角形周長
virtual double GetCircumference();
// 獲取三角形面積
virtual double GetArea();
// 保護方法蔚鸥,只有其子類才能訪問
protected:
double a, b, c; // 三角形的三個邊長
};
virtual 關(guān)鍵字提示了子類應(yīng)該定義適合自身版本的操作惜论。
派生類(子類)必須通過使用類派生列表(class derivation list)明確指出它是從哪個或者哪些基類繼承而來的。
派生列表的形式是: 首先是一個冒號止喷,后面緊跟著以逗號分隔的基類列表馆类,其中每個基類前面可以有訪問說明符(訪問說明符即 public, protected, private。 如果省略這個訪問說明符弹谁,那么默認的訪問權(quán)限是什么呢乾巧?讀者可自行測試一下,這也是新手經(jīng)常遇到的坑)预愤。
需要注意的是沟于,C++支持多重繼承,而很多其他語言并沒有這一特性植康。比如Java中旷太,一個子類有且僅有一個父類。多重繼承有時候會讓問題變的復雜销睁,使用的時候要精心設(shè)計供璧,倍加小心。
現(xiàn)在我們從上面普通的三角形類派生出一個子類冻记,直角三角形類睡毒。
#include "Triangle.h"
// 直角三角形類
class RightTriangle : public Triangle
{
public :
// 構(gòu)造函數(shù)。直角三角形可以只初始化兩個直角邊
RightTriangle(double a, double b);
// 獲取三角形周長
virtual double GetCircumference();
// 獲取三角形面積
virtual double GetArea();
private:
};
上面的直角三角形類共有繼承了普通三角形類冗栗,可以使用普通三角形類的非私有成員演顾。同時聲明了自己版本的獲取周長和面積的方法。
在考慮使用繼承時贞瞒,有一點是需要注意的偶房,那就是兩個類之間的關(guān)系應(yīng)該是“屬于”關(guān)系,也就是所謂的"is-a"關(guān)系军浆。例如上面的棕洋,直角三角形是三角形。如果我們再定義一個等腰三角形乒融,明顯地掰盘,等腰三角形也可以繼承三角形摄悯,因為它屬于三角形揍愁。等腰三角形和直角三角形雖然都屬于三角形剂跟,但這兩個之間無法有繼承和被繼承的關(guān)系日川。
多態(tài)
對于OOP而言凌停,多態(tài)性是指程序能通過引用或指針的動態(tài)類型(dynamic type)獲取特定行為的能力。
動態(tài)類型: 對象在運行時的類型狞玛,引用所引對象或者指針所指對象的動態(tài)類型可能與該引用或者指針的靜態(tài)類型不同类浪≡洌基類的指針或者引用可以指向一個派生類的對象邮偎。在這樣的情況中管跺,靜態(tài)類型是基類的引用(或指針),而動態(tài)類型則是派生類的引用(或指針)禾进。
接上面的例子豁跑,現(xiàn)在需要定義這樣一個函數(shù),它需要輸出傳入的三角形的周長和面積泻云。
// 打印信息
void Triangle::PrintInfo(std::ostream& os, Triangle & triangle)
{
if (!triangle.Judge())
{
os << "構(gòu)不成三角形" << std::endl;
return;
}
os << "該三角形的面積為:" << triangle.GetArea() << "艇拍,周長為" << triangle.GetCircumference() << std::endl;
}
上面的代碼中,形參為引用類型宠纯。代碼中調(diào)用了GetArea()和GetCircumference()方法卸夕,而這兩個方法都是虛函數(shù)。如果傳入的參數(shù)是普通的三角形(Triangle類)征椒,那么它就會調(diào)用普通三角形求周長和面積的方法娇哆。如果我們傳入的是直角三角形,那么它就調(diào)用的是直角三角形求周長和面積的方法勃救。具體調(diào)用哪個方法,只有程序運行的時候才能確定治力。這就是所謂的多態(tài)性蒙秒。
多態(tài)性一般可以分為編譯時多態(tài)和運行時多態(tài)。函數(shù)重載和模板都屬于編譯時多態(tài)(因為他們沒有虛表宵统,且使用時需要指定類型)晕讲。虛函數(shù)是運行時多態(tài)。
OOP的核心思想是多態(tài)性(polymorphism)马澈。指針或引用的靜態(tài)類型與動態(tài)類型不同這一事實正是C++語言支持多態(tài)性的根本所在瓢省。
為了更好的理解多態(tài)性,我舉一個生活中的例子痊班。我們?nèi)粘I钪姓f的“打球”勤婚,這個“打”就表示了一種抽象的信息,具有很多種含義涤伐。我們可以說馒胆,打乒乓球缨称,打籃球,打羽毛球祝迂,都使用“打”來表示某種球類運動睦尽。這實際上就是對運動行為的一個抽象。運行時可以確定是打什么球型雳。比如調(diào)用者是姚明当凡,我們就能確定是打籃球,調(diào)用者是大魔王張怡寧纠俭,我們就能確定是打乒乓球沿量。
純虛函數(shù)####
談到虛函數(shù),我們就不能對純虛(pure virtual)函數(shù)避而不談柑晒。純虛函數(shù)就是在虛函數(shù)聲明后書寫 =0欧瘪,這樣一個虛函數(shù)就變成了純虛函數(shù)。
值得注意的一點是匙赞,我們也可以為純虛函數(shù)提供定義佛掖,不過函數(shù)體必須定義在類的外部。
含有純虛函數(shù)的類都是抽象基類涌庭。
抽象基類負責定義接口芥被。 如果你熟悉Java或者C#中如何定義接口,你將有一種熟悉的感覺坐榆。
我們不能創(chuàng)建抽象基類的對象拴魄。 換句話說,抽象基類是不能實例化的席镀。趕緊畫個重點匹中,考試必考哈。
有些人可能已經(jīng)蒙圈了豪诲。啥是抽象基類顶捷,啥是接口?不一樣么屎篱? 這里簡單地講一下他們的區(qū)別服赎。
抽象類: 它是特殊的類,只是不能被實例化(將定義了純虛函數(shù)的類稱為抽象類)交播;除此以外重虑,具有類的其他特性;重要的是抽象類可以包括抽象方法秦士,這是普通類所不能的缺厉,但同時也能包括普通的方法。 C#和Java中用關(guān)鍵字Abstract定義的。
接口: 接口是一個概念芽死。它在C++中用抽象類來實現(xiàn)乏梁,在C#和Java中用interface來實現(xiàn)。
我們現(xiàn)在改寫一下我們的三角形類关贵,給它抽象出個接口出來遇骑。
ITriangle.h文件:
#pragma once
// 三角形的接口。這個一個虛基類揖曾,沒有其他普通函數(shù)落萎,也沒有類成員,只聲明了兩個虛函數(shù)炭剪。
class ITriangle
{
public:
// 獲取三角形周長
virtual double GetCircumference() = 0; // = 0,表明它是虛函數(shù)
// 獲取三角形面積
virtual double GetArea() = 0;
};
Triangle.h文件:
#pragma once
#include <ostream>
#include "ITriangle.h"
// 三角形類练链,繼承三角形的接口
class Triangle : ITriangle
{
// 共有方法
public:
// 構(gòu)造函數(shù)
Triangle(double _a, double _b, double _c);
// 默認構(gòu)造函數(shù)
Triangle() = default;
// 判斷三角形是否合法
bool Judge();
// 獲取三角形周長, override關(guān)鍵字表明覆蓋基類中的函數(shù)版本。
double GetCircumference() override;
// 獲取三角形面積
double GetArea() override;
// 打印信息
void PrintInfo(std::ostream& os, Triangle& triangle);
// 保護方法奴拦,只有其子類才能訪問
protected:
double a, b, c; // 三角形的三個邊長
};
0x04 友元##
先說一下為什么會有友元(friend)這玩意媒鼓。舉個生活中的例子,老王的兒子小明生病了错妖,帶他去看醫(yī)生绿鸣,恰巧老王的大表哥老宋是這家醫(yī)院的院長,在中國這個關(guān)系社會暂氯,老王多半會直接找到老宋潮模,開個后門,直接快速就醫(yī)痴施,免去了一下繁瑣的排隊流程擎厢。C++中友元干的事和這個差不多,在實現(xiàn)類之間數(shù)據(jù)共享時辣吃,減少系統(tǒng)開銷动遭,提高效率。
類可以允許其他類或者函數(shù)訪問它的非公有成員神得,方法是令其他類或者函數(shù)成為它的友元沽损。如果類想把一個函數(shù)作為它的友元,只需增加一條以friend關(guān)鍵字開始的函數(shù)聲明即可循头。
友元很明顯的一個缺點就是它破壞了封裝機制,除非不得已的情況炎疆,一般不使用友元函數(shù)卡骂。友元一般用在如下兩種情況:
- 運算符重載的某些場合
- 兩個類要共享數(shù)據(jù)
友元函數(shù)的位置###
友元函數(shù)是類外的函數(shù),所以它的聲明可以放在類的私有段或者公有段形入,且這沒什么分別全跨。
友元函數(shù)的調(diào)用###
可以直接調(diào)用友元函數(shù),不需要通過對象或指針亿遂。
友元函數(shù)和類的成員函數(shù)的區(qū)別###
成員函數(shù)有this指針浓若,而友元函數(shù)沒有this指針渺杉。
友元關(guān)系不存在傳遞性。友元函數(shù)是不能被繼承的挪钓,就像父親的朋友未必是兒子的朋友是越。
友元的簡單示例###
《C++ Prime》中一個很好的例子:有一個Window_mgr類的某些成員可能需要訪問它管理的Screen類的內(nèi)部數(shù)據(jù)。假設(shè)需要為Window_mgr添加一個加為clear的成員碌上,它負責把一個指定的Screen的內(nèi)容都設(shè)為空白倚评。為了完成這一個任務(wù),clear需要訪問Screen的私有成員馏予;而要想令這種訪問合法天梧,Screen需要把Window_mgr指定成它的友元。具體的代碼如下:
#include <vector>
#include <string>
using namespace std;
class Screen
{
// Window_mgr的成員的訪問Screen類的私有部分
friend class Window_mgr;
// Screen類的剩余部分
private:
string contents;
int height;
int width;
};
class Window_mgr
{
public:
// 窗口中每個屏幕的編號
using ScreenIndex = vector<Screen>::size_type;
// 按照編號將指定的Screen重置為空白
void clear(ScreenIndex);
private:
vector<Screen> screens;
};
void Window_mgr::clear(ScreenIndex i)
{
// s是一個Screen的引用霞丧,指向我們想要清空的那個屏幕
Screen& s = screens[i];
// 將那個選定的Screen重置為空白
s.contents = string(s.height * s.width, ' ');
}
0x05 final和override說明符
有時我們會遇到這樣一種情況呢岗,派生類如果定義了一個函數(shù)與基類中的虛函數(shù)的名字相同但形參列表不同,這仍然是一個合法的行為蛹尝。編譯器將認為新定義的這個函數(shù)和基類中原有的函數(shù)是相互獨立的后豫。這時,派生類的函數(shù)并沒有覆蓋掉基類中的版本箩言。然后蛋疼的問題就發(fā)生了硬贯,我原來想覆蓋掉基類中的虛函數(shù),可一不小心把形參給弄錯了陨收。這種bug其實很難找饭豹。好在C++11新標準中為我們提供了一個override關(guān)鍵字來說明派生類中的虛函數(shù)。如果我們使用override標記了某個函數(shù)务漩,但該函數(shù)沒有覆蓋已存在的虛函數(shù)拄衰,此時編譯器將報錯。 然后我們根據(jù)編譯器的報錯饵骨,就能迅速定位問題代碼翘悉,解決問題。
struct B
{
virtual void f1(int) const;
virtual void f2();
void f3();
};
struct D1 : B
{
void f1(int) const override; // 正確:f1與基類中f1的版本匹配
void f2(int) override; // 錯誤:B中沒有形如f2(int)的函數(shù)
void f3() override; // 錯誤:f3不是虛函數(shù)
void f4() override; // 錯誤:B中沒有名為f4的函數(shù)
};
還有一種情況居触,我們想要一個函數(shù)不能被覆蓋妖混。這時候我們可以把這個函數(shù)指定為final,如果我們把函數(shù)定義成final了轮洋,那么任何嘗試覆蓋該函數(shù)的操作都講引發(fā)錯誤制市。
struct D2 : B
{
// 從B繼承f2()和f3(),覆蓋f1(int)
void f1(int) const final; // 不允許后續(xù)的其他類覆蓋f1(int)
};
struct D3 : B
{
void f2(); // 正確:覆蓋從間接基類B中繼承而來的f2
void f1(int) const; // 錯誤:D2已經(jīng)將f2聲明成final
};
要注意一點的就是弊予,這兩個說明符在C++11新標準新可以使用祥楣。Java和C#中有類似的關(guān)鍵字,C++11新標準應(yīng)該是借鑒了他們的做法。
0x06 五種特殊的的成員函數(shù)
當定義一個類時误褪,我們顯式地或者隱式地指定在此類型的對象拷貝责鳍、移動和銷毀時做什么。這些操作是必須的兽间,你也許會疑問历葛,之前我們定義的類不是沒有這些操作么?那是因為萬能的編譯器自動定義了缺失的操作渡八,實際上這就導致了一個問題啃洋,編譯器補充定義的不是我想要的怎么辦?這種問題非常常見屎鳍,尤其是在你想拷貝一個類的時候宏娄。
一個類通過定義五種特殊的成員函數(shù)來控制拷貝,移動逮壁,賦值和銷毀孵坚,他們是:
拷貝構(gòu)造函數(shù)(copy constructor)
拷貝賦值運算符(copy-assignment operator)
移動構(gòu)造函數(shù)(move constructor)
移動賦值運算符(move-assignment operator)
析構(gòu)函數(shù)(destructor)
移動操作是新標準引入的操作,我們先來討論一下拷貝構(gòu)造函數(shù)窥淆、拷貝賦值函數(shù)和析構(gòu)函數(shù)卖宠。
拷貝構(gòu)造函數(shù)
如果一個參數(shù)的第一個參數(shù)是自身類類型的引用且任何額外參數(shù)都有默認值(一般沒有很少加額外參數(shù)),此構(gòu)造函數(shù)是拷貝構(gòu)造函數(shù)。
class Foo
{
public:
Foo(); // 默認的構(gòu)造函數(shù)
Foo(const Foo&); // 拷貝構(gòu)造函數(shù)
// 其他
};
拷貝構(gòu)造函數(shù)的第一個參數(shù)必須是一個引用類型忧饭,且此參數(shù)幾乎總是一個const的引用扛伍。
如果我們沒有為類定義一個拷貝構(gòu)造函數(shù),編譯器會為我們定義一個词裤。與合成默認構(gòu)造函數(shù)不同刺洒,即使我們定義了其他構(gòu)造函數(shù),編譯器也會為我們合成一個拷貝構(gòu)造函數(shù)吼砂。
只有當類沒有任何構(gòu)造函數(shù)時逆航,編譯器才會自動地生成默認構(gòu)造函數(shù)。
合成的拷貝構(gòu)造函數(shù)會將其參數(shù)的成員逐個拷貝到正在創(chuàng)建的對象中渔肩。編譯器從給定對象中依次將每個非static成員拷貝到正在創(chuàng)建的對象中因俐。
每個成員的類型決定了它如何拷貝:對類類型的成員,會使用其拷貝構(gòu)造函數(shù)類拷貝周偎;內(nèi)置類型的成員則直接拷貝抹剩。雖然我們不能直接拷貝一個數(shù)組,但合成拷貝構(gòu)造函數(shù)會逐元素地拷貝一個數(shù)組類型的成員蓉坎。如果數(shù)組元素的類型是類類型吧兔,則使用元素的拷貝構(gòu)造函數(shù)來進行拷貝。
直接初始化和拷貝初始化的差異:當使用直接初始化時袍嬉,我們實際上要求編譯器使用普通的函數(shù)匹配來選擇我們與我們提供的參數(shù)做匹配的構(gòu)造函數(shù)。當我們使用拷貝初始化(copy initialization),我們要求編譯器將右側(cè)運算對象拷貝到正在創(chuàng)建的對象中伺通,如果需要的話還要進行類型轉(zhuǎn)換箍土。
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷貝初始化
string book = "8-8888-888-8"; // 拷貝初始化
string lines = string(100, '8');// 拷貝初始化
拷貝初始化通常使用拷貝構(gòu)造函數(shù)來完成,但是罐监,如果一個類有一個移動構(gòu)造函數(shù)吴藻,則拷貝初始化有時會使用移動構(gòu)造函數(shù)而非拷貝構(gòu)造函數(shù)來完成 。
拷貝賦值運算符
當我們重載了賦值運算符(=)后弓柱,我們就可以使用=直接給類賦值沟堡。同樣,如果類未定義自己的拷貝賦值運算符矢空,編譯器會為它合成一個航罗。
重載運算符(overloaded operator) 本質(zhì)上是一個函數(shù),其名字由operator關(guān)鍵字后接表示要定義的運算符的符號組成(更詳細的內(nèi)容以后會做介紹屁药,現(xiàn)在只需要了解這個即可)粥血。賦值運算符就是一個名為 operator= 的函數(shù)。
重載運算符的參數(shù)表示運算符的運算對象酿箭。 某些運算符复亏,包括賦值運算符,必須定義為成員函數(shù)缭嫡。如果一個運算符是一個成員函數(shù)缔御,其左側(cè)運算對象就是一個綁定到隱式的this參數(shù)。對于一個二元運算符妇蛀,例如這里的賦值運算符耕突,其右側(cè)運算對象作為顯式參數(shù)傳遞。
class Foo
{
public:
Foo& operator=(const Foo&); // 賦值運算符
// 其他
};
賦值運算符通常應(yīng)該返回一個指向其左側(cè)運算對象的引用讥耗。
析構(gòu)函數(shù)
析構(gòu)函數(shù)執(zhí)行與構(gòu)造函數(shù)相反的操作:析構(gòu)函數(shù)初始化對象的非static數(shù)據(jù)成員有勾,還可以做一些其他工作;析構(gòu)函數(shù)釋放對象使用的資源古程,并銷毀對象的非static數(shù)據(jù)成員蔼卡。
析構(gòu)函數(shù)是類的一個成員函數(shù),名字由波浪號(~)接類名構(gòu)成挣磨,沒有返回值雇逞,也不接受參數(shù)。
class Foo
{
public:
~Foo(); // 析構(gòu)函數(shù)
// 其他
};
析構(gòu)函數(shù)完成的工作: 如同構(gòu)造函數(shù)有一個初始化部分和一個函數(shù)體茁裙,析構(gòu)函數(shù)也有一個函數(shù)體和一個析構(gòu)部分塘砸。在一個構(gòu)造函數(shù)中,成員的初始化是在函數(shù)體執(zhí)行之前完成的晤锥,且按照他們在類中出現(xiàn)的順序進行初始化掉蔬。在一個析構(gòu)函數(shù)中廊宪,首先執(zhí)行函數(shù)體,然后銷毀成員女轿。成員按初始化順序的逆序銷毀箭启。
什么時候會調(diào)用析構(gòu)函數(shù):
變量在離開其作用域的時被銷毀
當一個對象被銷毀時,其成員被銷毀蛉迹。
容器(無論是標準容器庫還是數(shù)組)被銷毀時傅寡,其元素被銷毀。
對于動態(tài)分配的對象北救,當對指向它的指針應(yīng)用delete運算符時被銷毀荐操。
對于臨時對象,當創(chuàng)建它的完整表達式結(jié)束時被銷毀珍策。
上述一個比較全面的例子:
#include<iostream>
using namespace std;
// 日期類
class Date
{
public:
Date(int y = 1970, int m = 1, int d = 1); // 構(gòu)造函數(shù)
Date(const Date &date); // 拷貝構(gòu)造函數(shù)
Date& operator=(const Date&); // 拷貝賦值運算符
~Date(); // 析構(gòu)函數(shù)
void Print(); // 打印信息
private:
int year, month, day; // 成員:年托启、月、日
};
// 構(gòu)造函數(shù)膛壹,使用初始化列表初始化類的成員
Date::Date(int y, int m, int d) :year(y), month(m), day(d)
{
cout << "Constructor called." << endl;
}
// 拷貝構(gòu)造函數(shù)
Date::Date(const Date &date)
{
year = date.year;
month = date.month;
day = date.day;
cout << "Copy Constructor called." << endl;
}
// 拷貝賦值運算符
Date& Date::operator=(const Date& rDate)
{
year = rDate.year;
month = rDate.month;
day = rDate.day;
cout << "Copy-assignment Operator called" << endl;
return *this;
}
// 打印信息
void Date::Print()
{
cout << year << "年" << month << "月" << day << "日" << endl;
}
// 析構(gòu)函數(shù)
Date::~Date()
{
cout << "Destructor called.\n";
}
int main()
{
Date day1(2016, 8, 21); // 調(diào)用構(gòu)造函數(shù)直接初始化
Date day2;
Date day3(day1); // 拷貝初始化
Date day4 = day3;
day2 = day4;
day2.Print();
return 0;
}
輸出結(jié)果是:
Constructor called. // day1調(diào)用了構(gòu)造函數(shù)
Constructor called. // day2調(diào)用了構(gòu)造函數(shù)
Copy Constructor called. // day3拷貝構(gòu)造函數(shù)
Copy Constructor called. // day4拷貝構(gòu)造函數(shù)
Copy-assignment Operator called // day4賦值運算符重載
2016年8月21日 // day2的信息
Destructor called. // day4被銷毀
Destructor called. // day3被銷毀
Destructor called. // day2被銷毀
Destructor called. // day1被銷毀
移動構(gòu)造函數(shù)和移動賦值構(gòu)造函數(shù)
這是新標準新加的兩個操作驾中,為什么會添加這樣的操作呢? 因為在我們使用類時模聋,很多情況下都要發(fā)生對象拷貝肩民,而其中某些情況下,對象拷貝后就立即被銷毀了链方,在這些情況下持痰,移動而非拷貝對象就會大幅度提升性能。 有點雕版印刷術(shù)和活版印刷術(shù)的意思祟蚀。
為了支持移動操作工窍,新標準引入了一種新的引用類型— 右值引用(rvalue reference)。所謂右值引用就是必須綁定到右值的引用前酿。我們通過&&而非&來獲得右值引用患雏。右值引用有一個重要性質(zhì)一只能綁定到一個將要銷毀的對象。 因此罢维,我們可以自由地將一個右值引用的資源“移動”到另一個對象中淹仑。
與之相對的還有一個叫左值引用(lvalue reference)。一般而言肺孵,一個左值表達式表示的是一個對象的身份匀借,而一個右值表達式表示的是對象的值。
返回左值引用的函數(shù)平窘,連同賦值吓肋、下標、解引用和前置遞增/遞減運算符瑰艘,都是返回左值表達式的例子是鬼。我們可以將一個左值引用綁定到這個類的表達式的結(jié)果上肤舞。
返回引用類型的函數(shù),連同算術(shù)屑咳、關(guān)系萨赁、位以及后置遞增/遞減運算符,都生成右值兆龙。我們不能將一個左值引用綁定到這類表達式上,但我們可以將一個const的左值引用或者一個右值引用綁定到這類表達式上敲董。
我們要記住以下兩點:
左值持久紫皇,右值短暫。
變量是左值腋寨。
接上面的例子聪铺,來看一下如何定義一個移動構(gòu)造函數(shù)和移動賦值構(gòu)造函數(shù)
// 函數(shù)聲明
Date (Date&& date) noexcept; // 移動構(gòu)造函數(shù)
Date& operator=(Date&&) noexcept; // 移動賦值運算符
// 移動構(gòu)造函數(shù)
Date::Date(Date &&date) noexcept : // 移動操作不應(yīng)該拋出異常
year(date.year), month(date.month), day(date.day)
{
year = month = day = 0;
cout << "Move Constructor called" << endl;
}
// 移動賦值運算符
Date& Date::operator=(Date&& rDate) noexcept // 移動操作不應(yīng)該拋出異常
{
// 直接檢測自賦值
if (this != &rDate)
{
year = rDate.year;
month = rDate.month;
day = rDate.day;
// 將rDate置于可析構(gòu)狀態(tài)
rDate.year = rDate.month = rDate.day = 0;
}
cout << "Move-assignment Operator called" << endl;
return *this;
}
// 其他見上面
0x07 一些法則
上面提到的五種特殊的成員函數(shù),實際上統(tǒng)稱為拷貝控制萄窜。以下的一些法則是實踐中的一些經(jīng)驗積累铃剔,我們應(yīng)當遵循這些法則,這樣能少犯一些錯誤查刻。
需要析構(gòu)函數(shù)的類也需要拷貝和賦值操作
需要拷貝操作的類也需要賦值操作键兜,反之亦然。
實際上穗泵,新標準中加入了移動操作后普气,如果一個類定義了任何一個拷貝操作,它就應(yīng)該定義所有的五個操作佃延。
0x08 阻止拷貝
既然可以進行拷貝现诀,那么也有阻止拷貝的機制。
在新標準之前履肃,一般的做法是把拷貝控制函數(shù)放在private訪問說明符下仔沿,這樣就可以阻止拷貝啦。
新標準簡單粗暴尺棋,直接把這些函數(shù)定義為刪除的函數(shù)(deleted function)封锉。具體做法就是在函數(shù)的參數(shù)列表后面加上=delete。如下:
Date(const Date &date) = delete; // 拷貝構(gòu)造函數(shù)
0x08 結(jié)束語
面向?qū)ο缶幊?OOP)是一種思想的轉(zhuǎn)變陡鹃,需要勤加練習烘浦,這樣才能掌握C++中的類。希望大家能面向?qū)ο缶幊唐季ǎ材苷嬲孛嫦驅(qū)ο缶幊蹋ɑ槪?/p>
- 文/潘曉璐 我一進店門萄传,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甚颂,“玉大人,你說我怎么就攤上這事秀菱≌裎埽” “怎么了?”我有些...
- 文/不壞的土叔 我叫張陵衍菱,是天一觀的道長赶么。 經(jīng)常有香客問我,道長脊串,這世上最難降的妖魔是什么辫呻? 我笑而不...
- 正文 為了忘掉前任,我火速辦了婚禮琼锋,結(jié)果婚禮上放闺,老公的妹妹穿的比我還像新娘。我一直安慰自己斩例,他們只是感情好雄人,可當我...
- 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著念赶,像睡著了一般础钠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叉谜,一...
- 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锭碳!你這毒婦竟也來了袁稽?” 一聲冷哼從身側(cè)響起,我...
- 正文 年R本政府宣布寥假,位于F島的核電站,受9級特大地震影響霞扬,放射性物質(zhì)發(fā)生泄漏糕韧。R本人自食惡果不足惜,卻給世界環(huán)境...
- 文/蒙蒙 一喻圃、第九天 我趴在偏房一處隱蔽的房頂上張望萤彩。 院中可真熱鬧,春花似錦斧拍、人聲如沸雀扶。這莊子的主人今日做“春日...
- 文/蒼蘭香墨 我抬頭看了看天上的太陽愚墓。三九已至,卻和暖如春昂勉,著一層夾襖步出監(jiān)牢的瞬間浪册,已是汗流浹背。 一陣腳步聲響...
推薦閱讀更多精彩內(nèi)容
- C++文件 例:從文件income. in中讀入收入直到文件結(jié)束库菲,并將收入和稅金輸出到文件tax. out。 檢查...
- 前言 人生苦多渠抹,快來 Kotlin 蝙昙,快速學習Kotlin闪萄! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
- 狼來了的故事我們都很熟悉。無論是小時候家長將給我們聽荆几,還是上幼兒園老師講給我們聽吓妆。而如今,我們做了家長做了老師吨铸,再...