C++的黑科技(深入探索C++對象模型)


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

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

當然那時我還不太明白,友元與虛繼承我都極少接觸過,只是知道有這些東西,回頭搜了一下“不能被繼承的類”的做法,具體如下:

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

吳總當時評價說,“呵呵,虛繼承,感覺完全是黑科技啊”

這個黑科技真是戳中我笑點,但想到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,在派生類中,它的構造函數是可以被使用的,即派生類不會因此受到限制。

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

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

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

  • 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++虛繼承的黑科技

針對虛繼承,可以坦承的一點就是

所有簡單的東西,遇到虛繼承,似乎都要單獨拿出來討論


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM