1. 程式人生 > >【7】C++進階系列(類的繼承與派生)

【7】C++進階系列(類的繼承與派生)

1、繼承的概念

繼承:在儲存原有類的屬性和功能的基礎上,擴充套件新的功能。

開發類庫的團隊和使用類庫的團隊很可能不是一個,有些東西是不能訪問的。

繼承和派生是同一個問題的不同視角:

保持已有類的特性而構建新類的過程成為繼承;在已有類的基礎上新增自己的特性而產生新類的過程叫做派生。

被繼承的已有類稱為基類或者父類;派生出的新類叫做派生類或者子類。另外還有直接基類和間接基類體現是不是直接參與派生。

繼承的目的:實現設計和程式碼的重用。

派生的目的:當新的問題出現,原有的程式無法解決或者不能完全解決時,需要對原有的程式進行改造。

        

                                         

派生類的構成:吸收基類的成員,改造基類成員(部分),新增新的成員(主要)。

吸收基類成員:預設情況下派生類包含了基類中除建構函式和解構函式之外的全部成員。c++11中規定可以用using語句繼承基類建構函式。

改造基類成員(部分):使用相同的成員名,覆蓋原有的成員。

新增新的成員(主要):完全增加新的成員和新的資料。

2、繼承的方式:公有繼承(public),私有繼承(private),保護繼承(protected)

不同的繼承方式的影響主要體現在:a、派生類成員對基類成員的訪問許可權;b、通過派生類物件對基類成員的訪問許可權。

公有繼承:

繼承的訪問控制:

基類的public和protected成員:訪問屬性在派生類中保持不變。

基類的private成員:不可直接訪問

訪問許可權:

派生類中的成員函式:可以之間訪問基類中的public和protected成員,但不能直接訪問基類的private成員;

通過派生類的物件:只能訪問public成員

舉例:

//Rectangle.h

#ifndef _RENCTANGLE_H
#define _RENCTANGLE_H

#include"Point.h"

class Rectangle:public Point//派生類定義部分
{
public://新增公有函式
	void initRectangle(float x, float y, float w, float h) {
		initPoint(x, y);//呼叫基類公有函式
		this->w = w;
		this->h = h;
	}
	~Rectangle();
	float getH() const { return h; }

	float getW() const { return w; }
private:
	float h, w;//新增私有資料成員
};

Rectangle::~Rectangle()
{
}
#endif // !_RENCTANGLE_H

//Point.h

#ifndef _POINT_H_
#define _POINT_H_


class Point
{
public:
	void initPoint(float x = 0, float y = 0) {
		this->x = x;
		this->y = y;
	}
	void move(float offx, float offy) {
		x += offx;
		y += offy;
	}
	float getX() const{ return x; }
	float getY() const{ return y; }

private:
	float x, y;
};
#endif // !_POINT_H_

//test.cpp

#include"Rectangle.h"
#include<iostream>
using namespace std;

int main() {
	Rectangle rect;//定義Rectangle類物件
	rect.initRectangle(2, 3, 20, 10);//設定舉行的資料
	rect.move(3, 2);//移動矩形的位置
	cout << "The data of rect(x,y,w,h)" << endl;
	cout << rect.getX() << "," << rect.getY() << "," << rect.getW() << "," << rect.getH() << "," << endl;
	return 0;
}

私有繼承:

繼承的訪問控制:

基類的public和protected成員:都以private身份出現在派生類中;

基類的private成員:不可以直接訪問

訪問許可權:

派生類的成員函式:可以直接訪問基類中的public和protected成員,但不能直接訪問基類的private成員;

通過派生類的物件:不能直接訪問從基類繼承的任何成員。

首先要記住的是:私有成員在類外是不能通過物件直接呼叫的。但是在類內可以呼叫。將上面的例子做一定修改:

//Rectangle.h

#ifndef _RENCTANGLE_H
#define _RENCTANGLE_H

#include"Point.h"

