《C++Primer》第十五章-面向對象編程-學習筆記(2)-構造函數和複製控制&作用域

《C++Primer》第十五章-面向對象編程-學習筆記(2)

日誌:
1,2020-03-09 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)

構造函數和複製控制

每個派生類對象由派生類中定義的(非 static)成員加上一個或多個基類子對象構成,當我們構造、複製、賦值和撤銷一個派生類對象時,也會構造、複製、賦值和撤銷這些基類子對象
構造函數複製控制成員不能繼承,每個類定義自己的構造函數和複製控制成員。像任何類一樣,如果類不定義自己的默認構造函數和複製控制成員,就將使用合成版本。

基類構造函數和複製控制

本身不是派生類的基類,其構造函數和複製控制基本上不受繼承影響。構造函數看起來像已經見過的許多構造函數一樣:

Item_base(const std::string &book = "",double sales_price = 0.0):
isbn(book), price(sales_price) { }

繼承對基類構造函數的唯一影響是,在確定提供哪些構造函數時,必須考慮一類新用戶。像任意其他成員一樣,構造函數可以爲 protected 或 private,某些類需要只希望派生類使用的特殊構造函數,這樣的構造函數應定義爲protected。

派生類構造函數

派生類的構造函數受繼承關係的影響,每個派生類構造函數除了初始化自己的數據成員之外,還要初始化基類。

合成的派生類默認構造函數

派生類的合成默認構造函數非派生的構造函數只有一點不同:除了初始化派生類的數據成員之外,它還初始化派生類對象的基類部分。基類部分由基類的默認構造函數初始化
對於 Bulk_item 類,合成的默認構造函數會這樣執行:

  1. 調用 Item_base 的默認構造函數,將 isbn 成員初始化空串,將 price成員初始化爲 0。
  2. 用常規變量初始化規則初始化 Bulk_item 的成員,也就是說,qty 和discount 成員會是未初始化的。

定義默認構造函數

因爲 Bulk_item 具有內置類型成員,所以應定義自己的默認構造函數:

class Bulk_item : public Item_base {
public:
Bulk_item(): min_qty(0), discount(0.0) { }
// as before
};

這個構造函數使用構造函數初始化列表初始化 min_qty 和discount 成員,該構造函數還隱式調用 Item_base 的默認構造函數初始化對象的基類部分。
運行這個派生類構造函數的效果是,首先使用 Item_base 的默認構造函數初始化Item_base 部分,那個構造函數將 isbn 置爲空串並將 price 置爲 0。
Item_base 的構造函數執行完畢後,再初始化 Bulk_item 部分的成員並執行構造函數的函數體(函數體爲空)。

向基類構造函數傳遞實參

除了默認構造函數之外,Item_base 類還使用戶能夠初始化 isbn 和 price成員,我們希望支持同樣 Bulk_item 對象的初始化,事實上,我們希望用戶能夠指定整個 Bulk_item 的值,包括折扣率和數量。
派生類構造函數的初始化列表只能初始化派生類的成員,不能直接初始化繼承成員。相反派生類構造函數通過將基類包含在構造函數初始化列表中來間接初始化繼承成員。

class Bulk_item : public Item_base {
public:
Bulk_item(const std::string& book, double sales_price,std::size_t qty = 0, double disc_rate = 0.0):Item_base(book, sales_price), min_qty(qty), discount(disc_rate) { }
// as before
};

這個構造函數使用有兩個形參的 Item_base 構造函數初始化基類子對象,它將自己的 book 和 sales_price 實參傳遞給該構造函數。這個構造函數可以這樣使用:

// arguments are the isbn, price, minimum quantity, and discount
Bulk_item bulk("0-201-82470-1", 50, 5, .19);

要建立 bulk,首先運行 Item_base 構造函數,該構造函數使用從Bulk_item 構造函數初始化列表傳來的實參初始化 isbn 和 price。Item_base構造函數執行完畢之後,再初始化 Bulk_item 的成員。最後,運行 Bulk_item 構造函數的(空)函數體。
構造函數初始化列表爲類的基類和成員提供初始值,它並不指定初始化的執行次序。首先初始化基類,然後根據聲明次序初始化派生類的成員。

在派生類構造函數中使用默認實參

當然,也可以將這兩個 Bulk_item 構造函數編寫爲一個接受默認實參的構造函數:

