C++——繼承
繼承
繼承是面向物件三大特性之一
有些類與類之間存在特殊的關係,例如下圖中:
我們發現,定義這些類時,下級別的成員除了擁有上一級的共性,還有自己的特性。
這個時候我們就可以考慮利用繼承的技術,減少重複程式碼
1 繼承的基本語法
例如我們看到很多網站中,都有公共的頭部,公共的底部,甚至公共的左側列表,只有中心內容不同
接下來我們分別利用普通寫法和繼承的寫法來實現網頁中的內容,看一下繼承存在的意義以及好處
普通實現:
//Java頁面 class Java { public: void header() { cout << "首頁、公開課、登入、註冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類列表)" << endl; } void content() { cout << "JAVA學科視訊" << endl; } }; //Python頁面 class Python { public: void header() { cout << "首頁、公開課、登入、註冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類列表)" << endl; } void content() { cout << "Python學科視訊" << endl; } }; //C++頁面 class CPP { public: void header() { cout << "首頁、公開課、登入、註冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類列表)" << endl; } void content() { cout << "C++學科視訊" << endl; } }; void test01() { //Java頁面 cout << "Java下載視訊頁面如下: " << endl; Java ja; ja.header(); ja.footer(); ja.left(); ja.content(); cout << "--------------------" << endl; //Python頁面 cout << "Python下載視訊頁面如下: " << endl; Python py; py.header(); py.footer(); py.left(); py.content(); cout << "--------------------" << endl; //C++頁面 cout << "C++下載視訊頁面如下: " << endl; CPP cp; cp.header(); cp.footer(); cp.left(); cp.content(); } int main() { test01(); system("pause"); return 0; }
繼承實現:
//公共頁面 class BasePage { public: void header() { cout << "首頁、公開課、登入、註冊...(公共頭部)" << endl; } void footer() { cout << "幫助中心、交流合作、站內地圖...(公共底部)" << endl; } void left() { cout << "Java,Python,C++...(公共分類列表)" << endl; } }; //Java頁面 class Java : public BasePage { public: void content() { cout << "JAVA學科視訊" << endl; } }; //Python頁面 class Python : public BasePage { public: void content() { cout << "Python學科視訊" << endl; } }; //C++頁面 class CPP : public BasePage { public: void content() { cout << "C++學科視訊" << endl; } }; void test01() { //Java頁面 cout << "Java下載視訊頁面如下: " << endl; Java ja; ja.header(); ja.footer(); ja.left(); ja.content(); cout << "--------------------" << endl; //Python頁面 cout << "Python下載視訊頁面如下: " << endl; Python py; py.header(); py.footer(); py.left(); py.content(); cout << "--------------------" << endl; //C++頁面 cout << "C++下載視訊頁面如下: " << endl; CPP cp; cp.header(); cp.footer(); cp.left(); cp.content(); } int main() { test01(); system("pause"); return 0; }
總結:
繼承的好處:可以減少重複的程式碼
class A : public B;
A 類稱為子類 或 派生類
B 類稱為父類 或 基類
派生類中的成員,包含兩大部分:
一類是從基類繼承過來的,一類是自己增加的成員。
從基類繼承過過來的表現其共性,而新增的成員體現了其個性。
2 繼承方式
繼承的語法:class 子類 : 繼承方式 父類
繼承方式一共有三種:
- 公共繼承
- 保護繼承
- 私有繼承
示例:
class Base1 { public: int m_A; protected: int m_B; private: int m_C; }; //公共繼承 class Son1 :public Base1 { public: void func() { m_A; //可訪問 public許可權 m_B; //可訪問 protected許可權 //m_C; //不可訪問 } }; void myClass() { Son1 s1; s1.m_A; //其他類只能訪問到公共許可權 } //保護繼承 class Base2 { public: int m_A; protected: int m_B; private: int m_C; }; class Son2:protected Base2 { public: void func() { m_A; //可訪問 protected許可權 m_B; //可訪問 protected許可權 //m_C; //不可訪問 } }; void myClass2() { Son2 s; //s.m_A; //不可訪問 } //私有繼承 class Base3 { public: int m_A; protected: int m_B; private: int m_C; }; class Son3:private Base3 { public: void func() { m_A; //可訪問 private許可權 m_B; //可訪問 private許可權 //m_C; //不可訪問 } }; class GrandSon3 :public Son3 { public: void func() { //Son3是私有繼承,所以繼承Son3的屬性在GrandSon3中都無法訪問到 //m_A; //m_B; //m_C; } };
3 繼承中的物件模型
問題:從父類繼承過來的成員,哪些屬於子類物件中?
示例:
class Base
{
public:
int m_A;
protected:
int m_B;
private:
int m_C; //私有成員只是被隱藏了,但是還是會繼承下去
};
//公共繼承
class Son :public Base
{
public:
int m_D;
};
void test01()
{
//父類中所以非靜態成員屬性都會被子類繼承下去
//父類中私有成員屬性是被編譯器給隱藏,因此訪問不到
//但是確實被繼承下去了
cout << "sizeof Son = " << sizeof(Son) << endl;
}
int main() {
test01();
system("pause");
return 0;
}
利用工具檢視:
開啟工具視窗後,定位到當前CPP檔案的碟符
然後輸入: cl /d1 reportSingleClassLayout檢視的類名 所屬檔名
效果如下圖:
結論: 父類中私有成員也是被子類繼承下去了,只是由編譯器給隱藏後訪問不到
4 繼承中構造和析構順序
子類繼承父類後,當建立子類物件,也會呼叫父類的建構函式
問題:父類和子類的構造和析構順序是誰先誰後?
示例:
class Base
{
public:
Base()
{
cout << "Base建構函式!" << endl;
}
~Base()
{
cout << "Base解構函式!" << endl;
}
};
class Son : public Base
{
public:
Son()
{
cout << "Son建構函式!" << endl;
}
~Son()
{
cout << "Son解構函式!" << endl;
}
};
void test01()
{
//繼承中 先呼叫父類建構函式,再呼叫子類建構函式,析構順序與構造相反
Son s;
}
int main() {
test01();
system("pause");
return 0;
}
總結:繼承中 先呼叫父類建構函式,再呼叫子類建構函式,析構順序與構造相反
5 繼承同名成員處理方式
問題:當子類與父類出現同名的成員,如何通過子類物件,訪問到子類或父類中同名的資料呢?
- 訪問子類同名成員 直接訪問即可
- 訪問父類同名成員 需要加作用域
示例:
class Base {
public:
Base()
{
m_A = 100;
}
void func()
{
cout << "Base - func()呼叫" << endl;
}
void func(int a)
{
cout << "Base - func(int a)呼叫" << endl;
}
public:
int m_A;
};
class Son : public Base {
public:
Son()
{
m_A = 200;
}
//當子類與父類擁有同名的成員函式,子類會隱藏父類中所有版本的同名成員函式
//如果想訪問父類中被隱藏的同名成員函式,需要加父類的作用域
void func()
{
cout << "Son - func()呼叫" << endl;
}
public:
int m_A;
};
void test01()
{
Son s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl;
s.func();
s.Base::func();
s.Base::func(10);
}
int main() {
test01();
system("pause");
return EXIT_SUCCESS;
}
總結:
- 子類物件可以直接訪問到子類中同名成員
- 子類物件加作用域可以訪問到父類同名成員
- 當子類與父類擁有同名的成員函式,子類會隱藏父類中同名成員函式,加作用域可以訪問到父類中同名函式
6 繼承同名靜態成員處理方式
問題:繼承中同名的靜態成員在子類物件上如何進行訪問?
靜態成員和非靜態成員出現同名,處理方式一致
- 訪問子類同名成員 直接訪問即可
- 訪問父類同名成員 需要加作用域
示例:
class Base {
public:
static void func()
{
cout << "Base - static void func()" << endl;
}
static void func(int a)
{
cout << "Base - static void func(int a)" << endl;
}
static int m_A;
};
int Base::m_A = 100;
class Son : public Base {
public:
static void func()
{
cout << "Son - static void func()" << endl;
}
static int m_A;
};
int Son::m_A = 200;
//同名成員屬性
void test01()
{
//通過物件訪問
cout << "通過物件訪問: " << endl;
Son s;
cout << "Son 下 m_A = " << s.m_A << endl;
cout << "Base 下 m_A = " << s.Base::m_A << endl;
//通過類名訪問
cout << "通過類名訪問: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
//同名成員函式
void test02()
{
//通過物件訪問
cout << "通過物件訪問: " << endl;
Son s;
s.func();
s.Base::func();
cout << "通過類名訪問: " << endl;
Son::func();
Son::Base::func();
//出現同名,子類會隱藏掉父類中所有同名成員函式,需要加作作用域訪問
Son::Base::func(100);
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
總結:同名靜態成員處理方式和非靜態處理方式一樣,只不過有兩種訪問的方式(通過物件 和 通過類名)
7 多繼承語法
C++允許一個類繼承多個類
語法: class 子類 :繼承方式 父類1 , 繼承方式 父類2...
多繼承可能會引發父類中有同名成員出現,需要加作用域區分
C++實際開發中不建議用多繼承
示例:
class Base1 {
public:
Base1()
{
m_A = 100;
}
public:
int m_A;
};
class Base2 {
public:
Base2()
{
m_A = 200; //開始是m_B 不會出問題,但是改為mA就會出現不明確
}
public:
int m_A;
};
//語法:class 子類:繼承方式 父類1 ,繼承方式 父類2
class Son : public Base2, public Base1
{
public:
Son()
{
m_C = 300;
m_D = 400;
}
public:
int m_C;
int m_D;
};
//多繼承容易產生成員同名的情況
//通過使用類名作用域可以區分呼叫哪一個基類的成員
void test01()
{
Son s;
cout << "sizeof Son = " << sizeof(s) << endl;
cout << s.Base1::m_A << endl;
cout << s.Base2::m_A << endl;
}
int main() {
test01();
system("pause");
return 0;
}
總結: 多繼承中如果父類中出現了同名情況,子類使用時候要加作用域
8 菱形繼承
菱形繼承概念:
兩個派生類繼承同一個基類
又有某個類同時繼承者兩個派生類
這種繼承被稱為菱形繼承,或者鑽石繼承
典型的菱形繼承案例:
菱形繼承問題:
-
羊繼承了動物的資料,駝同樣繼承了動物的資料,當草泥馬使用資料時,就會產生二義性。
-
草泥馬繼承自動物的資料繼承了兩份,其實我們應該清楚,這份資料我們只需要一份就可以。
示例:
class Animal
{
public:
int m_Age;
};
//繼承前加virtual關鍵字後,變為虛繼承
//此時公共的父類Animal稱為虛基類
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main() {
test01();
system("pause");
return 0;
}
總結:
- 菱形繼承帶來的主要問題是子類繼承兩份相同的資料,導致資源浪費以及毫無意義
- 利用虛繼承可以解決菱形繼承問題