繼承和多態(tài)

C++很重要的一個(gè)特征就是代碼重用。在C語言中重用代碼的方式就是拷貝梗逮、修改代碼项秉。C++可以用繼承或組合的方式來重用。通過組合或繼承現(xiàn)有的類來創(chuàng)建新類慷彤,而不是重新創(chuàng)建它們娄蔼。

單繼承

繼承是使用已經(jīng)編寫好的類來創(chuàng)建新類,新的類具有原有類的所有屬性和操作底哗,可以在原有類的基礎(chǔ)上作一些修改和增補(bǔ)岁诉。值得注意的是,有以下的一些成員函數(shù)不能自動(dòng)繼承:構(gòu)造函數(shù)跋选、析構(gòu)函數(shù)涕癣、=運(yùn)算符。新類稱為派生類或子類前标,原有類稱為基類或父類坠韩,派生類是基類的具體化。派生類的聲明語法如下炼列。

class 派生類名:繼承方式 基類名
{
    派生類新增成員的聲明只搁; //可以是數(shù)據(jù),也可以是函數(shù)
}

這里俭尖,繼承方式有3種氢惋,公有,保護(hù)和私有。它們的含義是明肮,父類成員的訪問權(quán)限如果比繼承方式高菱农,那么會(huì)降級(jí)為繼承方式。也就是說柿估,如果繼承方式為保護(hù)循未,那么父類的公有成員就變成了保護(hù)成員。

我們將類的公有成員函數(shù)稱為接口秫舌。公有繼承的妖,基類的公有成員函數(shù)在派生類中仍然是公有的,換句話說足陨,基類的接口成為了派生類的接口嫂粟,因而將它稱為接口繼承。對(duì)于私有墨缘、保護(hù)繼承星虹,派生類不能繼承基類的接口。派生類將不再支持基類的公有接口镊讼,它希望能重用基類的實(shí)現(xiàn)而已宽涌,因而將它稱為實(shí)現(xiàn)繼承。

基類的構(gòu)造函數(shù)不被繼承蝶棋,派生類中需要聲明自己的構(gòu)造函數(shù)卸亮。聲明構(gòu)造函數(shù)時(shí),只需要對(duì)本類中新增成員進(jìn)行初始化玩裙,對(duì)繼承來的基類成員的初始化通過調(diào)用基類構(gòu)造函數(shù)完成兼贸。派生類的構(gòu)造函數(shù)中需要給基類的構(gòu)造函數(shù)傳遞參數(shù)。

如果在派生類中聲明了和基類中名稱一樣的數(shù)據(jù)或函數(shù)(不是虛函數(shù))叫做重定義吃溅。如果需要訪問基類的成員溶诞,需要加上作用域運(yùn)算符。請(qǐng)看下面的這個(gè)例子罕偎。

#include <iostream>
using namespace std;

class A{
public:
    int a_;
    A(int a):a_(a){}
};

class B:public A{
public:
    int a_;
    int b_;
    B(int a,int b):A(a+1),a_(a),b_(b){}
};

int main(int argc, char const *argv[])
{
    B b(1,100);
    cout<<b.a_<<" "<<b.b_<<endl;
    cout<<b.A::a_<<endl;
    return 0;
}

代碼重用可以通過在一個(gè)類中含有另一個(gè)類對(duì)象來實(shí)現(xiàn)很澄,這種方式叫做組合。所以颜及,無論是繼承還是組合,本質(zhì)上都是把子對(duì)象放在新類型中蹂楣,兩者都是使用構(gòu)造函數(shù)的初始化列表去構(gòu)造這些子對(duì)象俏站。組合是希望新類內(nèi)部具有已存在的類的功能,而不是希望已存在類作為它的接口痊土。組合通過嵌入一個(gè)對(duì)象來實(shí)現(xiàn)新類的功能肄扎,而新類用戶看到的是新定義的接口,而不是來自老類的接口(has-a)。如果希望新類與已存在的類有相同的接口(在這基礎(chǔ)上可以增加自己的成員)犯祠。這時(shí)候需要用繼承旭等,也稱為子類型化(is-a)。

類型轉(zhuǎn)換

