溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

C++應(yīng)用程序性能優(yōu)化(三)——C++語(yǔ)言特性性能分析

發(fā)布時(shí)間:2020-07-25 18:34:26 來(lái)源:網(wǎng)絡(luò) 閱讀:5350 作者:天山老妖S 欄目:編程語(yǔ)言

C++應(yīng)用程序性能優(yōu)化(三)——C++語(yǔ)言特性性能分析

一、C++語(yǔ)言特性性能分析簡(jiǎn)介

通常大多數(shù)開(kāi)發(fā)人員認(rèn)為,匯編語(yǔ)言和C語(yǔ)言比較適合編寫(xiě)對(duì)性能要求非常高的程序,C++語(yǔ)言主要適用于編寫(xiě)復(fù)雜度非常高但性能要求并不是很高的程序。因?yàn)榇蠖鄶?shù)開(kāi)發(fā)人員認(rèn)為,C++語(yǔ)言設(shè)計(jì)時(shí)因?yàn)榭紤]到支持多種編程模式(如面向?qū)ο缶幊毯头缎途幊蹋┮约爱惓L幚淼龋瑥亩肓颂嘈碌恼Z(yǔ)言特性。新的語(yǔ)言特性往往使得C++編譯器在編譯程序時(shí)插入了很多額外的代碼,會(huì)導(dǎo)致最終生成的二進(jìn)制代碼體積膨脹,而且執(zhí)行速度下降。
但事實(shí)并非如此,通常一個(gè)程序的速度在框架設(shè)計(jì)完成時(shí)大致已經(jīng)確定,而并非因?yàn)椴捎肅++語(yǔ)言才導(dǎo)致速度沒(méi)有達(dá)到預(yù)期目標(biāo)。因此,當(dāng)一個(gè)程序的性能需要提高時(shí),首先需要做的是用性能檢測(cè)工具對(duì)其運(yùn)行的時(shí)間分布進(jìn)行一個(gè)準(zhǔn)確的測(cè)量,找出關(guān)鍵路徑和真正的性能瓶頸所在,然后針對(duì)性能瓶頸進(jìn)行分析和優(yōu)化,而不是主觀(guān)地將性能問(wèn)題歸咎于程序所采用的語(yǔ)言。工程實(shí)踐表明,如果框架設(shè)計(jì)不做修改,即使使用C語(yǔ)言或匯編語(yǔ)言重新改寫(xiě),也并不能保證提高總體性能。
因此,遇到性能問(wèn)題時(shí),首先應(yīng)檢查和反思程序的總體架構(gòu),然后使用性能檢測(cè)工具對(duì)其實(shí)際運(yùn)行做準(zhǔn)確的測(cè)量,再針對(duì)性能瓶頸進(jìn)行分析和優(yōu)化。
但C++語(yǔ)言中確實(shí)有一些操作、特性比其它因素更容易成為程序的性能瓶頸,常見(jiàn)因素如下:
(1)缺頁(yè)
缺頁(yè)通常意味著要訪(fǎng)問(wèn)外部存儲(chǔ),因?yàn)橥獠看鎯?chǔ)訪(fǎng)問(wèn)相對(duì)于訪(fǎng)問(wèn)內(nèi)存或代碼執(zhí)行,有數(shù)量級(jí)的差別。因此,只要有可能,應(yīng)該盡量想辦法減少缺頁(yè)。
(2)從堆中動(dòng)態(tài)申請(qǐng)和釋放內(nèi)存
C語(yǔ)言中的malloc/free和C++語(yǔ)言中的new/delete操作時(shí)非常耗時(shí)的,因此要盡可能優(yōu)先考慮從線(xiàn)程棧中獲取內(nèi)存。優(yōu)先考慮棧而減少?gòu)膭?dòng)態(tài)堆中申請(qǐng)內(nèi)存,不僅因?yàn)樵诙阎蟹峙鋬?nèi)存比在棧中要慢很多,而且還與盡量減少缺頁(yè)有關(guān)。當(dāng)程序執(zhí)行時(shí),當(dāng)前棧幀空間所在的內(nèi)存頁(yè)肯定在物理內(nèi)存中,因此程序代碼對(duì)其中變量的存取不會(huì)引起缺頁(yè);如果從堆空間生成對(duì)象,只有指向?qū)ο蟮闹羔樤跅I?,?duì)象本身則存儲(chǔ)在堆空間中。堆一般不可能都在物理內(nèi)存中,而且由于堆分配內(nèi)存的特性,即使兩個(gè)相鄰生成的對(duì)象,也很有可能在堆內(nèi)存位置上相距很遠(yuǎn)。因此,當(dāng)訪(fǎng)問(wèn)兩個(gè)對(duì)象時(shí),雖然分別指向兩個(gè)對(duì)象的指針都在棧上,但通過(guò)兩個(gè)指針引用對(duì)象時(shí)很有可能會(huì)引起兩次缺頁(yè)。
(3)復(fù)雜對(duì)象的創(chuàng)建和銷(xiāo)毀
復(fù)雜對(duì)象的創(chuàng)建和銷(xiāo)毀會(huì)比較耗時(shí),因此對(duì)于層次較深的遞歸調(diào)用需要重點(diǎn)關(guān)注遞歸內(nèi)部的對(duì)象創(chuàng)建。其次,編譯器生成的臨時(shí)對(duì)象因?yàn)樵诔绦虻脑创a中看不到,更不容易察覺(jué),因此需要重點(diǎn)關(guān)注。
(4)函數(shù)調(diào)用
由于函數(shù)調(diào)用有固定的額外開(kāi)銷(xiāo),因此當(dāng)函數(shù)體的代碼量相對(duì)較少,并且函數(shù)被非常頻繁調(diào)用時(shí),函數(shù)調(diào)用時(shí)的固定開(kāi)銷(xiāo)容易成為不必要的開(kāi)銷(xiāo)。C語(yǔ)言的宏和C++語(yǔ)言的內(nèi)聯(lián)函數(shù)都是為了在保持函數(shù)調(diào)用的模塊化特征基礎(chǔ)上消除函數(shù)調(diào)用的固定額外開(kāi)銷(xiāo)而引入的。由于C語(yǔ)言的宏在×××能優(yōu)勢(shì)的同時(shí)也給開(kāi)發(fā)和調(diào)試帶來(lái)不便,因此C++語(yǔ)言中推薦使用內(nèi)聯(lián)函數(shù)。

二、構(gòu)造函數(shù)與析構(gòu)函數(shù)

1、構(gòu)造函數(shù)與析構(gòu)函數(shù)簡(jiǎn)介

構(gòu)造函數(shù)和析構(gòu)函數(shù)的特點(diǎn)是當(dāng)創(chuàng)建對(duì)象時(shí)自動(dòng)執(zhí)行構(gòu)造函數(shù);當(dāng)銷(xiāo)毀對(duì)象時(shí),析構(gòu)函數(shù)自動(dòng)被執(zhí)行。構(gòu)造函數(shù)是一個(gè)對(duì)象最先被執(zhí)行的函數(shù),在創(chuàng)建對(duì)象時(shí)調(diào)用,用于初始化對(duì)象的初始狀態(tài)和取得對(duì)象被使用前需要的一些資源,如文件、網(wǎng)絡(luò)連接等;析構(gòu)函數(shù)是一個(gè)對(duì)象最后被執(zhí)行的函數(shù),用于釋放對(duì)象擁有的資源。在對(duì)象的生命周期內(nèi),構(gòu)造函數(shù)和析構(gòu)函數(shù)都只會(huì)執(zhí)行一次。
創(chuàng)建一個(gè)對(duì)象有兩種方式,一種是從線(xiàn)程運(yùn)行棧中創(chuàng)建,稱(chēng)為局部對(duì)象。銷(xiāo)毀局部對(duì)象并不需要程序顯示地調(diào)用析構(gòu)函數(shù),而是當(dāng)程序運(yùn)行出對(duì)象所屬的作用域時(shí)自動(dòng)調(diào)用對(duì)象的析構(gòu)函數(shù)。
創(chuàng)建對(duì)象的另一種方式是從全局堆中動(dòng)態(tài)分配,通常使用new或malloc分配堆空間。

Obejct* p = new Object();//1
// do something //2
delete p;//3
p = NULL;//4

執(zhí)行語(yǔ)句1時(shí),指針p所指向?qū)ο蟮膬?nèi)存從全局堆空間中獲得,并將地址賦值給p,p本身是一個(gè)局部變量,需要從線(xiàn)程棧中分配,p所指向?qū)ο髲娜侄阎蟹峙鋬?nèi)存存放。從全局堆中創(chuàng)建的對(duì)象需要顯示調(diào)用delete進(jìn)行銷(xiāo)毀,delete會(huì)調(diào)用指針p指向?qū)ο蟮奈鰳?gòu)函數(shù),并將對(duì)象所占的全局堆內(nèi)存空間返回給全局堆。執(zhí)行語(yǔ)句3后,指針p指向的對(duì)象被銷(xiāo)毀,但指針p還存在于棧中,直到程序退出其所在作用域。將p指針?biāo)赶驅(qū)ο箐N(xiāo)毀后,p指針仍指向被銷(xiāo)毀對(duì)象的全局堆空間位置,此時(shí)指針p變成一個(gè)懸空指針,此時(shí)使用指針p是危險(xiǎn)的,通常推薦將p賦值NULL。
在Win32平臺(tái),訪(fǎng)問(wèn)銷(xiāo)毀對(duì)象的全局堆空間內(nèi)存會(huì)導(dǎo)致三種情況:
(1)被銷(xiāo)毀對(duì)象所在的內(nèi)存頁(yè)沒(méi)有任何對(duì)象,堆管理器已經(jīng)將所占堆空間進(jìn)一步回收給操作系統(tǒng),此時(shí)通過(guò)指針訪(fǎng)問(wèn)會(huì)引起訪(fǎng)問(wèn)違例,即訪(fǎng)問(wèn)了不合法內(nèi)存,引起進(jìn)程崩潰。
(2)被銷(xiāo)毀對(duì)象所在的內(nèi)存頁(yè)存在其它對(duì)象,并且被銷(xiāo)毀對(duì)象曾經(jīng)占用的全局堆空間被回收后尚未分配給其它對(duì)象,此時(shí)通過(guò)指針p訪(fǎng)問(wèn)取得的值是無(wú)意義的,雖然不會(huì)立刻引起進(jìn)程崩潰,但針對(duì)指針p的后續(xù)操作行為是不可預(yù)測(cè)的。
(3)被銷(xiāo)毀對(duì)象所在的內(nèi)存頁(yè)存在其它對(duì)象,并且被銷(xiāo)毀對(duì)象曾經(jīng)占用的全局堆空間被回收后已經(jīng)分配給其它對(duì)象,此時(shí)通過(guò)指針p取得的值是其它對(duì)象,雖然對(duì)指針p的訪(fǎng)問(wèn)不會(huì)引起進(jìn)程崩潰,但極有可能引起對(duì)象狀態(tài)的改變。

2、對(duì)象的構(gòu)造過(guò)程

創(chuàng)建一個(gè)對(duì)象分為兩個(gè)步驟,即首先取得對(duì)象所需的內(nèi)存(從線(xiàn)程?;蛉侄眩?,然后在內(nèi)存空間上執(zhí)行構(gòu)造函數(shù)。在構(gòu)造函數(shù)構(gòu)建對(duì)象時(shí),構(gòu)造函數(shù)也分為兩個(gè)步驟。第一步執(zhí)行初始化(通過(guò)初始化參數(shù)列表),第二步執(zhí)行構(gòu)造函數(shù)的函數(shù)體。

class Derived : public Base
{
public:
    Derived(): id(1), name("UnNamed")   // 1
    {
        // do something     // 2
    }
private:
    int id;
    string name;
};

語(yǔ)句1中冒號(hào)后的代碼即為初始化列表,每個(gè)初始化單元都是變量名(值)的模式,不同單元之間使用逗號(hào)分隔。構(gòu)造函數(shù)首先根據(jù)初始化列表執(zhí)行初始化,然后執(zhí)行構(gòu)造函數(shù)的函數(shù)體(語(yǔ)句2)。初始化操作的注意事項(xiàng)如下:
(1)構(gòu)造函數(shù)其實(shí)是一個(gè)遞歸操作,在每層遞歸內(nèi)部的操作遵循嚴(yán)格的次序。遞歸模式會(huì)首先執(zhí)行父類(lèi)的構(gòu)造函數(shù)(父類(lèi)的構(gòu)造函數(shù)操作也相應(yīng)包含執(zhí)行初始化和執(zhí)行構(gòu)造函數(shù)函數(shù)體兩個(gè)部分),父類(lèi)構(gòu)造函數(shù)返回后構(gòu)造類(lèi)自己的成員變量。構(gòu)造類(lèi)自己的成員變量時(shí),一是嚴(yán)格按照成員變量在類(lèi)中的聲明順序進(jìn)行,與成員變量在初始化列表中出現(xiàn)的順序完全無(wú)關(guān);二是當(dāng)有些成員變量或父類(lèi)對(duì)象沒(méi)有在初始化列表出現(xiàn)時(shí),仍然在初始化操作中對(duì)其進(jìn)行初始化,內(nèi)建類(lèi)型成員變量被賦值給一個(gè)初值,父類(lèi)對(duì)象和類(lèi)成員變量對(duì)象被調(diào)用其默認(rèn)構(gòu)造函數(shù)初始化,然后父類(lèi)的構(gòu)造函數(shù)和子成員變量對(duì)象在構(gòu)造函數(shù)執(zhí)行過(guò)程中也遵循上述遞歸操作,直到類(lèi)的繼承體系中所有父類(lèi)和父類(lèi)所含的成員變量都被構(gòu)造完成,類(lèi)的初始化操作才完成。
(2)父類(lèi)對(duì)象和一些成員變量沒(méi)有出現(xiàn)在初始化列表中時(shí),其仍然會(huì)被執(zhí)行默認(rèn)構(gòu)造函數(shù)。因此,相應(yīng)對(duì)象所屬類(lèi)必須提供可以調(diào)用的默認(rèn)構(gòu)造函數(shù),為此要求相應(yīng)的類(lèi)必須顯式提供默認(rèn)構(gòu)造函數(shù),要么不能阻止編譯器隱式生成默認(rèn)構(gòu)造函數(shù),定義除默認(rèn)構(gòu)造函數(shù)外的其它類(lèi)型的構(gòu)造函數(shù)將會(huì)阻止編譯器生成默認(rèn)構(gòu)造函數(shù)。如果編譯器在編譯時(shí),發(fā)現(xiàn)沒(méi)有可供調(diào)用的默認(rèn)構(gòu)造函數(shù),并且編譯器也無(wú)法生成默認(rèn)構(gòu)造函數(shù),則編譯無(wú)法通過(guò)。
(3)對(duì)兩類(lèi)成員變量,需要強(qiáng)調(diào)指出(即常量型和引用型)。由于所有成員變量在執(zhí)行函數(shù)體前已經(jīng)被構(gòu)造,即已經(jīng)擁有初始值,因此,對(duì)于常量型和引用型變量必須在初始化列表中正確初始化,而不能將其初始化放在構(gòu)造函數(shù)體內(nèi)。
(4)初始化列表可能沒(méi)有完全列出其子成員或父類(lèi)對(duì)象成員,或者順序與其在類(lèi)中的聲明順序不同,仍然會(huì)保證嚴(yán)格被全部并且嚴(yán)格按照順序被構(gòu)建。即程序在進(jìn)入構(gòu)造函數(shù)體前,類(lèi)的父類(lèi)對(duì)象和所有子成員變量對(duì)象已經(jīng)被生成和構(gòu)造。如果在構(gòu)造函數(shù)體內(nèi)為其執(zhí)行賦值操作,顯然屬于浪費(fèi)。如果在構(gòu)造函數(shù)時(shí)已經(jīng)知道如何為類(lèi)的子成員變量初始化,則應(yīng)該將初始化信息通過(guò)構(gòu)造函數(shù)的初始化列表賦予子成員變量,而不是在構(gòu)造函數(shù)體內(nèi)進(jìn)行初始化,因?yàn)檫M(jìn)入構(gòu)造函數(shù)時(shí),子成員變量已經(jīng)初始化一次。

3、對(duì)象的析構(gòu)過(guò)程

析構(gòu)函數(shù)和構(gòu)造函數(shù)一樣,是遞歸的過(guò)程,但存在不同。一是析構(gòu)函數(shù)不存在初始化操作部分,析構(gòu)函數(shù)的主要工作就是執(zhí)行析構(gòu)函數(shù)的函數(shù)體;二是析構(gòu)函數(shù)執(zhí)行的遞歸與構(gòu)造函數(shù)相反,在每一層遞歸中,成員變量對(duì)象的析構(gòu)順序也與構(gòu)造函數(shù)相反。
析構(gòu)函數(shù)只能選擇類(lèi)的成員變量在類(lèi)中聲明的順序作為析構(gòu)的順序參考(正序或逆序)。因?yàn)闃?gòu)造函數(shù)選擇了正序,而析構(gòu)函數(shù)的工作與構(gòu)造函數(shù)相反,因此析構(gòu)函數(shù)選擇逆序。又因?yàn)槲鰳?gòu)函數(shù)只能使用成員變量在類(lèi)中的聲明順序作為析構(gòu)順序的依據(jù)(正序或逆序),因此構(gòu)造函數(shù)也只能選擇成員變量在類(lèi)中的聲明順序作為構(gòu)造的順序依據(jù),而不能采用初始化列表的順序作為順序依據(jù)。
如果操作的對(duì)象屬于一個(gè)復(fù)雜繼承體系的末端節(jié)點(diǎn),其析構(gòu)過(guò)程也將十分耗時(shí)。
在C++程序中,創(chuàng)建和銷(xiāo)毀對(duì)象是影響性能的一個(gè)非常突出的操作。首先,如果是從全局堆空間中生成對(duì)象,則需要先進(jìn)行動(dòng)態(tài)內(nèi)存分配操作,而動(dòng)態(tài)內(nèi)存的分配與回收是非常耗時(shí)的操作,因?yàn)樯婕暗綄ふ移ヅ浯笮〉膬?nèi)存塊,找到后可能還需要截?cái)嗵幚恚缓筮€需要修改維護(hù)全局堆內(nèi)存使用情況信息的鏈表。頻繁的內(nèi)存操作會(huì)嚴(yán)重影響性能的下降,使用內(nèi)存池技術(shù)可以減少?gòu)娜謩?dòng)態(tài)堆空間申請(qǐng)內(nèi)存的次數(shù),提高程序的總體性能。當(dāng)取得內(nèi)存后,如果需要生成的內(nèi)對(duì)象屬于復(fù)雜繼承體系的末端類(lèi),則構(gòu)造函數(shù)的調(diào)用將會(huì)引起一連串的遞歸構(gòu)造操作,在大型復(fù)雜系統(tǒng)中,大量的此類(lèi)對(duì)象構(gòu)造將會(huì)消耗CPU操作的主要部分。
由于對(duì)象的創(chuàng)建和銷(xiāo)毀會(huì)影響性能,在盡量減少自己代碼生成對(duì)象的同時(shí),需要關(guān)注編譯器在編譯時(shí)臨時(shí)生成的對(duì)象,盡量避免臨時(shí)對(duì)象的生成。
如果在實(shí)現(xiàn)構(gòu)造函數(shù)時(shí),在構(gòu)造函數(shù)體中進(jìn)行了第二次的賦值操作,也會(huì)浪費(fèi)CPU時(shí)間。

4、函數(shù)參數(shù)傳遞

減少對(duì)象創(chuàng)建和銷(xiāo)毀的常見(jiàn)方法是在聲明中將所有的值傳遞改為常量引用傳遞,如:

int func(Object obj);// 1
int func(const Object& obj);// 2

值傳遞驗(yàn)證示例如下:

#include <iostream>

using namespace std;

class Object
{
public:
    Object(int i = 1)
    {
        n = i;
        cout << "Object(int i = 1): " << endl;
    }
    Object(const Object& another)
    {
        n = another.n;
        cout << "Object(const Object& another): " << endl;
    }
    void increase()
    {
        n++;
    }
    int value()const
    {
        return n;
    }
    ~Object()
    {
        cout << "~Object()" << endl;
    }
private:
    int n;
};
void func(Object obj)
{
    cout << "enter func, before increase(), n = " << obj.value() << endl;
    obj.increase();
    cout << "enter func, after increase(), n = " << obj.value() << endl;
}
int main()
{
    Object a;   // 1
    cout << "before call func, n = " << a.value() << endl;
    func(a);    // 2
    cout << "after call func, n = " << a.value() << endl;// 3

    return 0;
}
// output:
//Object(int i = 1):        // 4
//before call func, n = 1
//Object(const Object& another):    // 5
//enter func, before increase(), n = 1  // 6
//enter func, after increase(), n = 2   // 7
//~Object() // 8
//after call func, n = 1    // 9
//~Object()

語(yǔ)句4的輸出為語(yǔ)句1處的對(duì)象構(gòu)造,語(yǔ)句5輸出則是語(yǔ)句2處的func(a)函數(shù)調(diào)用,調(diào)用開(kāi)始時(shí)通過(guò)拷貝構(gòu)造函數(shù)生成對(duì)象a的復(fù)制品,緊跟著在函數(shù)內(nèi)檢查n的輸出值輸出語(yǔ)句6,輸出值與func函數(shù)外部元對(duì)象a的值相同,然后復(fù)制品調(diào)用increase函數(shù)將n值加1,此時(shí)復(fù)制品的n值為2,并輸出語(yǔ)句7。func函數(shù)執(zhí)行完畢后銷(xiāo)毀復(fù)制品,輸出語(yǔ)句8。main函數(shù)內(nèi)繼續(xù)執(zhí)行,打印原對(duì)象a的n值為1,輸出語(yǔ)句9。
當(dāng)函數(shù)需要修改傳入?yún)?shù)時(shí),應(yīng)該用引用傳入?yún)?shù);當(dāng)函數(shù)不會(huì)修改傳入?yún)?shù)時(shí),如果函數(shù)聲明中傳入?yún)?shù)為對(duì)象,則函數(shù)可以達(dá)到設(shè)計(jì)目的,但會(huì)生成不必要的復(fù)制品對(duì)象,從而引入不必要的構(gòu)造和析構(gòu)操作,應(yīng)該使用常量引用傳入?yún)?shù)。
構(gòu)造函數(shù)的重復(fù)賦值對(duì)性能影響驗(yàn)證示例如下:

#include <iostream>
#include <time.h>

using namespace std;