class Bulk_item : public Item_base {
public:
Bulk_item(const std::string& book, double sales_price,
std::size_t qty = 0, double disc_rate = 0.0):Item_base(book, sales_price),
min_qty(qty), discount(disc_rate) { }
// as before
};

這裏爲每個形參提供了默認值,因此,可以用 0 至 4 個實參使用該構造函數。

只能初始化直接基類

一個類只能初始化自己的直接基類。直接就是在派生列表中指定的類。如果類 C 從類 B 派生,類 B 從類 A 派生,則 B 是 C 的直接基類。雖然每個 C 類對象包含一個 A 類部分,但 C 的構造函數不能直接初始化 A 部分。相反,需要類 C 初始化類 B,而類 B 的構造函數再初始化類 A。這一限制的原因是,類B 的作者已經指定了怎樣構造和初始化 B 類型的對象。像類 B 的任何用戶一樣,類 C 的作者無權改變這個規約。
作爲更具體的例子,書店可以有幾種折扣策略。除了批量折扣外,還可以爲購買某個數量打折,此後按全價銷售,或者,購買量超過一定限度的可以打折,在該限度之內不打折。
這些折扣策略都需要一個數量和一個折扣量,可以定義名爲 Disc_item 的新類存儲數量和折扣量,以支持這些不同的折扣策略。Disc_item 類可以不定義net_price 函數,但可以作爲定義不同折扣策略的其他類(如 Bulk_item 類)的基類。要實現這個設計,首先需要定義 Disc_item 類:

// class to hold discount rate and quantity
// derived classes will implement pricing strategies using these data
class Disc_item : public Item_base {
public:
Disc_item(const std::string& book = "",
double sales_price = 0.0,
std::size_t qty = 0, double disc_rate = 0.0):
Item_base(book, sales_price),
quantity(qty), discount(disc_rate) { }
protected:
std::size_t quantity; // purchase size for discount to apply
double discount; // fractional discount to apply
};

這個類繼承 Item_base 類並定義了自己的 discount 和 quantity 成員。它唯一的成員函數是構造函數,用以初始化基類和 Disc_item 定義的成員。其次,可以重新實現 Bulk_item 以繼承 Disc_item,而不再直接繼承Item_base:

// discount kicks in when a specified number of copies of same book are sold
// the discount is expressed as a fraction to use to reduce the normal price
class Bulk_item : public Disc_item {
public:
	Bulk_item(const std::string& book = "",
	double sales_price = 0.0,
	std::size_t qty = 0, double disc_rate = 0.0):
	Disc_item(book, sales_price, qty, disc_rate) { }
// redefines base version so as to implement bulk purchase discount policy
	double net_price(std::size_t) const;
};

Bulk_item 類現在有一個直接基類 Disc_item,還有一個間接基類Item_base。每個 Bulk_item 對象有三個子對象:一個(空的)Bulk_item 部分和一個 Disc_item 子對象,Disc_item 子對象又有一個 Item_base 基類子對象。
雖然 Bulk_item 沒有自己的數據成員,但爲獲取值用來初始化其繼承成員,它定義了一個構造函數。
派生類構造函數只能初始化自己的直接基類,在 Bulk_item 類的構造函數初始化列表中指定Item_base 是一個錯誤。

複製控制和繼承

像任意其他類一樣,派生類也可以使用第十三章所介紹的合成複製控制成員。合成操作對對象的基類部分連同派生部分的成員一起進行復制、賦值或撤銷,使用基類的複製構造函數、賦值操作符或析構函數對基類部分進行復制、賦值或撤銷。
類是否需要定義複製控制成員完全取決於類自身的直接成員。基類可以定義自己的複製控制而派生類使用合成版本,反之亦然。
只包含類類型或內置類型數據成員、不含指針的類一般可以使用合成操作,複製、賦值或撤銷這樣的成員不需要特殊控制。具有指針成員的類一般需要定義自己的複製控制來管理這些成員
Item_base 類及其派生類可以使用複製控制操作的合成版本。複製Bulk_item 對象時,調用(合成的)Item_base 複製構造函數複製 isbn 和 price成員。使用 string 複製構造函數複製 isbn,直接複製 price 成員。一旦複製了基類部分,就複製派生部分。Bulk_item 的兩個成員都是 double 型,直接復
制這些成員。賦值操作符和析構函數類似處理。

定義派生類複製構造函數

如果派生類顯式定義自己的複製構造函數或賦值操作符,則該定義將完全覆蓋默認定義。被繼承類的複製構造函數和賦值操作符負責對基類成分以及類自己的成員進行復制或賦值。
如果派生類定義了自己的複製構造函數,該複製構造函數一般應顯式使用基類複製構造函數初始化對象的基類部分:

class Base { /* ... */ };
class Derived: public Base {
public:
// Base::Base(const Base&) not invoked automatically
	Derived(const Derived& d):
	Base(d) /* other member initialization */ { /*... */ }
};

初始化函數 Base(d) 將派生類對象 d 轉換爲它的基類部分的引用,並調用基類複製構造函數。如果省略基類初始化函數,如下代碼:

// probably incorrect definition of the Derived copy constructor
Derived(const Derived& d) /* derived member initizations */
{/* ... */ }

效果是運行 Base 的默認構造函數初始化對象的基類部分。假定 Derived成員的初始化從 d 複製對應成員,則新構造的對象將具有奇怪的配置:它的Base 部分將保存默認值,而它的 Derived 成員是另一對象的副本。

派生類賦值操作符

賦值操作符通常與複製構造函數類似:如果派生類定義了自己的賦值操作符,則該操作符必須對基類部分進行顯式賦值。

// Base::operator=(const Base&) not invoked automatically
Derived &Derived::operator=(const Derived &rhs)
{
	if (this != &rhs) { //賦值操作符必須防止自身賦值。假定左右操作數不同
	Base::operator=(rhs); // assigns the base part
// do whatever needed to clean up the old value in the derived part
// assign the members from the derived
	}
	return *this;
}

賦值操作符必須防止自身賦值。假定左右操作數不同,則調用 Base 類的賦值操作符給基類部分賦值。該操作符可以由類定義,也可以是合成賦值操作符,這沒什麼關係——我們可以直接調用它。基類操作符將釋放左操作數中基類部分的值,並賦以來自 rhs 的新值。該操作符執行完畢後,接着要做的是爲派生類中的成員賦值。

派生類析構函數

析構函數的工作與複製構造函數和賦值操作符不同:派生類析構函數不負責撤銷基類對象的成員。編譯器總是顯式調用派生類對象基類部分的析構函數。每個析構函數只負責清除自己的成員:

class Derived: public Base {
public:
// Base::~Base invoked automatically
~Derived() { /* do what it takes to clean up derived members
*/ }
};

對象的撤銷順序與構造順序相反:首先運行派生析構函數,然後按繼承層次依次向上調用各基類析構函數。

虛析構函數

自動調用基類部分的析構函數對基類的設計有重要影響。
刪除指向動態分配對象的指針時,需要運行析構函數在釋放對象的內存之前清除對象。處理繼承層次中的對象時,指針的靜態類型可能與被刪除對象的動態類型不同,可能會刪除實際指向派生類對象的基類類型指針。
如果刪除基類指針,則需要運行基類析構函數並清除基類的成員,如果對象實際是派生類型的,則沒有定義該行爲要保證運行適當的析構函數,基類中的析構函數必須爲虛函數

class Item_base {
public:
// no work, but virtual destructor needed
// if base pointer that points to a derived object is ever deleted
virtual ~Item_base() { } //虛析構函數
};

如果析構函數爲虛函數,那麼通過指針調用時,運行哪個析構函數將因指針所指對象類型的不同而不同

Item_base *itemP = new Item_base; // same static and dynamic type
delete itemP; // ok: destructor for Item_base called
itemP = new Bulk_item; // ok: static and dynamic types differ
delete itemP; // ok: destructor for Bulk_item called

像其他虛函數一樣,析構函數的虛函數性質都將繼承。因此,如果層次中根類的析構函數爲虛函數,則派生類析構函數也將是虛函數,無論派生類顯式定義析構函數還是使用合成析構函數,派生類析構函數都是虛函數。
基類析構函數是三法則(第 13.3 節)的一個重要例外。三法則指出,如果類需要析構函數,則類幾乎也確實需要其他複製控制成員。基類幾乎總是需要構造函數,從而可以將析構函數設爲虛函數。如果基類爲了將析構函數設爲虛函數則具有空析構函數,那麼,類具有析構函數並不表示也需要賦值操作符或複製構造函數。
即使析構函數沒有工作要做,繼承層次的根類也應該定義一個虛析構函數。

