C++ Primer 第十五章 面向對象程序設計 15.2 定義基類和派生類 練習和總結

基類和派生類是相對的概念,如果類A繼承自類B,則類B相對於類A是基類,類A相對於類B是派生類。

15.2 定義基類和派生類

在定義基類時,我們需要將析構函數和需要子類重寫的函數定義爲虛函數因爲當我們使用指針或者引用調用虛函數時,會根據實際綁定的動態類型來調用基類或者子類的函數。

爲什麼要這樣做?
在定義派生類的時候解釋。

之前在修飾類的成員時,我們只使用了private和public,這裏還有一個protected用來修飾成員,表示派生類可以直接訪問,但是其他用戶(類,函數)不能對修飾爲protected的成員訪問。

15.2.1 定義基類

練習

15.1

使用virtual關鍵字修飾的函數時虛函數

15.2

用protected修飾的成員,使用類的用戶不能直接訪問,但是繼承該類的派生類可以訪問。

使用private修飾的成員,使用這個類的用戶不能直接訪問,繼承該類的派生類也不可以直接訪問。

15.3
class Quote
{
public:
	Quote()=default;
	virtual ~Quote()=default;
	Quote(const string& s, double p) :bookNo(s), price(p) {};
	virtual double net_price(std::size_t n) const {
		return n * price;
	}
private:
	std::string bookNo;
	protected:
		double price = 0.0;
};

15.2.2 定義派生類

使用類派生列表來指明當前類是哪些基類的派生類,C++支持多繼承,但是一般只用單繼承

class A:public class B{
	
};

如果指定了從哪個基類派生,則必須要定義這個派生類,在聲明類A時,不用寫繼承自哪個類。 所以下面的代碼時錯誤的

class A:public class B;

如果我們需要重寫父類的某一個函數,爲了支持動態綁定,這個函數在父類中需要被聲明爲virtual,此後這個函數就被隱式的聲明爲virtual了,如果想要顯式的說明子類寫的函數時重寫符類的函數,則在函數的參數列表後面使用overrid關鍵字,此時如果父類對應的函數不是虛函數編譯器就會報錯。

class A{
	public:
	virtual void func();
	A(int a):value_a(a){};
	private:
	int value_a;
}
class B:public class A{
	public:
	virtual void func() override;
	//virtual void func() const override;
	//virtual void func() & override;
	private:
	int value_b;
}

我們可以使用基類的引用或者指針綁定派生類的對象,這是由於派生類是有多個部分組成的,它由基類的成員和自己的非靜態成員構成可以理解爲一個派生類的對象包含了自身定義的對象以及基類的對象(不包含靜態成員)。

所以我們用一個基類對象的指針或者引用綁定的實際是這個派生對象中基類的子對象。

那麼虛函數有什麼用?

假設類B是類A的派生類,類B重寫了類A的函數func(); 那麼一個類B的對象中就包含了兩個func(),一個是類A自己的(因爲包含了類A的子對象),一個類B重寫的。那麼如果我們使用類A的引用綁定類B的對象,此時調用func()到底是調用類A的func還是類B重寫的func()?

A a;
B& =a;
a.func();

答案是:如果func()是虛函數而且類B重寫了func(),那麼調用的類B的func(),如果func()不是虛函數,那麼類B就算重寫了調用的還是類A的func()

虛函數的意義就在於此,它可以讓一個變量調用正確的函數。

至於爲什麼析構也必須是虛函數,將在後面介紹。

派生類實際上是由基類成員和派生類定義的成員兩部分組成的,在派生類的構造函數中,我們需要初始化基類的成員,我們通過調用基類的構造函數來完成。如果我們沒有指出基類如何初始化,則基類執行默認初始化。

如下所示,使用
//B(int a,int b):value_a(a),value_b(b){};
這種方式是錯誤的,因爲a實際上是private,類B無法直接訪問。

class A{
	public:
	virtual void func();
	A(int a):value_a(a){};
	private:
	int value_a;
}
class B:public class A{
	public:
	virtual void func() override;
	//這是錯誤的
	//B(int a,int b):value_a(a),value_b(b){};
	//這纔是正確的
	B(int a,int b):A(a),value_b(b){};
	private:
	int value_b;
}

另外兩點,
1.對於基類的靜態數據成員 整個繼承體系下只存在一份。即基類和派生類公用靜態成員。

2.如果我們不像讓一個類被繼承,則在類後加上final進行修飾。

下面的類B無法被繼承

class B final:public class A{
}

練習

15.4

a。錯誤,不能自己繼承自己
b。正確
c。錯誤,聲明的時候不需要寫繼承關係,寫了繼承關係就一定要定義。

15.5,15.6 15.7

頭文件

#pragma once
#include "Quote.h"
class Bulk_quote :
	public Quote
{
public:
	Bulk_quote() = default;
	~Bulk_quote() = default;
	Bulk_quote(const string& s, double p, size_t q, double dis);
	double net_price(std::size_t n) const override;
private:
	size_t min_qty = 0;
	double discount = 0.0;
};

class Over_part_original_price :public Quote {
public:
	Over_part_original_price(const string&s, double p, size_t q, double dis);
	double net_price(std::size_t n) const override;

private:
	size_t over_threshould = 0.0;
	double discount = 0.0;
};

cpp文件

#include "pch.h"
#include "Bulk_quote.h"
#include <iostream>

Bulk_quote::Bulk_quote(const string & s, double p, size_t q, double dis):Quote(s,p),min_qty(q),discount(dis)
{

}

double Bulk_quote::net_price(std::size_t n) const
{
	std::cout << "Bulk_quote::net_price" <<std::endl;
	double money=0.0;
	if (n>=min_qty)
	{
		money =  price * n*discount;
	}
	else {
		money = n * price;
	}
	return money;
}

Over_part_original_price::Over_part_original_price(const string & s, double p, size_t q, double dis):Quote(s,p), over_threshould(q), discount(dis)
{

}

double Over_part_original_price::net_price(std::size_t n) const
{
	std::cout << "Over_part_original_price::net_price(std::size_t n) " << std::endl;
	double money = 0.0;
	if (n<=over_threshould)
	{
		money = n * price*discount;
	}
	else {
		money = (n - over_threshould)*price + over_threshould * price*discount;
	}
	return money;
}

測試文件:

Quote q("123",20);
Bulk_quote q1("123", 20, 10, 0.8);
Over_part_original_price q2("123", 20, 10, 0.8);
Quote& d = q;
Quote& d1 = q1;
Quote& d2 = q2;

cout<<d.net_price(20)<<endl;
cout<<d1.net_price(20)<<endl;
cout<<q1.Quote::net_price(20)<<endl;
cout << q2.net_price(20) << endl;

15.2.3 類型轉換與繼承

用指針和引用綁定對象時,往往要求指針或者引用的類型和所綁定的類型需要一致。
**但是一個基類的引用或者指針可以綁定一個派生類的對象,**只要好好理解派生類的對象由派生類定義的非靜態成員和基類定義的非靜態成員組成,就能明白爲什麼能將派生類的對象綁定到基類的引用或指針上。

首先需要明白一個靜態類型動態類型,靜態類型就是變量或者表達式本身的類型。這在程序沒有運行時就已經確定了。

A& a,a就是a&類型,是靜態類型
動態類型變量或者表達式所涉及的在內存中的對象中的內存。這需要在程序運行時才能確定。

A& a = b; a的動態類型就是B。
如果一個對象不是指針和引用類型,那麼它的靜態類型和動態類型是一樣的。

爲什麼a可以綁定B的對象因爲B中由類A的子對象,所以綁定的實際上是類B的類A子對象。
對於指針同樣如此。

但是我們不能使用派生類的引用或者指針去綁定一個基類的對象,因爲基類對象中不存在派生類的成員。

下面的方式雖然基類綁定到了一個派生類,但是由於編輯器在檢查時只檢查靜態類型,所以b1無法綁定a的對象,但是我們可以使用static_cast<B*>(a)來轉化a。

B b;
A* a = &b;
B* b1 = a;

我們可以使用派生類的對象來初始化一個基類的對象或者爲一個基類對象賦值,這是由於基類的拷貝構造、拷貝賦值函數往往是引用類型,而引用類型就可以綁定派生類的對象,從而進行初始化。但是初始化或者賦值的對象和原來的派生類的對象是獨立的。

練習

15.8

靜態類型就是變量或者表達式的生成類型。
動態類型就是變量或者表達式在內存中的對象的類型。

即靜態類型在沒運行程序的時候,我就知道這個變量是什麼類型,但是這個變量指向的對象是什麼類型,我是不知道的。

15.9

基類的引用綁定派生類的對象
基類的指針指向派生類的對象
或者指針指向對一個對象,因爲靜態類型是類類型的指針,而動態類型是一個類類型

15.10

ifstream是istream的派生類,所以istream的引用可以接收一個ifstream的對象。
使得istream& 實際指向的是一個ifstream對象,而ifstream override了輸入運算符,所以使用istream調用>>實際調用的是ifstream實現的版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章