class DArray
{
public:
    DArray(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
    void init(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
private:
    double d[1000];
};

class Object
{
public:
    Object(double v)
    {
        d.init(v);
    }
private:
    DArray d;
};

int main()
{
    clock_t start, finish;
    start = clock();
    for(int i = 0; i < 100000; i++)
    {
        Object obj(2.0 + i);
    }
    finish = clock();
    cout << "Used Time: " << double(finish - start) << "" << endl;

    return 0;
}

耗時(shí)為600000單位,如果通過(guò)初始化列表對(duì)成員變量進(jìn)行初始化,其代碼如下:

#include <iostream>
#include <time.h>

using namespace std;

class DArray
{
public:
    DArray(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
    void init(double v = 1.0)
    {
        for(int i = 0; i < 1000; i++)
        {
            d[i] = v + i;
        }
    }
private:
    double d[1000];
};

class Object
{
public:
    Object(double v): d(v)
    {
    }
private:
    DArray d;
};

int main()
{
    clock_t start, finish;
    start = clock();
    for(int i = 0; i < 100000; i++)
    {
        Object obj(2.0 + i);
    }
    finish = clock();
    cout << "Used Time: " << double(finish - start) << "" << endl;

    return 0;
}

耗時(shí)為300000單位,性能提高約50%。

三、繼承與虛函數(shù)

1、虛函數(shù)與動(dòng)態(tài)綁定機(jī)制

虛函數(shù)是C++語(yǔ)言引入的一個(gè)重要特性,提供了動(dòng)態(tài)綁定機(jī)制,動(dòng)態(tài)綁定機(jī)制使得類(lèi)繼承的語(yǔ)義變得相對(duì)明晰。
(1)基類(lèi)抽象了通用的數(shù)據(jù)及操作。對(duì)于數(shù)據(jù)而言,如果數(shù)據(jù)成員在各個(gè)派生類(lèi)中都需要用到,需要將其聲明在基類(lèi)中;對(duì)于操作而語(yǔ)言,如果操作對(duì)于各個(gè)派生類(lèi)都有意義,無(wú)論其語(yǔ)義是否會(huì)被修改和擴(kuò)展,需要將其聲明在基類(lèi)中。
(2)某些操作,對(duì)于各個(gè)派生類(lèi)而言,語(yǔ)義完全保持一致,而無(wú)需修改和擴(kuò)展,則相應(yīng)操作聲明為基類(lèi)的非虛成員函數(shù)。各個(gè)派生類(lèi)在聲明為基類(lèi)的派生類(lèi)時(shí),默認(rèn)繼承非虛成員函數(shù)的聲明和實(shí)現(xiàn),如果默認(rèn)繼承基類(lèi)的數(shù)據(jù)成員一樣,而不必另外做任何聲明,構(gòu)成代碼復(fù)用。
(3)對(duì)于某些操作,雖然對(duì)于各個(gè)派生類(lèi)都有意義,但其語(yǔ)義并不相同,則相應(yīng)的操作應(yīng)該聲明為虛成員函數(shù)。各個(gè)派生類(lèi)雖然也繼承了虛成員函數(shù)的聲明和實(shí)現(xiàn),但語(yǔ)義上應(yīng)該對(duì)虛成員函數(shù)的實(shí)現(xiàn)進(jìn)行修改或擴(kuò)展。如果在實(shí)現(xiàn)修改、擴(kuò)展虛成員函數(shù)的過(guò)程中,需要用到額外的派生類(lèi)獨(dú)有的數(shù)據(jù)時(shí),則將相應(yīng)的數(shù)據(jù)聲明為派生類(lèi)自己的數(shù)據(jù)成員。
當(dāng)更高層次的程序框架(繼承體系的使用者)使用此繼承體系時(shí),處理的是抽象層次的對(duì)象集合,對(duì)象集合的成員本質(zhì)是各種派生類(lèi)對(duì)象,但在處理對(duì)象集合的對(duì)象時(shí),使用的是抽象層次的操作。高層程序框架并不區(qū)分相應(yīng)操作中哪些操作對(duì)于派生類(lèi)是不變的,哪些操作對(duì)于派生類(lèi)是不同的,當(dāng)實(shí)際執(zhí)行到各操作時(shí),運(yùn)行時(shí)系統(tǒng)能夠識(shí)別哪些操作需要用到動(dòng)態(tài)綁定。從而找到對(duì)應(yīng)派生類(lèi)的修改或擴(kuò)展的操作版本。即對(duì)繼承體系的使用者而言,繼承體系內(nèi)部的多樣性是透明的,不必關(guān)心其繼承細(xì)節(jié),處理的是一組對(duì)使用者而言整體行為一致的對(duì)象。即使繼承體系內(nèi)部增加、刪除了某個(gè)派生類(lèi),或某個(gè)派生類(lèi)的虛函數(shù)實(shí)現(xiàn)發(fā)生了改變,使用者的代碼也不必做任何修改,使程序的模塊化程度得到極大提高,其擴(kuò)展性、維護(hù)性和代碼可讀性也會(huì)提高。對(duì)于對(duì)象繼承體系使用者而言,只看到抽象類(lèi)型,而不必關(guān)心具體是哪種具體類(lèi)型。

2、虛函數(shù)的效率分析

虛函數(shù)的動(dòng)態(tài)綁定特性雖然很好,但存在內(nèi)存空間和時(shí)間開(kāi)銷(xiāo),每個(gè)支持虛函數(shù)的類(lèi)(基類(lèi)或派生類(lèi))都會(huì)有一個(gè)包含其所有支持的虛函數(shù)的虛函數(shù)表的指針。每個(gè)類(lèi)對(duì)象都會(huì)隱含一個(gè)虛函數(shù)表指針(virtual pointer),指向其所屬類(lèi)的虛函數(shù)表。當(dāng)通過(guò)基類(lèi)的指針或引用調(diào)用某個(gè)虛函數(shù)時(shí),系統(tǒng)需要首先定位指針或引用真正對(duì)應(yīng)的對(duì)象所隱含的虛函數(shù)指針,然后虛函數(shù)指針根據(jù)虛函數(shù)的名稱(chēng)對(duì)其所指向的虛函數(shù)表進(jìn)行一個(gè)偏移定位,再調(diào)用偏移定位處的函數(shù)指針對(duì)應(yīng)的虛函數(shù),即動(dòng)態(tài)綁定的解析過(guò)程。C++規(guī)范只需要編譯器能夠保證動(dòng)態(tài)綁定的語(yǔ)義,但大多數(shù)編譯器都采用上述方法實(shí)現(xiàn)虛函數(shù)。
(1)每個(gè)支持虛函數(shù)的類(lèi)都有一個(gè)虛函數(shù)表,虛函數(shù)表的大小與類(lèi)擁有的虛函數(shù)的多少成正比。一個(gè)程序中,每個(gè)類(lèi)的虛函數(shù)表只有一個(gè),與類(lèi)對(duì)象的數(shù)量無(wú)關(guān)。支持虛函數(shù)的類(lèi)的每個(gè)類(lèi)對(duì)象都有一個(gè)指向類(lèi)的虛函數(shù)表的虛函數(shù)指針,因此程序運(yùn)行時(shí)虛函數(shù)指針引起的內(nèi)存開(kāi)銷(xiāo)與生成的類(lèi)對(duì)象數(shù)量成正比。
(2)支持虛函數(shù)的類(lèi)生成每個(gè)對(duì)象時(shí),在構(gòu)造函數(shù)中會(huì)調(diào)用編譯器在構(gòu)造函數(shù)內(nèi)部插入的初始化代碼,來(lái)初始化其虛函數(shù)指針,使其指向正確的虛函數(shù)表。當(dāng)通過(guò)指針或引用調(diào)用虛函數(shù)時(shí),會(huì)根據(jù)虛函數(shù)指針找到相應(yīng)類(lèi)的虛函數(shù)表。

3、虛函數(shù)與內(nèi)聯(lián)

內(nèi)聯(lián)函數(shù)通??梢蕴岣叽a執(zhí)行速度,很多普通函數(shù)會(huì)根據(jù)情況進(jìn)行內(nèi)聯(lián)化,但虛函數(shù)無(wú)法利用內(nèi)聯(lián)化的優(yōu)勢(shì)。因?yàn)閮?nèi)聯(lián)是在編譯階段編譯器將調(diào)用內(nèi)聯(lián)函數(shù)的位置用內(nèi)聯(lián)函數(shù)體替代(內(nèi)聯(lián)展開(kāi)),但虛函數(shù)本質(zhì)上是運(yùn)行期行為。在編譯階段,編譯器無(wú)法知道某處的虛函數(shù)調(diào)用在真正執(zhí)行的時(shí)后需要調(diào)用哪個(gè)具體的實(shí)現(xiàn)(即編譯階段無(wú)法確定其具體綁定),因此,編譯階段編譯器不會(huì)對(duì)通過(guò)指針或引用調(diào)用的虛函數(shù)進(jìn)行內(nèi)聯(lián)化。如果需要利用虛函數(shù)的動(dòng)態(tài)綁定的設(shè)計(jì)優(yōu)勢(shì),必須放棄內(nèi)聯(lián)帶來(lái)的速度優(yōu)勢(shì)。
如果不使用虛函數(shù),可以通過(guò)在抽象基類(lèi)增加一個(gè)類(lèi)型標(biāo)識(shí)成員用于在運(yùn)行時(shí)識(shí)別具體的派生類(lèi)對(duì)象,在派生類(lèi)對(duì)象構(gòu)造時(shí)必須指定具體的類(lèi)型。繼承體系的使用者調(diào)用函數(shù)時(shí)不再需要一次間接地根據(jù)虛函數(shù)表查找虛函數(shù)指針的操作,但在調(diào)用前仍然需要使用switch語(yǔ)句對(duì)其類(lèi)型進(jìn)行識(shí)別。
因此虛函數(shù)的缺點(diǎn)可以認(rèn)為只有兩條,即虛函數(shù)表的空間開(kāi)銷(xiāo)以及無(wú)法利用內(nèi)聯(lián)函數(shù)的速度優(yōu)勢(shì)。由于每個(gè)含有虛函數(shù)的類(lèi)在整個(gè)程序只有一個(gè)虛函數(shù)表,因此虛函數(shù)表引起的空間開(kāi)銷(xiāo)時(shí)非常小的。所以,可以認(rèn)為虛函數(shù)引入的性能缺陷只是無(wú)法利用內(nèi)聯(lián)函數(shù)。
通常,非虛函數(shù)的常規(guī)設(shè)計(jì)假如需要增加一種新的派生類(lèi)型,或者刪除一種不再支持的派生類(lèi)型,都必須修改繼承體系所有使用者的所有與類(lèi)型相關(guān)的函數(shù)調(diào)用代碼。對(duì)于一個(gè)復(fù)雜的程序,某個(gè)繼承體系的使用者會(huì)很多,每次對(duì)繼承體系的派生類(lèi)的修改都會(huì)波及使用者。因此,不使用虛函數(shù)的常規(guī)設(shè)計(jì)增加了代碼的耦合度,模塊化不強(qiáng),導(dǎo)致項(xiàng)目的可擴(kuò)展性、可維護(hù)性、代碼可讀性都會(huì)降低。面向?qū)ο缶幊痰囊粋€(gè)重要目的就是增加程序的可擴(kuò)展性和可維護(hù)性,即當(dāng)程序的業(yè)務(wù)邏輯發(fā)生改變時(shí),對(duì)原有程序的修改非常方便,降低因?yàn)闃I(yè)務(wù)邏輯改變而對(duì)代碼修改時(shí)出錯(cuò)的概率。
因此,在性能和其它特性的選擇方面,需要開(kāi)發(fā)人員根據(jù)實(shí)際情況進(jìn)行進(jìn)行權(quán)衡和取舍,如果性能檢驗(yàn)確認(rèn)性能瓶頸不是虛函數(shù)沒(méi)有利用內(nèi)聯(lián)的優(yōu)勢(shì)引起,可以不必考慮虛函數(shù)對(duì)性能的影響。

四、臨時(shí)對(duì)象

1、臨時(shí)對(duì)象簡(jiǎn)介

對(duì)象的創(chuàng)建與銷(xiāo)毀對(duì)程序的性能影響很大,尤其是對(duì)象的類(lèi)處于一個(gè)復(fù)雜繼承體系的末端,或者對(duì)象包含很多成員對(duì)象(包括其所有父類(lèi)對(duì)象,即直接或者間接父類(lèi)的所有成員變量對(duì)象)時(shí),對(duì)程序性能影響尤其顯著。因此,作為一個(gè)對(duì)性能敏感的程序員,應(yīng)該盡量避免創(chuàng)建不必要的對(duì)象,以及隨后的銷(xiāo)毀。除了減少顯式地創(chuàng)建對(duì)象,也要盡量避免編譯器隱式地創(chuàng)建對(duì)象,即臨時(shí)對(duì)象。

#include <iostream>
#include <cstring>

class Matrix
{
public:
    Matrix(double d = 1.0)
    {
        for(int i = 0; i < 10; i++)
        {
            for(int j = 0; j < 10; j++)
            {
                m[i][j] = d;
            }
        }
        cout << "Matrix(double d = 1.0)" << endl;
    }
    Matrix(const Matrix& another)
    {
        cout << "Matrix(const Matrix& another)" << endl;
        memcpy(this, &another, sizeof(another));
    }

    Matrix& operator=(const Matrix& another)
    {
        if(this != &another)
        {
            memcpy(this, &another, sizeof(another));
        }
        cout << "Matrix& operator=(const Matrix& another)" << endl;
        return *this;
    }
    friend const Matrix operator+(const Matrix& m1, const Matrix& m2);
private:
    double m[10][10];
};

const Matrix operator+(const Matrix& m1, const Matrix& m2)
{
    Matrix sum; // 1
    for(int i = 0; i < 10; i++)
    {
        for(int j = 0; j < 10; j++)
        {
            sum.m[i][j] = m1.m[i][j] + m2.m[i][j];
        }
    }
    return sum; // 2
}

int main()
{
    Matrix a(2.0), b(3.0), c; // 3
    c = a + b; // 4
    return 0;
}

由于GCC編譯器默認(rèn)進(jìn)行了返回值優(yōu)化(Return Value Optimization,簡(jiǎn)稱(chēng)RVO),因此需要指定-fno-elide-constructors選項(xiàng)進(jìn)行編譯:
g++ -fno-elide-constructors main.cpp
輸出結(jié)果如下:

Matrix(double d = 1.0)      //  1
Matrix(double d = 1.0)      //  2
Matrix(double d = 1.0)      //  3
Matrix(double d = 1.0)      //  4
Matrix(const Matrix& another)   //  5
Matrix& operator=(const Matrix& another)    //  6

分析代碼,語(yǔ)句3生成3個(gè)Matrix對(duì)象,調(diào)用3次構(gòu)造函數(shù),語(yǔ)句4調(diào)用operator+執(zhí)行到語(yǔ)句1時(shí)生成臨時(shí)變量sum,調(diào)用1次構(gòu)造函數(shù),語(yǔ)句4調(diào)用賦值操作,不會(huì)生成新的Matrix對(duì)象。輸出5則是因?yàn)閍+b調(diào)用operator+函數(shù)時(shí)需要返回一個(gè)Matrix變量sum,然后進(jìn)一步通過(guò)operator=函數(shù)將sum變量賦值給變量c,但a+b返回時(shí),sum變量已經(jīng)被銷(xiāo)毀,即在operator+函數(shù)調(diào)用結(jié)束時(shí)被銷(xiāo)毀,其返回的Matrix變量需要在調(diào)用a+b函數(shù)的棧中開(kāi)辟空間來(lái)存放,臨時(shí)的Matrix對(duì)象是在a+b返回時(shí)通過(guò)Matrix拷貝構(gòu)造函數(shù)構(gòu)造,即輸出5打印。
如果使用默認(rèn)GCC編譯選項(xiàng)編譯,GCC編譯器默認(rèn)會(huì)進(jìn)行返回值優(yōu)化。
g++ main.cpp
程序輸出如下:

Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix(double d = 1.0)
Matrix& operator=(const Matrix& another)

