1. 程式人生 > 其它 >C++繼承

C++繼承

技術標籤:C/C++

繼承 與 派生

繼承(Inheritance)可以理解為一個類從另一個類獲取成員變數和成員函式的過程。

派生(Derive)

繼承是兒子接收父親的產業,派生是父親把產業傳承給兒子。

被繼承的類稱為父類或基類,繼承的類稱為子類或派生類。

class 派生類名:[繼承方式] 基類名{
    // 成員
};

例:
class Student : public People { };

繼承方式

繼承方式包括 public(公有的)private(私有的)protected(受保護的),此項是可選的,如果不寫,那麼預設為 private

  1. public繼承方式
  • 基類中所有 public 成員在派生類中為 public 屬性;
  • 基類中所有 protected 成員在派生類中為 protected 屬性;
  • 基類中所有 private 成員在派生類中不能使用。
  1. protected繼承方式
  • 基類中的所有 public 成員在派生類中為 protected 屬性;
  • 基類中的所有 protected 成員在派生類中為 protected 屬性;
  • 基類中的所有 private 成員在派生類中不能使用。
  1. private繼承方式
  • 基類中的所有 public 成員在派生類中均為 private 屬性;
  • 基類中的所有 protected 成員在派生類中均為 private 屬性;
  • 基類中的所有 private 成員在派生類中不能使用。

繼承方式中的 public

protectedprivate 是用來指明基類成員在派生類中的 最高訪問許可權

基類中的 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 只能改變基類中 publicprotected 成員的訪問許可權,不能改變 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

初始化順序 成員變數familyschoolperson

輸出結果可以清晰的看到:初始化 基類優先,基類中先繼承的優先

命名衝突

當兩個或多個基類中有同名的成員時,如果直接訪問該成員,就會產生命名衝突,編譯器不知道使用哪個基類的成員。

因此需要在成員名字前面加上 類名域解析符 ::,以顯式地指明到底使用哪個類的成員,消除二義性。

#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

子類 是由 基類 派生而來,因此將 子類物件賦值給基類物件,便可以修改基類相關的引數就會發生改變

image

只能用派生類物件給基類物件賦值,而不能用基類物件給派生類物件賦值 ,因為 基類不包含派生類的成員變數,無法對派生類的成員變數賦值。同理,同一基類的不同派生類物件之間也不能賦值。

將派生類指標賦值給基類指標

編譯器通過指標來訪問成員變數,指標指向哪個物件就使用哪個物件的資料;編譯器通過指標的型別來訪問成員函式,指標屬於哪個類的型別就使用哪個類的函式。

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

向上轉型後通過基類的物件、指標、引用只能訪問從基類繼承過去的成員(包括成員變數和成員函式),不能訪問派生類新增的成員