派生類對(duì)象也是基類對(duì)象衡载。這意味著在使用基類的地方也可以使用派生類來替換搔耕。公有繼承時(shí),編譯器可自動(dòng)執(zhí)行以下轉(zhuǎn)換:派生類對(duì)象指針(引用)自動(dòng)轉(zhuǎn)化為基類對(duì)象指針(引用)痰娱;派生類對(duì)象自動(dòng)轉(zhuǎn)化為基類對(duì)象(特有的成員消失)弃榨。

當(dāng)保護(hù)、私有繼承時(shí)梨睁,派生對(duì)象指針(引用)轉(zhuǎn)化為基類對(duì)象指針(引用)需用強(qiáng)制類型轉(zhuǎn)化鲸睛。但不能用static_cast,要用reinterpret_cast坡贺。

不能把派生類對(duì)象強(qiáng)制轉(zhuǎn)換為基類對(duì)象官辈,當(dāng)基類對(duì)象指針(引用)可強(qiáng)制類型轉(zhuǎn)換為派生類對(duì)象指針(引用)。值得注意的是遍坟,向下轉(zhuǎn)型不安全钧萍,沒有自動(dòng)轉(zhuǎn)換的機(jī)制。

RTTI(Run-Time Type Identification)

RTTI(運(yùn)行時(shí)類型識(shí)別)的功能由兩個(gè)運(yùn)算符實(shí)現(xiàn)政鼠。

  • typeid運(yùn)算符风瘦,用于返回表達(dá)式的類型。
  • dynamic_cast運(yùn)算符公般,用于將基類的指針或引用安全地轉(zhuǎn)換成派生類的指針或引用万搔。

它們的簡單示例如下。

int main(int argc, char const *argv[])
{
    A *a = new B();
    if(B *b = dynamic_cast<B*>(a)){
        b->func();
    }
    const type_info &info1 = typeid(a);
    cout<<info1.name()<<endl;
    return 0;
}

多重繼承和虛繼承

C++還支持多重繼承官帘,即一個(gè)派生類可以有多個(gè)基類瞬雹。它的語法如下。

class 類名:繼承方式 基類1刽虹,繼承方式 基類2酗捌,...
{
    派生類新增成員的聲明;
}

多重繼承同時(shí)繼承多個(gè)基類的成員涌哲,更好的軟件重用胖缤。但多重繼承可能會(huì)有大量的二義性,多個(gè)基類中可能包含同名變量或函數(shù)阀圾。要解決歧義的問題可以使用作用域運(yùn)算符明確指定要訪問哪個(gè)基類中的成員來解決哪廓。

當(dāng)派生類從多個(gè)基類派生,而這些基類又從同一個(gè)基類派生初烘,這時(shí)涡真,派生類會(huì)有多個(gè)基類的基類的拷貝分俯。如下面的代碼。

#include <iostream>
using namespace std;

class A{
public:
    int val_;
    A(int val):val_(val){}
};

class B:public A{
public:
    B(int val):A(val){}
};

class C:public A{
public:
    C(int val):A(val){}
};

class D:public B,public C{
public:
    D(int b, int c):B(b),C(c){}
};

int main(int argc, char const *argv[])
{
    D d(1,2);
    // cout<<d.val_<<endl; //ambiguous
    cout<<d.B::val_<<" "<<d.C::val_<<endl;
    system("pause");
    return 0;
}

這時(shí)候哆料,可以通過使用作用域操作符來避免歧義缸剪,但是卻無法避免產(chǎn)生多個(gè)A類的對(duì)象。為了解決這個(gè)問題东亦,C++引入了虛基類杏节。虛基類的聲明比較簡單,繼承的時(shí)候使用virtual修飾基類即可讥此,如class A:virtual public B拢锹。虛基類解決了多繼承時(shí)可能發(fā)生的對(duì)同一基類繼承多次而產(chǎn)生的二義性問題。為最遠(yuǎn)的派生類提供唯一的基類成員萄喳,而不重復(fù)產(chǎn)生多次拷貝卒稳。

虛基類的成員是由最遠(yuǎn)派生類的構(gòu)造函數(shù)通過調(diào)用虛基類的構(gòu)造函數(shù)進(jìn)行初始化的。在整個(gè)繼承結(jié)構(gòu)中他巨,直接過間接繼承虛基類的所有派生類充坑,都必須在構(gòu)造函數(shù)的成員初始化表中給出對(duì)虛基類的構(gòu)造函數(shù)的調(diào)用。如果未列出染突,則表示調(diào)用該虛基類的默認(rèn)構(gòu)造函數(shù)捻爷。在建立對(duì)象時(shí),只有最遠(yuǎn)派生類的構(gòu)造函數(shù)調(diào)用虛基類的構(gòu)造函數(shù)份企,該派生類的其他虛基類構(gòu)造函數(shù)的調(diào)用被忽略也榄。

可以使用虛基類把上面的例子改寫。

#include <iostream>
using namespace std;

class A{
public:
    int val_;
    A(int val):val_(val){}
};

class B:virtual public A{
public:
    B(int val):A(val){}
};

class C:virtual public A{
public:
    C(int val):A(val){}
};

class D:public B,public C{
public:
    D(int a,int b, int c):A(a),B(b),C(c){}
};

int main(int argc, char const *argv[])
{
    D d(1,2,3);
    cout<<d.val_<<endl; //the result is 1
    return 0;
}

多態(tài)和虛函數(shù)

多態(tài)性是面向?qū)ο蟪绦蛟O(shè)計(jì)的重要特征之一司志。多態(tài)性是指發(fā)出同樣的消息被不同類型的對(duì)象接收時(shí)可能導(dǎo)致完全不同的行為甜紫。多態(tài)是通過動(dòng)態(tài)綁定來實(shí)現(xiàn)的,也就是綁定過程在程序運(yùn)行時(shí)完成骂远,在程序運(yùn)行時(shí)才確定將要調(diào)用的函數(shù)囚霸。相對(duì)應(yīng)的是靜態(tài)綁定,綁定過程出現(xiàn)在編譯階段激才,在編譯期就已確定要調(diào)用的函數(shù)拓型。

虛函數(shù),即在基類中使用virtual關(guān)鍵字修飾的成員函數(shù)瘸恼,虛函數(shù)的定義如下劣挫。

class 類名{
    virtual 函數(shù)類型 函數(shù)名稱(參數(shù)列表);
};

如果一個(gè)函數(shù)在基類中被聲明為虛函數(shù),則他在所有派生類中都是虛函數(shù)钞脂。只有通過基類指針或引用調(diào)用虛函數(shù)才能引發(fā)動(dòng)態(tài)綁定揣云。虛函數(shù)不能聲明為靜態(tài)。

虛函數(shù)中還需要說明的是虛析構(gòu)函數(shù)冰啃,構(gòu)造函數(shù)不能是虛函數(shù)邓夕,但析構(gòu)函數(shù)可以是虛函數(shù)。當(dāng)通過一個(gè)基類指針刪除一個(gè)派生類對(duì)象時(shí)阎毅,如果希望調(diào)用派生類的構(gòu)造函數(shù)焚刚,需要將基類的析構(gòu)函數(shù)定義為虛析構(gòu)函數(shù)。下面是一個(gè)具體的例子扇调。

#include <iostream>
using namespace std;

class A{
public:
    int val_;
    virtual void func(){
        cout<<"A::func()"<<endl;
    }
    A(int val):val_(val){}
    virtual ~A(){
        cout<<" ~A()"<<endl;
    }
};

class B: public A{
public:
    void func(){
        cout<<"B::func()"<<endl;
    }
    B(int val):A(val){}
    ~B(){
        cout<<" ~B()"<<endl;
    }
};

int main(int argc, char const *argv[])
{
    A *a = new B(1);
    a->func();/* 調(diào)用B::func() */
    B b(2);
    A a1 = b;
    a1.func(); /* 調(diào)用A::func(),發(fā)生了對(duì)象的裁剪 */
    delete a; /* 會(huì)調(diào)用B類的析構(gòu)函數(shù)矿咕,如果去掉~A()的virtual關(guān)鍵字則不會(huì) */
    return 0;
}

純虛函數(shù)和抽象類

虛函數(shù)是實(shí)現(xiàn)多態(tài)的前提,所以狼钮,我們可以在基類中定義共同的接口碳柱,即把成員函數(shù)定義為虛函數(shù)。但有時(shí)候在基類中不能給出有意義的虛函數(shù)定義熬芜,如形狀類shape有一個(gè)draw方法莲镣,但是基類是沒有形狀的,所以無法實(shí)現(xiàn)draw方法涎拉。這時(shí)候瑞侮,可以將這些接口定義為純虛函數(shù),也就是說鼓拧,它是沒有意義的半火,它的定義需要派生類來實(shí)現(xiàn),所以純虛函數(shù)不需要實(shí)現(xiàn)季俩。純虛函數(shù)的聲明形式如下钮糖。

