C++繼承
技術標籤:C/C++
繼承 與 派生
繼承(Inheritance)可以理解為一個類從另一個類獲取成員變數和成員函式的過程。
派生(Derive)
繼承是兒子接收父親的產業,派生是父親把產業傳承給兒子。
被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。
class 派生類名:[繼承方式] 基類名{
// 成員
};
例:
class Student : public People { };
繼承方式
繼承方式包括 public(公有的)
、private(私有的)
和 protected(受保護的)
,此項是可選的,如果不寫,那麼預設為 private
。
- public繼承方式
- 基類中所有 public 成員在派生類中為 public 屬性;
- 基類中所有 protected 成員在派生類中為 protected 屬性;
- 基類中所有 private 成員在派生類中不能使用。
- protected繼承方式
- 基類中的所有 public 成員在派生類中為 protected 屬性;
- 基類中的所有 protected 成員在派生類中為 protected 屬性;
- 基類中的所有 private 成員在派生類中不能使用。
- private繼承方式
- 基類中的所有 public 成員在派生類中均為 private 屬性;
- 基類中的所有 protected 成員在派生類中均為 private 屬性;
- 基類中的所有 private 成員在派生類中不能使用。
繼承方式中的 public
protected
、private
是用來指明基類成員在派生類中的 最高訪問許可權 的
基類中的 protected 成員
可以在 派生類
中使用,而基類中的 private 成員
不能在 派生類
中使用
基類的 private 成員是能夠被繼承的,並且(成員變數)會佔用派生類物件的記憶體,只是在派生類中不可見,導致無法使用。private 成員的這種特性,能夠很好的對派生類隱藏基類的實現,以體現面向物件的封裝性。
#include <iostream> #include <string> using namespace std; // 模板類 class People { private: // 私有,無論是直接還是繼承 都無法訪問 string m_name; public: // 共有,私有變數介面,訪問並設定私有變數 string get_name() const { return m_name; }; void set_name(string name) { m_name = name; }; protected: // 保護,無法直接訪問,可以通過 繼承 被 子類 呼叫 string Identity; string get_Identity() const { return Identity; }; void set_Identity(string Identity) { this->Identity = Identity; }; }; class Student : public People { // 不能訪問 People 類的 private 變數,但可以使用 get函式 和 set函式 private: // 私有變數 string school_num; public: // 共有,私有變數介面,訪問並設定私有變數 string get_school_num() const { return school_num; }; void set_school_num(string school_num) { this->school_num = school_num; }; // 成員函式 void message() { this->set_Identity("xxx xxx xxx xxx xxx xxx"); // 訪問 保護 的成員函式 cout << "姓名:" << this->get_name() << endl << "身份證:" << this->Identity << endl << "學號:" << this->school_num << endl; } }; int main() { Student Sir; Sir.set_name("馬保國"); Sir.set_school_num("2020111401"); // Sir 不能訪問 protected 的成員函式 Sir.message(); return 0; } 結果: 姓名:馬保國 身份證:xxx xxx xxx xxx xxx xxx 學號:2020111401
在派生類中訪問基類 private 成員的唯一方法就是藉助基類的非 private 成員函式,如果基類沒有非 private 成員函式,那麼該成員在派生類中將無法訪問。
改變訪問許可權
使用 using 關鍵字可以改變基類成員在派生類中的訪問許可權
修飾符:
using 基類 : : 成員名;
注意:using 只能改變基類中 public
和 protected
成員的訪問許可權,不能改變 private 成員的訪問許可權
#include <iostream>
#include <string>
using namespace std;
// 基類
class People
{
private:
// 私有,無論是直接還是繼承 都無法訪問
string m_name;
public:
// 共有,私有變數介面,訪問並設定私有變數
string get_name() const { return m_name; };
void set_name(string name) { m_name = name; };
protected:
// 保護,無法直接訪問,可以通過 繼承 被 子類 呼叫
string Identity;
string get_Identity() const { return Identity; };
void set_Identity(string Identity) { this->Identity = Identity; };
};
// 子類
class Student : public People
{
// 修改許可權,不能修改 基類定義的 private 成員
private:
// 私有變數
string school_num;
// 修改許可權
using People::Identity;
public:
// 修改許可權,基類中為protected,繼承過來依舊是 protected,外部無法訪問,修改為 public ,可直接通過 類的例項物件訪問
using People::set_Identity;
using People::get_Identity;
// 共有,私有變數介面,訪問並設定私有變數
string get_school_num() const { return school_num; };
void set_school_num(string school_num) { this->school_num = school_num; };
// 成員函式
void message()
{
cout
<< "姓名:" << this->get_name() << endl
<< "身份證:" << this->Identity << endl
<< "學號:" << this->school_num << endl;
}
};
int main()
{
Student Sir;
Sir.set_name("馬保國");
Sir.set_Identity("xxx xxx 1951xxxx xxxx"); // 修改許可權,訪問受保護的函式
Sir.set_school_num("2020111401");
Sir.message();
return 0;
}
結果:
姓名:馬保國
身份證:xxx xxx 1951xxxx xxxx
學號:2020111401
繼承時的名字遮蔽問題
如果派生類中的成員(包括成員變數和成員函式)和基類中的成員重名,那麼就會遮蔽從基類繼承過來的成員。
#include <iostream>
#include <string>
using namespace std;
// 基類
class People
{
public:
int sum(int a, int b)
{
return a + b;
}
};
// 派生類
class Student : public People
{
public:
string sum(string a, string b)
{
return a + b;
}
};
void Study()
{
Student Sir;
//Sir.sum(6,9); 直接 error,強制型別轉換
cout << Sir.sum("渾圓形意太極門掌門人", "馬保國");
}
如果成員被遮蔽,但仍要要訪問, 則就要加上類名和域解析符 來訪問
Sir.People::sum(6,9);
基類成員函式和派生類成員函式不構成過載
成員函式,不管函式的引數如何,只要名字一樣就會造成遮蔽
基類和派生類的建構函式
類的建構函式不能被繼承
在派生類的建構函式中呼叫基類的建構函式,對基類的 private變數 進行 初始化
實現方式:
- 在 派生類建構函式定義 時,對 派生類成員變數 初始化,以及 基類 建構函式 初始化
#include <iostream>
#include <string>
using namespace std;
// 基類
class People
{
protected:
string m_name;
public:
// 建構函式
People(string name) : m_name(name) { cout << this->m_name << endl; };
// 解構函式
~People() { };
};
// 子類
class Student : public People
{
private:
string m_work;
public:
// 建構函式
Student(string name, string work) : m_work(work), People(name) { cout << this->m_work << endl; };
// 解構函式
~Student() { };
};
class Pupil : public Student
{
private:
int m_age;
public:
// 建構函式
Pupil(string name, string work,int age) : m_age(age), Student(name,work) { cout << m_age << endl; };
// 解構函式
~Pupil() { };
};
int main()
{
Pupil Sir("馬保國", "渾圓形意太極門掌門人",69);
return 0;
}
結果:
馬保國
渾圓形意太極門掌門人
69
注意:基類建構函式的呼叫放在函式頭部,不能放在函式體中。 因為基類建構函式不會被繼承,不能當做普通的成員函式來呼叫。
基類建構函式總是被優先呼叫,這說明建立派生類物件時,會先呼叫基類建構函式,再呼叫派生類建構函式
C++ 當存在多級繼承,A -> B -> C , 其中禁止在 C 中顯式地呼叫 A 的建構函式。
注意:構造引數顯式的定義,系統不會再生成預設的建構函式,就必須傳參,若不傳參,就需要手動定義一個空的建構函式
Student Sir; // 錯誤,建立物件,系統不會建立預設的建構函式
基類和派生類的解構函式
和建構函式類似,解構函式也不能被繼承
派生類的解構函式中不用顯式地呼叫基類的解構函式
解構函式的執行順序:
- 建立派生類物件時,建構函式的執行順序和繼承順序相同,即先執行基類建構函式,再執行派生類建構函式。
- 而銷燬派生類物件時,解構函式的執行順序和繼承順序相反,即先執行派生類解構函式,再執行基類解構函式。
#include <iostream>
#include <string>
using namespace std;
// 基類
class People
{
protected:
string m_name;
public:
// 建構函式
People(string name) : m_name(name) { };
// 解構函式
~People() { cout << this->m_name << endl; };
};
// 子類
class Student : public People
{
private:
string m_work;
public:
// 建構函式
Student(string name, string work) : m_work(work), People(name) { };
// 解構函式
~Student() { cout << this->m_work << endl; };
};
// 子孫類
class Pupil : public Student
{
private:
int m_age;
public:
// 建構函式
Pupil(string name, string work,int age) : m_age(age), Student(name,work) { };
// 解構函式
~Pupil() { cout << m_age << endl; };
};
int main()
{
Pupil Sir("馬保國", "渾圓形意太極門掌門人",69);
return 0;
}
結果:
69
渾圓形意太極門掌門人
馬保國
多繼承(多重繼承)
派生類都只有一個基類,稱為單繼承(Single Inheritance)。
一個派生類可以有兩個或多個基類, 稱為多繼承(Multiple Inheritance)。
多繼承容易讓程式碼邏輯複雜、思路混亂,一直備受爭議,中小型專案中較少使用,後來的 Java、C#、PHP 等乾脆取消了多繼承。
格式:
class 子類 : 修飾符 基類1, 修飾符 基類2 ·····{
// 成員
}
例如:
class Student : public School , protected Family, private Person{
// 成員
}
建構函式:
子類(形參列表): 基類1(實參列表), 基類2(實參列表) ···{
// 其他
}
例如:
Student (int num, string grade, int height , int weight) : School(grade) , Family(num), Person(height, weight){
// 其他
}
基類建構函式的呼叫順序 和它們在派生類建構函式中出現的順序無關,而是 和宣告派生類時基類出現的順序相同。
#include <iostream>
#include <string>
using namespace std;
// 基類 1
class School
{
private:
string m_grade;
public:
// 建構函式
School(string grade) : m_grade(grade) { cout << "班級:" << this->m_grade << endl; };
};
// 基類 2
class Family
{
private:
int m_num;
public:
// 建構函式
Family(int num) : m_num(num) { cout << "家庭人口數:" << this->m_num << endl; };
};
// 基類 3
class Person
{
private:
int m_height;
int m_weight;
public:
// 建構函式
Person(int height,int weight) : m_height(height),m_weight(weight) {
cout
<< "身高:" << this->m_height << endl
<< "體重:" << this->m_height << endl;
};
};
class Student : public School, protected Family, private Person {
private:
string m_name;
public:
// 建構函式
Student(string name,int num, string grade, int height, int weight) : m_name(name) ,Family(num), School(grade), Person(height, weight) {
// 其他
cout << "姓名:" <<this->m_name << endl;
}
};
int main()
{
Student Sir("小明",5,"軟體工程",168,120);
return 0;
}
結果:
班級:軟體工程
家庭人口數:5
身高:168
體重:168
姓名:小明
例子中,繼承的基類順序 school
family
person
初始化順序 成員變數
、family
、school
、person
輸出結果可以清晰的看到:初始化 基類優先,基類中先繼承的優先
命名衝突
當兩個或多個基類中有同名的成員時,如果直接訪問該成員,就會產生命名衝突,編譯器不知道使用哪個基類的成員。
因此需要在成員名字前面加上 類名 和 域解析符 ::
,以顯式地指明到底使用哪個類的成員,消除二義性。
#include <iostream>
#include <string>
using namespace std;
// 基類 1
class Family
{
protected:
string m_name;
public:
Family(string name) : m_name(name) {};
};
// 基類 2
class Person
{
protected:
string m_name;
public:
Person(string name) : m_name(name) {};
};
// 派生類
class Student : protected Family, private Person {
public:
Student(string n1,string n2) : Family(n1), Person(n2)
{
// cout << m_name << endl; // error,存在二義性,不確定是哪一個
cout << Family::m_name << endl;
}
};
int main()
{
Student Sir("小強","小亮");
return 0;
}
結果:
小強
虛繼承和虛基類詳解
多繼承(Multiple Inheritance)是指從多個直接基類中產生派生類的能力,多繼承的派生類繼承了所有父類的成員。
graph LR
A-->B
A-->C
B-->D
C-->D
類 A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,此時類 A 中的成員變數和成員函式繼承到類 D 中變成了兩份,一份來自 A–>B–>D ,另一份來自 A–>C–>D 。
在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變數中分別存放不同的資料,但大多數情況下這是多餘的:因為保留多份成員變數不僅佔用較多的儲存空間,還會產生命名衝突。
#include <iostream>
#include <string>
using namespace std;
// 基類
class Family
{
public:
void say() { cout << "A類" << endl; }
};
// 子類 1
class Person : public Family{};
// 子類 2
class Student : public Family{};
// 派生類,多繼承
class Man : public Person, public Student{};
int main()
{
Man Sir;
// Sir.say(); error,錯誤。不明確,不知道來自哪
Sir.Student::say();
Sir.Person::say();
return 0;
}
虛繼承
虛繼承(Virtual Inheritance)使得在派生類中只保留一份間接基類的成員。
虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。
virtual
關鍵字 代表 虛繼承
#include <iostream>
#include <string>
using namespace std;
// 基類
class Family
{
public:
void say() { cout << "A類" << endl; }
};
// 子類 1
class Person : virtual public Family{};
// 子類 1
class Student : virtual public Family{};
// 派生類,多繼承
class Man : public Person, public Student{};
int main()
{
Man Sir;
// 均可以訪問
Sir.say();
Sir.Student::say();
Sir.Person::say();
return 0;
}
C++標準庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自一個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須採用虛繼承,否則將導致 iostream 類中保留兩份 base_ios 類的成員。
在虛繼承的最終派生類中只保留了一份虛基類的成員,所以該成員可以被直接訪問,不會產生二義性
假設 A 定義了一個名為 x 的成員變數,當我們在 D 中直接訪問 x 時,會有三種可能性:
-
如果 B 和 C 中都沒有 x 的定義,那麼 x 將被解析為 A 的成員,此時不存在二義性。
-
如果 B 或 C 其中的一個類定義了 x,也不會有二義性,派生類的 x 比虛基類的 x 優先順序更高。
#include <iostream>
#include <string>
using namespace std;
// 基類
class Family
{
public:
void say() { cout << "A類" << endl; }
};
// 子類 1
class Person : virtual public Family{
public:
void say() { cout << "Person類" << endl; }
};
// 子類 1
class Student : virtual public Family{};
// 派生類,多繼承
class Man : public Person, public Student{};
int main()
{
Man Sir;
Sir.say();
Sir.Student::say();
Sir.Person::say();
return 0;
}
- 如果 B 和 C 中都定義了 x,那麼直接訪問 x 將產生二義性問題。
#include <iostream>
#include <string>
using namespace std;
// 基類
class Family
{
public:
void say() { cout << "A類" << endl; }
};
// 子類 1
class Person : virtual public Family{
public:
void say() { cout << "Person類" << endl; }
};
// 子類 1
class Student : virtual public Family{
public:
void say() { cout << "Student類" << endl; }
};
// 派生類,多繼承
class Man : public Person, public Student{};
int main()
{
Man Sir;
//Sir.say(); error,錯誤,存在二義性
Sir.Student::say();
Sir.Person::say();
return 0;
}
虛繼承時的建構函式
最終派生類的建構函式必須要呼叫虛基類的建構函式
虛基類是間接基類(間接繼承),而不是直接基類(繼承)。
#include <iostream>
#include <string>
using namespace std;
// 基類
class Family
{
private:
int m_a;
public:
// 建構函式
Family(int a) : m_a(a){
cout << "m_a=" << m_a << ";";
};
};
// 子類 1
class Person : virtual public Family{
private:
int m_b;
public:
// 建構函式
Person(int a, int b): m_b(b) ,Family(a) {
cout << "m_b=" << m_b << ";";
};
};
// 子類 2
class Student : virtual public Family{
private:
int m_c;
public:
// 建構函式
Student(int a,int c): m_c(c), Family(a) {
cout << "m_c=" << m_c << ";";
};
};
// 派生類,多繼承
class Man : public Person, public Student{
private:
int m_d;
public:
// 建構函式
Man(int a, int b ,int c ,int d): m_d(d),Person(a,b),Family(a),Student(a,c) {
cout << "m_d=" << m_d << ";";
};
};
int main()
{
Man Sir(1,2,3,4);
cout << endl;
Person p(1, 2);
cout << endl;
Student s(1, 2);
cout << endl;
return 0;
}
結果:
m_a=1;m_b=2;m_c=3;m_d=4;
m_a=1;m_b=2;
m_a=1;m_c=2;
編譯器總是先呼叫 虛基類的建構函式 ,再按照 出現的順序 呼叫 其他的建構函式
派生類賦值給基類
類 是一種 資料型別 ,可以發生 資料型別轉換 ,這種轉換隻有在 基類 和 派生類 之間才有意義,並且只能將派生類賦值給基類 ,包括將派生類物件賦值給基類物件、將派生類指標賦值給基類指標、將派生類引用賦值給基類引用,稱為向上轉型(Upcasting)。
將基類賦值給派生類稱為 向下轉型(Downcasting)
將派生類物件賦值給基類物件
賦值的本質是將現有的資料寫入已分配好的記憶體中,物件的記憶體只包含了成員變數,所以物件之間的賦值是成員變數的賦值,成員函式不存在賦值問題。
#include <iostream>
#include <string>
using namespace std;
// 基類
class People
{
public:
int m_num;
public:
// 建構函式
People(int num) : m_num(num) {};
// 成員變數
void show()
{
cout << "num = " << this->m_num << endl;
}
};
// 子類
class Student : public People {
public:
int m_code;
public:
// 建構函式
Student(int code, int num) :m_code(code), People(num) {};
// 成員變數
void show()
{
cout << "code = " << this->m_code << ";"
<< "num = " << this->m_num << endl;
}
};
int main()
{
People sir_1(10);
sir_1.show();
Student sir_2(996, 777);
sir_2.show();
// 將 子類 賦值 基類
sir_1 = sir_2;
sir_1.show();
sir_2.show();
return 0;
}
結果:
num = 10
code = 996;num = 777
num = 777
code = 996;num = 777
子類 是由 基類 派生而來,因此將 子類物件賦值給基類物件,便可以修改基類相關的引數就會發生改變
只能用派生類物件給基類物件賦值,而不能用基類物件給派生類物件賦值 ,因為 基類不包含派生類的成員變數,無法對派生類的成員變數賦值。同理,同一基類的不同派生類物件之間也不能賦值。
將派生類指標賦值給基類指標
編譯器通過指標來訪問成員變數,指標指向哪個物件就使用哪個物件的資料;編譯器通過指標的型別來訪問成員函式,指標屬於哪個類的型別就使用哪個類的函式。
int main()
{
People *sir_1 = new People(10);
sir_1->show();
Student* sir_2 = new Student(996, 777);
sir_2->show();
// 將 子類 賦值 基類
sir_1 = sir_2;
sir_1->show();
sir_2->show();
return 0;
}
結果:
num = 10
code = 996;num = 777
num = 777
code = 996;num = 777
將派生類引用賦值給基類引用
引用和指標的類似,是因為引用和指標本質上區別不大,引用僅僅是對指標進行了簡單封裝
int main()
{
Student sir_2(996, 777);
sir_2.show();
People &sir_1 = sir_2;
sir_1.show();
sir_2.show();
return 0;
}
結果:
code = 996;num = 777
num = 777
code = 996;num = 777
向上轉型後通過基類的物件、指標、引用只能訪問從基類繼承過去的成員(包括成員變數和成員函式),不能訪問派生類新增的成員