C++第五章(類的繼承派生)下篇
派生類的建構函式和解構函式
為什麼會有派生類的建構函式? 我們怎麼使用?
建構函式的主要作用是初始化。我們前面說過 ,基類的建構函式是不能繼承的,在宣告派生類時,派生類並沒有把基類的建構函式給繼承過來,所以基類的初始化工作也要由派生類的建構函式承擔。所以在設計派生類的建構函式時,不僅要考慮派生類所增加的資料成員,還要考慮基類成員的初始化。也就是說,希望在執行派生類建構函式時,使派生類的資料成員和基類的資料成員同時都被初始化。
怎麼解決這個問題呢?
在執行派生類的建構函式時,呼叫基類的建構函式。
簡單的說就是,你在派生類中的建構函式也要呼叫基類的建構函式。
一。簡單類的派生函式
class Student { public: Student(int n, string nam, char s) : num(n), name(nam), s(sex){} ~Student(){} protected: int num; string name; char sex; }; class Student1: public Student { public : Student1(int n, string nam, char s, int ag, string add) : Stdudent(n, nam, s) { age = ag; addr = addr; } ~Student1(){} protected : int age; string addr; }
一般形式
派生類建構函式名(總引數表):基類建構函式名(引數表)
{派生類中新增資料成員的初始化}
在基類建構函式名後面的引數表 傳的是實參而不是形參。Stuendt(n, nam, s)中 n nam, s是實參。
類外定義
Student1(int n, string nam, char s, int ag, string add); //宣告派生類建構函式 //在派生類外定義建構函式 Student1::Student1(int n, string nam, char s, int ag, string add) : Student(n, nam, s) { age = ag; addr = add; }
在呼叫基類建構函式時的實參是從派生類建構函式的總引數表中得到的,也可以不從派生類建構函式的總引數表中傳遞過來,而直接使用常量或全域性變數。
Student1(string nam, char s, int ag, string add) : Student(1001, nam, s)
一個是常量,另外兩個從派生類中繼承。
初始化引數表
Student1::Student1(int n, string nam, char s, int ag, string add) : Student(n, nam, s), age(ag), addr(add){}
派生類中建構函式的順序(無子物件的情況下)
先基類再派生類
二。有子對像的派生類
class Student
{
public:
Student(int n, string nam) : num(n), name(nam){}
~Student(){}
protected:
int num;
string name;
};
class Student1: public Student
{
public :
Student1(int n, string nam, int n1, int nam1, int ag, string add);
~Student1(){}
protected :
Student monitor; //子物件
int age;
string addr;
}
Student1::Student1(int n, string nam, int n1, int nam1, int ag, string add) : Student(n, nam), monitor(n1, nam1), age(ag), addr(add){}
Student stud(170, "Mr.Li", 171, "Mr.Dun", 20, "Henu")
派生類建構函式的任務應該包括3個部分:
(1)對基類資料成員初始化
(2)對子物件資料成員的初始化
(3)對派生類資料成員初始化
一般形式
派生類建構函式名(總引數表):基類建構函式名(引數表),子物件名(引數表)
{派生類中新增資料成員的初始化}
基類建構函式和子物件的次序是任意的。
Student1(int n, string nam, char s, int n1, int nam1, int s1, int ag, string add) : monitor(n1, nam1), Student(n, nam)
三。多層派生時的建構函式
多重繼承的形式入下
Student(int n, string nam) : num(n), name(nam){}
Student1(int n, string nam, int ag) : Student(n, nam), age(ag){}
Studnet2(int n, string nam, int ag,int s):Student1(n, nam, ag), score(s){}
不需要列出每一層派生類的建構函式,只需要寫出上一層的派生類(即它的直接基類)的建構函式。
呼叫順序: 先基類——》派生類 ——》再派生
class Student
{
public:
Student(int n, string nam) : num(n), name(nam){}
void display()
{
cout << "num : " << endl;
cout << "name : " << endl;
}
~Student(){}
protected:
int num;
string name;
};
class Student1: public Student
{
public :
Student1(int n, string nam, int ag) : Student(n, nam), age(ag){}
void show()
{
display();
cout << "age : " << age << endl;
}
~Student1(){}
protected :
int age;
}
class Student2 : public Student1
{
public :
Studnet2(int n, string nam, int ag,int s):Student1(n, nam, ag), score(s){}
void show1()
{
show();
cout << "score : " << endl;
}
private :
int score;
}
四。派生類建構函式的特殊形式
(1)當不需要對派生類新增成員進行初始化時,派生類建構函式的函式體內可以空。
Student1(int n, string nam) : Student(n, nam){}
(2)如果基類中沒有定義建構函式,或定義了沒有引數的建構函式,那麼在定義派生類建構函式時,可以不寫基類建構函式。因為此時派生類建構函式沒有向基類建構函式傳遞引數的任務。在呼叫派生類建構函式時,系統會自動首先呼叫基類預設建構函式。
如果基類和子物件型別宣告中都沒有定義帶引數的建構函式,而且你也不需要對派生類中的建構函式進行初始化,那就不需要定義派生類的建構函式。系統會自動呼叫預設建構函式。
如果在基類中即定義了無參的,有定義的了有引數的建構函式,則在定義派生類建構函式時,既可以包含基類建構函式,也可以不包含。在呼叫派生類建構函式時,根據建構函式的內容決定呼叫哪個建構函式。
五。派生類的解構函式
析構順序: 本身 ——》子物件——》基類
六。多重繼承
一個派生類可以繼承多個基類
宣告方式:
class D:public A,private B, protected C
{類新加的成員}
多重繼生派生類的建構函式
派生類建構函式名(總引數表):基類1建構函式(引數表),基類2構造引數表(引數表),基類3建構函式(引數表)
{派生類中新增成員初始化}
呼叫順序由 派生順序決定。
#include<iostream>
#include <string.h>
using namespace std;
class Teacher
{
public :
Teacher(string nam, int a, string t): name(nam), age(a), title(t){}
void display()
{
cout << "name : " << name << endl;
cout << "age : " << age << endl;
cout << "title : " << title << endl;
}
protected:
string name;
int age;
string title;
};
class Student
{
public :
Student(string nam, char s, float sco) : name1(nam), sex(s), score(sco){}
void display()
{
cout << "name : " << name1 << endl;
cout << "sex : " << sex << endl;
cout << "score : " << score << endl;
}
protected :
string name1;
char sex;
float score;
};
class Graduate: public Teacher, public Student
{
public :
Graduate(string nam, int a, char s, string t, float sco, float w) : Teacher(nam, a, t), Student(nam, s, sco), wage(w){}
void show()
{
cout << "name : " << name << endl;
cout << "age : " << age << endl;
cout << "title : " << title << endl;
cout << "name : " << name1 << endl;
cout << "sex : " << sex << endl;
cout << "score : " << score << endl;
}
private :
float wage;
};
當我們將Student 中的string name1 改成 name 和 Teacher 中的 name 名字相同時,這時 Graduate 中引用 name就會出現二義性,怎麼解決呢?
在兩個基類中可以都使用同一個資料名name ,而在show函式內中引用資料成員時應指明其作用域
cout << "name : " << Teacher::name << endl;
多重繼承還有一個巨大缺點:我們會重複繼承一些資料,例如上面的名字,會造成資料的冗餘。
多重繼承引起的二義性
class A{
public:
int a;
void display();
};
class B{
public :
int a;
void display();
};
class C : public A, public B
{
public :
int b;
void show();
};
C c1; //定義一個c1物件
c1.a; //引用資料成員a。錯誤 二義性
c1.display(); //引用成員函式,錯誤,二義性
C c1;
c1.A::a; //正確
c1.B::display(); //正確
在類C的成員函式show()中 呼叫 a 和 display()函式前要加作用域
A::a;
B::display();
這時 我們如果在類C中 改為
class C : public A, public B
{
public :
int a;
void display();
};
這時在C內呼叫或在
C c1;
c1.a; //正確 ,覆蓋基類中的a 就近原則
c1.display();
規則
基類的同名成員在派生類中被遮蔽,成為“不可見的”,或者的說,派生類新增加的同名成員覆蓋了基類中的同名成員。
不同的成員函式,只有在函式名和引數個數相同,型別匹配情況下才發生同名覆蓋,如果只有函式名相同而引數不同,不會發生同名覆蓋,而屬於函式的過載。
七。虛基類
如果一個派生類有多個直接基類,而這些直接基類又有一個共同的基類,則在最終派生的基類中會保留該間接基類資料成員的多份同名成員,在引用這些同名成員時,必須在派生類物件後增加直接基類名,以避免二義性,使其唯一的標識一個成員,例如c1.A::display();
在一個類中保留間接共同基類的多份同名成員,雖然有時是有必要的,可以在不同的資料成員中分別存放不同的資料,也可以通過建構函式分別對它們進行初始化。但是多數清況下 ,這種情況人們並不需要。
這種菱形繼承的缺點:保留了多份資料成員,佔用較多的儲存空間,還增加了訪問這些成員時的困難,容易出錯。事實上不需要多份拷貝。
C++提供虛基類,使得繼承間接共同基類時只儲存只保留一份成員。
class A
{~~~}
class B : virtual public A
{}
class C : virtual public A
{}
class 派生類名:virtual 繼承方式 基類名
當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次,也就是基類成員只保留一次。
為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中宣告為虛基類,否則仍然會出現對基類的多次繼承。
虛基類的初始化
class A
{
A(int n){}
};
class B : virtual public A
{
B(int n) : A(n){}
};
class C : virtual public A
{
C(int n) : A(n){}
};
class D : public B, public C
{
D(int n) : A(n), B(n), C(n){}
};
在最好的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化。
上面雖然對基類進行了三次構造。但是真正在編譯時只執行最後派生類對虛基類的建構函式的呼叫,而忽略虛基類的其他派生類(B和C)對虛基類建構函式的呼叫,這樣就保證了虛基類的資料成員不會被多次初始化。
這種多重繼承並不是太好,因為會引起二義性,而且資料冗餘。在JAVA 等語言就不支援多重繼承。
基類與派生類的轉換
基類和派生類之間存在賦值相容關係。
(1)派生類的物件可以基類物件賦值;
A a1; //定義基類A 的對a1
B b1; //定義A的派生類B 的物件b1
a1 = b1; //用派生類物件 b1 給基類 a1賦值
派生時捨棄派生類自己的成員,用形象的話來說就是 “ 大才小用 ”。
(2)只能用子類物件對其基類物件賦值,而不能用基類物件對其子類物件賦值。
(3)同一基類的不同派生類物件之間也不能賦值
(4)派生類物件可以替代基類物件向基類物件的引用進行賦值或初始化。
A a1; //定義基類A 的對a1
B b1; //定義A的派生類B 的物件b1
A &r = a1; //定義A物件的引用r ,並用a1進行初始化
A &r = b1; //定義A物件的引用r ,並用b1進行初始化
r = b1; //用派生類 B 物件b1 對a1 的引用r賦值
(5)如果函式的引數是基類物件或基類物件的引用,相應的引數可以用子類物件
void fun(A &r){ //形參是類A的物件引用
cout << r.num << endl; //輸出該引用中的資料成員
}
fun (b1); //輸出類B的物件b1 的基類資料成員Num 的值
//在fun函式中只能輸出派生類中基類成員的值
(6)派生類物件的地址可以賦值給指向基類物件的指標變數,也就是說,指向基類物件的指標變數也可以用來指向派生類物件。簡單的說,指向基類的指標可以指向派生類
通過指向基類物件的指標,只能訪問派生類中的基類成員,而不能訪問派生類增加的成員。
繼承與組合
在一個類中以另一個類的物件作為資料成員,稱為類的組合。
#include<iostream>
using namespace std;
class Teacher{
public :
private :
int num ;
string name;
char sex;
};
class Birthday{
public :
private:
int year;
int month;
int day;
};
class Professor : public Teacher{
public :
private :
Birthday birthday; //Birthday類的物件作為資料成員
};
類的組合和繼承我認為很重要 也很常用,能夠有效減少多重繼承的資料冗餘,並且提高程式碼效率
通過繼承建立了派生類與基類的關係,它是一種 “ 是 ” 的關係,如 ” 白貓是貓 “ 。
派生類是基類的具體實現化,是基類的具體化實現。
而在上述 Brithday 是成員類,Professor 是組合類(在一個類中包含另一個類的物件成員)。他們之間不是 “ 是 ” 的關係 而是 ” 有 “ 的關係。 不能說教授是一個生日,可以說教授有一個生日。