構造函數和賦值操作符不是虛函數

複製控制成員中,只有析構函數應定義爲虛函數,構造函數不能定義爲虛函數。構造函數是在對象完全構造之前運行的,在構造函數運行的時候,對象的動態類型還不完整。
雖然可以在基類中將成員函數 operator= 定義爲虛函數,但這樣做並不影響派生類中使用的賦值操作符。每個類有自己的賦值操作符,派生類中的賦值操作符有一個與類本身類型相同的形參,該類型必須不同於繼承層次中任意其他類的賦值操作符的形參類型。
將賦值操作符設爲虛函數可能會令人混淆,因爲虛函數必須在基類和派生類中具有同樣的形參。基類賦值操作符有一個形參是自身類類型的引用,如果該操作符爲虛函數,則每個類都將得到一個虛函數成員,該成員定義了參數爲一個基類對象的 operator=。但是,對派生類而言,這個操作符與賦值操作符是不同的。將類的賦值操作符設爲虛函數很可能會令人混淆,而且不會有什麼用處。

構造函數和析構函數中的虛函數

構造派生類對象時首先運行基類構造函數初始化對象的基類部分。在執行基類構造函數時,對象的派生類部分是未初始化的。實際上,此時對象還不是一個派生類對象。
撤銷派生類對象時,首先撤銷它的派生類部分,然後按照與構造順序的逆序撤銷它的基類部分。
在這兩種情況下,運行構造函數或析構函數的時候,對象都是不完整的。爲了適應這種不完整,編譯器將對象的類型視爲在構造或析構期間發生了變化。在基類構造函數或析構函數中,將派生類對象當作基類類型對象對待。
構造或析構期間的對象類型對虛函數的綁定有影響。如果在構造函數或析構函數中調用虛函數,則運行的是爲構造函數或析構函數自身類型定義的版本
無論由構造函數(或析構函數)直接調用虛函數,或者從構造函數(或析構函數)所調用的函數間接調用虛函數,都應用這種綁定。
要理解這種行爲,考慮如果從基類構造函數(或析構函數)調用虛函數的派生類版本會怎麼樣。虛函數的派生類版本很可能會訪問派生類對象的成員,畢竟,如果派生類版本不需要使用派生類對象的成員,派生類多半能夠使用基類中的定義。但是,對象的派生部分的成員不會在基類構造函數運行期間初始化,實際上,如果允許這樣的訪問,程序很可能會崩潰。

繼承情況下的類作用域

每個類都保持着自己的作用域,在該作用域中定義了成員的名字。在繼承情況下,派生類的作用域嵌套在基類作用域中。如果不能在派生類作用域中確定名字,就在外圍基類作用域中查找該名字的定義。
正是這種類作用域的層次嵌套使我們能夠直接訪問基類的成員,就好象這些成員是派生類成員一樣。如果編寫如下代碼:

Bulk_item bulk;
cout << bulk.book();

名字 book 的使用將這樣確定:

  1. bulk 是 Bulk_item 類對象,在 Bulk_item 類中查找,找不到名字 book。
  2. 因爲從 Item_base 派生 Bulk_item,所以接着在 Item_base 類中查找,找到名字 book,引用成功地確定了。

名字查找在編譯時發生

對象、引用或指針的靜態類型決定了對象能夠完成的行爲。甚至當靜態類型和動態類型可能不同的時候,就像使用基類類型的引用或指針時可能會發生的,靜態類型仍然決定着可以使用什麼成員。例如,可以給 Disc_item 類增加一個成員,該成員返回一個保存最小(或最大)數量和折扣價格的 pair 對象:

class Disc_item : public Item_base {
public:
	std::pair<size_t, double> discount_policy() const
	{ return std::make_pair(quantity, discount); }
// other members as before
};

只能通過 Disc_item 類型或 Disc_item 派生類型的對象、指針或引用訪問discount_policy:

Bulk_item bulk;
Bulk_item *bulkP = &bulk; // ok: static and dynamic types are the same
Item_base *itemP = &bulk; // ok: static and dynamic types differ
bulkP->discount_policy(); // ok: bulkP has type Bulk_item*
itemP->discount_policy(); // error: itemP has type Item_base*
//基類類型的指針(引用或對象)只能訪問對象的基類部分,而在基類中沒有定義 discount_policy 成員

重新定義 itemP 的訪問是錯誤的,因爲基類類型的指針(引用或對象)只能訪問對象的基類部分,而在基類中沒有定義 discount_policy 成員。

名字衝突與繼承

雖然可以直接訪問基類成員,就像它是派生類成員一樣,但是成員保留了它的基類成員資格。一般我們並不關心是哪個實際類包含成員,通常只在基類和派生類共享同一名字時才需要注意。
與基類成員同名的派生類成員將屏蔽對基類成員的直接訪問

struct Base {
	Base(): mem(0) { }
protected:
	int mem;
};
struct Derived : Base {
	Derived(int i): mem(i) { } // initializes Derived::mem
	int get_mem() { return mem; } // returns Derived::mem
protected:
	int mem; // hides mem in the base
};

get_mem 中對 mem 的引用被確定爲使用 Derived 中的名字。如果編寫如下代碼:

Derived d(42);
cout << d.get_mem() << endl; // prints 42

則輸出將是 42。

使用作用域操作符訪問被屏蔽成員

可以使用作用域操作符訪問被屏蔽的基類成員:

struct Derived : Base {
int get_base_mem() { return Base::mem; }
};

作用域操作符指示編譯器在 Base 中查找 mem。
設計派生類時,只要可能,最好避免與基類成員的名字衝突。

作用域與成員函數

在基類和派生類中使用同一名字的成員函數,其行爲與數據成員一樣:在派生類作用域中派生類成員將屏蔽基類成員。即使函數原型不同,基類成員也會被屏蔽

struct Base {
	int memfcn();
};
struct Derived : Base {
	int memfcn(int); // hides memfcn in the base
};
Derived d; Base b;
b.memfcn(); // calls Base::memfcn
d.memfcn(10); // calls Derived::memfcn
d.memfcn(); // error: memfcn with no arguments is hidden
d.Base::memfcn(); // ok: calls Base::memfcn

Derived 中的 memfcn 聲明隱藏了 Base 中的聲明。這並不奇怪,第一個調用通過 Base 對象 b 調用基類中的版本,同樣,第二個調用通過 d 調用Derived 中的版本。可能比較奇怪的是第三個調用:

d.memfcn(); // error: Derived has no memfcn that takes no arguments

要確定這個調用,編譯器需要查找名字 memfcn,並在 Derived 類中找到。一旦找到了名字,編譯器就不再繼續查找了。這個調用與 Derived 中的 memfcn定義不匹配,該定義希望接受 int 實參,而這個函數調用沒有提供那樣的實參,因此出錯。
回憶一下,局部作用域中聲明的函數不會重載全局作用域中定義的函數(第 7章),同樣,派生類中定義的函數也不重載基類中定義的成員。通過派生類對象調用函數時,實參必須與派生類中定義的版本相匹配,只有在派生類根本沒有定義該函數時,才考慮基類函數。

重載函數

像其他任意函數一樣,成員函數(無論虛還是非虛)也可以重載派生類可以重定義所繼承的 0 個或多個版本。

如果派生類重定義了重載成員,則通過派生類型只能訪問派生類中重定義的那些成員。

如果派生類想通過自身類型使用的重載版本,則派生類必須要麼重定義所有重載版本,要麼一個也不重定義。
有時類需要僅僅重定義一個重載集中某些版本的行爲,並且想要繼承其他版本的含義,在這種情況下,爲了重定義需要特化的某個版本而不得不重定義每一個基類版本,可能會令人厭煩。
派生類不用重定義所繼承的每一個基類版本,它可以爲重載成員提供using聲明。一個 using 聲明只能指定一個名字,不能指定形參表,因此,爲基類成員函數名稱而作的 using 聲明將該函數的所有重載實例加到派生類的作用域。將所有名字加入作用域之後,派生類只需要重定義本類型確實必須定義的那些函數,對其他版本可以使用繼承的定義。

虛函數與作用域

