在《C++的頭文件和實現文件分別寫什么》文章中,我對於的C++的數據成員,逐個分析了可以作用在它們上邊的限定符都有哪些,以及它們所對應的進行初始化的位置。可以看出這些修飾符其實就是const和static的兩種的組合,但是卻有不同的效用。
本文,我想講關於static的問題,《C++的頭文件和實現文件分別寫什么》已經指出如果數據成員被聲明為static,那么它在編譯時就必須被初始化。僅含static的則放在類之外,實現文件之中;同時含有的const的則放在類之內,直接跟在數據的定義之后。
在我實際代碼編寫中碰到的問題是:static成員的初始化比較的復雜,步驟較多,需要調用另一個函數來完成。此時,簡單使用賦值語句就不太能完成那些目的了。
這個問題來源於我在OpenGL中想使用gluCylinder()函數,該函數需要傳入一個指向GLUquadric 對象的指針。其初始化的過程如下:
GLUquadric * quad = gluNewQuadric();
gluQuadricDrawStyle(quad, GLU_FILL);
gluQuadricNormals(quad, GLU_SMOOTH);
我將我要畫的圖抽象成了一個類,將gluCylinder()的畫圖過程封裝在了一個函數之中。可以看出quad實際上只是用來指示所畫的圖形的樣式和表面向量的計算方式。因此,我覺得完全可以定義為static讓所有類對象共享。
於是我就碰到了static初始化難的問題。同樣的問題還可能發生在容器(Container)類型中,比如我們想要對static的容器類型裝入一些初始的值。
如果是C#,它提供了靜態構造函數(static constructor)。在MSDN上,對於靜態構造函數的表述如下:
靜態構造函數既沒有訪問修飾符,也沒有參數。
在創建第一個實例或引用任何靜態成員之前,將自動調用靜態構造函數來初始化類。
- 靜態構造函數既沒有訪問修飾符,也沒有參數。
- 在創建第一個實例或引用任何靜態成員之前,將自動調用靜態構造函數來初始化類。
- 無法直接調用靜態構造函數。
- 在程序中,用戶無法控制何時執行靜態構造函數。
- 靜態構造函數的典型用途是:當類使用日志文件時,將使用這種構造函數向日志文件中寫入項。
- 靜態構造函數在為非托管代碼創建包裝類時也很有用,此時該構造函數可以調用 LoadLibrary 方法。
- 如果靜態構造函數引發異常,運行時將不會再次調用該構造函數,並且在程序運行所在的應用程序域的生存期內,類型將保持未初始化。
可惜C++沒有提供靜態構造函數的構造方式,只能另辟蹊徑來尋求解決方案:
- 提供一個靜態(非靜態亦可)輔助函數來完成初始化操作;
- 放棄static修飾符,讓每個類都有自己的一份。
對於方案1,我們就需要在使用靜態成員之前,在代碼中顯示地調用一次初始化函數。這就沒有C#靜態構造函數“自動”、“隱式”的優點了。但是一旦忘記調用初始化方程,我們就會得到錯誤的數據。而且遺忘的可能性又是如此之大。
當然我們可以限定成Get方法,在調用數據的地方(無論是類外部,還是類其他函數的調用)都使用Get方法,這樣我們可以一定程度上做到“自動”和“隱式”。
當然我們需要防止成員被初始化兩次以上。對於指針類型,我們可以用判斷是否為空指針來辨別是否已經初始化了(這就相當於是個Singleton Pattern);對於容器類型,我們可以判定容器內成員的數目來辨別(如果我們的初始話數目是一定的)。但是如果是一個普通的對象,似乎就需要多一個標記來進行指示了。
class Picture { public: virtual ~Picture(){}
GLUquadric* GetQudric() { //General way to avoid twice initialization. static bool inited = false; if (!inited) { quad = gluNewQuadric(); gluQuadricDrawStyle(quad, GLU_FILL); gluQuadricNormals(quad, GLU_SMOOTH);
inited = true; } return quad; } protected: static GLUquadric* quad; }; GLUquadric* Picture::quad;
對於方案2,如果是像我在OpenGL里面需要的成員只是作為輔助,顯然沒有問題。可是假如我們就是希望能夠對數據進行共享的話,自然就失去了意義。
其實很多問題,前人都已經做了優美的解決方法,主動學習要好於閉門造車。所以Google一下,在stackoverflow高手們就給了一個更加接近於靜態構造函數的方法:
To get the equivalent of a static constructor, you need to write a separate ordinary class to hold the static data and then make a static instance of that ordinary class.
(將需要使用static的數據用一個普通類來進行封裝, 在該類的構造函數中進行所需的初始化步驟。然后在原來的類中定義一個該類的靜態對象。)
以我所需的GLUquadric為例,我構建了一個新的Quadric類,該類具有GLUquadric指針成員,並且提供了一個對外的接口。然后,我在要使用的GLUquadric的靜態指針對象類里,改用Quadric靜態對象。這樣我們就能做到了隱式地自動地進行初始化了。
//Header File #ifndef QUADRIC_H #define QUADRIC_H class Quadric { public: Quadric() { quad = gluNewQuadric(); gluQuadricDrawStyle(quad, GLU_FILL); gluQuadricNormals(quad, GLU_SMOOTH); } ~ Quadric(){ if(quad)gluDeleteQuadric(quad); } GLUquadric * Object(){return quad;} private: GLUquadric * quad; }; class Picture { public: virtual ~Picture(){} void DrawTrunk(); protected: static Quadric quadric; }; #endif
//Implementation File
//Definition static member data Quadric Picture::quadric; void Picture::DrawTrunk() { glPushMatrix(); glRotated(-90, 1, 0, 0); glColor3f(0.625, 0.14, 0.14); glScalef(7, 7, 21); gluCylinder(quadric.Object(), 1, 1, 1, 20, 20); glPopMatrix();
}
如果所用靜態數據是對象而非指針的話,對外的接口返回類型可以從指針換成引用類型。
另外,因為我們有類將其包裹,所以我們可以把數據的銷毀過程也封裝在類的析構函數之中。不過因為對象是以static形式被使用的,所以從程序開始被創建,直到程序結束被自動銷毀。所以大部分時候,其實不需要去考慮析構的問題。
唯一的不足,可能就是原本是直接調用我們需要的對象或直接,現在則需要通過新類的接口訪問。
不過我們也可以重載一些轉換運算符來一定程度的規避這個問題:
class Quadric { public: Quadric() { quad = gluNewQuadric(); gluQuadricDrawStyle(quad, GLU_FILL); gluQuadricNormals(quad, GLU_SMOOTH); } ~ Quadric(){ if(quad)gluDeleteQuadric(quad); } GLUquadric * Object(){return quad;} //operators overloading operator GLUquadric *(){ return quad; } operator GLUquadric &(){ return* quad; } GLUquadric& operator *(){ return* quad; } private: GLUquadric * quad; };