臨時(shí)對(duì)象與臨時(shí)變量并不相同。通常,臨時(shí)變量是指為了暫時(shí)存放某個(gè)值的變量,顯式出現(xiàn)在源碼中;臨時(shí)對(duì)象通常指編譯器隱式生成的對(duì)象。
臨時(shí)對(duì)象在C++語(yǔ)言中的特征是未出現(xiàn)在源代碼中,而是從棧中產(chǎn)生未命名對(duì)象,開(kāi)發(fā)人員并沒(méi)有聲明要使用臨時(shí)對(duì)象,由編譯器根據(jù)情況產(chǎn)生,通常開(kāi)發(fā)人員不會(huì)注意到其產(chǎn)生。
返回值優(yōu)化(Return Value Optimization,簡(jiǎn)稱(chēng)RVO)是一種優(yōu)化機(jī)制,當(dāng)函數(shù)需要返回一個(gè)對(duì)象的時(shí)候,如果自己創(chuàng)建一個(gè)臨時(shí)對(duì)象用戶(hù)返回,那么臨時(shí)對(duì)象會(huì)消耗一個(gè)構(gòu)造函數(shù)(Constructor)的調(diào)用、一個(gè)復(fù)制構(gòu)造函數(shù)的調(diào)用(Copy Constructor)以及一個(gè)析構(gòu)函數(shù)(Destructor)的調(diào)用的代價(jià),而如果稍微做一點(diǎn)優(yōu)化,就可以將成本降低到一個(gè)構(gòu)造函數(shù)的代價(jià)。

2、臨時(shí)對(duì)象生成

通常,產(chǎn)生臨時(shí)對(duì)象的場(chǎng)合如下:
(1)當(dāng)實(shí)際調(diào)用函數(shù)時(shí)傳入的參數(shù)與函數(shù)定義中聲明的變量類(lèi)型不匹配。
(2)當(dāng)函數(shù)返回一個(gè)對(duì)象時(shí)。
在函數(shù)傳遞參數(shù)為對(duì)象時(shí),實(shí)際調(diào)用時(shí)因?yàn)楹瘮?shù)體內(nèi)的對(duì)象與實(shí)際傳入的對(duì)象并不相同,而是傳入對(duì)象的拷貝,因此有開(kāi)發(fā)者認(rèn)為函數(shù)體內(nèi)的拷貝對(duì)象也是一個(gè)臨時(shí)對(duì)象,但嚴(yán)格來(lái)說(shuō),函數(shù)體內(nèi)的拷貝對(duì)象并不符合未出現(xiàn)在源碼中。
對(duì)于類(lèi)型不匹配生成臨時(shí)對(duì)象的情況,示例如下:

#include <iostream>

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r;
    r = 100;  // 2
}

int main()
{
    func();
    return 0;
}

執(zhí)行語(yǔ)句2時(shí),由于Rational沒(méi)有重載operator=(int i),編譯器會(huì)合成一個(gè)operator=(const Rational& another)函數(shù),并執(zhí)行逐位拷貝賦值操作,但由于100不是一個(gè)Rational對(duì)象,但編譯器會(huì)盡可能查找合適的轉(zhuǎn)換路徑,以滿(mǎn)足編譯的需要。編譯器發(fā)現(xiàn)存在一個(gè)Rational(int a = 0, int b = 1)構(gòu)造函數(shù),編譯器會(huì)將語(yǔ)句2右側(cè)的100通過(guò)Rational100, 1)生成一個(gè)臨時(shí)對(duì)象,然后用編譯器合成的operator=(const Rational& another)函數(shù)進(jìn)行逐位賦值,語(yǔ)句2執(zhí)行后,r對(duì)象內(nèi)部的real為100,img為1。
C++編譯器為了成功編譯某些語(yǔ)句會(huì)生成很多從源碼中不易察覺(jué)的輔助函數(shù),甚至對(duì)象。C++編譯器提供的自動(dòng)類(lèi)型轉(zhuǎn)換確實(shí)提高了程序的可讀性,簡(jiǎn)化了程序編寫(xiě),提高了開(kāi)發(fā)效率。但類(lèi)型轉(zhuǎn)換意味著臨時(shí)對(duì)象的產(chǎn)生,對(duì)象的創(chuàng)建和銷(xiāo)毀意味著性能的下降,類(lèi)型轉(zhuǎn)換還意味著編譯器會(huì)生成其它的代碼。因此,如果不需要編譯器提供自動(dòng)類(lèi)型轉(zhuǎn)換,可以使用explicit對(duì)類(lèi)的構(gòu)造函數(shù)進(jìn)行聲明。

#include <iostream>

using namespace std;
class Rational
{
public:
    explicit Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r; // 2
    r = 100;    // 3
}

int main()
{
    func();
    return 0;
}

此時(shí),進(jìn)行代碼編譯會(huì)報(bào)錯(cuò):
error: no match for ‘operator=’ (operand types are ‘Rational’ and ‘int’)
錯(cuò)誤信息提示沒(méi)有匹配的operator=函數(shù)將int和Rational對(duì)象進(jìn)行轉(zhuǎn)換。C++編譯器默認(rèn)合成的operator=函數(shù)只接受Rational對(duì)象,不能接受int類(lèi)型作為參數(shù)。要想代碼編譯能夠通過(guò),方法一是提供一個(gè)重載的operator=賦值函數(shù),可以接受整型作為參數(shù);方法二是能夠?qū)⒄娃D(zhuǎn)換為Rational對(duì)象,然后進(jìn)一步利用編譯器合成的賦值運(yùn)算符。將整型轉(zhuǎn)換為Rational對(duì)象,可以提供能只傳遞一個(gè)整型作為參數(shù)的Rational構(gòu)造函數(shù),考慮到缺省參數(shù),調(diào)用構(gòu)造函數(shù)可能會(huì)是無(wú)參、一個(gè)參數(shù)、兩個(gè)參數(shù),此時(shí)編譯器可以利用整型變量作為參數(shù)調(diào)用Rational構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象。由于explicit關(guān)鍵字限定了構(gòu)造函數(shù)只能被顯示調(diào)用,不允許編譯器運(yùn)用其進(jìn)行類(lèi)型轉(zhuǎn)換,此時(shí)編譯器不能使用構(gòu)造函數(shù)將整型100轉(zhuǎn)換為Rational對(duì)象,所以導(dǎo)致編譯報(bào)錯(cuò)。
通過(guò)重載以整型作為參數(shù)的operator=函數(shù)可以成功編譯,代碼如下:

#include <iostream>

using namespace std;
class Rational
{
public:
    explicit Rational(int a = 0, int b = 1): real(a), imag(b)    // 1
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational& operator=(int r)
    {
        real = r;
        imag = 1;
        return *this;
    }
private:
    int real;
    int imag;
};

void func()
{
    Rational r; // 2
    r = 100;    // 3
}

int main()
{
    func();
    return 0;
}

重載operator=函數(shù)后,編譯器可以成功將整型數(shù)轉(zhuǎn)換為Rational對(duì)象,同時(shí)成功避免了臨時(shí)對(duì)象產(chǎn)生。
當(dāng)一個(gè)函數(shù)返回的是非內(nèi)建類(lèi)型的對(duì)象時(shí),返回結(jié)果對(duì)象必須在某個(gè)地方存放,編譯器會(huì)從調(diào)用相應(yīng)函數(shù)的棧幀中開(kāi)辟空間,并用返回值作為參數(shù)調(diào)用返回值對(duì)象所屬類(lèi)型的拷貝構(gòu)造函數(shù)在所開(kāi)辟的空間生成對(duì)象,在調(diào)用函數(shù)結(jié)束并返回后可以繼續(xù)利用臨時(shí)對(duì)象。

#include <iostream>
#include <string>

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 0): real(a), imag(b)
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational(const Rational& another): real(another.real), imag(another.imag)
    {
        cout << " Rational(const Rational& another)" << endl;
    }
    Rational& operator = (const Rational& other)
    {
        if(this != &other)
        {
            real = other.real;
            imag = other.imag;
        }
        cout << " Rational& operator = (const Rational& other)" << endl;
        return *this;
    }
    friend const Rational operator+(const Rational& a, const Rational& b);
private:
    int real;
    int imag;
};

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    Rational c;
    c.real = a.real + b.real;
    c.imag = a.imag + b.imag;
    cout << " operator+ end" << endl;
    return c; // 2
}

int main()
{
Rational r, a(10, 10), b(5, 8); 
    r = a + b;// 1
    return 0;
}

執(zhí)行語(yǔ)句1時(shí),相當(dāng)于在main函數(shù)中調(diào)用operator+(const Rational& a, const Rational& b)函數(shù),在main函數(shù)的棧中會(huì)開(kāi)辟一塊Rational大小的空間,在operator+(const Rational& a, const Rational& b)函數(shù)內(nèi)部的語(yǔ)句2處,函數(shù)返回使用被銷(xiāo)毀的c對(duì)象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)在main函數(shù)棧中開(kāi)辟空間生成一個(gè)Rational對(duì)象。然后使用operator =執(zhí)行賦值操作。編譯如下:
g++ -fno-elide-constructors main.cpp
輸出如下:

 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 operator+ begin
 Rational(int a = 0, int b = 0)
 operator+ end
 Rational(const Rational& another)
 Rational& operator = (const Rational& other)

