C++語言學習(十三)——C++對象模型分析

C++語言學習(十三)——C++對象模型分析

1、C++對象模型分析

一、類對象模型的內存佈局

class是一種特殊的struct,class與struct遵循相同的內存對齊原則,class中的成員函數與成員變量是分開存放的,每一個對象擁有獨立的成員變量,全部的對象共享類中的成員函數。
運行時,類對象退化爲結構體的形式:
A、全部成員變量在內存中依次排布
B、因爲內存對齊的存在,成員變量間可能存在內存間隙
C、能夠經過內存地址訪問成員變量
D、訪問權限關鍵字在運行時失效ios

#include <iostream>

using namespace std;

class A
{
    int i;
    int j;
    char c;
    double d;
public:
    void print()
    {
        cout << "i = " << i << ", "
             << "j = " << j << ", "
             << "c = " << c << ", "
             << "d = " << d << endl;
    }
};

struct B
{
    int i;
    int j;
    char c;
    double d;
};

int main(int argc, char *argv[])
{
    A a;
    //64 bit machine
    cout << "sizeof(A) = " << sizeof(A) << endl;    // 24
    cout << "sizeof(a) = " << sizeof(a) << endl;    // 24
    cout << "sizeof(B) = " << sizeof(B) << endl;    // 24

    a.print();

    B* p = reinterpret_cast<B*>(&a);

    p->i = 1;
    p->j = 2;
    p->c = 'c';
    p->d = 3.14;
    a.print();

    return 0;
}

上述代碼中,class A對象與struct B對象在內存中的排布相同。數組

二、派生類類對象模型

子類是由父類成員疊加子類成員獲得的。數據結構

#include <iostream>

using namespace std;

class Parent
{
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d)
    {
        m_i = i;
        m_j = j;
        m_d = d;
    }
    void print()
    {
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
private:
    double m_d;
};

struct Test
{
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//8
    cout << sizeof(Child) << endl;//16
    Child child(1,2,3.14);
    child.print();
    Test* test = reinterpret_cast<Test*>(&child);
    cout << "i = " << test->i << endl;
    cout << "j = " << test->j << endl;
    cout << "d = " << test->d << endl;

    test->i = 100;
    test->j = 200;
    test->d = 3.1415;
    child.print();
    return 0;
}

2、C++多態的實現機制

一、C++多態的實現簡介

當類中聲明虛函數時,C++編譯器會在類中生成一個虛函數表。虛函數表是一個用於存儲virtual成員函數地址的數據結構。虛函數表由編譯器自動生成與維護,virtual成員函數會被編譯器放入虛函數表中。存在虛函數時,每一個對象中都有一個指向類的虛函數表的指針。
因爲對象調用虛函數時會查詢虛函數表,所以虛函數的調用效率比普通成員函數低。
當建立類對象時,若是類中存在虛函數,編譯器會在類對象中增長一個指向虛函數表的指針。父類對象中虛函數表存儲的是父類的虛函數,子類對象中虛函數表存儲的是子類對象的虛函數。虛函數表指針存儲在類對象存儲空間的開始的前4(8)個字節。ide

二、虛函數表

若是一個類包含虛函數,其類包含一個虛函數表。
若是一個基類包含虛函數,基類會包含一個虛函數表,其派生類也會包含一個本身的虛函數表。
虛函數表是一個函數指針數組,其數組元素是虛函數的函數指針,每一個元素對應一個虛函數的函數指針。非虛成員函數的調用並不須要通過虛函數表,因此虛函數表的元素並不包括非虛成員函數的函數指針。 
虛函數表中虛函數指針的賦值發生在編譯器的編譯階段,即在代碼編譯階段虛函數表就生成。函數

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "Child::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "Child::display()" << endl;
    }
private:
    double m_d;
};

struct Test
{
    void* vptr;
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//12
    cout << sizeof(Child) << endl;//24
    Child child(1,2,3.14);
    Test* test = reinterpret_cast<Test*>(&child);
    cout << "virtual Function Table Pointer:" << endl;
    cout << "vptr = " << test->vptr << endl;
    //虛函數表指針位於類對象的前4字節
    cout << "child Object address: " << &child << endl;
    cout << "Member Variables Address: " << endl;
    cout << "&vptr = " << &test->vptr << endl;
    cout << "&i = " << &test->i << endl;
    cout << "&j = " << &test->j << endl;
    cout << "&d = " << &test->d << endl;

    //函數指針方式訪問類的虛函數
    cout << "Virtual Function Table: " << endl;
    cout << "Virtual print Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 0)) <<endl;
    cout << "Virtual sum Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 1)) <<endl;
    cout << "Virtual display Function Address: " << endl;
    cout << (long*)(*((long *)(*((long *)&child)) + 2)) <<endl;
    typedef void (*pPrint)();
    pPrint print = (pPrint)(*((long *)(*((long *)&child)) + 0));
    print();

    typedef double (*pSum)(void);
    pSum sum = (pSum)(*((long *)(*((long *)&child)) + 1));
    sum();

    typedef void (*pDisplay)(void);
    pDisplay display = (pDisplay)(*((long *)(*((long *)&child)) + 2));
    display();
    return 0;
}

上述代碼中,經過類對象的虛函數表指針能夠訪問類的虛函數表,虛函數表順序存儲了類的虛函數的函數地址,經過函數指針的方式能夠調用類的虛函數,包括聲明爲private的虛函數。但因爲使用函數指針方式訪問類的虛函數時,類的虛函數在執行過程當中其this指針指向的對象是不肯定的,所以訪問到的類對象的成員變量的值是垃圾值。佈局

三、虛函數表指針

虛函數表屬於類,而不是屬於某個具體的類對象,一個類只須要一個虛函數表。同一個類的全部對象都使用類的惟一虛函數表。 爲了指定類對象的虛函數表,類對象內部包含一個指向虛函數表的指針,指向類的虛函數表。爲了讓每一個類對象都擁有一個虛函數表指針,編譯器在類中添加了一個指針*__vptr,用來指向虛函數表。當類對象在建立時便擁有__vptr指針,且__vptr指針的值會自動被設置爲指向類的虛函數表。性能

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    void func()
    {

    }
protected:
    int m_i;
    int m_j;
};

上述代碼中,類的虛函數表以下:
類Parent對象的內存佈局中,虛函數表指針位於類對象存儲空間的開頭,其值0X409004是類Parent的虛函數表的首地址,虛函數表中的第一個數組元素是虛函數Parent::print的地址,第二個數組元素是虛函數Parent::sum,第三個數組元素是虛函數Parent::display,非虛函數不在虛函數表中。
C++語言學習(十三)——C++對象模型分析學習

四、類對象的內存佈局

對於含有虛函數的類,虛函數表指針位於類對象內存佈局的開始位置,而後依次排列類繼承自父類的成員變量,最後依次排列類自身的非靜態成員變量。this

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual double sum()
    {
        cout << "Parent::" << __func__<< endl;
        double ret = m_i + m_j;
        cout <<ret << endl;
        return ret;
    }
    virtual void display()
    {
        cout << "Parent::display()" << endl;
    }
    int add(int value)
    {
        return m_i + m_j + value;
    }
protected:
    void func()
    {

    }
protected:
    int m_i;
    int m_j;
    static int m_count;
};
int Parent::m_count  = 0;

class ChildA : public Parent
{
public:
    ChildA(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildA::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildA::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildA::display()" << endl;
    }
private:
    double m_d;
};

class ChildB : public Parent
{
public:
    ChildB(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
    }
    virtual void print()
    {
        cout << "ChildB::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    virtual double sum()
    {
        cout << "ChildB::" << __func__<< endl;
        double ret = m_i + m_j + m_d;
        cout << ret << endl;
        return ret;
    }
private:
    void display()
    {
        cout << "ChildB::display()" << endl;
    }
private:
    double m_d;
};

struct ParentTest
{
    void* vptr;
    int i;
    int j;
};

struct ChildTest
{
    void* vptr;
    int i;
    int j;
    double d;
};

int main(int argc, char *argv[])
{
    cout << sizeof(Parent) << endl;//12
    cout << sizeof(ChildA) << endl;//24
    cout << endl;
    cout << "Parent..." <<endl;
    Parent parent(1,2);
    ParentTest* parenttest = reinterpret_cast<ParentTest*>(&parent);
    cout << "Member Variable Value:"<< endl;
    //虛函數表的首地址
    cout << parenttest->vptr << endl;//編譯時肯定
    cout << parenttest->i << endl;//1
    cout << parenttest->j << endl;//2
    cout << "Member Variable Address:" << endl;
    cout << &parenttest->vptr << endl;
    cout << &parenttest->i << endl;
    cout << &parenttest->j << endl;
    cout << endl;
    cout << "Child..." << endl;
    ChildA child(1,2,3.14);
    ChildTest* childtest = reinterpret_cast<ChildTest*>(&child);
    cout << "Member Variable Value:"<< endl;
    //虛函數表的首地址
    cout << childtest->vptr << endl;//編譯時肯定
    cout << childtest->i << endl;//1
    cout << childtest->j << endl;//2
    cout << childtest->d << endl;//3.14
    cout << "Member Variable Address:" << endl;
    cout << &childtest->vptr << endl;
    cout << &childtest->i << endl;
    cout << &childtest->j << endl;
    cout << &childtest->d << endl;

    return 0;
}

C++語言學習(十三)——C++對象模型分析

五、動態綁定的實現

Parent、ChildA、ChildB三個類都有虛函數,C++編譯器編譯時會爲每一個類都建立一個虛函數表,即類Parent的虛函數表(Parent vtbl),類ChildA的虛函數表(ChildA vtbl),類ChildB的虛表(ChildB vtbl)。類Parent、ChildA、ChildB的對象都擁有一個虛函數表指針*vptr,用來指向本身所屬類的虛函數表。 
類Parent包括三個虛函數,Parent類的虛函數表包含三個指針,分別指向Parent::print()、Parent::sum()、Parent::display()三個虛函數函數。 
類ChildA繼承於類Parent,所以類ChildA能夠調用父類Parent的函數,但類ChildA重寫Parent::print()、Parent::sum()、Parent::display()三個虛函數,所以類ChildA 虛函數表的三個函數指針分別指向ChildA::print()、ChildA::sum()、ChildA::display()。 
類ChildB繼承於類Parent,所以類ChildB能夠調用類Parent的函數,但因爲類ChildB重寫Parent::print()、Parent::sum()函數,類ChildB虛函數表有三個函數指針,第一個函數指針指向Parent::display()虛函數,第二個第三個依次指向ChildB::print()、ChildB::sum()虛函數。 spa

ChildA childA;
Parent* p = &childA;

當定義一個ChildA類的對象childA時,childA對象包含一個虛函數表指針,指向ChildA類的虛函數表。
當定義一個Parent類的指針p指向childA對象時,p指針只能指向ChildA對象的父類Parent部分,但因爲虛函數表指針位於對象存儲空間的開始,所以p指針能夠訪問childA對象的虛函數表指針。因爲childA對象的虛函數表指針指向ChildA類的虛函數表,所以p指針能夠訪問類ChildA的虛函數表。
當使用指針調用print函數,程序在執行p->print()時,會發現p是個指針,且調用的函數是虛函數。 
首先,根據虛函數表指針p->vptr來訪問對象childA對應的虛函數表。
而後,在虛函數表中查找所調用的虛函數對應的條目。因爲虛函數表在編譯階段就生成,因此能夠根據所調用的函數定位到虛函數表中的對應條目。對於 p->print()的調用,類ChildA虛函數表的第一項便是print函數指針對應的條目。 
最後,根據虛函數表中找到的函數指針,調用函數ChildA::print()。

Parent base;
Parent* p = &base;
p->print();

當base對象在建立時,base對象的虛函數表指針vptr已設置爲指向Parent類的虛函數表,p->vptr指向Parent虛函數表。print在Parent虛函數表中相應的條目指向Parent::print()函數,因此 p->print()會調用Parent::print()函數。
虛函數的調用的三個步驟用表達式(*(p->vptr)[n])(p)能夠歸納。

六、函數指針實現多態

#include <iostream>

using namespace std;

typedef void (*vfunc)();

class Parent
{
public:
    vfunc print;
    Parent()
    {
        print = Parent::display;
    }
    static void display()
    {
        cout << "Parent::" << __func__<< endl;
    }
};

class Child : public Parent
{
public:
    Child()
    {
        print = Child::display;
    }
    static void display()
    {
        cout << "Child::" << __func__<< endl;

    }
};

int main(int argc, char *argv[])
{
    Parent parent;
    parent.print();
    Child child;
    child.print();
    Parent* p = &child;
    p->print();

    return 0;
}

// output:
// Parent::display
// Child::display
// Child::display

上述代碼使用函數指針實現了多態,繞過了虛函數表,避免了虛函數表的性能損失。

3、虛函數經典問題

一、構造函數不能爲虛函數

因爲在構造函數執行完後,類對象的虛函數表指針才被正確初始化。所以構造函數不能爲虛函數。類對象中的虛函數表指針是在調用構造函數的時候完成初始化的。所以,在構造函數調用前,虛函數表指針尚未完成初始化,沒法調用虛的構造函數。
在構造函數進入函數體前,進行虛函數表指針的初始化,將虛函數表指針初始化爲當前類的虛函數表地址,即在基類調用構造函數的時候,會把基類的虛函數表地址賦值給虛函數表指針,而若是進執行到子類的構造函數時,把子類的虛函數表地址賦值給虛函數表指針。所以,在派生類對象的構造時,虛函數表指針指向的虛函數表地址是動態變化的。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j): " << this << endl;
        //虛函數表指針
        int* vptr = (int*)*((int*)this);
        cout << "vptr: " << vptr << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent(): " << this << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
        cout << "Child(int i, int j, double d): " << this << endl;
        //虛函數表指針
        int* vptr = (int*)*((int*)this);
        cout << "vptr: " << vptr << endl;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    ~Child()
    {
        cout << "~Child(): " << this <<endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent* p = new Child(1,2,3.14);
    p->print();
    delete p;
    return 0;
}

二、析構函數中能夠爲虛函數

析構函數能夠爲虛函數,能夠發生多態。工程實踐中,若是基類中有虛成員函數,建議將析構函數聲明爲虛函數,確保對象銷燬時觸發正確的析構函數調用,保證資源的正確回收。

#include <iostream>

using namespace std;

class Parent
{
public:
    Parent(int i, int j)
    {
        m_i = i;
        m_j = j;
        cout << "Parent(int i, int j)" << endl;
    }
    virtual void print()
    {
        cout << "Parent::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
    }
    virtual ~Parent()
    {
        cout << "~Parent()" << endl;
    }
protected:
    int m_i;
    int m_j;
};

class Child : public Parent
{
public:
    Child(int i, int j, double d):Parent(i, j)
    {
        m_d = d;
        cout << "Child(int i, int j, double d)" << endl;
    }
    virtual void print()
    {
        cout << "Child::" << __func__<< endl;
        cout << "m_i = "<< m_i << endl;
        cout << "m_j = "<< m_j << endl;
        cout << "m_d = "<< m_d << endl;
    }
    ~Child()
    {
        cout << "~Child()" <<endl;
    }
private:
    double m_d;
};

int main(int argc, char *argv[])
{
    Parent* p = new Child(1,2,3.14);
    p->print();
    delete p;
    return 0;
}

三、構造函數內不能發生多態行爲

在調用基類的構造函數時,其虛函數表指針指向的是基類的虛函數表,而在調用派生類的構造函數時,其虛函數表指針指向的是派生類的虛函數表。所以,構造函數內不能發生多態行爲。

四、析構函數內不能發生多態行爲

在調用派生類的析構函數時,其虛函數表指針指向的是派生類的虛函數表;在調用基類的析構函數時,其虛函數表指針指向的是基類的虛函數表,而且派生類的虛函數表已經被銷燬。

相關文章
相關標籤/搜索