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;
}