抽象基類 現有如下代碼: class Abstract_base { public: virtual ~Abstract_base() = 0; virtual void interface() const = 0; virtual const char* mumble() const { re ...
抽象基類
現有如下代碼:
class Abstract_base
{
public:
virtual ~Abstract_base() = 0;
virtual void interface() const = 0;
virtual const char* mumble() const { return _mumble; }
protected:
char* _mumble;
}
以上抽象基類聲明有幾個問題:
- 即使class被聲明為抽象基類,其依然需要explicit constructor來初始化protected data member _mumble,否則derived class無法決定_mumble初值
- 抽象基類的virtual destructor不要聲明為pure。因為每個derived class destructor會被編譯器擴張,以靜態方式調用每個virtual base class和上一層base class的destructor
- mumble()不應聲明為virtual function,因為其定義的內容和類型無關,derived class並不會改寫此函數
//合理的聲明
class Abstract_base
{
public:
virtual ~Abstract_base();
virtual void interface() = 0;
const char* mumble() const { return _mumble; }
protected:
Abstract_base( char* pc = 0 );
char* _mumble;
}
- 一般而言,class的data member應被初始化,且只在constructor中或在class的其他member function中指定初值。其他操作都會破壞封裝性質,讓class的維護和修改變得愈加困難
- 我們可以靜態地定義和調用一個pure virtual function,但不能經由虛擬機制
- c++保證繼承體系中每個class object的destructors都能被調用,編譯期不可壓抑這一操作,且編譯器並沒有足夠知識合成pure virtual destructor函數定義
- 虛擬基類中,不要把所有的member functions都聲明為virtual function,再靠編譯器的優化把非必要的虛擬調用去除
- 虛擬基類中,virtual function不使用const
對象構造
不繼承
現有如下片段:
typedef struct
{
float x, y, z;
}Point;
Point global;
Point foobar()
{
Point local;
Point* heap = new Point;
*heap = local;
delete heap;
return local;
}
-
對於Point這樣的聲明,在c++會被貼上Plain OI' Data標簽。編譯器並不會為其聲明default constrcutor、destructor、copy constructor、copy assignment operator
-
對於 Point global; 這樣的定義,在c++中members並沒有被定義或調用,行為和c如出一轍。編譯器並不會調用constructor和destructor。除非在c中,global被視作臨時性定義
臨時性定義:因為沒有顯示初始化操作,一個臨時性定義可以在程式多次發生,但編譯器最終會將這些實例鏈接摺疊起來,只留下一個實例,放在data segment中"保留給未初始化的global object使用的"空間 但在c++中並不支持臨時性定義,對於此例,會阻止後續的定義
-
c++中所有全局對象都被以初始化過的數據對待
-
對於 Point* heap = new Point;編譯器並不會調用default constructor,只是 Point* heap = __new( sizeof(Point) )。delete亦是如此
-
對於*heap = local;編譯器並不會調用copy assignment operator做拷貝,但只是像c那樣做簡單的bitwise
-
return操作也是,只是簡單的bitwise,並沒有調用copy constructor
現有如下片段:
class Point
{
public:
Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x(x), _y(y), _z(z) { }
//沒有copy constructor,copy operator,destructor
private:
float _x, _y, _z;
}
void do()
{
Point local1 = {1.0, 1.0, 1.0};
}
-
對於將class中成員設定常量值,使用explicit initialization list更有效率。因為當函數的活動記錄(activation record)被放進堆棧,initialization list的常量即可放入local1記憶體中
活動記錄過程的調用是過程的一次活動,當過程語句(及其調用)結束後,活動生命周期結束。變數的生命周期為其從被定義後有效存在的時間
-
但explicit initialization list也有不足:
- class member需要為public
- 只能指定常量,因為其常量在編譯器即可求值
- 編譯器並沒有自動施行它,初始化很可能失敗
-
若調用之前的例子,放在現在編譯器也不會調用destructor,因為並沒有顯示地提供destructor
delete heap;
現有如下片段:
class Point
{
public:
Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x(x), _y(y), _z(z) { }
virtual float z();
//沒有copy constructor,copy operator,destructor
private:
float _x, _y;
}
-
導入了virtual functions會引發編譯器對class Point的膨脹:
- 定義的constructor附加一些代碼,以便將vptr初始化。這些代碼附加在任何base class constructors調用後,user code之前
- 合成一個copy constructor 和 一個assignment operator,且不再是有用的(trivial),但implicit desctructor仍為有用的
-
這種情況下,編譯器在優化狀態下可能會把object的連續內容拷貝到另一個object上,且不是memberwise。因此,編譯器會儘量延遲nontrivial members的合成操作,直到遇到合適場合
-
例如之前的例子,此時就很有可能合成copy assignment operator,以及inline expansion。對於return,又因為合成copy constructor,函數內部又需改寫;但若編譯器支持NRV,內部會改寫為constructor,此時將不用調用copy constructor
*heap = local; return local;
繼承
T object;
對於以上定義,編譯器會擴充每一個constructor。一般而言擴充如下:
- 所有virtual base class constructors必須被調用,從左到右,從最深到最淺
- 若class被列於member initialization list,如果有任何顯示指定的參數,都應傳過去。若沒有列於list,而class有default constructor,則調用此
- 此外,class中的每個virtual base class subobject的offset必須在執行期可被存取
- 若 class object 是最底層的class,其constructors可能被調用
- 所有上層的base class constructors必須被調用,以base class的聲明順序
- 若class被列於member initialization list,如果有任何顯示指定的參數,都應傳過去。若沒有列於list,而class有default constructor,則調用此
- 若base class是多重繼承下的第二或後繼base class,那麼this指針需調整
- 若class object有virtual table pointers,其需指定初值
- 記錄在member initialization list的data member初始化會被放進constructor函數本體,以members聲明順序為順序。若有一個member沒有出現在member initialization list,但其有default constructor,那麼該default constructor必須被調用
實例:
class Point
{
public:
Point( float x = 0.0, float y = 0.0 );
Point( const Point& );
Point& operator=( const Point& );
virtual ~Point();
virtual float z() { return 0.0; }
protected:
float _x, _y;
};
class Line
{
Point _begin, _end;
public:
Line( float = 0.0, float = 0.0, float = 0.0, float = 0.0 );
Line( const Point&, const Point& ) : _end(end), _begin(begin);
draw();
...
};
//擴充
Line* Line::Line( Line* this, const Point&, const Point& )
{
this->begin.Point::Point( begin );
this->end.Point::Point( end );
return this;
}
//合成隱式的Line destructor。若Line派生自Point,合成的將會是virtual
Line a;
- 對於虛擬繼承,以上constructor擴張方式將不再支持,這是因為virtual base class的共用性,否則會導致多次virtual base class的constructor。針對虛擬繼承,每個derived class constructor擴張方式需發生改變,添加 bool __most_derived來判斷是否為派生最底層,若為最底層則調用virtual base class consturctor
vptr初始化
現有如下片段:
//假設每個class都定義了virtual function size(),傳回class大小,每個constrcutor中調用size()
Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
PVertex(x,y,z);
//當我們定義PVertex object,前五個constructor各自調用自己的size()
- 做到如上機制,我們需要在執行一個constructor時,必須限制一組virtual functions候選名單。通過控制vptr的初始化和設定
- vptr的初始化應在base class constructors調用後,在member initialization list所列member初始化操作前
此時,constructor也需改變:
- 在derived class constructor中,調用所有virtual base classes及上一層base class 的constructors
- 然後,初始化vptr,指向相關virtual table
- 若有member initialization list,將在constructor展開,但必須在vptr被設定後才施行
- 最後,執行user code
//改變後的constructor擴張
PVertex* PVertex::PVertex( PVertex* this, bool __most__derived, float x, float y, float z )
{
//調用virtual base constructor
if( __most_derived != false ) this->Point::Point(x,y);
//調用上層base class
this->Vertex3d::Vertex3d(x,y,z);
//初始化vptr
this->__vptr_PVertex = __vtbl_PVertex;
this->__vptr_Point__PVertex = __vtbl_Point__PVertex;
//size()
...
return this;
}
當然以上方案並不完美:
Point::Point( float x, float y ) : _x(x), _y(y) { }
Point3d::Point3d( float x, float y, float z ) : Point(x,y), _z(z) { }
此時,若聲明PVertex,由於對其base class constructor的最新定義,其vptr將不再需要在每個base class constructor中被設定。因此,我們需要把constructor分裂為一個完整的object實例和sunobject實例
- 在class的constructor的member initialization list中調用該class 的virtual functions,在語意上可能不安全,因為函數本身可能得依賴未被設立初值的members
對象複製
設計一個class,並以一個class object指定給另一個class object,我們有三種選擇:
- 什麼都不做,實施預設行為
- 提供一個explicit copy assignment operator
- 顯示拒絕把class object指定給另一個class object。也就是將copy assignment operator聲明為private,且不提供定義
- 只有在預設的member wise copy行為不安全或不正確時,才需要設計一個copy assignment operator。且如果class有bitwise copy,隱式的assignment operator不會合成
class對於default copy assignment operator,在以下情況,不會表現bitwise copy:
- 當class 內含member object,而其class有一個copy assignment operator
- 當class的base class有一個copy assignment operator
- 當class聲明瞭任何virtual functions。一定別拷貝右邊class object的vptr地址,它很有可能是derived class object
- 當class繼承自virtual base class
-
copy assignment operators並不表示bitwise copy是nontrivial。只有nontrivial instances才被合成
-
即使賦值由bitwise copy完成,並沒有調用copy assignment operator,但還是需要提供一個copy constructor,以此打開NRV優化
-
儘可能不要允許一個virtual base class的拷貝操作。不要在任何virtual base class中聲明數據
對象效能
-
對於單一繼承和多重繼承,若class使用bitwise copy,一般不會合成copy constructor,就不會增加效率成本
-
對於虛擬繼承,bitwise copy不再支持,而是合成copy assignment operator和inline copy constructor,導致成本大大增加。且繼承體系複雜度增加,對象拷貝和構造的成本也會增加
對象析構
- 若class沒定義destructor,只有在class內含member object含有destructor時,編譯器才會合成destructor
- 若base class不含desturctor,那麼derived class也不需要desturctor
destructor被擴展的方式。與constructor相似,但順序相反:
- destructor函數本體先被執行
- 若class含有member class object,而後者含有destructors,他們會以其聲明順序的相反順序被調用
- 若object內含vptr,現需被重新指定,指向適當的base class的virtual table
- 若有任何直接的nonvirtual base classes含有destructor,它們會以其相反的聲明順序被調用
- 若由任何virtual base classes含有destructor,如之前的PVertex例子,會以其原來的構造順序的相反順序調用