class Rectangle:private Point//派生類定義部分
{
public://新增公有函式
	void initRectangle(float x, float y, float w, float h) {
		initPoint(x, y);//呼叫基類公有函式
		this->w = w;
		this->h = h;
	}
	~Rectangle();
	void move(float offx, float offy) { Point::move(offx, offy); }
	
	float getX() const { return Point::getX(); }
	float getY()const { return Point::getY(); }
	float getH() const { return h; }
	float getW() const { return w; }
private:
	float h, w;//新增私有資料成員
};

Rectangle::~Rectangle()
{
}
#endif // !_RENCTANGLE_H


僅將Rectangle.h做如上修改。其他兩個檔案都不變。主要變化:將公有繼承改為了私有繼承,使得原來Point類中的公有成員變成了私有成員,所以再次呼叫move和getX,getY的時候需要在類內呼叫。

保護繼承:

繼承的訪問控制:

基類的public和protected成員:都以protected身份出現在派生類中;

基類的private成員:不可以直接訪問

訪問許可權:

派生類中的成員函式:可以直接訪問基類中的public和protected成員,但不能直接訪問基類的private成員;

通過派生類的物件:不能直接訪問從基類繼承的任何成員。

                                                       

多繼承的情況:看下面的例子,在類外不能通過物件直接訪問私有成員。

        

3、型別轉換(基類和派生類之間)

公有派生類物件可以被當作基類物件使用,反之則不可。

派生類的物件可以隱含的轉換為基類物件

派生類的物件可以初始化基類的引用;

派生類的指標可以隱含的轉換為基類的指標。

通過基類物件名,指標只能使用從基類繼承的成員。

例子:假設有一個基類,其中能夠有一個顯示函式實現一個輸出功能。我們用類2實現基類的派生,並且修改顯示函式,再用類3派生類2並修改顯示函式。並且我們希望有一個通用的函式使得可以通過不同的派生型別顯示不同的輸出情況。

#include<iostream>

using namespace std;

class Base1
{
public:
	void display()const {
		cout << "Base1::display()" << endl;
	}

private:

};

class  Base2:public Base1
{
public:
	void display() const {
		cout << "Base2::display()" << endl;
	}
private:

};

class  Derived :public Base2
{
public:
	void display() const {
		cout << "Derived::display()" << endl;
	}
private:

};

void fun(Base1 *ptr) {
	ptr->display();
}

int main() {
	Base1 base1;
	Base2 base2;
	Derived derived;

	fun(&base1);
	fun(&base2);
	fun(&derived);
	return 0;
}

程式並沒有像我們想象中的那樣子輸出。者對於程式的可讀性很不好。所以:不要重新定義繼承而來的非虛擬函式。(虛擬函式後面會講),虛擬函式可以達到我們想要的輸出那個樣子。

4、派生類的建構函式

一般說來,每個額勒的建構函式只負責本類成員的初始化。

預設情況下:

基類的建構函式不被繼承;

派生類需要定義自己的建構函式。——派生類的建構函式還要負責向基類建構函式傳遞引數,實現基類的初始化

c++11規定:可以使用using語句繼承基類建構函式;但是隻能初始化從基類繼承的成員。語法格式:using B::B;。即using 基類名::建構函式。適用於派生類很少增加新的資料成員或者不增加新的資料成員這種情況。因為派生類新增的成員不能通過建構函式初始化了。如果派生類定義了類內的初始值,就可以通過類內的初始值去初始化,如果沒有定義類內的初始值,就只能按預設的方式初始化:就是不進行初始化,裡面存放的是垃圾值。

建議:如果派生類需要自己新增的成員進行可定製的初始化,也就是說可以用建構函式的引數去指定的初始化,不用繼承基類的建構函式而是自己寫初始函式。但是問題來了:

問題:從基類繼承來的成員怎麼樣去初始化呢?來看看派生類有哪些必須完成的任務?

若不繼承基類的建構函式:

派生類新增的成員:派生類定義建構函式初始化

繼承來的成員:自動呼叫基類建構函式進行初始化。可是引數從哪裡來呢?——派生類建構函式來傳遞

派生類的建構函式還需要給基類的建構函式傳遞引數。

