C++ day23 繼承(三) 靜態聯編, 動態聯編,虛函數

聯編

聯編,binding,的任務是:確定程序調用每一個函數時具體去執行哪一個代碼塊

聯編有兩種類型,各有千秋各有用途。

大部分聯編工作在編譯時進行,比如C就是通過把函數名作爲地址,直接找到目標代碼塊的。C++由於有函數重載,不能只靠函數名,還要看參數類型和數目,即特徵標,但是編譯器一般會做名稱修飾,所以也在編譯期間就可以確定到目標代碼位置。

但有時候也需要在運行時進行確定代碼塊位置,比如上一篇文章的多態特性展示,遍歷一個基類指針數組的指針,調用虛函數ViewAcct,當指針指向的對象是基類對象時則程序調用Brass::ViewAcct()方法,指針指向的對象是派生類對象時,就調用BrassPlus::ViewAcct()方法。於是,必須在運行過程中才能確定要爲該調用執行哪一塊代碼。

靜態聯編static binding or 早期聯編early binding

使用對象的指針或者引用去調用非虛成員函數時,使用靜態聯編,因爲是根據指針和引用的類型來判斷調用的方法是哪一個,因此編譯時就可以確定。

靜態聯編的效率更高,因爲不需要設置一些跟蹤指針用於跟蹤動態變量或者對象,不會增加額外的處理開銷。
所以靜態聯編是C++的默認選擇。

動態聯編dynamic binding or 晚期聯編late binding

使用對象的指針或者引用去調用虛成員函數時,使用動態聯編,因爲不是根據指針和引用的類型來判斷調用的方法是哪一個,而是要根據指向的對象的類型,對象的類型在運行時纔可以確定,因爲有可能會有隱式類型準換,因此編譯時不可以確定。

  • 如果類不會被用作基類,那就不會用到動態聯編;
  • 如果類被用作基類,但是派生類並沒有重寫基類的任何方法,那麼也不需要用到動態聯編。
  • 所以動態聯編只有在需要用時才用。而虛函數也只是在程序需要時才使用。比如不應該把在派生類中不被重寫的方法聲明爲虛函數,只把要重寫的定義爲虛函數。但是,畢竟寫基類的時候不能預知會不會被重寫,所以說類的設計經常需要反反覆覆,不是一次成型。

把基類的方法設置爲虛方法,就會啓動動態聯編

用對象的指針和引用去調用方法(虛成員方法)

虛函數,或者虛方法,是虛成員函數的簡稱。即虛函數一定是成員函數,友元函數等不能被定義爲虛函數。

虛函數爲什麼得此名呢?那是因爲虛函數真的很“虛空”,“虛無”,基類的虛函數定義可以在派生類中重寫,所以基類的虛函數的定義就“虛無”了, “隱匿”了,本來有那個定義,但是在重寫了該函數的派生類看來又是沒有那些定義的,只有自己剛寫的新定義,所以就似有還無,似無還有,虛無縹緲,故得此名。(此處推測純屬個人開腦洞,反正能幫助理解且說的通就是了)

先複習C++對類型一致性的嚴格要求(類型轉換)

最近一直在說,C++不允許把一種類型的地址賦給另一個類型的指針,也不允許把一個類型的引用指向另一個類型

double x;
long * pl = &x;//報錯
int & = x;//報錯
向上強制轉換upcasting 和 向下強制轉換downcasting
  • 向上強制轉換upcasting (隱式)
    但是,繼承第一篇文章說過了,遇到基類和派生類時可以有個單向的例外:即基類的指針和引用可以指向派生類對象。
BrassPlus bp;
Brass * p = &bp;
Brass & r = bp;

這種把派生類對象的指針和引用轉換爲基類的指針和引用的過程實際上是一種隱式類型轉換,且是向上強制類型轉換,upcasting,這使得公有繼承無需進行強制類型轉換了。

示例 用指針或引用調用虛成員方法

//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"
const int NUM = 2;
void eatline();
void fr(Brass &);
void fp(Brass *);
void fv(Brass);

