C++的黑科技

週二面了騰訊,以前只投了TST內推,貌似就是TST面試了面試

其中有一個問題,「如何產生一個不能被繼承的類」,這道題我反反覆覆只想到,將父類的構造函數私有,讓子類不能調用,最後歸結出一個單例模式,但面試官說,單例模式做爲此題的解答不夠靈活,後來面試官提示說,能夠用友元+虛繼承,能夠完美實現這樣一個類函數

固然那時我還不太明白,友元與虛繼承我都極少接觸過,只是知道有這些東西,回頭搜了一下「不能被繼承的類」的作法,具體以下:指針

1,聲明一個類,CNoHeritance,構造函數爲private,並聲明友元類CParent;
2,讓CParent虛繼承CNoHeritance
這樣CParent就成爲一個能夠被正常實例化,但又不能被繼承的類code

吳總當時評價說,「呵呵,虛繼承,感受徹底是黑科技啊」對象

這個黑科技真是戳中我笑點,但想到C++常常有些奇妙的東西,如今想總結一下繼承

1,C++構造函數的黑科技

對於閱讀過進階C++書籍的都該知道,編譯器會在「須要」的時候,那麼什麼是須要的時候呢?四種狀況:內存

  • 1,「帶有Default Constructor」的Member Class Object
  • 2,「帶有Default Constructor」的Base Class
  • 3,「帶有至少一個Virtual Function」的Class
  • 4,「帶有一個Virtual Base Class」的Class

自動合成的構造函數每每都是public,在派生類中,它的構造函數是能夠被使用的,即派生類不會所以受到限制。ci

那麼,如何能使派生類不能使用基類的函數或成員呢?編譯器

  • private:只能由:1,該類中的函數;2,其友元函數訪問
  • protected:能夠被:1,該類中的函數;2,其友元函數;3,派生類(子類)的函數訪問
  • public:能夠被:1,該類中的函數;2,其友元函數;3,子類的函數;4,該類的對象訪問

若是一個類的構造函數聲明爲private,則其派生類甚至該類的對象都不能訪問,意味着兩點:string

  • 1,該類不能被繼承
  • 2,該類不能由系統實例化,即它實例化的對象不會在棧內存上

那麼怎麼使用該類呢?通常而言,會經過該類的函數來建立

class A
{
private:
    A(){}
public:
    A& createA()
    {
        A* p=new A();
        return *p;
    }
};

然而,這樣又引伸一個問題:類沒有實例化,如何能使用其成員函數呢?

答案是將該成員函數聲明爲static,這樣不須要實例化便可訪問,即將上述改成:

class A
{
private:
    A(){}
public:
    static A& createA()
    {
        A* p=new A();
        return *p;
    }
};

A Object=A::createA();

很明顯,上面的實例化過程很不方便,簡直是艱辛呀,單例模式的其中一種實現就是如此,在此先不講。這樣實現的類,不能被繼承,但本身也很差過

so,若是用友元來實現,是怎麼實現的呢?

聲明一個類,及其友元

class A
{
private:
    A(){}

    friend class B;
};

那麼B是能夠調用A的private的構造函數的,那麼讓B虛繼承A會發生什麼事呢?

由《深度探索C++對象模型》看到,B內存中將有一份A類的實體,調用A的構造函數構造的,這對於友元類B是可行的

class A
{
private:
    A(){}

    friend class B;
};

class B : virtual A
{
};

那麼這樣的B能不能被繼承呢?假設有個類繼承了B,以下

class A
{
private:
    A(){}

    friend class B;
};

class B : virtual A
{
};

class C : B
{
};

考慮到虛繼承的特性,C也將調用A的構造函數構造出一個A,但!!C並非A的友元類,因此根本不能執行A私有的構造函數,這段程序,若是不實例化C,編譯器不會報錯,但一旦實例化C,則將報錯。

而B是能夠正常實例化的一個類,這樣就完美實現了一個不能被繼承的類:B

2,C++構造函數初始化列表的黑科技