如果派生類只繼承一個基類,這種情況叫做單繼承:

單繼承情況下建構函式的定義語法:

派生類類名::派生類類名(基類所需的形參,本類成員所需的形參):基類名(引數表),本類成員初始化列表{//其他初始化;};

例子:

#include<iostream>

using namespace std;

class B
{
public:
	B();
	B(int i);
	~B();
	void print()const;
private:
	int b;
};

B::B()
{
	b = 0;
	cout << "B預設建構函式:" << endl;
}
B::B(int i)
{
	b = i;
	cout << "B建構函式:" << endl;
}

B::~B()
{
	cout << "B解構函式:" << endl;
}
void B::print()const {
	cout << b << endl;
}

class C:public B
{
public:
	C();
	C(int i, int j);
	~C();
	void print()const;
private:
	int c;
};

C::C() {
c = 0;
cout << "C建構函式:" << endl;

}

C::~C()
{
	cout << "C解構函式:" << endl;
}
void C::print()const {
	cout << c << endl;
}
C::C(int i, int j):B(i),c(j) {

}
int main() {
	C obj(5, 6);
	obj.print();
	return 0;
}

多繼承時建構函式的定義語法:

派生類名::派生類名(引數表):基類名1(基類1初始化引數表),基類名2(基類2初始化引數表),……基類名n(基類n初始化引數表),本類成員初始化列表{//其他初始化;};

派生類與基類的建構函式:

當基類中有預設建構函式時:派生類建構函式可以不想基類建構函式傳遞引數。

構造派生類的物件時,積累的預設建構函式將被呼叫

如需執行基類中帶引數的建構函式:派生類建構函式應當為基類建構函式提供引數。

當又有繼承又有物件成員的時候:多繼承且有物件成員時派生的夠贊函式定義語法:

派生類名::派生類名(形參表):基類名1(引數),基類名2(引數),……基類名n(引數),本類成員(含物件成員)初始化列表{//其他初始化;};這裡的本類成員初始化列表裡面包含物件成員基本型別的資料成員。

呼叫次序:

                                                   

派生類的建構函式舉例:(既有繼承又有組合)

#include<iostream>

using namespace std;

class Base1
{
public:
	Base1(int i) { cout << "Constructing Base1" <<" "<< i << endl; }
	~Base1();

private:

};

Base1::~Base1()
{
}

class Base2
{
public:
	Base2(int j) { cout << "Constructing Base2" << " "<<j << endl; }
	~Base2();

private:

};

Base2::~Base2()
{
}

class Base3
{
public:
	Base3() { cout << "Constructing Base3" << endl; }
	~Base3();

private:

};

Base3::~Base3()
{
}
class Derived: public Base2,public Base1,public Base3
{//派生新類Derived,注意基類名順序
public://派生類公有成員
	Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b){}
	//注意基類名的個數和順序,注意成員物件名的個數和順序
	~Derived();

private://派生類私有成員
	Base1 member1;
	Base2 member2;
	Base3 member3;
};

Derived::~Derived()
{
}

int main() {
	Derived obj(1, 2, 3, 4);

	return 0;
}

順序:被繼承的順序從左至右public Base2,public Base1,public Base3,物件成員初始化順序Base1 member1;Base2 member2;Base3 member3;。

若派生類沒有申明覆制建構函式:

編譯器會在需要時生成一個隱含的複製建構函式;

先呼叫基類的複製建構函式;

再為派生類新增的成員執行復制。

若派生類定義複製建構函式:

一般都要為基類的複製建構函式傳遞引數;複製建構函式只能接受一個引數,即用來初始化派生類定義的成員,也將被傳遞給基類的複製建構函式。基類的複製建構函式引數型別是基類物件的引用,實參可以是派生類物件的引用。

例如:C::C(const C& c1):B(c1){……}

5、派生類的解構函式

解構函式不被繼承。派生類如果需要,要自行宣告解構函式。

宣告方法與無繼承關係時類的解構函式相同。

不需要顯式的呼叫基類的解構函式,系統會自動隱式呼叫。

先執行派生類解構函式的函式體,在呼叫基類的解構函式。

#include<iostream>

using namespace std;

class Base1
{
public:
	Base1(int i) { cout << "Constructing Base1" <<" "<< i << endl; }
	~Base1();

private:

};

Base1::~Base1()
{
	cout << "析構Base1" << endl;
}

class Base2
{
public:
	Base2(int j) { cout << "Constructing Base2" << " "<<j << endl; }
	~Base2();

private:

};

Base2::~Base2()
{
	cout << "析構Base2" << endl;
}

class Base3
{
public:
	Base3() { cout << "Constructing Base3" << endl; }
	~Base3();

private:

};

Base3::~Base3()
{
	cout << "析構Base3" << endl;
}
class Derived: public Base2,public Base1,public Base3
{//派生新類Derived,注意基類名順序
public://派生類公有成員
	Derived(int a,int b,int c,int d):Base1(a),member2(d),member1(c),Base2(b){}
	//注意基類名的個數和順序,注意成員物件名的個數和順序
	~Derived();

private://派生類私有成員
	Base1 member1;
	Base2 member2;
	Base3 member3;
};

Derived::~Derived()
{
	cout << "析構Derived" << endl;
}

int main() {
	Derived obj(1, 2, 3, 4);

	return 0;
}

6、從派生類訪問基類的成員

當派生類與基類中有相同的成員時,若未特別限定,則通過派生類物件使用的是派生類中的同名成員。

如果通過派生類物件訪問基類中被隱藏的同名成員,應使用基類名和作用域操作符(::)來限定。

例子:

#include<iostream>

using namespace std;

class Base1
{
public:
	int var;
	void fun() {
		cout << "*********1111" << endl;
	}
};

class Base2
{
public:
	int var;
	void fun() {
		cout << "*********2222222" << endl;
	}
};


class Derived: public Base1,public Base2
{
public:
	int var;
	void fun() {
		cout << "*********d" << endl;
	}
};

int main() {
	Derived b;
	Derived *p = &b;
	//物件名.成員名標識
	b.var = 1;
	b.fun();
	//Base1作用域分辨符標識
	b.Base1::var = 2;
	b.Base1::fun();
	//Base2
	p->Base2::var = 3;
	p->Base2::fun();
	return 0;
}

二義性問題:如果從不同的基類中繼承了同名的成員,但是他在派生類中沒有定義同名成員,“派生類物件名或引用名.成員名”、“派生類指標->成員名”訪問成員存在二義性問題。這時可以簡單的通過類名來限定。

                                             

雖然可以通過類名來限定訪問類成員,但是也會造成記憶體冗餘的情況。——可以通過虛基類來解決

7、虛基類

如果某個派生類的多個基類實際上又曾經派生過一個共同的最遠基類。那麼這些積累裡面就有相同的成員,最後在派生類中有匯聚到了一起。就會產生冗餘,並有可能因冗餘問題帶來不一致性。

虛基類宣告:以virtual說明基類繼承方式

例如:class B1:virtual public B

作用:主要用來解決多繼承時可能發生的對同意積累繼承多次而產生的二義性問題。為最遠派生類提供唯一的基類成員,二部重複產生多次複製。

注意:在第一級繼承時就要將共同基類設計為虛基類

舉例:

                                          

實際上,從最遠基類繼承來的成員,在最遠派生類中只有一份。

雖然虛基類解決了冗餘問題,但是他同時帶來了新的問題:派生類的建構函式怎麼寫?

建立物件時所指定的類稱為最遠派生類,虛基類的成員是有最遠派生類的建構函式通過呼叫虛基類的建構函式進行初始化的。

在整個繼承結構中,直接或者間接繼承虛基類的所有派生類都必須在建構函式的成員初始化列表中為虛基類的建構函式列出引數。如果未列出,則表示呼叫該虛基類的預設建構函式。

在建立物件時,只有最遠派生類的建構函式呼叫虛基類的建構函式,其他類對虛基類建構函式的呼叫被忽略。

       

Base0的建構函式只被呼叫了一次。只有最遠派生類的傳遞的引數被Base0接收了。