面向對象中關於繼承的總結。
#@author: gr
#@date: 2015-07-26
#@email: forgerui@gmail.com
一、類的隱藏
重載(overload)、覆蓋(override)與隱藏(hidden)。
重載:
相同的范圍(在同一個類中)
函數名字相同
參數不同
virtual 關鍵字可有可無
覆蓋(重寫):
不同的范圍(分別位於派生類與基類)
函數名字相同
參數相同
基類函數必須有 virtual 關鍵字
隱藏:
如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無 virtual 關鍵字,基類的函數將被隱藏(注意別與重載混淆)
如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有 virtual關鍵字。此時,基類的函數被隱藏,基類有virtual關鍵字的話就是覆蓋了。
父類是virtual方法 形參表相同 ---> 構成重寫
父類是virtual方法 形參表不同 ---> 隱藏父類方法
父類不是virtual方法 形參表相同 --->隱藏父類方法
父類不是virtual方法 形參表不同 --->隱藏父類方法
總結:
重載肯定是在一個域中的,C++不支持跨域重載,即子類同名函數永遠不與父類函數形成重載,而是隱藏掉父類同名函數。
覆蓋的要求比較嚴格,子類與父類只有一種情況構成重寫,三種情況下子類與父類同名函數構成隱藏。
繼承類函數會隱藏子類中所有同名的函數,即使是子類中的虛函數也會隱藏所有同名函數,即使將父類的函數聲明為virtual
都不會改善這種情況。
class A {
public:
void print() { //加上virtual不會改善這種情況
cout << "hello base class" << endl;
}
};
class B : public A {
public:
void print(int) { //加上virtual也不會改善這種情況
cout << "hello inheritance class" << endl;
}
};
上面的print(int)
會隱藏掉所有基類中叫print
的函數。
B b;
b.print(); //報錯,子類的該函數已被隱藏
如果想要調用A中的函數,需要使用類作用域來限定:
B b;
b.A::print(); //類作用域調用
除此之外,還可以在子類中使用using
聲明基類函數,如下:
class B : public A {
public:
using A::print; //注意這里沒有括號
void print(int) {
cout << "hello inheritance class" << endl;
}
};
二、多態實現
普通函數表現在靜態綁定上,即根據當前類類型決定調用函數,而不是實際的對象類型。如下:
class A {
public:
void print() {
cout << "hello base class" << endl;
}
};
class B : public A {
public:
void print() {
cout << "hello inheritance class" << endl;
}
};
void main() {
A* pA = new B();
pA->print(); //此句會調用A::print()
}
上面pA只會調用A類的print()
函數,如果我們想根據實際對象的類型來調用相應的函數,那么就需要進行動態綁定。將基類的print()
函數聲明為virtual
,可以解決這個問題。
class A {
public:
virtual void print() {
cout << "hello base class" << endl;
}
};
class B : public A {
public:
void print() {
cout << "hello inheritance class" << endl;
}
};
void main() {
A* pA = new B();
pA->print(); //根據對象的實際類型調用B::print()
}
虛函數的成本:
當然,"天下沒有免費的午餐"。實現這種功能必須花費一定的成本。
一般函數是不會增加對象空間的,一般C++對象的大小由非靜態成員變量,虛函數,以及對齊決定。
而使用虛函數,主要有兩個成本:
- 一個類會在內存中建立一個虛函數表(vtbl),用來記錄虛函數信息,所有該類對象共享該虛函數表。類的虛函數表是一塊連續的內存,每個內存單元中記錄一個JMP指令的地址,也就是每個函數的地址。
- 其次,會在每個類對象內增加一個指針(vptr, 32位的話占用4個字節),用以指向虛函數表。
因為基類、子類有兩個不同的虛函數表,且兩個虛函數指針指向各自的虛函數表,所以在運行時可以體現出多態。同理,如果作為函數參數,不能以值傳遞,這樣對象會被slice
,必須傳遞指針或引用。虛函數需要在運行時才能確定運行的函數,但在編譯時就要給出可以執行的命令,主要是如下幾步:
- 根據pC1所指對象的虛函數指針(vptr)索引到虛函數表(vtbl)。
- 在虛函數表中找到函數的偏移,記為i。
- 調用第二步中的函數,
*(pC1->vptr[i])(pC1);
三、類的權限控制
-
繼承方式
public繼承: public => public, protect =>protect, private => 不可訪問
protect繼承:public => protect, protect =>protect, private => 不可訪問
private繼承:public => private, protect => private, private => 不可訪問 -
子類成員
public成員:public => public, protect => protect, private => private
protect成員:public => protect, protect => protect, private => private
private成員:都不可訪問
默認繼承方式是private繼承(所以如果需要使用子類成員,需要public繼承),默認成員是private。
四、虛基類
含有純虛函數的類叫做虛基類,類似Java里的接口(interface
)。虛基類無法被實例化,只能通過繼承,實現子類。
class AbstractBase {
public:
virtual void solve() = 0; //純虛函數
virtual ~AbstractBase(){}
};
class Derivation : public class AbstractBase {
public:
void solve(){}
};
int main() {
AbstractBase* thing = new Derivation();
thing->solve();
}
五、繼承與組合
繼承是is a
關系,組合是has a
關系。有時需要根據事實正確塑模出其關系。
六、多繼承與虛繼承
為了滿足一個類包含兩個類的性質,C++提供多繼承。
為了解決多繼承帶來的包含多個基類問題,使用虛繼承,可以只包含一個基類。
class A {};
class B1 : virtual public A {}; //虛繼承
class B2 : virtual public A {}; //虛繼承
class C : public B1, public B2 {}; //使用虛繼承,C中只包含一個A