int main()
{
	using std::cout;
	using std::cin;

    Brass ross("Ross Galler", 123786, 5600.0);//基類對象
    BrassPlus mona("Mona White", 467838, 4500.0);//派生類對象
    //按引用傳遞
    fr(ross);//Brass 給Brass &
    fr(mona);//BrassPlus 給Brass &,隱式向上強制轉換
    //按指針傳遞
    std::cout << '\n';
    fp(&ross);//Brass *給Brass *
    fp(&mona);//BrassPlus *給Brass *,隱式向上強制轉換
    //按值傳遞
    std::cout << '\n';
    fv(ross);//Brass給Brass,調用複製構造函數Brass(const Brass &)
    std::cout << '\n';
    fv(mona);//BrassPlus給Brass,相當於只是把mona中的Brass類對象傳進去了,,調用複製構造函數Brass(const Brass &)
    std::cout << '\n';

	return 0;//析構ross和mona對象,後者會先調用~BrassPlus,然後在其中調用~Brass
}

void eatline()
{
    while (std::cin.get() != '\n')
        ;
}
void fr(Brass & r){r.ViewAcct();}
void fp(Brass * p){p->ViewAcct();}
void fv(Brass v){v.ViewAcct();}

當Brass::ViewAcct()不是虛方法,三種傳遞方式都是隻調用基類方法Brass::ViewAcct()

//按引用傳遞
fr(ross);//Brass::ViewAcct()
fr(mona);//Brass::ViewAcct()
//按指針傳遞
fp(&ross);//Brass::ViewAcct()
fp(&mona);//Brass::ViewAcct()
//按值傳遞
fv(ross);//Brass::ViewAcct()
fv(mona);//Brass::ViewAcct()
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00

Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00

Client: Ross Galler
Account number: 123786
Balance: $5600.00
In ~Brass()

Client: Mona White
Account number: 467838
Balance: $4500.00
In ~Brass()

In ~BrassPlus()
In ~Brass()
In ~Brass()

當Brass::ViewAcct()是虛方法,可以看到按指針和按引用都使用了BrassPlus::ViewAcct(),但是按值傳遞仍然使用了Brass::ViewAcct()

//按引用傳遞
fr(ross);//Brass::ViewAcct()
fr(mona);//BrassPlus::ViewAcct()
//按指針傳遞
fp(&ross);//Brass::ViewAcct()
fp(&mona);//BrassPlus::ViewAcct()
//按值傳遞
fv(ross);//Brass::ViewAcct()
fv(mona);//Brass::ViewAcct()
Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

Client: Ross Galler
Account number: 123786
Balance: $5600.00
In ~Brass()

Client: Mona White
Account number: 467838
Balance: $4500.00
In ~Brass()

In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
  • 向下強制轉換downcasting (顯式)

把基類指針或引用轉換爲派生類是向下強制轉換,是不允許的(因爲這樣做可能帶來不安全的操作),除非顯式強制轉換。

//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"

int main()
{
	using std::cout;

    Brass ross("Ross Galler", 123786, 5600.0);//基類對象
    BrassPlus mona("Mona White", 467838, 4500.0);//派生類對象

    Brass & rb = mona;//隱式向上強制轉換
    BrassPlus & rbp = (BrassPlus &)ross;//必須顯式向上強制轉換
    rb.ViewAcct();
    cout << '\n';
    rbp.ViewAcct();//如果ViewAcct()不是虛方法,則會出錯
    cout << '\n';

	return 0;
}

當Brass::ViewAcct()不是虛方法,程序出錯,因爲rbp是派生類的引用,則要調用派生類的ViewAcct(),而這個方法中用到了派生類的數據成員,ross沒有,所以就錯了。

Client: Mona White
Account number: 467838
Balance: $4500.00


Process returned -1073741819 (0xC0000005)   execution time : 12.498 s
Press any key to continue.

當Brass::ViewAcct()是虛方法,程序不會報錯,因爲虛方法使得程序調用正確的方法

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

Client: Ross Galler
Account number: 123786
Balance: $5600.00

In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()

示例 派生類重寫基類虛方法必須保證函數原型一樣!否則就不是繼承,而是新方法,還會覆蓋基類的同名方法

否則程序就認爲派生類的不是重寫方法,而是一個全新的方法,會根據根據引用和指針的類型調用方法

這個例子展示了指針和引用調用虛方法都沒有按照對象類型去判斷使用哪一個函數,而是根據引用和指針的類型了

