《深入探索C++對象模型》筆記 Chapter2 構造函數
《深入探索C++對象模型》筆記 Chapter3 成員變量
《深入探索C++對象模型》筆記 Chapter4 成員函數
第4章 成員函數
4.1 總述
非靜態(tài)成員函數
C++的設計準則之一就是,非靜態(tài)成員函數至少必須要和普通函數有相同的效率。
編譯器會將成員函數轉換為了對等的普通函數。轉換步驟如下:
- 改寫函數原型(signature),傳入參數添加一個this指針
- 將對成員變量(非靜態(tài))的操作改為通過this指針來存取
- 對成員函數的函數名稱做命名重整(mangling)
上述所說的 mangling 過程,通常函數名會被加上class名稱的前綴,以及參數類型的后綴,從而確保每個函數名都是獨一無二的。
虛函數
//虛函數調用
ptr->func();
//轉為為
(*ptr->vptr[1])(ptr);
以上的轉換,vptr表示指向虛函數表的指針;1是虛函數表中該函數的索引值,和func()函數關聯;函數參數中的ptr表示this指針。
靜態(tài)成員函數
首先來看看為什么需要靜態(tài)成員函數。
在引入靜態(tài)成員函數之前,C++規(guī)定所有成員函數都必須由對象來調用,然而有些成員函數不對成員變量操作,也就沒必要傳入this指針,而C++并不能辨識這種情況。
于是,如果將靜態(tài)成員變量聲明為private,就必須提供成員函數來調用它,也就必須要實例化一個對象才能調用,以至出現了以下奇葩的寫法:
((ClassName*) 0 ) -> get_staticmember_func();
將0強轉成class指針,然后調用成員函數返回一個靜態(tài)成員變量。
而有了靜態(tài)成員函數,才解決了上述所說的問題。靜態(tài)成員函數的主要特性是沒有this指針,以此延伸出了其他特性:
- 不能存取非靜態(tài)成員
- 不能被聲明為 const volatile virtual
- 不需要經由對象才能被調用
由于靜態(tài)成員函數沒有this指針,所以它在內存上的存儲類似于普通函數。
4.2 虛函數
在C++中,多態(tài)表示以一個 public base class 的指針(或 reference),尋址出一個 derived class object 的意思。
單繼承

如上圖, Point3d 繼承 Point2d , Point2d 繼承 Point 。
virtual table 在編譯期就已經確定,virtual table 的每一個函數地址稱之為一個 slot ,派生類定義虛函數會 overriding 基類相應函數的 slot 。這樣 ptr->z() ,不管 ptr 指向的對象是基類還是派生類,它調用 z() 時函數地址一定是放在第四個 slot ,于是編譯器轉換代碼為 (*ptr->vptr[4])(ptr) 。
對于純虛函數,slot 里放置的是 pure_virtual_called() 函數,這個函數如果被意外調用,通常會結束掉這個程序。可以利用這個特性做運行期異常處理。
多繼承

虛繼承

4.3 效率
經過測試,普通函數、非靜態(tài)成員函數、靜態(tài)成員函數的效率很相近。inline成員函數效率驚人。
4.4 指向成員函數的指針
A::* 意為指向類A成員的指針,我們可以用 void (A::*pmf)() = &A::func 表示指向A中 func() 成員函數的指針。對于非虛函數,這個指針指向一個函數地址,對于虛函數,由于地址在編譯時期是未知的,所以存放的是該函數在虛函數表中的索引值。(個人以為,此時再說 pmf 是個指針就比較牽強了,它只是個記錄函數相對位置的int值)
于是,(ptr->*pmf)() 就會被編譯器翻譯為 (*ptr->vptr[(int)pmf])(ptr) ??梢詫憘€demo做個驗證:
#include <iostream>
#include <stdio.h>
using namespace std;
class A {
public:
void common(){}
virtual void foo() { printf("A::foo(): this = 0x%p\n", this); }
};
class B :public A{
public:
virtual void foo() { printf("B::foo(): this = 0x%p\n", this); }
virtual void bar() { printf("B::bar(): this = 0x%p\n", this); }
virtual void foo(int i) { printf("B::bar(): this = 0x%p\n", this); }
};
void (A::*pafoo)() = &A::foo;
void (B::*pbfoo)() = &B::foo;
void (B::*pbbar)() = &B::bar;
void (A::*pcommon)() = &A::common;
int main(){
A* a = new A;
B* b = new B;
printf("A::commont: %x\n",pcommon);
printf("A::foo: %x\n",pafoo);
printf("B::foo: %x\n",pbfoo);
printf("B::bar: %x\n",pbbar);
}
輸出如下:
A::commont: 31425c9e
A::foo: 1
B::foo: 1
B::bar: 9
至于為什么索引值從1開始,書上沒有說,不過想來和 3.6指向成員函數的指針 所說的類似,為了區(qū)分指向成員函數的空指針。
而索引值按8遞增,那是因為我的云主機是64位,一個指針8字節(jié)。
單繼承的情況,一個索引值就足夠調用不同虛函數了,但是考慮到多繼承呢?一個派生類繼承自A和B,調用它的虛函數時,this指針作為傳入參數可能是A,也可能是B,所以多繼承還需要考慮this指針的偏移。我本來想仿照此篇博客 C/C++雜記:深入理解數據成員指針、函數成員指針 打印出this指針的偏移,但是事與愿違,偏移值始終為0,也只能歸因于不同編譯器的實現方式不同了。
4.5 內聯函數
inline關鍵字并不能強迫將任何函數都變成 inline ,而是給編譯器一個建議。至于編譯器是否采納這個建議,需要一系列復雜的測試,包括計算賦值、調用函數、調用虛函數的次數,每種操作都會有一個權值,這些操作與權值的乘積之和就是函數的復雜度。
對于inline函數,如果傳入的參數是變量或者常量,那么直接對函數體內代碼執(zhí)行參數替換就可以了,但是如果傳入的參數是個表達式呢?難道函數體內的代碼每次碰到這個表達式都要算一遍?顯示編譯器不會這么傻。針對這種情況,編譯器會產生一個臨時對象,以避免重復求值。
再考慮一種情況,如果inline函數中有局部變量會怎么樣?如果直接把代碼擴展到調用inline函數的函數,萬一后者有同名的變量呢?所以需要把局部變量放在封閉區(qū)段中,讓它們有自己的作用域。這個過程依舊會產生局部變量。
可以看到,局部變量和表達式參數都會使inline函數產生臨時對象,所以使用inline不當會產生大量臨時對象反而降低效率。