C++入門系列博客六 類

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ù)卡骂。友元一般用在如下兩種情況:

  1. 運算符重載的某些場合
  2. 兩個類要共享數(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>

真正地面向?qū)ο缶幊?/div>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闷叉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子脊阴,更是在濱河造成了極大的恐慌握侧,老刑警劉巖蚯瞧,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異品擎,居然都是意外死亡埋合,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門萄传,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甚颂,“玉大人,你說我怎么就攤上這事秀菱≌裎埽” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵衍菱,是天一觀的道長赶么。 經(jīng)常有香客問我,道長脊串,這世上最難降的妖魔是什么辫呻? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮琼锋,結(jié)果婚禮上放闺,老公的妹妹穿的比我還像新娘。我一直安慰自己斩例,他們只是感情好雄人,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著念赶,像睡著了一般础钠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叉谜,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天旗吁,我揣著相機與錄音,去河邊找鬼停局。 笑死很钓,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的董栽。 我是一名探鬼主播码倦,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锭碳!你這毒婦竟也來了袁稽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤擒抛,失蹤者是張志新(化名)和其女友劉穎推汽,沒想到半個月后补疑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡歹撒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年莲组,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片暖夭。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡锹杈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迈着,到底是詐尸還是另有隱情嬉橙,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布寥假,位于F島的核電站,受9級特大地震影響霞扬,放射性物質(zhì)發(fā)生泄漏糕韧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一喻圃、第九天 我趴在偏房一處隱蔽的房頂上張望萤彩。 院中可真熱鬧,春花似錦斧拍、人聲如沸雀扶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愚墓。三九已至,卻和暖如春昂勉,著一層夾襖步出監(jiān)牢的瞬間浪册,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工岗照, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留村象,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓攒至,卻偏偏與公主長得像厚者,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子迫吐,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,516評論 1 51
  • C++文件 例:從文件income. in中讀入收入直到文件結(jié)束库菲,并將收入和稅金輸出到文件tax. out。 檢查...
    SeanC52111閱讀 2,776評論 0 3
  • 前言 人生苦多渠抹,快來 Kotlin 蝙昙,快速學習Kotlin闪萄! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,201評論 9 118
  • 時間似乎又過去了一大半奇颠,翻開日歷败去,今天已經(jīng)立秋,如期而至的一場雨淋透了所有的炙熱烈拒,仿佛這個夏已經(jīng)被掩埋至過去圆裕。 打...
    櫻花正開閱讀 193評論 2 2
  • 狼來了的故事我們都很熟悉。無論是小時候家長將給我們聽荆几,還是上幼兒園老師講給我們聽吓妆。而如今,我們做了家長做了老師吨铸,再...
    教育修行者閱讀 562評論 0 0