【C++】C++類的學習(四)——繼承與虛擬函式
前言
面向物件程式設計的核心思想是資料抽象、繼承和動態繫結(也稱之為動態聯編)。通過資料抽象將類的介面與實現分離;使用繼承可以定義相似的型別並對相似的關係建模;使用動態繫結可以在一定程度上忽視型別的區別,使用統一的方式使用他們的物件。
類是C++實現面向物件程式設計的手段,一個類把一類事物的相同屬性通過資料成員和成員函式的形式囊括起來,方便直接使用。前面的幾篇博文中講述了關於類的一些基本性質,今天介紹一下類的繼承。
某個學校為了統計學生資訊(包括姓名、年紀)設計了一個Student類,這個類的成員變數包括姓名和年紀。而生物醫學工程系也想統計自己學院學生的資訊(包括姓名、年紀、年級、加權成績),這是就可以利用類的繼承,通過BME_Student類繼承Student類,只需要在BME_Student類中新增年級、加權成績這兩個屬性即可,這無疑減小了工作量。
上面只是一個舉例,是想說明當遇到需要擴充套件某一個類的功能時,C++提供了一種比修改和擴充套件類更好的方法叫做繼承。下面我們還是以這兩個類為例進行介紹。
基本概念
被繼承的類稱為基類,也可以叫父類。對應地,直接或者間接從基類繼承得到的類稱為派生類,也可以稱之為子類。一般基類比較抽象,用於定義其所有子類的公共屬性,派生類繼承了這些公有屬性並且新增自己的屬性從而使特徵和功能更加具體化。
繼承的形式
在C++中支援一個派生類繼承多個基類,其形式如下;
class 派生類名: 派生方式基類名1, 派生方式 基類名2,... { 資料成員和成員函式宣告 };
這裡的派生方式包括公有的派生(public)、私有的派生(private)、保護的派生(protect)。無論哪種派生方式,基類中的private成員在派生類中都是不可見的,基類中的private成員不允許外部函式或派生類中的任何成員訪問,派生類想要訪問基類的私有成員只能通過基類的公有和保護的方法訪問。
公用的派生方式表示派生類從基類公有地繼承,基類中的成員在派生類中的訪問屬性除了私有的不可見以外,其他的保持不變;而私有的繼承方式,讓基類中公有成員和保護成員在派生類中都變成私有的;保護的繼承方式讓基類中公有成員和保護成員在派生類中都變成受保護的。具體的訪問形式如下面的表格所示。
派生方式 |
Private |
Public |
Protect |
||||||
基類成員 |
private |
public |
protect |
private |
public |
protect |
private |
public |
protect |
派生類 |
不可見 |
可見 (private) |
可見 (private) |
不可見 |
可見 (public) |
可見 (protect) |
不可見 |
可見 (protect) |
可見 (protect) |
外部函式 |
不可見 |
不可見 |
不可見 |
不可見 |
可見 |
不可見 |
不可見 |
不可見 |
不可見 |
Student類
Student類記錄了學生的姓名及年齡。另外還定義了一個公有的成員函式show();
class Student
{
public:
Student();
Student(string fn,string ln,int ages);
~Student();
Student(const Student &stu);
void show();
//int s = 5;
private:
string firstname="Zhengyu";
string lastname="Pan";
int age=20;
};
//----------實現基類的函式;
Student::Student()
{
cout << "呼叫基類預設建構函式" << endl;
}
Student::Student(string fn, string ln, int ages) :firstname(fn), lastname(ln), age(ages)
{
cout << "呼叫基類建構函式" << endl;
}
Student::~Student()
{
cout << "呼叫基類解構函式" << endl;
}
Student::Student(const Student &stu)
{
firstname = stu.firstname;
lastname = stu.lastname;
age = stu.age;
cout << "呼叫拷貝建構函式" << endl;
}
void Student::show()
{
cout << "學生 " << lastname << " " << firstname <<" "<<"的年齡是 "<<age<< endl;
}
BME_Student類
class BME_Student
:public Student
{
public:
BME_Student();
~BME_Student();
BME_Student(int grade, double score, string fn, string ln, int ages);
void show();
private:
int grade_rank=2; //新增新的資料成員年級和加權成績
double weight_score=90.0;
};
BME_Student::BME_Student()
{
cout << "呼叫派生類預設建構函式" << endl;
}
BME_Student::~BME_Student()
{
cout << "呼叫派生類解構函式" << endl;
}
BME_Student::BME_Student(int grade, double score, string fn, string ln, int ages)
:grade_rank(grade), weight_score(score), Student(fn,ln,ages)
{
cout << "呼叫派生類的建構函式" << endl;
}
void BME_Student::show()
{
Student::show();
cout << "年級是 " << grade_rank << "加權成績是 " << weight_score << endl;
}
在這裡BME_Student類公有地繼承Student類,所以BME_Student類物件具有了Student類的屬性(即資料成員),並且可以使用Student類的成員函式,說明了一下兩點;
派生類物件儲存了基類的資料成員(派生類繼承了基類的實現);
派生類物件可以使用基類的方法(派生類繼承了基類的介面)。
在上述程式碼中,BME_Student類添加了自己新增的資料成員(年級、加權成績),並且也定義了自身的建構函式,解構函式及show()函式。
派生類的建構函式及解構函式
在建立一個派生類的物件時,程式首先建立基類的物件,如上面程式碼中BME_Student類的建構函式所示,通過顯式地呼叫Student類的建構函式進行基類成員的初始化;
Student(fn,ln,ages)
如果不顯式地呼叫基類的建構函式,則派生類的建構函式會自動呼叫基類的預設建構函式進行基類成員的初始化。總的來說,派生類的建構函式有以下幾個要點;
1. 首先建立基類物件(首先呼叫基類建構函式初始化基類成員);
2. 通過成員初始化列表將基類資訊傳遞給基類建構函式;
3. 應該初始化自身新增的資料成員。
相對應的,在對派生類物件過期時,首先呼叫派生類解構函式,然後再呼叫基類解構函式,這與建構函式的呼叫是一個相反的過程。
在生成派生類物件時,派生類建構函式的執行順序如下:
- 呼叫基類建構函式;
- 呼叫資料成員中類物件的建構函式;
- 呼叫子類的建構函式;
派生類和基類的關係
1. 派生類物件可以使用基類的方法,條件是方法不是私有的。
2. 基類指標可以指向派生類物件,派生類指標不可以指向基類物件。因為當派生類指標指向基類物件時,如果利用指標呼叫派生類的方法往往是非法的,因為派生類中新增了一些方法,基類物件不能使用。
3. 與2 中同樣的道理,基類引用可以在不進行型別轉換的情況下引用派生類物件。派生類引用不能引用基類物件。
虛擬函式
在上面的程式碼中,我們給基類和派生類分別定義了show()函式,當使用兩個類的物件呼叫show()函式時,程式會自動呼叫對應的函式。但是有一種情況會出乎我們的意料,當使用基類的指標或引用作用於派生類的物件時,這時候呼叫的是基類的show()函式,明顯不滿足我們的要求。
BME_Student PAN;
Student *p_Stu=&PAN;
p_Stu->show(); //此時呼叫基類的show()函式
這時候就需要虛擬函式。
虛擬函式的形式
virtual void fun();
虛擬函式在類中宣告時在函式前加上了virtual關鍵字,當他在類外實現時,不需要再新增這個關鍵字。
void Student::fun()
{
函式體;
}
虛擬函式的特性
- 虛擬函式並不是說這個函式可以不被實現,定義虛擬函式是為了允許用基類的指標來呼叫子類的這個函式,實現動態多型,要與純虛擬函式分開。
- 某成員函式在基類中被定義為虛擬函式,在派生類中該函式無需新增virtual也預設為虛擬函式;若沒有在派生類重新定義,則將使用基類的版本;
- 建構函式不能使虛擬函式,解構函式應當是虛擬函式。因為,如果基類指標指向的是子類物件,將呼叫子類的解構函式,然後自動呼叫基類的解構函式。如果解構函式不是虛擬函式,則不會呼叫派生類的虛擬函式。而且,當基類的解構函式宣告為虛擬函式後,即便子類的解構函式與其名字不同,也一樣自動成為虛擬函式。
- 友元不是成員函式,只有成員函式才可以是虛的,因此友元不能是虛擬函式。但可以通過讓友元函式呼叫虛成員函式來解決友元的虛問題。
- 只有通過指標和引用才能展現虛擬函式的特性。
- 重新定義虛擬函式不是過載,而是覆蓋(或是隱藏),即使派生類中重新定義了虛擬函式,無論函式列別是否相同,都將隱藏同名的基類方法。所以當基類中虛擬函式被過載時,在派生類中應該定義所有的版本。
- 靜態成員函式不能是虛擬函式; 行內函數不能為虛擬函式;
過載、覆蓋、隱藏
成員函式被過載是發生在同一個類中,特徵是:
- 相同的範圍(在同一個類中);
- 函式名字相同;
- 引數不同;
- virtual 關鍵字可有可無。
覆蓋是指派生類函式覆蓋基類函式,特徵是:
- 不同的範圍(分別位於派生類與基類);
- 函式名字相同;
- 引數相同;
- 基類函式必須有virtual 關鍵字。
“隱藏”是指派生類的函式遮蔽了與其同名的基類函式,規則如下:
- 如果派生類的函式與基類的函式同名,但是引數不同。此時,不論有無virtual關鍵字,基類的函式將被隱藏(注意別與過載混淆)。
- 如果派生類的函式與基類的函式同名,並且引數也相同,但是基類函式沒有virtual 關鍵字。此時,基類的函式被隱藏(注意別與覆蓋混淆)
虛擬函式與動態聯編
將原始碼中的函式呼叫解釋為特定函式程式碼塊的行為被稱為函式名聯編(binding),在C++中,編譯器必須檢視函式引數以及函式名才能確定使用哪個函式,編譯器在編譯過程完成聯編,在編譯過程中完成的聯編稱為靜態聯編(static binding)。然而虛擬函式無法在編譯時確定使用哪個函式,所以編譯器只能在程式執行時選擇正確的虛擬函式程式碼,這種聯編稱為動態聯編(dynamic binding)。
注意:動態聯編髮生在基類指標呼叫虛擬函式時,因為此時無法判斷呼叫的是哪個函式,但是當物件呼叫虛擬函式是,編譯器可以直接判斷出呼叫哪個函式,因此是靜態聯編。
程式碼
head01.h
/*
//---head01.h
//---類的繼承與虛擬函式
//---潘正宇2018.04.21
*/
#ifndef HEAD01_H
#define HEAD01_H
#include <string>
#include <iostream>
using namespace std;
//-----------基類為學生類--------------//
class Student
{
public:
Student();
Student(string fn,string ln,int ages);
virtual ~Student();
Student(const Student &stu);
virtual void show();
//int s = 5;
private:
string firstname="Zhengyu";
string lastname="Pan";
int age=20;
};
//----------實現基類的函式;
Student::Student()
{
cout << "呼叫基類預設建構函式" << endl;
}
Student::Student(string fn, string ln, int ages) :firstname(fn), lastname(ln), age(ages)
{
cout << "呼叫基類建構函式" << endl;
}
Student::~Student()
{
cout << "呼叫基類解構函式" << endl;
}
Student::Student(const Student &stu)
{
firstname = stu.firstname;
lastname = stu.lastname;
age = stu.age;
cout << "呼叫拷貝建構函式" << endl;
}
void Student::show()
{
cout << "學生 " << lastname << " " << firstname <<" "<<"的年齡是 "<<age<< endl;
cout << endl;
}
//----------派生類為生物醫學工程系的學生BME_Student--//
class BME_Student
:public Student
{
public:
BME_Student();
~BME_Student();
BME_Student(int grade, double score, string fn, string ln, int ages);
void show();
private:
int grade_rank=2; //新增新的資料成員年級和加權成績
double weight_score=90.0;
};
BME_Student::BME_Student()
{
cout << "呼叫派生類預設建構函式" << endl;
}
BME_Student::~BME_Student()
{
cout << "呼叫派生類解構函式" << endl;
}
BME_Student::BME_Student(int grade, double score, string fn, string ln, int ages)
:grade_rank(grade), weight_score(score), Student(fn,ln,ages)
{
cout << "呼叫派生類的建構函式" << endl;
}
void BME_Student::show()
{
Student::show();
cout << "年級是 " << grade_rank << "加權成績是 " << weight_score << endl;
cout << endl;
}
#endif
test.cpp
/*
//---test.cpp
//---類的繼承與虛擬函式
//---潘正宇2018.04.21
*/
#include <iostream>
#include "head01.h"
using namespace std;
void main()
{
{
BME_Student PAN;
BME_Student NIHONG(3, 83.2, "Hong", "Ni", 25);
PAN.show();
NIHONG.show();
Student *p_Stu=&PAN;
p_Stu->show();
}
{
Student ZhangZou("Zhang", "Zou", 24);
ZhangZou.show();
}
system("pause");
return;
}
執行結果
結果分析
從結果中不難看出,在建立派生類物件時,首先呼叫基類建構函式,然後再呼叫派生類建構函式,而在進行析構時,先呼叫派生類物件,再呼叫基類建構函式。
因為是公用的繼承,所以Student類中的show()函式可以直接被BME_Student類中的show( )函式呼叫,但是一定要注以Student::,否則將陷入無限迴圈。
由於show( )被定義為虛擬函式,所以指標p_Stu呼叫的是派生類的show()函式。
已完。。
參考書籍《C++ Primer 第五版》、《C++ Primer Plus 第六版》