C++程式設計學習筆記 複習/拾遺 7
組合與繼承
程式碼重用是面向物件最引人注目的功能之一:
- 可以通過建立新類來複用程式碼,而不必再重頭開始編寫。
- 可以使用別人已經開發並除錯好的類。
類的重用
- 在新類中使用其他類的物件。即新類由多種類的物件組成,這種方法稱為組合。
- 在現有類的基礎上建立新類,在其中新增新程式碼,這種方法稱為繼承。
類的組合:在定義一個類時,若類的資料成員或者成員函式的引數是另一個類的物件。
- 適用兩個物件之間是has-a的關係。
- 類組合語法很簡單,只要把已存在類的物件放到新類中即可。
- “has a”關係在UML圖中表示聚合,體現了類之間的整體和區域性的關係,並且沒有了整體,區域性也可單獨存在。
在類圖使用空心的菱形表示聚合,菱形從區域性指向整體。
“contains-a”關係在UML圖中表示組合(Composition) ,是一種強烈的包含關係。組合類負責被組合類的生命週期。是一種更強的聚合關係。部分不能脫離整體存在。
在類圖使用實心的菱形表示,菱形從區域性指向整體。
示例
通過“has a”即可簡單地把Point物件放在類Line中。
Line類的UML圖中還給出關聯時的數量關係。兩個Point物件決定一條Line,所以用“1”和“2”表示。如果將Line換成多邊形,多邊形應該包含大於等於3個的Point,則應將“2”換成“3…*”。
Line的物件不能直接存取Point類的物件的私有資料成員,但可以通過Point類的物件成員pt1和pt2存取(物件名.成員名,public訪問許可權)。
類的組合的建構函式
設計
原則:不僅要負責對本類中的基本型別成員資料賦初值,也要對物件成員初始化。
宣告形式:類名::類名(物件成員所需的形參,本類成員形參) :物件1(引數),物件2(引數),...... { 本類初始化 }
組合的解構函式宣告方法與一般(無繼承關係時)類的解構函式相同。當不需要顯式地呼叫解構函式時,系統會自動隱式呼叫。組合的解構函式的呼叫次序與組合的建構函式呼叫次序相反。
呼叫
- 建構函式呼叫順序:
- 先呼叫內嵌物件的建構函式(按內嵌時的宣告順序,先宣告者先構造)。
然後呼叫本類的建構函式。 - 若呼叫預設建構函式(即無形參的),則內嵌物件的初始化也將呼叫相應的預設建構函式。
- 當需要執行類中帶形參的建構函式來初始化資料時,組合物件的建構函式應在初始化列表中為組合物件的建構函式提供引數。
示例
例8.1:要求用類的組合方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
public:
Point(double a=0.0,double b=0.0)
{ x=a;y=b; }
void Show()
{ cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle{
public:
Circle(double a=0.0,double b=0.0,double c=0.0):p(a,b)
{ r=c; } //還要負責對本類中的成員r賦初值
Circle(Point &a,double c=0.0):p(a) //要對物件成員初始化
{ r=c; }
void Show();
private:
double r;
Point p;//定義一個Point物件
};
void Circle::Show()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<p.GetX()<<","<<p.GetY()<<endl;//等價於p.Show();//通過物件名.公有成員函式
}
int main()
{
Point p1(10,10);//定義點類物件
Circle c1(100,100,10),c2(p1,5);//定義圓類物件
c1.Show();//呼叫成員函式
c2.Show();//呼叫成員函式
return 0;
}
Circle類以組合方式重用Point類程式碼,沒有打破Point類對資料的封裝保護
類的繼承:根據已有類來定義新類,新類擁有已有類的所有功能。
-
基類/父類(super class)是所有派生類/子類(derived class)的公共屬性及方法的集合,子類則是父類的特殊化。
-
C++支援類的單繼承,也支援類的多繼承,本講重點是單繼承,即每個子類有一個直接父類。
-
繼承的目的:根據已有類來定義新類,新類擁有已有類的所有功能,實現程式碼重用。
-
- 基類/父類
被直接或間接繼承的類
- 基類/父類
-
派生的目的:當新的問題出現,原有程式無法解決(或不能完全解決)時,需要對原有程式進行改造。
-
- 派生類(derived-class)/子類
繼承所有祖先的狀態和行為
子類可以增加變數和方法
子類也可以覆蓋/重寫(override)繼承的方法
- 派生類(derived-class)/子類
基類與派生類的對應關係
- 單繼承(也叫單一繼承)
派生類只從一個基類派生。 - 多繼承(也叫多重繼承)
派生類從多個基類派生。 - 多重派生
由一個基類派生出多個不同的派生類。 - 多層派生
派生類又作為基類,繼續派生新的類。這樣就形成類的一個家族—類族。在類族中,直接參與派生出某類的基類稱為直接基類,基類的基類甚至更高層的基類也稱為間接基類
派生類物件與基類物件存在“is a”(或“is kind of”)的關係
“is a”關係在UML圖中表示泛化,體現了類之間的繼承關係。使用一條帶有空心三角箭頭的實線
指向基類表示泛化關係
繼承用中空箭頭表示,箭頭指向父類。
派生類
-
定義格式
class 派生類名: 繼承方式 基類名1,…,繼承方式 基類名n
//每一個“繼承方式”,只用於限制對緊隨其後之基類的繼承。
{
派生類成員宣告;
}; -
繼承方式
public:公有繼承
protected:保護繼承
private:私有繼承
注意:如果不顯式給出繼承方式,系統預設就是私有繼承(private)。
//不同繼承方式的影響主要體現在:派生類成員對基類成員的訪問許可權;通過派生類物件對基類成員的訪問許可權。
繼承成員的訪問控制
注意:基類的建構函式和解構函式不被繼承。
- 公有繼承
基類的public和protected成員的訪問屬性在派生類中保持不變,但基類的private成員不可直接訪問。
派生類中的成員函式可以直接訪問基類中的public和protected成員,但不能直接訪問基類的private成員。
通過派生類的物件只能訪問基類的public成員。
示例
例8.2:公有繼承示例1,要求用類的公有繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//將Point的資料成員設計為 protected,不必設計返回資料成員的函式。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:public Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接呼叫繼承的公有成員函式
void ShowC();
private:
double r;
};
//Circle類以公有繼承方式重用Point類程式碼,不要忘記Circle類已經繼承了Point類的資料成員和成員函式
void Circle::ShowC()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<GetX()<<","<<GetY()<<endl;//等價於ShowP();
}
int main()
{Circle c1;//定義圓類物件
c1.SetC(100,100,10);//呼叫成員函式
c1.ShowC();//呼叫成員函式
//公有繼承,派生類物件.成員方式可以訪問基類的公有成員
cout<<c1.GetX()<<","<<c1.GetY()<<endl;//等價於c1.ShowP();
return 0;
}
Circle類的SetC函式不僅要提供資料成員–半徑的賦值,還要提供基類Point類的x、y的賦值,即使x、y的是Point類的私有的資料成員,Circle類不能直接訪問它們,但是不要忘記Circle類已經繼承了Point類的資料成員。
例8.5:公有繼承示例2,要求用類的公有繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{protected:
//將Point的資料成員設計為 protected,這樣就不必設計返回資料成員的函式。
double x,y;
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{ cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; }
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<x<<","<<y<<endl;//等價於ShowP();
//類中直接呼叫繼承的保護資料成員
}
int main()
{ Circle c1;//定義圓類物件
c1.SetC(100,100,10);//呼叫成員函式
c1.ShowC();//呼叫成員函式
return 0;
}
Circle類以公有繼承方式重用Point類程式碼,不要忘記Circle類已經繼承了Point類的資料成員和成員函式
Circle類的SetC函式不僅要提供資料成員–半徑的賦值,還要提供基類Point類的x、y的賦值,x、y的是Point類的保護的資料成員,Circle類中可以直接訪問它們。
- 私有繼承
基類的public和protected成員都以private身份出現在派生類中,但基類的private成員不可直接訪問。
派生類中的成員函式可以直接訪問基類中的public和protected成員,但不能直接訪問基類的private成員。
通過派生類的物件不能直接訪問基類中的任何成員。
示例
例8.4:私有繼承示例,要求用類的私有繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//將Point的資料成員設計為 protected,不必設計返回資料成員的函式。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:private Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接呼叫繼承的公有成員函式
void ShowC();
private:
double r;
};
void Circle::ShowC()
{cout<<"半徑="<<r<<endl<<"圓心=";
cout<<GetX()<<","<<GetY();//等價 ShowP();
}
//私有繼承,派生類物件不能訪問基類的公有成員,但是派生類內部可以
int main()
{
Circle c1;//定義圓類物件
c1.SetC(100,100,10);//呼叫成員函式
c1.ShowC();//呼叫成員函式
//私有繼承不能訪問,下面語句被註釋
//cout<<c1.GetX()<<","<<c1.GetY();//等價 c1.ShowP();×
return 0;
}
- 保護繼承
基類的public和protected成員都以protected身份出現在派生類中,但基類的private成員不可直接訪問。
派生類中的成員函式可以直接訪問基類中的public和protected成員,但不能直接訪問基類的private成員。
通過派生類的物件不能直接訪問基類中的任何成員。
示例
例8.3:保護繼承示例,要求用類的保護繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{private:
double x,y;
//將Point的資料成員設計為 protected,不必設計返回資料成員的函式。
public:
void SetP(double a,double b)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
double GetX()
{return x;}
double GetY()
{return y;}
};
class Circle:protected Point
{
public:
void SetC(double a,double b,double c)
{ SetP(a,b);r=c; } //直接呼叫繼承的公有成員函式
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<GetX()<<","<<GetY()<<endl;//等價 ShowP();
}
//保護繼承,派生類物件不能訪問基類的公有成員,但是派生類內部可以
int main()
{
Circle c1;//定義圓類物件
c1.SetC(100,100,10);//呼叫成員函式
c1.ShowC();//呼叫成員函式
//保護繼承不能訪問下面語句,因此被註釋
//cout<<c1.GetX()<<","<<c1.GetY();//等價 c1.ShowP();×
return 0;
}
結論
protected成員對建立其所在類物件的模組來說,它與 private 成員的性質相同。對於其派生類來說,它與 public 成員的性質相同。既實現了資料隱藏,又方便繼承,實現程式碼重用。
建構函式
派生類名::派生類名(基類所需的形參,本類成員所需的形參):
基類名(引數)
//自動呼叫基類建構函式完成繼承來的成員初始化
{
本類成員初始化賦值語句;
//只需要對本類中新增成員進行初始化
};
- 基類的建構函式不被繼承,派生類中需要宣告自己的建構函式。
- 派生類的建構函式需要給基類的建構函式傳遞引數。
- 當基類中宣告有預設建構函式或未宣告建構函式時,派生類建構函式可以不向基類建構函式傳遞引數,也可以不宣告,構造派生類的物件時,基類的預設建構函式將被呼叫。
- 當需要執行基類中帶形參的建構函式來初始化基類資料時,派生類建構函式應在初始化列表中為基類建構函式提供引數。
執行順序
- 先呼叫基類建構函式,如果基類有多個,則基類建構函式的呼叫順序按照它們被繼承時宣告的順序(從左向右)。
- 如果派生類中有組合物件,其次對成員物件進行初始化,初始化順序按照它們在類中宣告的順序。
- 最後執行派生類的建構函式
例8.6:建構函式示例,要求用類的公有繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b; }
void ShowP()
{cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b) //基類初始化
{ r=c; } //本類初始化
void ShowC();
private:
double r;
};
void Circle::ShowC()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<x<<","<<y<<endl;//等價於ShowP();
}
int main()
{Circle c1(100,100,10);//定義圓類物件,帶引數
c1.ShowC();//呼叫成員函式
return 0;
}
Protected用#表示
Circle類以公有繼承方式重用Point類程式碼
Circle類已經繼承了Point類的資料成員和成員函式
解構函式
- 派生類的解構函式
- 解構函式也不被繼承,派生類自行宣告
- 宣告方法與一般(無繼承關係時)類的解構函式相同。
- 不需要顯式地呼叫基類的解構函式,系統會自動隱式呼叫。
- 解構函式的呼叫次序與建構函式相反。
例8.7:解構函式示例,在例8.6基礎上修改,增加建構函式和解構函式的呼叫提示,並關注建構函式與解構函式呼叫順序。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b;cout<<"Point類"<<endl; }
void ShowP()
{ cout<<x<<","<<y<<endl; }
~Point(){cout<<"~Point類"<<endl; }
};
class Circle:public Point
{public:
Circle(double a=0,double b=0,double c=0):Point(a,b)
{ r=c; cout<<"Circle類"<<endl; }
void ShowC();
~Circle() {cout<<"~Circle類"<<endl; }
private:
double r;
};
void Circle::ShowC()
{ cout<<"半徑="<<r<<endl<<"圓心=";
cout<<x<<","<<y<<endl;//等價於ShowP();
}
int main()
{Circle c1(100,100,10);//定義圓類物件
c1.ShowC();//呼叫成員函式
return 0;
}
同名覆蓋規則:物件一定先呼叫自己的同名成員,如果自己沒有同名成員,則呼叫直接基類的同名成員,以此類推。
當派生類與基類中有相同成員時:
若未強行指名,則通過派生類物件使用的是派生類中的同名成員。
如要通過派生類物件訪問基類中被覆蓋的同名成員,應使用基類名限定。
程式設計過程中,一定要注意避免定義的二義性。可以使用作用域分辨符“::”解決二義性問題。
例8.8:同名覆蓋函式示例,要求用類的公有繼承方法輸出圓資訊,包括半徑和圓心。
#include <iostream>
using namespace std;
class Point
{protected:
double x,y;
public:
Point(double a=0,double b=0)
{x=a;y=b; }
void Show()
{cout<<x<<","<<y<<endl; }
};
class Circle:public Point
{
public:
Circle(double a=0,double b=0,double c=0):Point(a,b) //基類初始化
{ r=c; } //本類初始化
void Show();
private:
double r;
};
void Circle::Show()
{cout<<"半徑="<<r<<endl<<"圓心=";
cout<<x<<","<<y<<endl;
//等價於Point::Show();//呼叫被隱藏的基類同名成員函式
}
int main()
{Circle c1(100,100,10);//定義圓類物件,帶引數
c1.Show();//呼叫成員函式
return 0;
}
Circle類以公有繼承方式重用Point類程式碼
Circle類已經繼承了Point類的資料成員和成員函式