1 private 和 protected 繼承,子類指針不能賦值給父類指針
如下代碼,有一個基類 Base,Derived1,Derived2,Derived3 3 個子類繼承了基類 Base,分別是 private 繼承,protected 繼承,public 繼承。在 main 函數里,分別使用 new 來創建 3 個子類,將 3 個子類指針賦值給 Base 指針,private 和 protected 繼承的時候,子類指針無法賦值給父類指針。
private 繼承或者 protected 繼承下,父類的屬性和函數在子類中的權限發生了變動。所以使用父類指針指向子類,可能會導致不安全的行為。
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base()" << std::endl;}~Base() {std::cout << "~Base()" << std::endl;}virtual void Do() {std::cout << "Base() Do()" << std::endl;}
};class Derived1 : private Base {
public:Derived1() {std::cout << "Derived1()" << std::endl;}~Derived1() {std::cout << "~Derived1()" << std::endl;}
};class Derived2 : protected Base {
public:Derived2() {std::cout << "Derived2()" << std::endl;}~Derived2() {std::cout << "~Derived2()" << std::endl;}
};class Derived3 : public Base {
public:Derived3() {std::cout << "Derived3()" << std::endl;}~Derived3() {std::cout << "~Derived3()" << std::endl;}
};int main() {Base *b1 = new Derived1();Base *b2 = new Derived2();Base *b3 = new Derived3();b1->Do();b2->Do();b3->Do();return 0;
}
編譯結果如下:
2 子類和父類中的同名函數
在類繼承中,一般子類可以覆蓋父類中的虛函數,這是我們使用繼承和多態常用的方式。如果子類中的函數和父類中的函數同名,并且這個函數不是虛函數呢,父類指針調用函數的時候調用的是父類中的函數,還是子類中的函數 ?在實際開發中,一般不會這么使用,這樣的代碼是毫無意義的,是自相矛盾的,因為繼承了父類,那么就是要復用父類中的方法與屬性,即使要覆蓋也應該覆蓋虛函數,如果不是虛函數,那么顯得不倫不類。這里只是對這種情況做一個探討。
#include <iostream>
#include <string>class Base {
public:Base() {std::cout << "Base()" << std::endl;}~Base() {std::cout << "~Base()" << std::endl;}void Do() {std::cout << "Base() Do()" << std::endl;}virtual void VDo() {std::cout << "Base() VDo()" << std::endl;}
};class Derived : public Base {
public:Derived() {std::cout << "Derived()" << std::endl;}~Derived() {std::cout << "~Derived()" << std::endl;}void Do() {std::cout << "Derived() Do()" << std::endl;}void VDo() {std::cout << "Derived() VDo()" << std::endl;}
};int main() {std::cout << "----------------" << std::endl;Base *b = new Derived();b->Do();b->VDo();std::cout << "----------------" << std::endl;Base b1 = *(new Derived);b1.Do();b1.VDo();std::cout << "----------------" << std::endl;Base &b2 = *(new Derived);b2.Do();b2.VDo();return 0;
}
Derived 和 Base 之間的賦值,不管是指針賦值,引用賦值還是值賦值,Base 指針,引用,值調用的都是 Base 中的 Do() 函數;Base 指針和引用調用的是 Derived 中的 VDo(),Base 值調用的是 Base 中的 VDo。
3 編譯器默認生成的函數
面向對象的語言中 =、& 這么基礎的運算符也是需要函數來實現的,這就是 c++ 的特點,一切皆對象。這個在 c 語言中很難理解的。
如果聲明了一個對象,這個對象中什么也沒有聲明,那么為了使用這些基礎的運算,編譯器會默認生成一些函數。
構造函數 | 類的構造 |
拷貝構造函數 | 拷貝構造 |
析構函數 | 析構 |
賦值運算符 | 賦值 |
取地址運算符 | 取值 |
取值運算符 | 取地址 |
#include <iostream>
#include <string>class Base {
};int main() {Base b1;Base b2;b2 = b1;Base b3 = b2;std::cout << "&b1 = " << &b1 << std::endl;std::cout << "&b2 = " << &b2 << std::endl;return 0;
}
c++ 默認生成的函數,比如構造函數,如果我們在定義類的時候定義了自己的構造函數,那么編譯器就不會生成默認的了。
如下代碼中,Base 是一個基類,Derived 繼承了 Base 類。在 Base 類中聲明了構造函數 Base(int i),這樣就不會有默認構造函數 Base() 了。在 Derived 類構造時,沒有顯式調用 Base 的構造函數,那么就會調用默認構造函數 Base(),但是默認構造函數又不存在,所以在編譯的時候就會報錯。需要在 Derived 構造列表中對 Base 進行構造,比如 Base(10)。
#include <iostream>
#include <string>class Base {
public:Base(int i) : i_(i) {std::cout << "Base(), i = " << i_ << std::endl;}private:int i_;
};class Derived : public Base {
public:Derived() {std::cout << "Derived(), i " << Base::i_ << std::endl;}
};int main() {Derived d;return 0;
}
4 私有繼承,繼承和組合
在討論設計模式的時候,有一個原則經常被提到: 多用組合,少用繼承,能用組合就用組合,萬不得已的時候才使用繼承。
私有繼承的兩個特點:
(1)私有繼承的派生類指針不能賦值給基類指針
(2)私有繼承之后,基類中的 public 和 protected 成員,在子類中都變成了 private 屬性,不能通過對象來訪問
比如對于汽車這個對象,包括汽車發動機,懸架,轉向 3 個主要的系統,我們在描述汽車的時候,如果使用私有繼承的方式繼承了發動機,懸架,轉向 3 個類,是可以實現的,但是從語義上是不通的;使用組合的方式,在語義上是相通的,因為汽車是由這 3 個對象組合而成的。
如下是一個私有繼承和組合的例子,Steering 表示轉向系統,BydCar 私有繼承了 Steering 類,TeslaCar 使用的組合。
#include <iostream>
#include <string>class Steering {
public:Steering() {std::cout << "Steering()" << std::endl;}~Steering() {std::cout << "~Steering()" << std::endl;}void Turn() {std::cout << "Steering() Turn()" << std::endl;}
};class BydCar : private Steering {
public:void Turn() {std::cout << "BydCar() Turn()" << std::endl;Steering::Turn();}
};class TeslaCar {
private:Steering steer;public:void Turn() {std::cout << "TeslaCar() Turn()" << std::endl;steer.Turn();}
};int main() {BydCar byd;TeslaCar tesla;byd.Turn();tesla.Turn();return 0;
}
使用組合的時候,一般組合的各個子元素都是已經實現的最終的結果,組合的元素是一個基本的元素,組合之后不能對這些基本元素進行修改,并且不能訪問元素中的私有成員;使用繼承的時候,基類可以是一個抽象類,派生類中也可以重寫基類的函數。
設計原則中有一個是對修改關閉,對擴展開開放。這兩個方面和繼承與組合也有一定的對應關系,組合相當于擴展,繼承可以修改,對組合開放相當于優先選用組合,對修改關閉相當于少用繼承。