指向數據成員的指針,是一個有點神祕又頗有用處的語言特性,特別是如果你需要詳細調查class members的底層佈局的話。這樣的調查可以用於決定vptr是放在class的起始處或者尾端。另外一個用途是可以用來決定class中的access sections的次序。
考慮下面的Point3d聲明。其中有一個virtual function,一個static data member,以及三個座標:
class Point3d{
public:
virtual ~Point3d();
//…
protected:
static Point3d origin;
float x,y,z;
}
每一個Point3d的對象含有三個座標值,依次爲x、y、z,以及一個vptr。至於靜態數據成員origin,將被放在class object之外。唯一可能因編譯器不同而不同的是vptr的位置。C++標準允許vptr被放在對象中的任何位置:在起始處,在尾端,或者是在各個members之間。然而實際上,所有編譯器不是把vptr放在對象的頭部,就是放在對象的尾部。
那麼,取某個座標成員的地址,代表什麼意思呢?例如,以下操作所得到的值代表什麼:
&Point3d::z;
上述操作將得到z座標在class object中的偏移量(offset)。最低限度其值將是x和y的大小總和,因爲C++語言要求同一個access level中的members的排列次序應該和其聲明次序相同。在一臺32位機器上,每一個float是4個字節,所以我們應該期望剛纔獲得的值要不是8,就是12(在32位機器上,一個vptr是4個字節)。
然而,這樣的期望還少了1個字節。對於C和C++程序員來說,這多少算是個有點年代的錯誤了。如果vptr放在對象的末尾,則三個座標值在對象佈局中的偏移量分別爲0、4、8;如果vptr放在對象的開頭,則三個座標值在對象佈局中的偏移量分別爲4、8、12。然而你若去取data members的地址,傳回的值總是多1,也就是1、5、9或5、9、12等等。
#include <iostream>
class Point3d{
public:
virtual ~Point3d(){};
//…
public://如果換成private或者protected,則報錯
static Point3d origin;
float x;
float y;
float z;
};
int main()
{
printf("&Point3d::x = %p/n", &Point3d::x);
printf("&Point3d::y = %p/n", &Point3d::y);
printf("&Point3d::z = %p/n", &Point3d::z);
std::cout<<"&Point3d::x = "<<&Point3d::x<<std::endl;
std::cout<<"&Point3d::y = "<<&Point3d::y<<std::endl;
std::cout<<"&Point3d::z = "<<&Point3d::z<<std::endl;
return 0;
}
輸出結果爲:
&Point3d::x = 00000004
&Point3d::y = 00000008
&Point3d::z = 0000000C
&Point3d::x = 1
&Point3d::y = 1
&Point3d::z = 1
Press any key to continue
在vc6.0下,並沒有增加1,原因可能是visual c++做了特殊的處理。
在vc6.0下,通過printf或者cout的形式,都可以正常運行,只不過,得到的結果不一致。使用std::cout時,都輸出的是1,應該作何解釋呢?
以上程序,如果數據成員爲private或者protected的,則無法編譯通過,而書上的例子,卻是protected,作者的測試程序可能是怎樣的呢?
(以上程序在vc6.0,virsual studio2008,DEV-C++下測試過,與《深入探索C++對象模型》P131對應的說明有些出入)
爲啥傳回的值會多1個字節呢?這一個字節,主要用來區分“沒有指向任何數據成員的指針”和“指向第一個數據成員的指針”這兩種情況。考慮下面這樣的例子:
float Point3d::*p1 =0;
float Point3d::*p2 = &Point3d::x;
//Point3d::* 的意思是“指向Point3d data member”的指針類型
if( p1 == p2 ){
cout <<” p1 & p2 contain the same value.”;
cout <<”they must address the same member!”<<endl;
}
爲了區分p1和p2,每一個真正的member offset值都被加上1。因此,不論編譯器或者使用者都必須記住,在真正使用該值以指出一個member之前,請先減去1。
在充分認識“指向數據成員的指針”之後,要解釋:
&Point3d::z;和 &origin.z
之間的差異,就非常明確了:取一個非靜態數據成員的地址,將會得到它在class中的offset,取一個綁定於真正class object身上的數據成員的地址,將會得到該數據成員在內存中真正的地址。&origin.z的返回值類型應該是:float * 而不是:float Point3d::* 。
#include <iostream>
class Point3d{
public:
virtual ~Point3d(){};
//…
public:
float x;
float y;
float z;
};
int main()
{
Point3d origin;
printf("&origin.z = %p/n", &origin.z);
return 0;
}
輸出結果爲:
&origin.z = 0013FF7C
參考資料:
《深度探索C++對象模型》