由于r對(duì)象在默認(rèn)構(gòu)造后并沒(méi)有使用,可以延遲生成,代碼如下:

#include <iostream>
#include <string>

using namespace std;
class Rational
{
public:
    Rational(int a = 0, int b = 0): real(a), imag(b)
    {
        cout << " Rational(int a = 0, int b = 0)" << endl;
    }
    Rational(const Rational& another): real(another.real), imag(another.imag)
    {
        cout << " Rational(const Rational& another)" << endl;
    }
    Rational& operator = (const Rational& other)
    {
        if(this != &other)
        {
            real = other.real;
            imag = other.imag;
        }
        cout << " Rational& operator = (const Rational& other)" << endl;
        return *this;
    }
    friend const Rational operator+(const Rational& a, const Rational& b);
private:
    int real;
    int imag;
};

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    Rational c;
    c.real = a.real + b.real;
    c.imag = a.imag + b.imag;
    cout << " operator+ end" << endl;
    return c; // 2
}

int main()
{
    Rational a(10, 10), b(5, 8);
    Rational r = a + b;  // 1
    return 0;
}

編譯過(guò)程如下:
g++ -fno-elide-constructors main.cpp
輸出如下:

 Rational(int a = 0, int b = 0)
 Rational(int a = 0, int b = 0)
 operator+ begin
 Rational(int a = 0, int b = 0)
 operator+ end
 Rational(const Rational& another)
 Rational(const Rational& another)

分析代碼,編譯器執(zhí)行語(yǔ)句1時(shí)語(yǔ)義發(fā)生了較大變化,編譯器對(duì)=的解釋不再是賦值操作符,而是對(duì)象r的初始化。在取得a+b的結(jié)果時(shí),在main函數(shù)棧中開(kāi)辟空間,使用c對(duì)象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象,然后使用臨時(shí)對(duì)象作為參數(shù)調(diào)用拷貝構(gòu)造函數(shù)生成r對(duì)象。
因此,對(duì)于非內(nèi)建對(duì)象,盡量將對(duì)象延遲到確切直到其有效狀態(tài)時(shí),可以有效減少臨時(shí)對(duì)象生成。如將Rational r;r = a + b;改寫(xiě)為Rational r = a + b;
進(jìn)一步,可以將operator+函數(shù)改寫(xiě)為如下:

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    return Rational(a.real + b.real, a.imag + b.imag); // 2
}

通常,operator+與operator+=需要以其實(shí)現(xiàn),Rational的operator+=實(shí)現(xiàn)如下:

Rational operator+=(const Rational& a)
    {
        real += a.real;
        imag = a.imag;
        return *this;
    }

operator+=沒(méi)有產(chǎn)生臨時(shí)對(duì)象,盡量用operator+=代替operator+操作??紤]到代碼復(fù)用性,operator+可以使用operator+=實(shí)現(xiàn),代碼如下:

const Rational operator+(const Rational& a, const Rational& b)
{
    cout << " operator+ begin" << endl;
    return Rational(a) += b; // 2
}

對(duì)于前自增操作符實(shí)現(xiàn)如下:

const Rational operator++()
{
        ++real;
        return *this;
}

對(duì)于后自增操作如下:

const Rational operator++(int)
{
        Rational temp(*this);
        ++(*this);
        return temp;
}

前自增只需要將自身返回,后自增需要返回一個(gè)對(duì)象,因此需要多生成兩個(gè)對(duì)象:函數(shù)體內(nèi)的局部變量和臨時(shí)對(duì)象,因此對(duì)于非內(nèi)建類(lèi)型,在保證程序語(yǔ)義下盡量使用前自增。

3、臨時(shí)對(duì)象的生命周期

C++規(guī)范中定義了臨時(shí)對(duì)象的生命周期從創(chuàng)建時(shí)開(kāi)始,到包含創(chuàng)建它的最長(zhǎng)語(yǔ)句執(zhí)行完畢。

string a, b;
const char* str;
if(strlen(str = (a + b).c_str()) > 5) // 1
{
    printf("%s\n", str);// 2
}

分析代碼,語(yǔ)句1處首先創(chuàng)建一個(gè)臨時(shí)對(duì)象存放a+b的值,然后將臨時(shí)對(duì)象的內(nèi)容通過(guò)c_str函數(shù)得到賦值給str,如果str長(zhǎng)度大于5則執(zhí)行語(yǔ)句2,但臨時(shí)對(duì)象生命周期在包含其創(chuàng)建的最長(zhǎng)語(yǔ)句已經(jīng)結(jié)束,當(dāng)進(jìn)入if語(yǔ)句塊時(shí),臨時(shí)對(duì)象已經(jīng)被銷(xiāo)毀,執(zhí)行其內(nèi)部字符串的str指向的是一段已經(jīng)回收的內(nèi)存,結(jié)果是無(wú)法預(yù)測(cè)的。但存在一個(gè)特例,當(dāng)用一個(gè)臨時(shí)對(duì)象來(lái)初始化一個(gè)常量引用時(shí),臨時(shí)對(duì)象的生命周期會(huì)持續(xù)到與綁定其上的常用引用銷(xiāo)毀時(shí)。示例代碼如下:

string a, b;
if(true)
{
    const string& c = a + b; // 1

}

語(yǔ)句1將a+b結(jié)果的臨時(shí)對(duì)象綁定到常量引用c,臨時(shí)對(duì)象生命周期會(huì)持續(xù)到c的作用域結(jié)束,不會(huì)在語(yǔ)句1結(jié)束時(shí)結(jié)束。

五、內(nèi)聯(lián)函數(shù)

1、C++內(nèi)聯(lián)函數(shù)簡(jiǎn)介

C++語(yǔ)言的設(shè)計(jì)中,內(nèi)聯(lián)函數(shù)的引入完全是為了性能的考慮,因此在編寫(xiě)對(duì)性能要求較高的C++程序時(shí),極有必要考量?jī)?nèi)聯(lián)函數(shù)的使用。
內(nèi)聯(lián)是將被調(diào)用函數(shù)的函數(shù)體代碼直接地整個(gè)插入到函數(shù)被調(diào)用處,而不是通過(guò)call語(yǔ)句進(jìn)行。C++編譯器在真正進(jìn)行內(nèi)聯(lián)時(shí),由于考慮到被內(nèi)聯(lián)函數(shù)的傳入?yún)?shù)、自己的局部變量以及返回值的因素,不只進(jìn)行簡(jiǎn)單的代碼拷貝,還有許多細(xì)致工作。

2、C++函數(shù)內(nèi)聯(lián)的聲明

開(kāi)發(fā)人員可以有兩種方法告訴C++編譯器需要內(nèi)聯(lián)哪些類(lèi)成員函數(shù),一種是在類(lèi)的定義體外,一種是在類(lèi)的定義體內(nèi)。
(1)在類(lèi)的定義體外時(shí),需要在類(lèi)成員函數(shù)的定義前加inline關(guān)鍵字,顯式地告訴C++編譯器本函數(shù)在調(diào)用時(shí)需要內(nèi)聯(lián)處理。

class Student
{
public:
    void setName(const QString& name);
    QString getName()const;
    void setAge(const int age);
    getAge()const;
private:
    QString m_name;
    int m_age;
};

inline void Student::setName(const QString& name)
{
    m_name = name;
}
inline QString Student::getName()const
{
    return m_name;
}
inline void Student::setAge(const int age)
{
    m_age = age;
}
inline Student::getAge()const
{
    return m_age;
}

(2)在類(lèi)的定義體內(nèi)且聲明成員函數(shù)時(shí),同時(shí)提供類(lèi)成員函數(shù)的實(shí)現(xiàn)體。此時(shí),inline關(guān)鍵字不是必須的。

class Student
{
public:
    void setName(const QString& name)
    {
        m_name = name;
    }
    inline QString getName()const
    {
        return m_name;
    }
    inline void setAge(const int age)
    {
        m_age = age;
    }
    inline getAge()const
    {
        return m_age;
    }
private:
    QString m_name;
    int m_age;
};

(3)普通函數(shù)(非類(lèi)成員函數(shù))需要被內(nèi)聯(lián)時(shí),需要在普通函數(shù)的定義前加inline關(guān)鍵字,顯式地告訴C++編譯器本函數(shù)在調(diào)用時(shí)需要內(nèi)聯(lián)處理。

inline int add(int a, int b)
{
    return a + b;
}

3、C++內(nèi)聯(lián)機(jī)制

C++是以編譯單元為單位編譯的,通常一個(gè)編譯單元基本等同于一個(gè)CPP文件。在編譯的預(yù)處理階段,預(yù)處理器會(huì)將#include的各個(gè)頭文件(支持遞歸頭文件展開(kāi))完整地復(fù)制到CPP文件的對(duì)應(yīng)位置處,并進(jìn)行宏展開(kāi)等操作。預(yù)處理器處理后,編譯才真正開(kāi)始。一旦C++編譯器開(kāi)始編譯,C++編譯器將不會(huì)意識(shí)到其它CPP文件的存在,因此并不會(huì)參考其它CPP文件的內(nèi)容信息。因此,在編譯某個(gè)編譯單元時(shí),如果本編譯單元會(huì)調(diào)用到某個(gè)內(nèi)聯(lián)函數(shù),那么內(nèi)聯(lián)函數(shù)的函數(shù)定義(函數(shù)體)必須包含在編譯單元內(nèi)。因?yàn)镃++編譯器在使用內(nèi)聯(lián)函數(shù)體代碼替換內(nèi)聯(lián)函數(shù)調(diào)用時(shí),必須知道內(nèi)聯(lián)函數(shù)的函數(shù)體代碼,并且不能通過(guò)參考其它編譯單元信息獲得。
如果多個(gè)編譯單元會(huì)用到同一個(gè)內(nèi)聯(lián)函數(shù),C++規(guī)范要求在多個(gè)編譯單元中同一個(gè)內(nèi)聯(lián)函數(shù)的定義必須是完全一致的,即ODR(One Definition Rule)原則??紤]到代碼的可維護(hù)性,通常將內(nèi)聯(lián)函數(shù)的定義放在一個(gè)頭文件中,用到內(nèi)聯(lián)函數(shù)的所有編譯單元只需要#include相應(yīng)的頭文件即可。

#include <iostream>
#include <string>

using namespace std;
class Student
{
public:
    void setName(const string& name)
    {
        m_name = name;
    }
    inline string getName()const
    {
        return m_name;
    }
    inline void setAge(const int age)
    {
        m_age = age;
    }
    inline int getAge()const
    {
        return m_age;
    }
private:
    string m_name;
    int m_age;
};

void Print()
{
    Student s;
    s.setAge(20);
    cout << s.getAge() << endl;
}

int main()
{
    Print();
    return 0;
}

上述代碼中,在不開(kāi)啟內(nèi)聯(lián)時(shí)調(diào)用函數(shù)Print的函數(shù)時(shí)相關(guān)的操作如下:
(1)進(jìn)入Print函數(shù)時(shí),從其棧幀中開(kāi)辟了放置s對(duì)象的空間。
(2)進(jìn)入函數(shù)體后,首先在開(kāi)辟的s對(duì)象存儲(chǔ)空間執(zhí)行Student的默認(rèn)構(gòu)造函數(shù)構(gòu)造s對(duì)象。
(3)將常數(shù)20壓棧,調(diào)用s的setAge函數(shù)(開(kāi)辟setAge函數(shù)的棧幀,返回時(shí)回退銷(xiāo)毀此棧幀).
(4)執(zhí)行s的getAge函數(shù),并將返回值壓棧.
(5)調(diào)用cout操作符操作壓棧的結(jié)果,即輸出。
開(kāi)啟內(nèi)聯(lián)后,Print函數(shù)的等效代碼如下:

void Print()
{
    Student s;
    {
        s.m_age = 20;
    }
    int tmp = s.m_age;
    cout << tmp << endl;
}

函數(shù)調(diào)用時(shí)的參數(shù)壓棧,棧幀開(kāi)辟與銷(xiāo)毀等操作不再需要,結(jié)合內(nèi)聯(lián)后代碼,編譯器會(huì)進(jìn)一步優(yōu)化為如下結(jié)果:

int main()
{
    cout << 20 << endl;
    return 0;
}

如果不考慮setAge/getAge函數(shù)內(nèi)聯(lián),對(duì)于非內(nèi)聯(lián)函數(shù)一般不會(huì)在頭文件中定義,因此setAge/getAge函數(shù)可能在本編譯單元之外的其它編譯單元定義,Print函數(shù)所在的編譯單元會(huì)看不到setAge/getAge,不知道函數(shù)體的具體代碼信息,不能作出進(jìn)一步的代碼優(yōu)化。
因此,函數(shù)內(nèi)聯(lián)的優(yōu)點(diǎn)如下:
(1)減少因?yàn)楹瘮?shù)調(diào)用引起的開(kāi)銷(xiāo),主要是參數(shù)壓棧、棧幀開(kāi)辟與回收、寄存器保存與恢復(fù)。
(2)內(nèi)聯(lián)后編譯器在處理調(diào)用內(nèi)聯(lián)函數(shù)的函數(shù)時(shí),因?yàn)榭晒┓治龅拇a更多,因此編譯器能做的優(yōu)化更深入徹底。
程序的唯一入口main函數(shù)肯定不會(huì)被內(nèi)聯(lián)化,編譯器合成的默認(rèn)構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)、析構(gòu)函數(shù)以及賦值運(yùn)算符一般都會(huì)被內(nèi)聯(lián)化。編譯器并不保證使用inline修飾的函數(shù)在編譯時(shí)真正被內(nèi)聯(lián)處理,inline只是給編譯器的建議,編譯其完全會(huì)根據(jù)實(shí)際情況對(duì)其忽視。

4、函數(shù)調(diào)用機(jī)制

int add(int a, int b)
{
    return a + b;
}

void func()
{
    ...
    int c = add(a, b);
    ...
}

函數(shù)調(diào)用時(shí)相關(guān)操作如下:
(1)參數(shù)壓棧
參數(shù)是a,b;壓棧時(shí)通常按照逆序壓棧,因此是b,a;如果參數(shù)中有對(duì)象,需要先進(jìn)行拷貝構(gòu)造。
(2)保存返回地址
即函數(shù)調(diào)用結(jié)束后接著執(zhí)行的語(yǔ)句的地址。
(3)保存維護(hù)add函數(shù)棧幀信息的寄存器內(nèi)容,如SP(對(duì)棧指針),F(xiàn)P(棧棧指針)等。具體保存的寄存器與硬件平臺(tái)有關(guān)。
(4)保存某些通用寄存器的內(nèi)容。由于某些通用寄存器會(huì)被所有函數(shù)用到,所以在func函數(shù)調(diào)用add之前,這些通用寄存器可能已經(jīng)存儲(chǔ)了對(duì)func有用的信息。但這些通用寄存器在進(jìn)入add函數(shù)體內(nèi)執(zhí)行時(shí)可能會(huì)被add函數(shù)用到,從而被覆寫(xiě)。因此,func函數(shù)會(huì)在調(diào)用add函數(shù)前保存一份這些通用寄存器的內(nèi)容,在add函數(shù)返回后恢復(fù)。
(5)調(diào)用add函數(shù)。首先通過(guò)移動(dòng)棧指針來(lái)分配所有在其內(nèi)部聲明的局部變量所需的空間,然后執(zhí)行其函數(shù)體內(nèi)的代碼。
(6)add函數(shù)執(zhí)行完畢,函數(shù)返回時(shí),func函數(shù)需要進(jìn)行善后處理,如恢復(fù)通用寄存器的值,恢復(fù)保存func函數(shù)棧幀信息的寄存器的值,通過(guò)移動(dòng)棧指針?shù)N毀add函數(shù)的棧幀,將保存的返回地址出棧并賦值給IP寄存器,通過(guò)移動(dòng)棧指針回收傳給add函數(shù)的參數(shù)所占的空間。
如果函數(shù)的傳入?yún)?shù)和返回值都為對(duì)象時(shí),會(huì)涉及對(duì)象的構(gòu)造與析構(gòu),函數(shù)調(diào)用的開(kāi)銷(xiāo)會(huì)更大。

5、內(nèi)聯(lián)的效率分析