class 類名{
    virtual 返回值類型 函數(shù)名(參數(shù)表) = 0;
}

抽象類就是至少有一個(gè)純虛函數(shù)的類。抽象類無法創(chuàng)建對(duì)象酌住,就像你無法畫出一個(gè)抽象的形狀店归,從編程角度來說,抽象類中含有沒有給出實(shí)現(xiàn)的純虛函數(shù)赂韵,所以抽象類是不完整的娱节,因此無法創(chuàng)建對(duì)象。抽象類的作用是將有關(guān)的數(shù)據(jù)和行為組織在一個(gè)繼承層次結(jié)構(gòu)中祭示,保證派生類具有要求的行為肄满。對(duì)于暫時(shí)無法實(shí)現(xiàn)的函數(shù),可以聲明為純虛函數(shù)质涛,留給派生類去實(shí)現(xiàn)稠歉。抽象類不能用于字節(jié)創(chuàng)建對(duì)象實(shí)例,可以聲明抽象類的指針和引用汇陆∨ǎ可使用指向抽象類的指針支持運(yùn)行時(shí)多態(tài)性。派生類中必須實(shí)現(xiàn)基類中的純虛函數(shù)毡代,否則它仍將被看做一個(gè)抽象類阅羹。

重載勺疼、重寫和重定義

  • 重載是發(fā)生在相同的范圍(如同一個(gè)類中),必須滿足函數(shù)名稱相同且參數(shù)不同捏鱼,virtual關(guān)鍵字不影響函數(shù)的重載执庐。
  • 重寫是發(fā)生在派生類和基類中,基類函數(shù)必須使用virtual關(guān)鍵字修飾导梆,需要滿足函數(shù)名稱相同且參數(shù)相同轨淌。
  • 重定義發(fā)生在派生類和基類中,必須滿足函數(shù)相同名且參數(shù)相同看尼,并且基類函數(shù)無virtual關(guān)鍵字递鹉。

請(qǐng)看下面這個(gè)例子。

#include <iostream>
using namespace std;

class A{
public:
    virtual void func(){
        cout<<"A::func()"<<endl;
    }
    void func(int a){ /* 重載 */
        cout<<"A::func(int a)"<<endl;
    }
};

class B:public A{
public:
    void func(){ /* 重寫 */
        cout<<"B::func()"<<endl;
    }

    void func(int a){ /* 重定義 */
        cout<<"B::func(int a)"<<endl; 
    }

    void func(char c){ /* 重載藏斩,子類新增一個(gè)定義 */
        cout<<"B::func(char c)"<<endl;
    }
};


int main(int argc, char const *argv[])
{
    B b;
    b.func();
    b.A::func(); /* 虛函數(shù)也可以這樣強(qiáng)制調(diào)用 */
    b.func(1);
    b.A::func(1);
    b.func('A');
    return 0;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末躏结,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子灾茁,更是在濱河造成了極大的恐慌窜觉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件北专,死亡現(xiàn)場離奇詭異禀挫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拓颓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門语婴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驶睦,你說我怎么就攤上這事砰左。” “怎么了场航?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵缠导,是天一觀的道長。 經(jīng)常有香客問我溉痢,道長僻造,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任孩饼,我火速辦了婚禮髓削,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镀娶。我一直安慰自己立膛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布梯码。 她就那樣靜靜地躺著宝泵,像睡著了一般好啰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鲁猩,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天坎怪,我揣著相機(jī)與錄音罢坝,去河邊找鬼廓握。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嘁酿,可吹牛的內(nèi)容都是我干的隙券。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼闹司,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼娱仔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起游桩,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤牲迫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后借卧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盹憎,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡泽示,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年韩肝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片迷守。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡镰吵,死狀恐怖檩禾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情疤祭,我是刑警寧澤盼产,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站勺馆,受9級(jí)特大地震影響戏售,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜谓传,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一蜈项、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧续挟,春花似錦紧卒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轴总。三九已至,卻和暖如春博个,著一層夾襖步出監(jiān)牢的瞬間怀樟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工盆佣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留往堡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓共耍,卻偏偏與公主長得像虑灰,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子痹兜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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