一、C/C++物件模型的概念
這裡所說的物件模型並非指有些人可能會誤以為的Lippman在《Inside The C++ Object Model》一書中所描述的物件模型,Lippman描述的是物件導向的底層實現機制,但下面要討論的物件模型是語言層面上的資料抽象的架構,兩者是不同的,物件導向的語言設施建立在物件模型之上。在程式語言設計理論中,其實並沒有對象的概念,只有變數概念,但與C/C++的變數又不盡相同,相比之下,與之更加類似的反而是C/C++的物件模型。
與記憶體模型類似,C只提出了對象的單獨定義,而C++則提出了整體概念。兩者大多數基本語義是相同的,都是儲存中的一段地區,但函數不是對象,即對象是資料抽象而不是操作抽象,函數才是操作抽象。
二、完整對象和子物件
一個對象可以由其它對象組成,包含其中的對象稱為子物件(subobject)。C中子物件的例子是數組元素和結構成員,而C++比C複雜一些,除了數組元素和結構成員,還包括基類子物件和成員子物件。相對於子物件,一個對象如果不是任何其它對象的子物件,它就稱為完整對象(complete object),當我們說一個對象的完整對象的時候,它包含兩方面的含義:
1. 如果這個對象是完整對象,那麼這個對象的完整對象就是它本身;
2. 如果這個對象是其它對象的子物件,那麼這個對象的完整對象就是包含它的那個對象。
例如:
struct A { int i; };
struct B : A { int j; };
A a;
B b;
b的完整對象就是b本身,但b.j的完整對象不是j,而是b,a的完整對象也依然是a,這一點有些人可能會覺得驚訝,雖然A和B之間有繼承關係,但完整對象與子物件的劃分是基於執行個體的,b中的A類對象才是b的子物件,但b不是a的完整對象。
三、最終派生對象
為了與基類子物件進行區分,一個具有最終衍生類別(most derived class)類型的對象稱為最終派生對象(most derived object),何謂最終衍生類別類型?最終衍生類別類型指的是具有類類型的完整對象、資料成員或者數組元素的類型,但不包括內建資料類型的派生關係。例如:
struct A { int i; };
struct B : A { int j; };
struct C { A k; };
A a;
B b;
C c;
B d[ 10 ];
a和b都是最終派生對象,但b中的A類子物件不是最終派生對象,d每一個子物件都是最終派生對象,c.k是最終派生對象,但a.i和a.j不是最終派生對象。
一個最終派生對象的二進位寬度不能為0,至少具有一個或多個位元組的儲存,基類子物件才允許0二進位寬度。例如:
struct A { };
struct B : A { };
struct C : A { A i; };
A a;
B b;
C c;
雖然A沒有任何資料成員,但由於a是最終派生對象,因此sizeof( a )不能為0,但b和c中的A基類子物件允許為0;c.i由於是最終派生對象,因此sizeof( c.i )不能為0,從而導致sizeof( c )也不能為0。
最終派生對象概念用於解決空類執行個體化所產生的地址唯一性問題,最終派生對象的地址總是唯一的。
四、對象的屬性
除了上述完整對象、子物件和最終派對象的關係外,對象還具有很多屬性,分為名字、地址、值、類型和生命期等等。
1. 對象的名字
對象具有名字屬性,但並不是所有對象都有名字。對象聲明將對象與一個名字關聯起來,此時對象也叫做變數(variable),對象名字也稱為變數名,這裡關於變數和變數名的描述是C++的,C標準雖然在上下文中到處使用著變數這個名詞,卻沒有對變數定義提出明確的規定,這個行為讓人不得不感到莫名其妙,類似的例子還有實體(entity)。
無名對象也叫匿名對象,匿名對象可以由聲明、運算式的結果或中間結果產生,但C中的匿名對象聲明是未定義行為(匿名位域除外),C++則允許匿名聯合聲明。運算式計算所產生的匿名對象也叫臨時對象,有些人將臨時對象稱為臨時變數,這是錯誤的,因為變數是一段命名的儲存,它必定具有名字,不存在無名變數。
2. 對象的地址
對象的地址指示出對象在儲存中的位置。但並非所有對象都是可定址的,例如最小的對象位域,由於它的大小可以小於一個byte,因此位域是不能進行取地址運算的。基於同樣的原因,位域也不能作為完整對象出現,只能作為子物件。
3. 對象的值
對象的值就是對象所包含的內容,但對象的內容並不總能視為一個值,例如聚集和類的內容就不能視為值,只有標量類型的對象的內容才能視為值,在C/C++中,標量類型指的是數實值型別和指標類型。
4. 對象的儲存持久性和生命期
對象具有儲存持久性,C/C++將對象的儲存持久性分為三類:靜態、自動和動態。具有外部或內部連結性或被static關鍵字修飾的對象具有靜態儲存周期,生命期從程式開始到程式結束;無連結性及無static修飾的對象具有自動儲存周期,由編譯器自動管理該對象的建立和銷毀;具有動態儲存裝置周期的對象由malloc()、realloc()、calloc()或new建立,並由程式員自己負責銷毀。
對象生命期是最容易與Object Storage Service持久性混淆的概念,對象生命期指的是對象從建立到銷毀這個過程的時間持久性,儲存持久性雖然很大程度上決定了對象的生存方式,但本質上其實是對象的儲存管理方式。
5. 對象的類型
物件類型是對象最重要的屬性,它決定了對象的種類和大小。從常識上講,似乎物件類型屬於對象屬性是天經地義的事情。對於C++,的確如此,C++認為物件類型是與生俱來的,是在對象建立的那一刻就存在的,但對象的值可以由訪問該對象的運算式的類型進行解釋,即同一個對象的值可以具有不同的數值表示;但是,C的觀念與上述情形卻恰恰相反,C認為類型不是對象的屬性,無論對象本身還是內部都不存在類型資訊,C標準在rational中指出:
3. Terms and definitions
The definition of object does not employ the notion of type. Thus an object has no type in and of itself. However, since an object may only be designated by an
lvalue (see §6.3.2.1), the phrase “the type of an object” is taken to mean, here and in the Standard, “the type of the lvalue designating this object,” and “the value of an object” means “the contents of the object interpreted as a value of the type
of the lvalue designating the object.”
在C中,對象的類型是由引用該對象的左值運算式決定的,而非對象本身,當我們說一個對象具有某個特定類型時,指的是引用該對象的左值運算式的類型。
關於物件類型的觀念的巨大差異導致C和C++對於對象的值產生了不同的解釋,請看下面的代碼:
int i = 0x01010101; /*假設int為32位*/
short *p = ( short* )&i; /*假設short為16位*/
在C中,*p和i被認為引用了兩個不同類型的對象,且具有不同的值;而對於C++,*p和i引用了相同的對象,但從*p和i看來其值不一樣。儘管底層解釋不同,但從引用該對象的運算式的角度來看,效果是一樣的。
還有另一個問題,通過什麼方式訪問對象的內容?,C90、C99和C++之間還存在一些差異,下面分別進行討論。
在C90中,由於左值必須指示一個有效對象(這其實是一個失誤),因此C90規定對一個對象的值進行訪問只能通過其宣告類型(declaration type)進行,這個規定否定了上述代碼的指標p訪問方式(上述失誤導致的問題),但指標p的訪問方式是事實上存在且可行的,否定它明顯不合理。
於是,在C99中這個失誤得到了修正,C99不再規定左值必須引用一個有效對象,同時引入了有效類型(effective type)這個概念,C99將有效類型分成四種情況,但簡單來說,一個具有宣告類型的對象的有效類型就是這個宣告類型,而一個沒有宣告類型的對象的有效類型是進行該訪問的左值運算式的類型,後一個描述保證了上述指標p訪問方式的合法性。C99通過有效類型對對象內容進行操作。
在C++中,由於引入了OOP,存在繼承階層的類對象具有多態性,其類型稱為動態類型(dynamic type),由運行時刻決定,因此C++對對象的訪問是通過動態類型進行的。