相比於構造函數的各類trick,C++的初始化列表就顯得很容易了,只有那麼一點要注意:

C++的初始化列表的賦值順序,是與C++類裏面成員變量的聲明順序相關,與初始化列表裏的順序無關

舉個例子,如下就會出現莫名錯誤:

class A
{
public:
    A(int _x, int _y):y(_y), x(y){}
public:
    int x;
    int y;
};

根據聲明順序,在初始化列表中,是先完成x(y)這個步驟,但此時y並無被賦值,因此獲得的x是個隨機的值。

3,C++虛函數的黑科技

C++虛函數的問題,幾乎是面試必問,實際上須要瞭解的東西也挺多,我本身在前幾回面試,都有些理解有誤的地方,或者理解不夠完善

這裏總結幾點吧(如下類都是針對有虛函數的類):

  • 1,每一個類都有虛函數表,這個虛函數表是在編譯階段構建,在代碼段產生一個vtbl
  • 2,每次實例化的時候,構造函數在前幾個字節,產生一個指向虛函數表的指針,指向代碼段的那個虛函數表
  • 3,虛函數的實現與調整,是經過移動或變換虛函數表的指針來實現的。
  • 4,純虛函數是指只聲明,但未被實現的虛函數,具備純虛函數的類不能被實例化,爲抽象類

4,C++拷貝構造函數的黑科技

C++的拷貝構造函數是C++默認的四個函數之一:構造函數、析構函數、賦值函數、拷貝構造函數

拷貝構造函數是一種特別的構造函數,在《深度探索C++對象模型》書中說,有三種狀況,會致使拷貝構造函數被觸發:

  • 1,以一個object的內容做爲另外一個class object的初始值
class X {...}
X x;
X xx=x;
  • 2,當object被看成參數傳遞給某個函數時
void foo(X x);
X xx;
foo(xx);
  • 3,函數傳回一個class object的時候
X foo_bar()
{
  X xx;
  // ...
  return xx;
}

通常狀況下,若是沒有提供explicit copy constructor時,會發生什麼呢?

一個良好的編譯器能夠爲大部分class objects產生bitwise copies,由於它們有bitwise semantics...

這裏說的很神奇,好像咱們不須要本身寫copy constructor也沒問題同樣,實際上,bitwise copies在有些狀況下是很是不推崇的

首先解釋下什麼是bitwise copies:這是指,在拷貝過來的時候,把class的內存直接位拷貝過來,便可以當作是內存拷貝(對應的有值拷貝)

位拷貝有不少問題,典型的一個,若是class裏面含有分配內存的指針,那麼它會將指針指向的地址直接拷貝過來:

class A
{
public:
    int *p;
};

int main()
{
    A a1;
    a1.p=new int[10];
    A a2=a1;
    cout << a1.p << endl;
    cout << a2.p << endl;
    return 0;
}

這裏能夠發現,a1.p的地址與a2.p的地址是同樣的,那麼,我分配的內存,該由哪一個釋放呢?我釋放了,另外一個怎麼辦呢?

實際上,這種拷貝方式在STL的string裏面確定是要重寫的,不能用位拷貝。

《深度探索C++對象模型》中,說class不展示出「bitwise copy semantics」有四種狀況:

  • 1,當class含有member object而且後者有一個copy constructor(聲明或合成)
  • 2,當class繼承一個base class 然後者存在一個copy constructor的時候
  • 3,當class聲明瞭一個或多個virtual functions時
  • 4,當class派生自一個繼承串鏈,其中有一個或多個virtual base classes時

其實主要都是擔憂,指針在bitwise semantics下,隨便複製可能會致使不可預料的錯誤

在這裏說一下賦值函數拷貝構造函數在觸發上的區別:

當一個object從無到有時,觸發的必定是拷貝構造函數,賦值函數只會在已有的object賦值時,纔會觸發

5,C++虛繼承的黑科技

針對虛繼承,能夠坦承的一點就是

全部簡單的東西,遇到虛繼承,彷佛都要單獨拿出來討論

待續

相關文章
相關標籤/搜索