1. 程式人生 > >【C++】:面向物件三大特性之繼承

【C++】:面向物件三大特性之繼承

面向物件三大特性之繼承

1.繼承的概念及定義

繼承(inheritance)機制是面向物件程式設計使程式碼可以複用的最重要的手段,它允許程式設計師在保持原有類特性的基礎上進行擴充套件,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向物件程式設計的層次結構, 體現了由簡單到複雜的認知過程。面向過程中的複用都是函式複用,繼承是類設計層次

的複用。

class Person
{
public:
	void Print()
	{
		cout << "name " << name << endl;
		cout << "age " << age << endl;
	}
protected:
	string name = "zhangsan";
	int age = 20;
};
//1.Stu和Tearch繼承了Person
//2.成員變數name\age和成員函式都變為Person的一部分
//3.Stu和Tearch複用了Person程式碼
class Stu :
public Person { protected: int stuid;//學號 }; class Tearch : public Person { protected: int jobid;//工號 };

我們可以通過監視視窗看到現象:
在這裡插入圖片描述

繼承的定義格式如下:

class Stu :public Person
{
	public:
		int id;
} 

在上邊的例子中,Stu為派生類,Person為基類,然後冒號後邊的public代表繼承方式。C++中存在三種繼承方式:

  • public繼承
  • protected繼承
  • private繼承

基類的成員隨著繼承方式

的不同在派生類當中的訪問限定符是存在變化的:

類成員\繼承方式 public繼承 protected繼承 private繼承
基類的public成員 派生類public 派生類 protected 派生類private
基類的protected成員 派生類protected 派生類 protected 派生類private
基類的private成員 派生類 private 派生類 private 派生類private
  • 基類private成員在派生類中無論以什麼方式繼承都是不可見的。不可見是指基類的私有成員可以被繼承到了派生類物件中,但是語法上限制派生類物件不管在類裡面還是類外面都不能去訪問它。
  • 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
  • 使用關鍵字class時預設的繼承方式是private,使用struct時預設的繼承方式是public,不過最好顯示的寫出繼承方式。

2.基類和派生類物件賦值相互轉化

  • 派生類物件可以直接賦值給基類的指標、引用和物件。這種做法叫切片或者切割,意思是將繼承基類的那部分切割出來賦值給父類。但是基類的物件是不能賦值給子類的物件。
  • 基類的指標可以通過強制型別轉化賦值給派生類的指標。但是當基類的指標指向派生類的物件才是安全的。

總結:

  • 派生類物件可以賦值給基類的物件、引用、指標 * 基類物件不能賦值給派生類的物件
  • 基類的指標可以賦值給派生類的指標(當這個指標指向派生類物件時時安全的)
  • 基類的指標(指向基類的物件)賦值給派生類指標時,會存在越界訪問的問題

3.繼承時的作用域

  • 父類和子類有獨立的作用域
  • 子類中如果存在和父類同名的成員,則在子類中將隱藏父類中的該同名成員,也叫做重定義。在子類中,若想訪問父類的同名成員,可以使用基類名::基類成員訪問
  • 如果是成員函式,只要函式名相同,則就會構成隱藏
  • 區別隱藏和過載:過載指的是在同一個作用域中,相同的名字不同的函式引數構成過載。隱藏是指在父類和子類兩個不同的作用域中,子類中對父類的同名成員構成隱藏

4.派生類中的預設成員函式

關於C++類中的6大預設成員函式:https://blog.csdn.net/hansionz/article/details/83411374

  • 派生類的建構函式必須呼叫基類的建構函式來初始化基類的成員。如果基類中沒有預設的建構函式,則必須在派生類的初始化列表中顯式的呼叫基類的構造
  • 派生類的拷貝構造必須先呼叫基類的拷貝構造來完成基類的拷貝初始化
  • 派生類的operator=必須要呼叫基類的operator=完成基類的賦值
  • 派生類的解構函式會在被呼叫完成後自動呼叫基類的解構函式清理基類成員。因為這樣才能保證派生類物件先清理派生類成員再清理基類成員的順序

總結:

  • 派生類物件在初始化時,先呼叫基類構造,在呼叫派生類構造
  • 派生類物件在析構時,先呼叫派生類的析構,在呼叫基類的析構

實現一個不能被繼承的類:

 class NonInherit1
{
public:
  static NonInherit1 GetInstace()
  {
    return NonInherit1();
  }
  //c++98 構造私有化讓一個類不被繼承
private:
  NonInherit1()
  {}
};
//c++11給出了新的關鍵字final讓一個類不被繼承
class NonInherit2 final
{};

5.繼承的友元和靜態成員

友元:
關於友元總結在我的另一篇部落格:https://blog.csdn.net/hansionz/article/details/83478079

友元關係不能被繼承,說明基類的友元函式或者友元類不能訪問派生類私有成員

靜態成員:
關於靜態成員總結在我的另一篇部落格:https://blog.csdn.net/hansionz/article/details/83478079

基類如果定義了一個靜態成員,則在整個繼承體系中只有一個靜態成員,無論派生出多少個子類

統計一個基類的派生類存在多少個物件:

class A
{
public:
  A()
  {
    ++count;
  }
public:
  static int count;
protected:
  int name;
};
//靜態成員在類外定義
int A::count = 0;

class B : public A
{};
class C : public A
{};

int main()
{
  B b;
  C c;
  cout << A::count << endl;//2
  return 0;
}

6.菱形繼承和菱形虛擬繼承

C++中的幾種繼承:

  • 單繼承:一個子類只有一個直接父類
  • 多繼承:一個子類存在兩個或兩個以上的直接父類
  • 菱形繼承:菱形繼承時多繼承的一種特殊情況。具體看下邊這幅繼承關係圖。
  • 在這裡插入圖片描述

菱形繼承存在的問題:從上面的基礎模型可以看出在Ass類中會存在兩份Person的資料,所以菱形繼承會存在資料的冗餘和二義性

class Person
{
public:
    string name;
};

class Stu:public Person
{
protected:
  int num;//學號
};

class Tearch:public Person 
{
protected:
  int id;//工號
};

class Ass :public Stu, public Tearch
{
protected:
  string major;
};

int main()
{
  Ass a;
  //這麼訪問會存在資料二義性
  //a.name = "zhangsan";
  //這麼做可以消除二義性,但是依然存在資料冗餘
  a.Stu::name = "zhangsan";
  a.Tearch::name = "lisi";

  return 0;
}

虛擬菱形繼承可以解決菱形繼承的二義性和資料冗餘的問題。如上面的繼承關係,在Stu和Tearch的繼承Person時使用虛擬菱形繼承時,可以解決資料的二義性和冗餘問題。

class Person
{
public:
    string name;
};

class Stu:virtual public Person
{
protected:
  int num;//學號
};

class Tearch:virtual public Person 
{
protected:
  int id;//工號
};

class Ass :public Stu, public Tearch
{
protected:
  string major;
};

int main()
{
  Ass a;
  //可以很好的解決二義性和資料冗餘問題
  a.name = "zhangsan";

  return 0;
}

虛擬繼承原理:以一個簡單的菱形繼承模型分析。

  • 當沒有使用虛擬繼承時
class A
{
public:
	int a;
};
class B :public A
{
public:
	int b;
};
class C :public A
{
public:
	int c;
};
class D :public B, public C
{
public:
	int d;
};
int main()
{
	D d;
	d.B::a = 1;
	d.C::a = 2;
	d.b = 3;
	d.c = 4;
	d.d = 5;
	return 0;
}

監視視窗和記憶體視窗可以觀察到:
在這裡插入圖片描述

我們可以發現在物件d中確實存在兩個a,存在二義性和資料冗餘問題。

  • 當使用虛擬菱形繼承時
class A
{
public:
	int a;
};
class B : virtual public A
{
public:
	int b;
};
class C : virtual public A
{
public:
	int c;
};
class D :public B, public C
{
public:
	int d;
};
int main()
{
	D d;
	d.B::a = 1;
	d.C::a = 2;
	d.b = 3;
	d.c = 4;
	d.d = 5;
	return 0;
}

在記憶體監視視窗中可以看到菱形虛擬繼承的模型:
在這裡插入圖片描述

從上邊可以看出出D物件中將A放到的了物件組成的最下面,這個A同時屬於B和C,那麼B和C如何去找到公共的A呢。這裡是通過了B和C的兩個指標,指向的一張表,這兩個指標叫虛基表指標,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的A,這就是虛擬繼承的原理。

為什麼B和C一定要找到自己的a呢?

 //當下面的賦值發生時,d要去找出B/C成員中的a才能賦值過去,切片    
 D d;    
 B b = d;    
 C c = d;

注:C++的語法太過複雜可以體現在多繼承上,多繼承可以算上是C++的缺陷之1,它們的底層實現很複雜,一般不建議設計出多繼承。

7.組合和繼承

  • 公有繼承時一種is-a關係,它表示派生類物件都是一個基類物件。而組合是一種has-a的關係,它表示假設A組合了B,那麼每一個A物件中都包含一個B物件。例如:車和寶馬構成is-a關係,而車和輪胎構成has-a關係。
  • 繼承允許根據基類的實現來定義派生類的實現。這種通過生成派生類的複用通常被稱為白箱複用(white-box reuse)。在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類類有很大的影響。派生類和基類間的依賴關係很強,耦合度高
  • 物件組合是類繼承之外的另一種程式碼複用選擇。新的更復雜的功能可以通過組合物件來獲得。物件組合要求被組合的物件具有良好定義的介面。這種複用風格被稱為黑箱複用(black-box reuse), 因為物件的內部細節是不可見的。物件只以“黑箱”的形式出現。 組合類之間沒有很強的依賴關係, 耦合度低。優先使用物件組合有助於你保持每個類被封裝。
  • 實際中多去用組合,因為組合的耦合度低,程式碼維護性好。不過繼承也有用武之地的,有些關係就適合繼承那就用繼承,另外要實現多型,也必須要繼承。
  • 優先使用組合,耦合度低,內聚高。