還記得嗎,要獲得動態綁定,必須通過基類的引用或指針調用虛成員。當我們這樣做時,編譯器器將在基類中查找函數。假定找到了名字,編譯器就檢查實參是否與形參匹配。
現在可以理解虛函數爲什麼必須在基類和派生類中擁有同一原型了。如果基類成員與派生類成員接受的實參不同,就沒有辦法通過基類類型的引用或指針調用派生類函數。考慮如下(人爲的)爲集合:

class Base {
public:
	virtual int fcn();
};
class D1 : public Base {
public:
// hides fcn in the base; this fcn is not virtual
	int fcn(int); // parameter list differs from fcn in Base
// D1 inherits definition of Base::fcn()
};
class D2 : public D1 {
public:
	int fcn(int); // nonvirtual function hides D1::fcn(int)
	int fcn(); // redefines virtual fcn from Base
};

D1 中的 fcn 版本沒有重定義 Base 的虛函數 fcn,相反,它屏蔽了基類的fcn。結果 D1 有兩個名爲 fcn 的函數:類從 Base 繼承了一個名爲 fcn 的虛函數,類又定義了自己的名爲 fcn 的非虛成員函數,該函數接受一個 int 形參。
但是,從 Base 繼承的虛函數不能通過 D1 對象(或 D1 的引用或指針)調用,因爲該函數被 fcn(int) 的定義屏蔽了。
類 D2 重定義了它繼承的兩個函數,它重定義了 Base 中定義的 fcn 的原始版本並重定義了 D1 中定義的非虛版本。

通過基類調用被屏蔽的虛函數

通過基類類型的引用或指針調用函數時,編譯器將在基類中查找該函數而忽略派生類

Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn(); // ok: virtual call, will call Base::fcnat run time
bp2->fcn(); // ok: virtual call, will call Base::fcnat run time
bp3->fcn(); // ok: virtual call, will call D2::fcnat run time

三個指針都是基類類型的指針,因此通過在 Base 中查找 fcn 來確定這三個調用,所以這些調用是合法的。另外,因爲 fcn 是虛函數,所以編譯器會生成代碼,在運行時基於引用指針所綁定的對象的實際類型進行調用。在 bp2 的情況,基本對象是 D1 類的,D1 類沒有重定義不接受實參的虛函數版本,通過bp2 的函數調用(在運行時)調用 Base 中定義的版本。

重構

將 Disc_item 加到 Item_base 層次是重構(refactoring)的一個例子。重構包括重新定義類層次,將操作和數據從一個類移到另一個類。
爲了適應應用程序的需要而重新設計類以便增加新函數或處理其他改變時,最有可能需要進行重構。
重構常見在面向對象應用程序中非常常見。值得注意的是,雖然改變了繼承層次,使用 Bulk_item 類或 Item_base 類的代碼不需要改變。然而,對類進行重構,或以任意其他方式改變類,使用這些類的任意代碼都必須重新編譯。

尊重基類接口

構造函數只能初始化其直接基類的原因是每個類都定義了自己的接口。定義 Disc_item 時,通過定義它的構造函數指定了怎樣初始化Disc_item 對象。一旦類定義了自己的接口,與該類對象的所有交互都應該通過該接口,即使對象是派生類對象的一部分也不例外。同樣,派生類構造函數不能初始化基類的成員且不應該對基類成員賦值。
如果那些成員爲 public 或 protected,派生構造函數可以在構造函數函數體中給基類成員賦值,但是,這樣做會違反基類的接口。派生類應通過使用基類構造函數尊重基類的初始化意圖,而不是在派生類構造函數函數體中對這些成員賦值。

名字查找與繼承

理解 C++ 中繼承層次的關鍵在於理解如何確定函數調用確定函數調用遵循以下四個步驟

  1. 首先確定進行函數調用的對象、引用或指針的靜態類型。
  2. 在該類中查找函數,如果找不到,就在直接基類中查找,如此循着類的繼承鏈往上找,直到找到該函數或者查找完最後一個類。如果不能在類或其相關基類中找到該名字,則調用是錯誤的。
  3. 一旦找到了該名字,就進行常規類型檢查(第 7章),查看如果給定找到的定義,該函數調用是否合法。
  4. 假定函數調用合法,編譯器就生成代碼。如果函數是虛函數且通過引用或指針調用,則編譯器生成代碼以確定根據對象的動態類型運行哪個函數版本,否則,編譯器生成代碼直接調用函數。

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

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