c++對象模型

關於對象

加上封裝後的佈局成本

c語言中以下聲明一個結構體ios

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

struct point3d 轉化爲class Point3d以後c++

class Point3d
{
    public:
      Point3d(float x = 0.0f, float y = 0.0f; float z = 0.0f)
        :_x(x),_y(y),_z(z){}
    private:
      float _x,_y,_y;
}

封裝帶來的佈局成本增長了多少?實際是沒有增長佈局成本的。3個數據成員直接在class object內,member function在classs聲明卻不出如今class object中,所謂佈局的成本主要由virtual引發的。api

virtual function 機制用以支持運行時綁定(運行時多態)安全

virtual base class 機制支持屢次出如今集成體系中的base class有一個單一的被共享的實例。ide

基本c++對象模型

nostatic data members 被配置在class object以內,static data member存放在class object以外.函數

static 和nostatic function memners放在class object以外佈局

virtual function的處理步驟:優化

  1. 每一個class產生出一堆指向virtual functions的指針,放在表格中,這個表格稱爲虛表virtual table
  2. 每一個class object 安插一個虛表指針vptr指向虛表(virtual table).
  3. vptr的設定和重置都由每一個class的構造函數、拷貝賦值運算符、析構函數自動完成,每一個class所關聯的type_info object (用以支持runtime type identification, RTTI)也經由virtual table被指出,一般放在virtual table的第一個slot.

聲明一個class Point而後查看其對象模型this

class Point
{
    public:
      Point(float x);
      virtual ~Point();
      float x() const;
      static int PointCount();
    protected:
      virtual ostream& print(ostream& os) const;
      float _x;
      static int _point_count;
}

Point對象模型

加上繼承

c++支持單一繼承和多重繼承.base class subobject的data members直接被放置在derived class object,也就是說子類對象中包含基類子對象.基類成員的改變都會致使繼承類從新編譯.對於虛基類則是擴展子類本身的vittual table維護virtual base class的位置。編碼

class istream : virtual public ios{...};
class ostream : virtual public ios{...};
class iostream : public istream, public ostream{...};

在虛擬繼承的狀況下base class 無論在繼承鏈中被派生多少次,永遠只有一個實例存在即一個subobject.iostream之中只有virtual ios base class的一個實例.

NRV優化

函數返回基本是數據類型或者指針類型是經過eax寄存器進行傳遞的,返回對象對象則會進行命名返回值優化.之外部引用傳參的形式去掉函數內部的局部對象構造。

X foo(){
    X xx
    X* px = new X();
    xx.foo(); //func是一個虛函數
    px->foo()
    delete px;
    rerurn xx
}

如上函數有可能內部轉化爲以下代碼:

void foo(X &result){
    _result.X::X();
    
    px = _new(sizeof(X));
    if(px != 0){ px->X::X(); }
    
    func(&_result);//這裏涉及到成員函數的語義
    
    (*px->vtbl[2])(px) //使用virtual機制擴展px->func()
    
    if(px != 0) {
        (*px->[1])(px); //擴展delete px
        _delete(p)
    }
    return;   
}

NRV優化和虛函數調用

指針類型
  • 指針類型會指導編譯器如何解釋某個特定地址中內容及其大小
  • void*的指針只可以持有一個地址,而不可以經過他操做他所指向的object
  • cast是一種編譯指令它不改變一個指針的內容,隻影響被指出的大小和其內容的解釋方式
  • c++經過引用或者指針的方式支持多態,是由於他們不會引起任何與類型有關的內存委託,
  • 當一個基類對象直接被初始化爲一個子類對象是,子類對象會被切割以放入base type的內存中

構造函數語義學

默認構造函數被合成出來執行編譯器的所需操做

若是類class A含有一個以上的類成員對象,編譯器會擴張構造函數,在構造函數中安插代碼,以成員類的聲明順序調用每一個成員類的默認構造函數,這些代碼被安插在用戶代碼以前.

有四種狀況會形成編譯器爲未聲明構造函數的類合成一個默認的構造函數,接着調用member object或者base class的默認構造函數,完成虛函數和虛基類機制。

  1. 帶有默認構造函數的成員類對象
  2. 帶有默認構造函數的基類
  3. 帶有virtual function的類,用來初始化vptr
  4. 帶有virtual base class的類,用來初始化vptr
拷貝構造函數

類中沒有任何member或者base class object帶有拷貝構造函數,也沒有任何的虛函數和虛基類,默認狀況下 對象的初始化會展現按位拷貝,這樣效率很高且安全.

當對一個object作顯示初始化或者object被當作參數交給函數時以及函數返回一個object時(傳參、返回值、初始化)構造函數會被調用。

copy 構造函數不展示按位逐次拷貝的時候有編譯器產生出來,有四種狀況不展示:

  1. 當成員類中生命有copy constructor
  2. 當基類中存在copy constructor
  3. 類中含有virtual function
  4. 類有virtual base class

一、2中編譯器講member或者bass class的拷貝構造哈數的調用安插到合成的拷貝構造函數中;3,4是爲了對vptr從新初始化.

在構造函數中調用memset或者memcopy會使vptr設置爲0
class Shape{
    public:
        Shape(){ memset(this, 0, sizeof(Shape);)}
        virtual ~Shape();
}

編譯器擴充構造函數的內容以下:

//擴充後的構造函數

Shape::Shape(){
    //vptr在用戶代碼以前被設定
    __vptr__Shape = __vtbl__Shape;
    //memset 會使vptr清0
    memset(this, 0, sizeof(Shape));
}
初始化成員列表

編譯器會操做初始化列表,以成員的聲明順序子構造函數內部在用戶代碼以前安插初始化代碼.
當類含有一下四種狀況的時候會須要使用成員初始化列表:

  1. 初始化一個引用成員
  2. 初始化一個constchengyuan
  3. 基類構造函數擁有參數
  4. 成員類構造函數擁有參數

Data語義學

數據成員的佈局

class X{};一個空類它隱藏1byte的大小,他是被編譯器安插進去的一個char,這使得這一class的兩個object在內存中配置有獨一無二的地址.

非靜態的數據成員直接存放在每個類對象中,對於繼承而來的費靜態成員也是如此。靜態數據成員則放在程序的全局數據段,且只存在一份數據實例.

對成員函數的分析,會在整個class聲明完成以後纔會出現.

在同一個訪問段中member的排列要符合較晚出現的成員在對象中有較高的地址,多個訪問段中的數據成員是自由排列的.

數據成員的訪問
  1. 靜態數據成員只有一個實例放在程序的數據段,編譯器會對每個靜態數據成員進行編碼以得到一個獨一無二的識別碼
  2. 非靜態數據成員,會使用隱式類對象機制訪問數據(this指針)成員函數的參數中隱藏了一個隱式對象指針.
  3. 指向數據成員的指針,其offset值老是被加上1,這樣可使編譯系統區分出「一個指向數據成員的指針,用以指出第一個成員」和「一個指向數據成員的指針,沒有指出任何成員」.
單一繼承無virtual function下的內存佈局

單一繼承下無佈局狀況下class和struct的佈局是同樣的.

單一繼承無virtual function下的數據佈局

單一繼承有virtaual function下的內存佈局

單一繼承有虛函數

Point3d中含有基類的子對象Point2d subobject,子類數據成員放置在基類子對象以後。

多重繼承下的數據佈局

類體系以下

class Point2d
{
    public:
        virtual ~Point2d(){};
    protected:
        float _x,_y;
};

class Point3d : public Point2d
{
    public:
     //...
     protected:
        float _z;
};

class Vertex
{
    public:
        virtual ~Vertex(){};
    protected:
        Vertex *next;
}

class Vertex3d: public Point3d, public Vertex
{
    public:
    //...
    protected:
        float mumble;
}

多重繼承體系下的數據佈局

要存取第二個基類中的數據成員,將會是怎樣的狀況須要付出額外的成本嗎?不 ,成員的位置在編譯期就時就固定了,所以存取數據成員知識一個簡單的offset操做,就像單一繼承同樣簡單--不論是經由一個指針或者引用或者是一個對象來存取.

虛擬繼承

對於虛擬繼承主要的問題是如何存取class的共享部分,虛擬繼承使用兩種策略來實現:指針策略和offset策略.

指針策略

爲了指出共享類對象每一個子類對象安插一些指針,每一個指針指向虛基類。

虛擬繼承使用指針策略產生的佈局

進一步的優化策略的實現:每個class object若是有一個或者多個virtual base classes,就會由編譯器安插一個指針指向virtual base class table.真正的虛基類指針放在虛基類表中.

offset策略

在虛函數表中放置虛基類的offset.

虛擬繼承offset策略

Function語義學

虛函數

基類的指針或者引用尋址出一個子類對象,虛函數分配表格索引,vptr指向virtual table, virtual table中存放虛函數指針.

inline函數

inline是一個請求,編譯器解說就必須認爲它用一個表達式合理的將這個函數擴展開來,擴展期間使用實參代替形參,局部變量在封裝的區域內名字惟一.

函數的調用方式
  1. 非靜態成員函數
  • 改函數簽名安插this指針,變爲一個非成員函數,可使類對象調用.
  • 調用對非靜態成員的存取有this指針完成
  • 經過name-maping 改成一個外部函數
float Point3d::getX()const{...}
extern getX_Point3dFv(const Point3d* this)

obj.getX()
等價於 getX_Point3dFv(&obj)

ptr->getX()
等價於
getX_Point3dFv(ptr)
  1. 靜態成員函數
    被轉爲非成員函數,不能訪問非靜態成員沒有this指針
  2. 虛成員函數
    (*ptr->vptr[1])(ptr) 經過拿到徐表中虛函數地址傳入this指針來調用

構造、拷貝、析構語義學

構造函數的擴充

順序: 先父類後成員最後本身的調用方式.

vptr的初始化在全部base 類構造以後,初始化列表以前(程序代碼)

  1. 虛基類的構造函數被調用從左到右從深到淺
  2. 基類的構造函數被調用,按照基類的生命順序
  3. 設置vptr的指針初值,初始化虛函數表
  4. 成員函數的初始化列表被放在構造偶函數內部以成員類的聲明順序,麼有構造函數則調用合成的默認的構造函數
  5. 構造本身,執行user code
析構函數

按照上面相反的順序調用
先本身析構而後類成員對象析構而後重置vptr而後基類析構而後虛基類析構

拷貝構造

拷貝構造函數和拷貝複製運算符

相關文章
相關標籤/搜索