因?yàn)楹瘮?shù)調(diào)用的準(zhǔn)備與善后工作最終都由機(jī)器指令完成,假設(shè)一個(gè)函數(shù)之前的準(zhǔn)備工作與之后的善后工作的指令所需的空間為SS,執(zhí)行指令所需的時(shí)間為T(mén)S,從時(shí)間和空間分析內(nèi)聯(lián)的效率如下:
(1)空間效率。通常認(rèn)為,如果不采用內(nèi)聯(lián),被調(diào)用函數(shù)代碼只有一份,在調(diào)用位置使用call語(yǔ)句即可。而采用內(nèi)聯(lián)后,被調(diào)用函數(shù)的代碼在所調(diào)用的位置都會(huì)有一份拷貝,因此會(huì)導(dǎo)致代碼膨脹。
如果函數(shù)func的函數(shù)體代碼為FuncS,假設(shè)func函數(shù)在整個(gè)程序內(nèi)被調(diào)用n次,不采用內(nèi)聯(lián)時(shí),對(duì)func函數(shù)的調(diào)用只有準(zhǔn)備工作與善后工作會(huì)增加最后的代碼量開(kāi)銷(xiāo),func函數(shù)相關(guān)的代碼大小為n*SS + FuncS。采用內(nèi)聯(lián)后,在各個(gè)函數(shù)調(diào)用位置都需要將函數(shù)體代碼展開(kāi),即func函數(shù)的相關(guān)代碼大小為n*FuncS。所以需要比較
n*SS + FuncSn*FuncS的大小,如果調(diào)用次數(shù)n較大,可以簡(jiǎn)化為比較SS與FuncS的大小。如果內(nèi)聯(lián)函數(shù)自己的函數(shù)體代碼量比因?yàn)楹瘮?shù)調(diào)用的準(zhǔn)備與善后工作引入的代碼量大,則內(nèi)聯(lián)后程序的代碼量會(huì)變大;如果內(nèi)聯(lián)函數(shù)自己的函數(shù)體代碼量比因?yàn)楹瘮?shù)調(diào)用的準(zhǔn)備與善后工作引入的代碼量小,則內(nèi)聯(lián)后程序的代碼量會(huì)變小;如果內(nèi)聯(lián)后編譯器因?yàn)楂@得更多的代碼信息,從而對(duì)調(diào)用函數(shù)的優(yōu)化更深入徹底,則最終的代碼量會(huì)更小。
(2)時(shí)間效率。通常,內(nèi)聯(lián)后函數(shù)調(diào)用都不再需要做函數(shù)調(diào)用的準(zhǔn)備與善后工作,并且由于編譯器可以獲得更多的代碼信息,可以進(jìn)行深入徹底的代碼優(yōu)化。內(nèi)聯(lián)后,調(diào)用函體內(nèi)需要執(zhí)行的代碼是相鄰的,其執(zhí)行的代碼都在同一個(gè)頁(yè)面或連續(xù)的頁(yè)面中。如果沒(méi)有內(nèi)聯(lián),執(zhí)行到被調(diào)用函數(shù)時(shí),需要調(diào)轉(zhuǎn)到包含被調(diào)用函數(shù)的內(nèi)存頁(yè)面中執(zhí)行,而被調(diào)用函數(shù)的所屬的頁(yè)面極有可能當(dāng)時(shí)不在物理內(nèi)存中。因此,內(nèi)聯(lián)后可以降低缺頁(yè)的概率,減少缺頁(yè)次數(shù)的效果遠(yuǎn)比減少一些代碼量執(zhí)行的效果要好。即使被調(diào)用函數(shù)所在頁(yè)面也在內(nèi)存中,但與調(diào)用函數(shù)在空間上相隔甚遠(yuǎn),可能會(huì)引起cache miss,從而降低執(zhí)行速度。因此,內(nèi)聯(lián)后程序的執(zhí)行時(shí)間會(huì)比沒(méi)有內(nèi)聯(lián)要少,即程序執(zhí)行速度會(huì)更快。但如果FunS遠(yuǎn)大于SS,且n較大,最終程序的大小會(huì)比沒(méi)有內(nèi)聯(lián)大的多,用來(lái)存放代碼的內(nèi)存頁(yè)也會(huì)更多,導(dǎo)致執(zhí)行代碼引起的缺頁(yè)也會(huì)增多,此時(shí),最終程序的執(zhí)行時(shí)間可能會(huì)因?yàn)榇罅康娜表?yè)變得更多,即程序變慢。因此,很多編譯器會(huì)對(duì)函數(shù)體代碼很多的函數(shù)拒絕其內(nèi)聯(lián)請(qǐng)求,即忽略inine關(guān)鍵字,按照非內(nèi)聯(lián)函數(shù)進(jìn)行編譯。
因此,是否采用內(nèi)聯(lián)時(shí)需要根據(jù)內(nèi)聯(lián)函數(shù)的特征(如函數(shù)體代碼量、程序被調(diào)用次數(shù)等)進(jìn)行判斷。判斷內(nèi)聯(lián)效果的最終和最有效方法還是對(duì)程序執(zhí)行速度和程序大小進(jìn)行測(cè)量,然后根據(jù)測(cè)量結(jié)果決定是否采用內(nèi)聯(lián)和對(duì)哪些函數(shù)進(jìn)行內(nèi)聯(lián)。

6、內(nèi)聯(lián)函數(shù)的二進(jìn)制兼容問(wèn)題

調(diào)用內(nèi)聯(lián)函數(shù)的編譯單元必須具有內(nèi)聯(lián)函數(shù)的函數(shù)體代碼信息,考慮到ODR規(guī)則和代碼可維護(hù)性,通常將內(nèi)聯(lián)函數(shù)的定義放在頭文件中,每個(gè)調(diào)用內(nèi)聯(lián)函數(shù)的編譯單元通過(guò)#include相應(yīng)頭文件。
在大型軟件中,某個(gè)內(nèi)聯(lián)函數(shù)因?yàn)楸容^通用,可能會(huì)被大多數(shù)編譯單元用到,如果對(duì)內(nèi)聯(lián)函數(shù)進(jìn)行修改會(huì)引起所有用到該內(nèi)聯(lián)函數(shù)的編譯單元進(jìn)行重新編譯。對(duì)于大型程序,重新編譯大部分編譯單元會(huì)消耗大量的編譯時(shí)間,因此,內(nèi)聯(lián)函數(shù)最好在開(kāi)發(fā)的后期引入,以避免可能不必要的大量編譯時(shí)間浪費(fèi)。
如果某開(kāi)發(fā)組使用了第三方提供的程序庫(kù),而第三方程序庫(kù)中可能包含內(nèi)聯(lián)函數(shù),因此在開(kāi)發(fā)組代碼中使用了第三方庫(kù)的內(nèi)聯(lián)函數(shù)位置都會(huì)將內(nèi)聯(lián)函數(shù)體代碼拷貝到函數(shù)調(diào)用位置。如果第三方庫(kù)提供商在下一個(gè)版本中修改了某些內(nèi)聯(lián)函數(shù)的定義,即使沒(méi)有修改任何函數(shù)的對(duì)外接口,開(kāi)發(fā)組想要使用新版本的第三方庫(kù)仍然需要重新編譯。如果程序已經(jīng)發(fā)布,則重新編譯的成本會(huì)極高。如果沒(méi)有內(nèi)聯(lián),第三方庫(kù)提供商只是修改了函數(shù)實(shí)現(xiàn),開(kāi)發(fā)組不必重新編譯即可使用最新的第三方庫(kù)版本。

7、遞歸函數(shù)的內(nèi)聯(lián)

內(nèi)聯(lián)的本質(zhì)是使用函數(shù)體代碼對(duì)函數(shù)調(diào)用進(jìn)行替換,對(duì)于遞歸函數(shù):

int sum(int n)
{
    if(n < 2)
    {
        return 1;
    }
    else
    {
        return sum(n - 1) + n;
    }
}

如果某個(gè)編譯單元內(nèi)調(diào)用了sum函數(shù),如下:

void func()
{
    ...
    int ret = sum(n);
    ...
}

如果在編譯本編譯單元且調(diào)用sum函數(shù)時(shí),提供的參數(shù)n不能夠知道實(shí)際值,則編譯器無(wú)法知道對(duì)sum函數(shù)進(jìn)行了多少次替換,編譯器會(huì)拒絕對(duì)遞歸函數(shù)sum進(jìn)行內(nèi)聯(lián);如果在編譯本編譯單元且調(diào)用sum函數(shù)時(shí),提供的參數(shù)n可以知道實(shí)際值,則編譯器可能會(huì)根據(jù)n的大小來(lái)判斷時(shí)都對(duì)sum函數(shù)進(jìn)行內(nèi)聯(lián),如果n很大,內(nèi)聯(lián)展開(kāi)可能會(huì)使最終程序的大小變得很大。

8、虛函數(shù)的內(nèi)聯(lián)

內(nèi)聯(lián)函數(shù)是編譯階段的行為,虛函數(shù)是執(zhí)行階段行為,因此編譯器一般會(huì)拒絕對(duì)虛函數(shù)進(jìn)行內(nèi)聯(lián)的請(qǐng)求。虛函數(shù)不能被內(nèi)聯(lián)是由于編譯器在編譯時(shí)無(wú)法知道調(diào)用的虛函數(shù)到底是哪一個(gè)版本,即無(wú)法確定虛函數(shù)的函數(shù)體,但在兩種情況下,編譯器能夠知道虛函數(shù)調(diào)用的真實(shí)版本,因此可以?xún)?nèi)聯(lián)。
一是通過(guò)對(duì)象而不是指向?qū)ο蟮闹羔樆蛞脤?duì)虛函數(shù)進(jìn)行調(diào)用,此時(shí)編譯器在編譯器已經(jīng)知道對(duì)象的確切類(lèi)型,因此會(huì)直接調(diào)用確切類(lèi)型的虛函數(shù)的實(shí)現(xiàn)版本,而不會(huì)產(chǎn)生動(dòng)態(tài)綁定行為的代碼。
二是雖然通過(guò)對(duì)象指針或?qū)ο笠谜{(diào)用虛函數(shù),但編譯器在編譯時(shí)能夠知道指針或引用指向?qū)ο蟮拇_切類(lèi)型,如在產(chǎn)生新對(duì)象時(shí)做的指針賦值或引用初始化與通過(guò)指針或引用調(diào)用虛函數(shù)處于同一編譯單元,并且指針沒(méi)有被改變賦值使其指向到其它不能知道確切類(lèi)型的對(duì)象,此時(shí)編譯器也不會(huì)產(chǎn)生動(dòng)態(tài)綁定的代碼,而是直接調(diào)用確切類(lèi)型的虛函數(shù)實(shí)現(xiàn)版本。

inline virtual int x::y(char* a)
{
    ...
}

void func(char* b)
{
    x_base* px = new x();
    x ox;
    px->y(b);
    ox.y(b);
}

9、C++內(nèi)聯(lián)與C語(yǔ)言宏的區(qū)別

C語(yǔ)言宏與C++內(nèi)聯(lián)的區(qū)別如下:
(1)C++內(nèi)聯(lián)是編譯階段行為,宏是預(yù)處理行為,宏的替代展開(kāi)由預(yù)處理器負(fù)責(zé),宏對(duì)于編譯器是不可見(jiàn)的。
(2)預(yù)處理器不能對(duì)宏的參數(shù)進(jìn)行類(lèi)型檢查,編譯器會(huì)對(duì)內(nèi)聯(lián)函數(shù)的參數(shù)進(jìn)行類(lèi)型檢查。
(3)宏的參數(shù)在宏體內(nèi)出現(xiàn)兩次以上時(shí)通常會(huì)產(chǎn)生副作用,尤其是當(dāng)在宏體內(nèi)對(duì)參數(shù)進(jìn)行自增、自減操作時(shí),內(nèi)聯(lián)不會(huì)。
(4)宏肯定會(huì)被展開(kāi),inline修飾的函數(shù)不一定會(huì)被內(nèi)聯(lián)展開(kāi)。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI