C++中幾個值得分析的小問題(1)

QingLiXueShi發表於2015-04-27

下面3個小問題都是我認為C++ Beginner應該能夠解答或辨別清楚的。希望我們能通過題目挖掘更多的資訊,而不僅僅侷限在解題。我最喜歡說的話:能力有限,所以作為拋磚引玉,希望共同討論,指出錯誤。

另外,我都是碰到一個覺得有必要記錄的問題,就寫下來說說,所以每一篇內容可能不是單一主題。


1、先來看一道簡單題目。有下面這個繼承類:

class Person
{
public:
    void Walk()                //普通人的“走”
    {
        cout << "Person::Walk I am an Ordinary People." << endl;
    };        
};

class Student : public Person
{
public:
    void Walk()                //學生的“走”
    {
        cout << "Student::Walk I am a student." << endl;
    };        
};

你沒看錯Walk()是非虛擬函式。請解釋下面程式碼:

Student s;
Person* pp = &s;
pp->Walk();

Student* ps= &s;
ps->Walk();

結果是這樣的:

分析:Walk()是非虛擬函式,被靜態繫結所限制,所以pp、ps是什麼型別就決定了呼叫的版本。這裡,我還要說明的一點是:明白介面繼承和實現繼承。宣告一個non-virtual函式的目的是為了令derived class繼承函式的介面及一份強制性實現。所以,絕不要重新定義繼承而來的non-virtual函式


 2、下面這個問題實質上也是靜態繫結與動態繫結的問題,但看起來不那麼明顯。

class Shape
{
public:
    enum ShapeColor{Red, Green, Blue};                //形狀顏色

    virtual void Draw(ShapeColor color = Red) const = 0;
};

class Circle : public Shape
{
public:
    virtual void Draw(ShapeColor color) const
    {
        cout << "I am Circle::Draw. ";
        cout << "My color = " << color << endl;
    }
};

class Rectangle : public Shape
{
public:
    virtual void Draw(ShapeColor color = Green) const            //預設的引數值被更改了
    {
        cout << "I am Rectangle::Draw. ";
        cout << "My color = " << color << endl;
    }
};

我主要想說兩個問題。

(1)當你下面這樣呼叫時,請解釋會發生什麼情況。

Circle cr;            //(1) 編譯不通過
cr.Draw();            

Shape *ps = &cr;    //(2)
ps->Draw();

沒錯,(1)通過物件呼叫而不指定引數是錯誤的,而(2)的結果是這樣的:color = 0代表Red這你應該是知道的。

分析:通過物件呼叫是靜態繫結,一定要指定引數值,因為靜態繫結這個函式不從base class繼承預設引數值。動態繫結卻可以從base class繼承引數值。注意,這裡我就不強調動態繫結和靜態繫結的概念了,但下面這個一定是靜態繫結:

Circle cr;
Circle *ps = &cr;    //這還是靜態繫結,靜態型別Circle *,編譯不通過
ps->Draw();

(2)第二個我想說的問題,請解釋下面的呼叫結果。

Shape* ps1 = new Rectangle;
ps1->Draw();

Shape* ps2 = new Circle;
ps2->Draw();

是這樣令人可喜的結果:

你是說,你在Rectangle中已經將Draw的預設值改為1(Green)了,怎麼沒效果?

分析:Rectangle::Draw的預設引數值為GREEN,但ps2的靜態型別為Shape*,所以此呼叫的預設引數值來自Shape class。

如果你非要讓Rectangle::Draw的引數有所改變,可以這樣呼叫(提供引數):

Shape* ps4 = new Rectangle;
ps4->Draw(Shape::Green);

Shape* ps5 = new Circle;
ps5->Draw(Shape::Green);

這個問題是想提醒你:virtual函式是動態繫結,預設引數值是靜態繫結。所以,不應該重新定義這個預設引數值。


3、多重繼承為什麼會含有多個虛表指標而不是一個?

這道題是我看一位同學面試經驗時,面試官提的,我試著回答一下,不知道在不在點子上,還請補充和指正。

答:多重繼承下,因為編譯器對一個derived class實現了n-1個虛表,n表示上一層base class的個數,當然假設每個base class都有至少有一個virtual函式,否則編譯器是不會為其新增vptr和vtbl了。所以說有多少個虛表,自然就有多少個指標指向,而不是一個。

這樣說我不知道合理不合理,可能面試官要問的點是“為什麼需要多個虛表?一個虛錶行不行?”

這個屬於編譯器廠商做的事情,標準並未規範。C++的父親就做出過這樣的一款編譯器原型,通過增大vtbl的體積,每個slot上不只有一個指標,還有一個offset,用來調整this指標的指向。

這樣做的弊端是:所有vtbl中的虛擬函式指標都包含這樣一個offset,並且假設不需要調整this指向,呼叫時還是要做offset的加法操作,儘管offset此時為0。另外,vtbl中每個slot體積的膨脹。這些都是效率問題。

實際上,用來調整this的指向用的比較多的是trunk技術,必須以彙編編寫才能獲得高效率。另外,sun編譯器就是把多個虛表連鎖為1個,每個表格中含有下一個表格的指標(通過offset方式),這樣就需要一個指標就好了。

理解能力有限,不知道問的是不是這麼一回事?

 


目錄:

C++中幾個值得分析的小問題(1)

C++中幾個值得分析的小問題(2)

相關文章