上面一個示例展示了用指針和引用調用虛方法會按照對象類型去判斷使用哪一個函數,但是那是在基類和派生類的對應虛方法的函數原型完全一樣的情況下的!!!

//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"

int main()
{
	using std::cout;
	using std::cin;

    Brass ross("Ross Galler", 123786, 5600.0);//基類對象
    BrassPlus mona("Mona White", 467838, 4500.0);//派生類對象

    Brass & rb = mona;//隱式向上強制轉換
    BrassPlus & rbp = (BrassPlus &)ross;//必須顯式強制轉換
    rb.ViewAcct();//我覺得應該調用BrassPlus::ViewAcct(),可是程序卻試圖調用Brass::ViewAcct(int),編譯出錯
    cout << '\n';
    rbp.ViewAcct();//運行時錯誤,因爲調用了BrassPlus::ViewAcct(),但ross沒有BrassPlus類的數據成員,所以錯誤,但我覺得應該調用Brass::ViewAcct(int)
    cout << '\n';

    Brass * rb = &mona;//隱式向上強制轉換
    BrassPlus * rbp = (BrassPlus *)&ross;//必須顯式強制轉換
    rb->ViewAcct();//我覺得應該調用BrassPlus::ViewAcct(),可是程序卻試圖調用Brass::ViewAcct(int),編譯出錯
    cout << '\n';
    rbp->ViewAcct();//運行時錯誤,因爲調用了BrassPlus::ViewAcct(),但ross沒有BrassPlus類的數據成員,所以錯誤,但我覺得應該調用Brass::ViewAcct(int)
    cout << '\n';

	ross.ViewAcct(2);//調用Brass::ViewAcct(int)
    //mona.ViewAcct(2);//報錯,因爲派生類新定義的同名但不同原型的函數BrassPlus::ViewAcct()覆蓋了基類的同名函數Brass::ViewAcct(int)
    mona.ViewAcct();//調用BrassPlus::ViewAcct()
	return 0;
}

基類

virtual void ViewAcct(int n) const;
void Brass::ViewAcct(int n) const
{
	format initialState = setFormat();
	precis prec = std::cout.precision(2);

	int i;
    for (i = 0; i < n; ++i)
    {
        std::cout << "Client: " << fullname << '\n'
                  << "Account number: " << account << '\n'
                  << "Balance: $" << balance << '\n';
    }

	restore(initialState, prec);
}

派生類

virtual void ViewAcct() const;
void BrassPlus::ViewAcct() const//虛方法
{
    format initialState = setFormat();
    precis prec = std::cout.precision(2);

    Brass::ViewAcct(1);//直接調用基類的viewacct方法即可,重用代碼
    std::cout << "Maximum loan: $" << maxLoan << '\n'
              << "Owed to bank: $" << owesBank << '\n';
    std::cout.precision(3);
    std::cout << "Loan rate: " << 100 * rate << "%\n";

    restore(initialState, prec);
}

因爲這個虛方法ViewAcct()在基類和派生類的原型不同,所以程序就認爲派生類的不是重寫方法,而是一個全新的方法

派生類新定義的同名但不同原型的函數BrassPlus::ViewAcct()覆蓋了基類的同名函數Brass::ViewAcct(int),對派生類對象只可使用BrassPlus::ViewAcct(),不能使用Brass::ViewAcct(int)

把主程序的rb.ViewAcct();和rb->ViewAcct();改爲rb.ViewAcct(2);和rb->ViewAcct(2);,則這兩句代碼正確,打印兩遍賬戶信息,因爲他們調用的是基類的函數,所以要有參數。

唯一例外:返回類型協變(即允許返回類型隨類的類型不同而變化,但特徵標必須一樣)(打臉了??)

上面示例說明了,繼承基類的方法時,方法的原型必須一毛一樣,纔是繼承,否則就是重新定義一個方法,且由於同名還會隱藏基類的方法。

但是原型在一個點上可以例外,不必一毛一樣,即基類方法的返回類型是指向基類的指針或引用時,這時候派生類繼承這個方法,可以允許把返回類型改爲指向派生類的指針或引用

示例

基類方法原型,要聲明爲虛函數,這樣派生類纔可以重寫,內聯版本

