程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 《深度探索C++對象模型》讀書筆記(4)

《深度探索C++對象模型》讀書筆記(4)

編輯:關於C++

***非靜態成員函數(Nonstatic Member Functions)***

C++的設計准則之一就是: nonstatic member function至少必須和一般的nonmember function有相同的效率。也就是說,如果我們 要在以下兩個函數之間作選擇:

float magnitude3d(const Point3d *this) { ... }
float Point3d::magnitude3d() const { ... }

那麼選擇member function不應該帶來 什麼額外負擔。因為編譯器內部已將“member函數實體”轉化為對等的“nonmember函 數實體”。下面是magnitude()的一個nonmember定義:

float Pointer3d::magnitude() const
{
return sqrt(_x*_x + _y*_y + _z*_z);
}
// 內部轉化為
float magnitude_7Point3dFv(const Point3d *this)  //已對函數名稱進行 “mangling”處理
{
return sqrt(this->_x*this->_x + this- >_y*this->_y + this->_z*this->_z);
}

現在,對該函數的每一個調 用操作也都必須轉換:

obj.magnitude();
// 轉換為
magnitude_7Point3dFv (&obj);

對於class中的memeber,只需在member的名稱中加上class名稱,即可形成 獨一無二的命名。但由於member function可以被重載化,所以需要更廣泛的mangling手法,以提供絕對 獨一無二的名稱。其中一種做法就是將它們的參數鏈表中各參數的類型也編碼進去。

class Point {
public:
void x(float newX);
float x();
...
};
// 內部轉化為
class Point {
void x_5PointFf(float newX);  // F表示function,f表示其第一個參數類型是float
float x_5PointFv();  // v表示其沒有參數
};

上述的mangling手法可在鏈接時期檢查出任何不正確的調用操作,但由於編碼 時未考慮返回類型,故如果返回類型聲明錯誤,就無法檢查出來。

***虛擬成員函數(Virtual Member Functions)***

對於那些不支持多態的對象,經由一個class object調用一個virtual function,這種操作應該總是被編譯器像對待一般的nonstatic member function一樣地加以決議:

// Point3d obj
obj.normalize();
// 不會轉化為
(*obj.vptr[1]) (&obj);
// 而會被轉化未
normalize_7Point3dFv(&obj);

***靜態 成員函數(Static Member Functions)***

在引入static member functions之前,C++要求所有 的member functions都必須經由該class的object來調用。而實際上,如果沒有任何一個nonstatic data members被直接存取,事實上就沒有必要通過一個class object來調用一個member function.這樣一來便 產生了一個矛盾:一方面,將static data member聲明為nonpublic是一種好的習慣,但這也要求其必須 提供一個或多個member functions來存取該member;另一方面,雖然你可以不靠class object來存取一 個static member,但其存取函數卻得綁定於class object之上。

static member functions正是 在這種情形下應運而生的。

編譯器的開發者針對static member functions,分別從編譯層面和 語言層面對其進行了支持:

(1)編譯層面:當class設計者希望支持“沒有class object 存在”的情況時,可把0強制轉型為一個class指針,因而提供出一個this指針實體:

// 函數調用的內部轉換
object_count((Point3d*)0);

(2)語言層 面:static member function的最大特點是沒有this指針,如果取一個static member function的地址 ,獲得的將是其在內存中的位置,其地址類型並不是一個“指向class member function的指針 ”,而是一個“nonmember函數指針”:

unsigned int Point3d::object_count() { return _object_count; }
&Point3d::object_count();
// 會得到一個地址,其類型不是
unsigned int (Point3d::*)();
// 而是
unsigned int (*)();

static member function經常被用作回調(callback)函數。

***虛擬成員函數(Virtual Member Functions)***

對於像ptr->z()的調用操作將 需要ptr在執行期的某些相關信息,為了使得其能在執行期順利高效地找到並調用z()的適當實體,我 們考慮往對象中添加一些額外信息。

(1)一個字符串或數字,表示class的類型;

(2) 一個指針,指向某表格,表格中帶有程序的virtual functions的執行期地址;

在C++中, virtual functions可在編譯時期獲知,由於程序執行時,表格的大小和內容都不會改變,所以該表格的 建構和存取皆可由編譯器完全掌握,不需要執行期的任何介入。

(3)為了找到表格,每一個 class object被安插上一個由編譯器內部產生的指針,指向該表格;

(4)為了找到函數地址, 每一個virtual function被指派一個表格索引值。

一個class只會有一個virtual table,其中內 含其對應的class object中所有active virtual functions函數實體的地址,具體包括:

(a) 這個class所定義的函數實體

它會改寫一個可能存在的base class virtual function函數實體。 若base class中不存在相應的函數,則會在derived class的virtual table增加相應的slot.

(b )繼承自base class的函數實體

這是在derived class決定不改寫virtual function時才會出現 的情況。具體來說,base class中的函數實體的地址會被拷貝到derived class的virtual table相對應 的slot之中。

(c)pure_virtual_called函數實體

對於這樣的式子:

ptr ->z();

運用了上述手法後,雖然我不知道哪一個z()函數實體會被調用,但卻知道 每一個z()函數都被放在slot 4(這裡假設base class中z()是第四個聲明的virtual function)。

// 內部轉化為
(*ptr->vptr[4])(ptr);

***多重繼承下的 Virtual Functions***

在多重繼承中支持virtual functions,其復雜度圍繞在第二個及後繼的 base classes身上,以及“必須在執行期調整this指針”這一點。

多重繼承到來的問題:

(1)經由指向“第二或後繼之base class”的指針(或reference)來調用 derived class virtual function,該調用操作連帶的“必要的this指針調整”操作,必須 在執行期完成;

以下面的繼承體系為例:

class Base1 {
public:
Base1();
virtual ~Base1();
virtual void speakClearly();
virtual Base1 *clone() const;
protected:
float data_Base1;
};

class Base2 {
public:
Base2();
virtual ~Base2();
virtual void mumble();
virtual Base2 *clone() const;
protected:
float data_Base2;
};

class Derived : public Base1, public Base2 {
public:
Derived();
virtual ~Derived ();
virtual Derived *clone() const;
protected:
float data_Derived;
};

對於下面一行:

Base2 *pbase2 = new Derived;

會被 內部轉化為:

// 轉移以支持第二個base class
Derived *temp = new Derived;
Base2 *pbase2 = temp ? temp + sizeof(Base1) : 0;

如果沒有這樣的調整,指針的 任何“非多態運用”都將失敗:

pbase2->data_Base2;

當程 序員要刪除pbase2所指的對象時:

// 必須調用正確的virtual destructor函數實體
// pbase2需要調整,以指出完整對象的起始點
delete pbase2;

指針必須被再一 次調整,以求再一次指向Derived對象的起始處。然而上述的offset加法卻不能夠在編譯時期直接設定, 因為pbase2所指的真正對象只有在執行期才能確定。

自此,我們明白了在多重繼承下所面臨的獨 特問題:經由指向“第二或後繼之base class”的指針(或reference)來調用derived class virtual function,該調用操作所連帶的“必要的this指針調整”操作,必須在執行 期完成。有兩種方法來解決這個問題:

(a)將virtual table加大,每一個virtual table slot 不再只是一個指針,而是一個聚合體,內含可能的offset以及地址。這樣一來,virtual function的調 用操作發生改變:

(*pbase2->vptr[1])(pbase2);
// 改變為
(*pbase2- >vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);

這個做法的缺點是,它相 當於連帶處罰了所有的virtual function調用操作,不管它們是否需要offset的調整。

(b)利 用所謂的thunk(一小段assembly碼),其做了以下兩方面工作:

(1)以適當的offset值調整 this指針;

(2)跳到virtual function去。

pbase2_dtor_thunk:
this += sizeof(base1);
Derived::~Derived(this);

Thunk技術允許virtual table slot繼續內含一個簡單的指針,slot中的地址可以直接指向virtual function,也可以指向一個相關的thunk. 於是,對於那些不需要調整this指針的virtual function而言,也就不需要承載效率上的額外負擔。

(2)由於兩種不同的可能:

(a)經由derived class(或第一個base class)調用;

(b)經由第二個(或其後繼)base class調用,同一函數在virtual table中可能需要多筆對應 的slot;

Base1 *pbase1 = new Derived;
Base2 *pbase2 = new Derived;

delete pbase1;
delete pbase2;

雖然兩個delete操作導致相同的Derived destructor,但它們需要兩個不同的virtual table slots:

(a)pbase1不需要調整this指針, 其virtual table slot需放置真正的destructor地址

(b)pbase2需要調整this指針,其virtual table slot需要相關的thunk地址

具體的解決方法是:

在多重繼承下,一個derived class內含n-1個額外的virtual tables,n表示其上一層base classes的數目。按此手法,Derived將內 含以下兩個tables:vtbl_Derived和vtbl_Base2_Derived.

(3)允許一個virtual function的返 回值類型有所變化,可能是base type,可能是publicly derived type,這一點可以通過Derived:: clone()函數實體來說明。

Base2 *pb1 = new Derived;

// 調用 Derived::clone()
// 返回值必須被調整,以指向Base2 subobject
Base2 *pb2 = pb1- >clone();

當運行pb1->clone()時,pb1會被調整指向Derived對象的起始地址, 於是clone()的Derived版會被調用:它會傳回一個指針,指向一個新的Derived對象;該對象的地址在 被指定給pb2之前,必須先經過調整,以指向Base2 subobject.當函數被認為“足夠小”的時 候,Sun編譯器會提供一個所謂的“split functions”技術:以相同算法產生出兩個函數, 其中第二個在返回之前,為指針加上必要的offset,於是無論通過Base1指針或Derived指針調用函數, 都不需要調整返回值;而通過Base2指針所調用的,是另一個函數。

***虛擬繼承下的Virtual Functions***

其內部機制實在太過詭異迷離,故在此略過。唯一的建議是:不要在一個virtual base class中聲明nonstatic data members.

***函數的效能***

由於nonmember、static member和nonstatic member函數都被轉化為完全相同的形式,故三者的效率安全相同。virtual member 的效率明顯低於前三者,其原因有兩個方面:(a)構造函數中對vptr的設定操作;(b)偏移差值模型 。

***指向Member Function的指針***

取一個nonstatic member function的地址,如果 該函數是nonvirtual,則得到的結果是它在內存中真正的地址。

我們可以這樣定義並初始化該指 針:

double (Point::*coord)() = &Point::x;

想調用它,可以這麼 做:

(origin.*coord)();
 (ptr->*coord)();

“指向 Virtual Member Functions”之指針將會帶來新的問題,請注意下面的程序片段:

float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

其中,pmf是一個指向member function的指針,被設值為Point::z()(一 個virtual function)的地址,ptr則被指向一個Point3d對象。

如果我們直接經由ptr調用z() :

ptr->z();  // 調用的是Point3d::z()

但如果我們經由pmf間接調 用z():

(ptr->*pmf)();  // 仍然調用的是Point3d::z()

也就是說 ,虛擬機制仍然能夠在使用“指向member function之指針”的情況下運行,但問題是如何實 現呢?

對一個nonstatic member function取其地址,將獲得該函數在內存中的地址;而對一個 virtual member function取其地址,所能獲得的只是virtual function在其相關之virtual table中的 索引值。因此通過pmf來調用z(),會被內部轉化為以下形式:

(*ptr->vptr[(int) pmf])(ptr);

但是我們如何來判斷傳給pmf的函數指針指向的是內存地址還是virtual table中的索引值呢?例如以下兩個函數都可指定給pmf:

// 二者都可以指定給pmf
float Point::x() { return _x; }  // nonvirtual函數,代表內存地址
float Point::z() { return 0; }  // virtual函數,代表virtual table中的索引值

cfront 2.0是通過判斷 該值的大小進行判斷的(這種實現技巧必須假設繼承體系中最多只有128個virtual functions)。

為了讓指向member functions的指針也能夠支持多重繼承和虛擬繼承,Stroustrup設計了下面一 個結構體:

// 用以支持在多重繼承之下指向member functions的指針
struct _mptr {
int delta;
int index;
union {
ptrtofunc faddr;
int v_offset;
};
};

其中,index表示virtual table索引,faddr表示 nonvirtual member function地址(當index不指向virtual table時,被設為-1)。

在該模型之 下,以下調用操作會被轉化為:

(ptr->*pmf)();
// 內部轉化為
(pmf.index < 0)
? (*pmf.faddr)(ptr)  // nonvirtual invocation
: (*ptr- >vptr[pmf.index](ptr)  // virtual invocation

對於如下的函數調用:

(pA.*pmf)(pB);  // pA、pB均是Point3d對象

會被轉化成:

pmf.iindex < 0
? (*pmf.faddr)(&pA + pmf.delta, pB)
: (*pA._vptr_Point3d[pmf.index].faddr)(&pA + pA._vptr_Point3d[pmf.index] + delta, pB);

***Inline Functions***

在inline擴展期間,每一個形式參數都會被對應 的實際參數取代。但是需要注意的是,這種取代並不是簡單的一一取代(因為這將導致對於實際參數的 多次求值操作),而通常都需要引入臨時性對象。換句話說,如果實際參數是一個常量表達式,我們可 以在替換之前先完成其求值操作;後繼的inline替換,就可以把常量直接綁上去。

舉個例子,假 設我們有以下簡單的inline函數:

inline int min(int i, int j)
{
return i < j ? i : j;
}

對於以下三個inline函數調用:

minval = min(val1,val2);
minval = min(1024,2048);
minval = min(foo(),bar() +1);

會分別被擴展為:

minval = val1 < val2 ? val1 : val2;  // 參數直接代換
minval = 1024;  // 代換之後,直接使用常量
int t1;
int t2;
minval = (t1 = foo()), (t2 = bar()+1),t1 < t2 ? t1 : t2;  //有副作用,所以導入臨時對 象

inline函數中的局部變量,也會導致大量臨時性對象的產生。

inline int min(int i, int j)
{
int minval = i < j ? i : j;
return minval;
}

則以下表達式:

minval = min(val1, val2);

將被轉化 為:

int _min_lv_minval;
minval = (_min_lv_minval = val1 < val2 ? val1 : val2),_min_lv_minval;

總而言之,inline函數中的局部變量,再加上有副作用的參數 ,可能會導致大量臨時性對象的產生。特別是如果它以單一表達式被擴展多次的話。新的Derived對象的 地址必須調整,以指向其Base2 subobject.

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved