C++物件模型:object

sgqmax發表於2024-11-01

一、object

typedef struct{
  float x;
  float y;
  float z;
}Point3d;

可以有以下方法列印上述型別欄位:

  1. 定義函式
void print_point3d(const Point3d* pd){
  printf("(%g,%g,%g)", pd->x, pd->y, pd->z);
}
  1. 若要更有效率,可以定義一個宏函式
#define Point3d_print(pd) \
  printf("(%g,%g,%g)", pd->x, pd->y, pd->z);
  1. 也可以由一個前置處理宏來處理
#define X(p,xval) (p.x)=(xval)
//...
Point3d pd;
X(pd, 0.0);

在C++中,Point3d的實現方式如下:

  1. 可以採用獨立的抽象資料型別ADT實現
class Point3d{
public:
  Point3d(float x=0.0, float y=0.0, float z=0.0):x_(x),y_(),z_(){}
  float x(){ return x_; }
  float y(){ return y_; }
  float z(){ return z_; }
  void x(float xval){ x_= xval; }
private:
  float x_;
  float y_;
  float z_;
};
inline ostream& operator<<(ostream& os, const Point3d& pt){
  os<<pt.x()<<pt.y()<<pt.z();
}
  1. 或使用多層的class層次結構實現
class Point{
public:
  Point(float x=0.0):x_(x){}
  float x(){ return x_; }
  void x(float xval){ x_=xval; }
private:
  float x_;
};

class Point2d:public Point{
public:
  Point2d(float x=0.0, float y=0.0):Point(x), y_(y){}
  float y(){ return y_; }
  void y(float yval){ y_=yval; }
private:
  float y_;
};

class Point3d:public Point2d{
public:
  Point3d(float x=0.0, float y=0.0, float z=0.0):Point2d(x,y),z_(z){}
  float z(){ return z_; }
  void z(float zval){ z_=zval; }
private:
  float z_;
};
  1. 更進一步,不管那種型別,都可以被引數化,座標型別引數化
template<class type>
class Point3d{};

// 或如下,座標型別和個數都引數化
/*
template<class type, int dim>
class Point{
public:
  Point();
  Point(type coords[dim]){
    for(int index=0; index<dim; index++){
      coords_[index]= coords[index]
    }
  }
  type& operator[](int index){
    assert(index<dim && index>=0);
    return coords_[index];
  }
  type& operator[](int index)const{
  }
private:
  type coords_[dim];
};
inline template<class type, int dim>
ostream& operator<<(ostream& os, const Point<type,dim>& pt){
  // print
}
*/

對於C++的封裝,是否有佈局上的成本

  1. 每個non-inline只會有一個函式例項,而每個擁有零個或一個定義的inline function則會在每個使用者中產生一個函式例項

  2. C++佈局和效率的負擔主要在virtual關鍵字上

  • 虛擬函式virtual function機制
    用以支援執行期繫結runtime binding
  • 虛繼承virtual base class機制
    用以實現多次出現在繼承體系中的基類,有單一且共享的例項

此外,還有一些多重繼承下的額外負擔,發生在“一個子類和其第二或後繼的父類間的轉換”之間

二、C++物件模型

class Point{
public:
    Point(float xval);
    virtual ~Point();

    float x()const;
    static int PointCount();
protected:
    virtual std::ostream& print(std::ostream& os)const;
    float x_;
    static int point_count_;
};

簡單物件模型

一個object對應一系列的slots,每個slot指向一個member

表格驅動物件模型

C++物件模型

  • 非靜態成員變數

  • 靜態成員變數

  • 靜態和非靜態成員函式

  • 虛擬函式
    類生成一系列指向虛擬函式的指標,放在虛擬函式表vtbl
    每個物件有一個指向vtbl的指標vptrvptr的設定和重置由每一個類的建構函式,解構函式和賦值運算子自動完成
    每個類所關聯的type_info object(用以支援RTTI),也由vtbl指出,通常放在第一個slot
    vptrvtbl的使用沿用cfront編譯器

  • 加上繼承

    1. 單一繼承
    class Library_materials{};
    class Book:public Library_materials{};
    class Rental_book:public Book{};
    
    1. 多重繼承
    class iostream:
      public istream, public ostream{};
    

    繼承關係可以指定為虛繼承

    class istream:virtual public ios{};
    class ostream:virtual public ios{};
    

    保證了在繼承關係中只會存在一個基類的例項subobject

    C++最初採用的繼承模型並不運用任何間接性:基類的成員變數直接放在子類物件中
    C++2.0引入的虛繼承,為每個關聯的虛基類加上指標

三、關鍵字差異

若沒有C的8種整數需要支援,overloaded function的解決方案會更簡單
若丟棄C的宣告語法,就無需花時間判斷下面到底是函式呼叫還是宣告

void (*pf)(1024);
void (*pq)();

struct關鍵字實現了C的資料抽象觀念,class關鍵字實現的是C++的ADT觀念
將單一陣列放在struct的尾端,於是每個struct物件將擁有可變大小的陣列

struct M{
  char pc[1];
};

int main(){
  const char* str="";
  struct M* m=(struct M*)malloc( sizeof(struct M)+strlen(str)+1 );
  strcpy(m->pc, str);

  return 0;
}

四、物件差異

C++程式設計正規化programming paradigms
程式模型procedural model
抽象資料型別模型ADT model
物件導向模型object oriented model

完成多型polymorphic操作

class Library_materials{
public:
  void check_in(){ std::cout<<"Library_materials"<<std::endl; }
};
class Book:public Library_materials{
public:
  void check_in(){ std::cout<<"Book"<<std::endl; }
};

Library_materials thing1;
Book b;
thing1= b;
thing1.check_in(); // 實際呼叫 Library_materials::check_in()

透過pointer或reference的間接處理實現多型,前提:check_in()是虛擬函式
thing1的定義反映的是ADT paradigm的行為

需要多少記憶體才能實現一個class object,一般而言包含如下:

  • 非靜態成員變數大小
  • 由於alignment的需要而填補padding的空間,即對齊
  • 為了支援virtual而產生的額外負擔
class ZooAnimal{
public:
  ZooAnimal(){}
  virtual ~ZooAnimal(){}
  virtual void rotate(){}
protected:
  int loc;
  char* name;
};

不同的指標型別,僅在於定址大小不同,void*的定址大小是未知的,這也是為什麼空指標不能訪問物件

加上多型

class Bear:public ZooAnimal{
public:
  Bear(){}
  ~Bear(){}
  void rotate(){}
  virtual void dance(){}
protected:
  enum class Dances{};
  Dances dances_known;
  int cell_block;
};

定義變數

Bear b;
ZooAnimal* pz= &b;
Bear* pb= &b;

包含虛擬函式的類,在例項化前需要實現虛擬函式,否則提示undefined reference to vtable for Bear
指標pzpb都指向物件b的第一個位元組,差別是pb涵蓋的地址包含整個Bear object,而pz涵蓋的地址只包含Bear object中的ZooAnimal subobject

pz想訪問Bear中的資料
經過一個顯式的轉換

(static_cast<Bear*>(pz))->dance();

或,更好的使用runtime operator,成本較高

if(Bear* pb2=dynamic_cast<Bear*>(pz)){
  pb2->dance();
}

若呼叫

pz->rotate()

pz的型別在編譯期決定,其型別決定rotate()所呼叫的例項
型別資訊的封裝並不是維護在pz中,而是維護在link中,此link存在於object的vptrvtbl之間

Bear b;
ZooAnimal za= b;
za.rotate();

為什麼za.rotate()呼叫的是ZooAnimal::rotate(),而不是Bear::rotate()
za只能是一個ZooAnimal,多型的潛在力量不會出現在直接存取object這件事上

若初始化函式將一個object內容完整複製到另一個object中,那麼為什麼za不指向Bearvtbl
因為編譯器會確保,若object含有一個或多個vptr時,則那些vptr的內容不會被base class object初始化或改變

一個pointer或reference之所以支援多型,是因為他們並不引發記憶體中的任何與型別有關的記憶體委託操作,改變的只是其所指向的記憶體的大小和內容的解釋方式而已

相關文章