virtual const Brass & Self() const {return *this;}//僅爲示例,返回自己

派生類方法原型,我以爲不用寫定義,結果主程序報錯,說沒有BrassPlus::Self()方法,加定義就好了。所以對虛方法不寫定義,則編譯器認爲沒有重寫,派生類就沒有這個方法,只能用基類的版本,但是又寫了原型,於是基類的就被覆蓋了,派生類對象沒法調用基類的Self方法。

virtual const BrassPlus & Self() const {return *this;}//如果不寫定義,則編譯器認爲沒重寫,就沒有BrassPlus::Self()方法

主程序

//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"

int main()
{
	using std::cout;
	using std::cin;

    Brass ross("Ross Galler", 123786, 5600.0);//基類對象
    BrassPlus mona("Mona White", 467838, 4500.0);//派生類對象

    Brass r = ross.Self();
    BrassPlus m = mona.Self();

    ross.ViewAcct();
    cout << '\n';
    r.ViewAcct();
    cout << '\n';
    mona.ViewAcct();//如果派生類只寫原型virtual const BrassPlus & Self() const;
    //則不僅覆蓋了基類的Self方法,還沒定義自己的方法,於是報錯undefined reference to `BrassPlus::Self() const'
    //所以要寫定義,即使定義和基類一毛一樣
    cout << '\n';
    m.ViewAcct();
    cout << '\n';

	return 0;
}

輸出

Client: Ross Galler
Account number: 123786
Balance: $5600.00

Client: Ross Galler
Account number: 123786
Balance: $5600.00

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()
In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()

感覺我這個示例打臉了返回類型協變。。。。結果顯示它並不是例外。

不過這個示例也體現了我的幼稚,我竟然以爲想繼承基類的虛函數,但是不改寫,則只需要在派生類中寫個原型。實際上,必須提供定義,編譯器纔算你真是個函數,否則只有原型,編譯器就當沒看見你。即使你的定義其實和基類的方法是一樣的,實際並沒重寫,那也算派生類有一個自己的方法,可以用派生類加作用域解析運算符訪問。

如果你在派生類不寫基類某方法的原型和定義,或者只寫原型,那編譯器都認爲沒有改寫,該方法仍然只有基類版本。但如果提供定義,則編譯器認爲你改寫了。會有一個派生類版本一個基類版本。

派生類對像都是基類對象

實際上,派生類對像都是基類對象派生類對像都是基類對象派生類對像都是基類對象

is-a公有繼承關係是可傳遞的

並且is-a的繼承關係是可傳遞的。如果你在BrassPlus類的基礎上再繼續繼承,開發一個BrassPlusPlus類,那麼Brass類的指針和引用可以指向Brass類,BrassPlus類,BrassPlusPlus類三個類的對象。

深入剖析虛函數的實現原理

編譯器到底是怎麼實現虛函數的(隱藏指針成員,虛函數表)

C++只是規定了虛函數的行爲,即遇到對象的指針或引用調用虛成員函數時,要根據對象的類型來判斷使用該方法的基類版本還是派生類版本。

很神奇。很智能。那編譯器的實現者要怎麼做到這一點呢?

通過給每個對象添加一個隱藏成員。這個成員是一個指針變量。這個指針指向一個數組。這個數組裏存儲了類的所有虛函數的地址。

這個數組被稱爲虛函數表,virtual function table, vtbl。是個函數地址表。調用虛函數時,程序就要查看對象中的隱藏指針,然後找到虛函數表,然後執行需要的那個函數。

具體來說,如果派生類沒有重寫基類的虛函數func,則派生類還是用func的基類版本,則派生類和基類的對象的隱藏指針成員執行的虛函數表中存儲的func函數地址一樣,都是基類的那個版本。
如果派生類重寫了虛函數func,則基類對象的隱藏指針成員指向的虛函數表中存儲了基類版本func的地址,而派生類對象的隱藏指針成員指向的虛函數表中存儲了派生類版本的func的地址。

虛函數的空間和時間成本

虛函數機制如上,在內存和執行速度方面都有一定成本:

內存上

  • 每個對象都需要增大一點空間,用於存儲隱藏指針成員。如果類中沒有虛函數,那麼該類的對象不會有這個隱藏成員哈。
  • 編譯器需要給每一個有虛函數的類創建一個數組,作爲他們的虛函數地址表。

速度上

  • 每次調用虛函數,需要比非虛函數的調用多執行一個操作:到虛函數表中查找函數地址。
    所以非虛函數的效率更高一些,但是隻有虛函數纔可以利用動態聯編實現牛逼的多態,這是非虛函數無法做到的。

其他知識點

虛函數的“傳遞性”(派生鏈)

在基類中聲明爲虛函數的方法,在派生類,以及派生類的派生類,····,中都是虛函數。前面說過了,只有基類方法原型前面的virtual關鍵字才管事兒,但是在派生類中方法原型前面加一個也無關痛癢,但編譯器會當你沒加一樣的看待。

如果從A類派生了AA類,從AA類又派生了AAA類,從AAA類又派生了AAAA類·····那麼這條派生鏈條中,後面的類會使用最新的虛函數版本。即如果A類把成員函數func定義爲虛函數,AA沒修改定義,AAA改了定義,那麼AA用的A類的func版本,AAA類對象和AAAA類對象用AAA修改的func版本。

這很好理解,因爲前面說虛函數底層原理時說了,如果基類有虛函數,那麼基類和後續所有派生類的對象都會被編譯器添加一個隱藏指針成員,存儲該類的虛函數地址表的地址。每個類的虛函數表中的虛函數的地址都是自己修改的函數版本的地址,或者自己上一層的對自己而言的基類的函數版本的地址。

構造函數不可以是虛函數

因爲派生類並不繼承基類的構造函數。繼承中,派生類都會自己寫自己的構造函數。並且創建派生類對象時,都是先調用派生類的構造函數,然後在派生類的構造函數中自動調用基類的某一個原型匹配的構造函數。

虛函數是爲了繼承中,需要修改基類方法定義的場合使用的。並不繼承構造函數,所以無需將構造函數定義爲虛函數。

析構函數一定要定義爲虛函數,只要類被作爲基類

就算基類的析構函數的函數體是空的,即什麼也不做,也要顯式寫出來:

virtual ~Brass(){}

這樣做是爲了確保程序一定要先調用派生類析構然後調用基類析構,因爲這樣的順序纔可以確保派生類的新數據成員被釋放。比如:

Brass * pb = new BrassPlus;
delete pb;//調用~Brass() or ~BrassPlus() ???

如果~ Brass()不是虛函數,則程序靜態聯編,根據指針pb的類型是基類,判斷出來該調用~ Brass(),於是BrassPlus新增的數據成員們就不能在這被釋放。

但是如果~ Brass()是虛函數,則動態聯編,根據pb指向的對象時BrassPlus類型,於是調用~ BrassPlus(),再在~ BrassPlus()中調用~ Brass(),成功把新舊數據成員都釋放了。保證不會出錯。

如果類不是基類,即沒被繼承,那就無需把析構函數聲明爲虛函數。但是如果你非要將其聲明爲虛函數,也不會造成語法錯誤,只不過效率上差一點罷了(因爲本可以使用靜態聯編的時候使用了動態聯編)。
在這裏插入圖片描述

友元函數不可爲虛函數(因爲虛函數只針對成員函數)

這個太簡單了,友元函數是friend,不是member,根本不是成員函數,更何談虛函數呢?虛函數是隻針對成員函數的。“虛函數”是“虛成員函數”的簡稱。

但是在友元函數中可以調用虛成員函數。

如果基類的虛方法被重載(同名不同特徵標),則派生類中必須把所有重載版本重寫

如果只寫其中一個或兩個,則沒寫的那些就會被隱藏。

示例

基類

//重載方法
virtual void ViewAcct() const;
virtual void ViewAcct(int n) const;
virtual void ViewAcct(double x) const;
void Brass::ViewAcct() const
{
	format initialState = setFormat();
	precis prec = std::cout.precision(2);

    std::cout << "Client: " << fullname << '\n'
              << "Account number: " << account << '\n'
              << "Balance: $" << balance << '\n';

	restore(initialState, prec);
}

void Brass::ViewAcct(int n) const
{
    format initialState = setFormat();
    precis prec = std::cout.precision(2);

    int i;
    for (i = 0; i < n; ++i)
        std::cout << "Client: " << fullname << '\n'
                  << "Account number: " << account << '\n'
                  << "Balance: $" << balance << '\n';

	restore(initialState, prec);
}

void Brass::ViewAcct(double x) const
{
	format initialState = setFormat();
	precis prec = std::cout.precision(2);

    std::cout << "Client: " << fullname << '\n'
              << "Account number: " << account << '\n'
              << "Balance: $" << balance << '\n';
    std::cout << "x is " << x << '\n';

	restore(initialState, prec);
}

派生類

//基類重載方法,必須全部重寫
virtual void ViewAcct() const;
virtual void ViewAcct(int n) const;
virtual void ViewAcct(double x) const;
void BrassPlus::ViewAcct() const//虛方法
{
    format initialState = setFormat();
    precis prec = std::cout.precision(2);

    Brass::ViewAcct();//直接調用基類的viewacct方法即可,重用代碼
    std::cout << "Maximum loan: $" << maxLoan << '\n'
              << "Owed to bank: $" << owesBank << '\n';
    std::cout.precision(3);
    std::cout << "Loan rate: " << 100 * rate << "%\n";

    restore(initialState, prec);
}

void BrassPlus::ViewAcct(int n) const
{
    format initialState = setFormat();

	int i;
    for (i = 0; i < n; ++i)
    {
        precis prec = std::cout.precision(2);

        Brass::ViewAcct();//直接調用基類的viewacct方法即可,重用代碼
        std::cout << "Maximum loan: $" << maxLoan << '\n'
                  << "Owed to bank: $" << owesBank << '\n';
        std::cout.precision(3);
        std::cout << "Loan rate: " << 100 * rate << "%\n";

        restore(initialState, prec);
    }
}

void BrassPlus::ViewAcct(double x) const
{
    format initialState = setFormat();
    precis prec = std::cout.precision(2);

    Brass::ViewAcct();//直接調用基類的viewacct方法即可,重用代碼
    std::cout << "Maximum loan: $" << maxLoan << '\n'
              << "Owed to bank: $" << owesBank << '\n';
    std::cout.precision(3);
    std::cout << "Loan rate: " << 100 * rate << "%\n";
    std::cout << "x is " << x << '\n';
    
    restore(initialState, prec);
}

主程序

//main.cpp
#include <iostream>
#include "Brass.h"
#include "BrassPlus.h"

int main()
{
	using std::cout;
	using std::cin;

    Brass ross("Ross Galler", 123786, 5600.0);//基類對象
    BrassPlus mona("Mona White", 467838, 4500.0);//派生類對象

    ross.ViewAcct();
    cout << '\n';
    ross.ViewAcct(2);
    cout << '\n';
    ross.ViewAcct(2.0);
    cout << '\n';

    mona.ViewAcct();
    cout << '\n';
    mona.ViewAcct(2);
    cout << '\n';
    mona.ViewAcct(2.0);
    cout << '\n';

	return 0;
}
Client: Ross Galler
Account number: 123786
Balance: $5600.00

Client: Ross Galler
Account number: 123786
Balance: $5600.00
Client: Ross Galler
Account number: 123786
Balance: $5600.00

Client: Ross Galler
Account number: 123786
Balance: $5600.00
x is 2.00

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $5e+002
Owed to bank: $0
Loan rate: 11.1%

Client: Mona White
Account number: 467838
Balance: $4500.00
Maximum loan: $500.00
Owed to bank: $0.00
Loan rate: 11.125%
x is 2.000

In ~BrassPlus()
In virtual ~Brass()
In virtual ~Brass()

如果我在派生類中把三個同名函數的任何一個或兩個的定義刪除,則相當於沒定義該函數,則剩餘的即派生類定義了的同名函數就會隱藏基類的特徵標和派生類未定義的函數一樣的函數,於是報錯

比如,我刪除了派生類對void BrassPlus::ViewAcct(double x) const的定義,則void BrassPlus::ViewAcct() const和void BrassPlus::ViewAcct(int n) const會隱藏void Brass::ViewAcct(double x) const,使得下面代碼出錯,因爲基類的也被隱藏了,自己類又沒有這個函數
在這裏插入圖片描述
undefined reference to `BrassPlus::ViewAcct(double) const

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