# [AIdrifter CS 浮生筆錄](https://hackmd.io/s/rypeUnYSb) <br>C++ Intrview # Smart Pointer(智能指針) > A1: 請問下面程式碼有什麼問題呢? ```C++ class object { public: int a; int b; } void Process(Object *p){ // fo somethinh with p } int main(){ while(true) { object* p = new Object(); Process(p); } } ``` Q1: 沒有人處理佔Heap空間的`object *p` -> 如果有一個幫忙管理pointer的釋放那該有多好? ```diff= - void Process(Object *p){ + void Process(SmartPOinter &p) - object* p(new Object()); + SmartPointer p(new Object); ``` :::info smart pointer其實不是一個指針,他是一個class,幫我們管理指針,維護其生命週期,再也不用擔心memory leak啦~ - How to implement smart pointer? - How to release memory?(如何) - When to release memory? (何時) ::: ## How to release memory? ### 1. 基本板 在destructor時,release memory(同歸於燼) ```C++ class SmpartPointer{ public: SmpartPointer(object *p){ ptr = p; } ~SmartPointer(){ delete ptr; } private: Object *ptr; } ``` ### 2. Reference Count 解決方方法:引用用計數! • 引用用計數:對一一個指針所指向的內存, 目前有多少個對象在使用用它 • 當引用用計數為0時,刪除對象 • 多個智能指針對象共享同一個引用用計數類 • 在進行行賦值等操作時,動態維護引用用計數 ![](https://i.imgur.com/VcLGsdn.png) 所以目前我們有兩個問題要解決 1. 如何對指針引用的內存進行計數? 2. 如何改進`SmartPointer` class使得它能動態維護引用記數? Ans: 創造一個引用計數類 ```c++ class Counter { friend class SmartPointerPro; public: Counter(){ ptr = NULL; cnt = 0; } Counter(Object *p){ ptr = p; cnt = 1; } ~Counter(){ delete ptr; } private: Object *ptr; int cnt; } ``` - 如何動態維護**引用用計數**? 引用用計數改變發生生在如下時刻: - 調用用構造函數時: ```C++ SmartPointer p(new Object()); ``` - 賦值構造函數時: ```C++ SmartPointer p(const SmartPointer &p); ``` - 賦值時: ```C++ SmartPointer p1(new Object()); SmartPointer p2 = p1 // here ``` - Final: 多考慮參數**傳遞問題** 和 **賦值問題** ```C++ class SmpartPointerPro{ public: SmpartPointerPro(object *p){ ptr_counter = new Counter(p); } SmartPointerPro(const SmartPointerPro &sp){ ptr_counter = sp.ptr_counter; ptr_counter->cnt++; } SmartPointerPro& operator (const SmartPointerPro &sp){ sp.pter_counter->cnt++; ptr_counter.cnt--; if (ptr_counter.cnt == 0) delete ptr_counter; ptr_counter = sp.ptr_counter; } ~SmartPointerPro(){ ptr_counter->cnt--; if (ptr_counter->cnt==0) delete ptr_counter; } private: Counter *ptr_counter; } ``` ## 如何獲取智能指針所包裝的指針呢? ### GetPtr() and GetObject() ```C++ class SmartPointerPro{ public: Object *Getptr(){ return ptr_counter->ptr; } Object& GetObject(){ return *(ptr_counter->ptr) } private: Counter *ptr_counter; } ``` ### Overloaded operators `->` and `*` 更自然的寫法,重載指針的操作符,使得SmartPointerPro class的對象可以像指針一樣被使用。 ```C++ class SmartPointerPro{ public: Object* operator->{ return ptr_counter->ptr; } Object& operator*(){ return *(ptr_counter->ptr) } private: Counter *ptr_counter; } int main(){ SmartPointerPro p(new Object()); p->a = 10; p->b = 20; int a_value = (*p).a; int b_value = (*p).b; } ``` ## C++ libraries 中的智能指針 ### auto_ptr C++庫中的智能指針 `std::auto_ptr`, 包含頭文文件`#include<memory>` 即可使用, 使用時注意以下問題: 1. 不是基於引用用計數的實現,一個指針在同一時刻只能被一個對象擁有 2. 儘量不要賦值,如果使用用了,請不要再使用用之前的對象 3. 不要當成參數傳遞 4. 不能放入入vector等容器中 ### boost::shared_ptr 基於reference count實做的智慧指針,需要`include <boost/smart_ptr.hpp>` - shared_ptr 看起來很完美,但是會有**循環引用**的問題: ```c++ class parent; class child; using namespace std; typedef boost::shared_ptr<parent> parent_ptr; typedef boost::shared_ptr<child> child_ptr; class parent{ public: ~parent(){ cout<<"destroying parent"<<endl;} child_ptr child; }; class child{ public: ~child() {cout<<"destroying child" << endl;} parent_ptr parent; } int main(){ parent_ptr father(new parent()); child_ptr son(new child()); // father->son->father->son.... father->children = son; son->parent = father; } ``` ### boost::weak_ptr 可以用來解決剛剛**循環引用**的問題 https://blog.csdn.net/u014294391/article/details/71435513 ### 其他問題 引申問題: java的垃圾回收機制 引用計數,分代回收,Mark and Sweep, Copy and Sweep ... https://www.cnblogs.com/dolphin0520/p/3783345.html https://www.cnblogs.com/laoyangHJ/articles/java_gc.html https://www.iteye.com/blog/jefferent-1123677 https://blog.csdn.net/initphp/article/details/30487407 # Singleton - 實例: - 螢幕上的鼠標只有一個 - 系統的日誌輸出 - 單例模式需要解決的問題: - 如何保證只產生一個object - 如何釋放該object的memory - 如何做到Thread Safe ### 1. 保證只產生一個object ```C++ #include <iostream> using namespace std; class singleton{ private: static singleton *m_instance; singleton(){}; public: static singleton *getInstance(){ if(m_instance == NULL) { m_instance = new singleton(); } return m_instance; } }; singleton *singleton::m_instance = NULL; int main(){ singleton *ptr = singleton::getInstance(); return 0; } ``` Q: 是不是還有什麼忘記考慮? Ans: 少考慮了**參數傳遞** 和 **賦值問題** ```diff= +singleton(const singleton &){} +singleton& operator=(const singleton& s){} ``` ### 2. 釋放該object的memory 如何簡單釋放最後的instacne? 機智的作法! ```diff= static singleton *getInstance(){ static singleton m_instance; return m_instance; } ``` ### 3. Thread Safe - way1: C++ 0x之後 ,剛剛的寫法就可以保證Thread Safe - way2: double check( 其實way1 就很夠了...) - check instance twice - **Lock()** and **Unlock()** ```diff class singleton{ private: static singleton *m_instance; singleton(){}; public: static singleton *getInstance(){ if(m_instance == NULL) { + Lock(); + if ( m_instance == NULL) { + Singleton* tmp = new singleton(); + m_instance = tmp; + } + Unlock(); } return m_instance; } }; ``` PS. 某些平台其實double check也是不安全的(與compiler 優化有關) http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html https://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/ # Struct 對齊問題 - struct的對齊係數和以下幾項有關 - 元素大小 - 元素順序 - `#pragma check`參數 :::info • struct中的子子項在內存中按順序排列,在沒有`#progm pack(n)`的情況,各個子項的對齊係數為自己的長度。 • 在有#progma pack(n)的情況下,各子項的對齊係數數為**min(自己的長度, n)** • struct整體的對齊係數為子項對齊係數最大的 ::: ## common > 用32bits 為access單位,擺放都是以可以一次access到對應的variable為主。 ```C++ struct A { char a; char b; char c; } ``` ![](https://i.imgur.com/XqWndHs.png) ```C++ struct A { int a; char b; short c; } ``` ![](https://i.imgur.com/qSd0uWA.png) ```C++ struct A { char b; int a; short c; } ``` ![](https://i.imgur.com/ZLQuuTU.png) ## `#prama pack` > 為了節省空間 ```C++ #define pack (2) struct A { char b; int a; short c; } ``` ![](https://i.imgur.com/yKgHdgy.png) ## 為什麼我們需要地址對齊? - 為什麼我們需要地址對齊? - 某些cpu架構不支持非對齊的地址存取 - 地址不對齊,會影響程序的效率如,如上例`int a`,會需要**兩次**地址訪問。 ![](https://i.imgur.com/yKgHdgy.png) # C++ 容易混淆的概念 ## reference vs pointer - Q:引用和指針有什麼區別? - A:本質:一個是別名,一個是地址 - 指針可以在運行時改變其所指向的值,引用一旦和某個對象綁定就不再改變 - 引用沒有const,指針有const - 從內存上看,指針會分配內存區域,而引用不會,它僅是一個別名 - 在參數傳遞時,引用用會做類型檢查,而指針不會 - 引用用不能為空,指針可以為空 ## const vs define vs inline ### const vs define - 本質:define只是字符串替換, const參與編譯運行 - define不會做類型檢查,const擁有類型,會執行相應的類型檢查 - define僅是宏替換,不佔用用內存,而而const會佔用內存 - const內存效率更高高,編譯器通常將const變量保存在符號表中,而**不會分配存儲空間**,這使得它成為一個**編譯期間的常量**,沒有**存儲**和**讀取**的操作 ### define vs inline - 本質:和前面面一樣,define只是字符串替換,inline由編譯器控制 - define只是簡單的宏替換,通常會產生二義性; 而inline會真正地編譯到代碼中 - inline函數是否展開由編譯器決定,有時候當函數太大時,編譯器可能選擇不展開相應的函數 ## malloc()/free() vs new/delete - malloc/free 是C語言的庫函數,new/delete是C++的Operator(操作符) - malloc僅用來分配內存,而不會執行相應的構造函數,函數返回值為void*,而new會調用用相應的構造函數,返回的是相應對象類型的指針 ## static ### 擴展生命週期 ex: singleton ### 限制作用域 用於全局變量,與普通全局變量不同的是。它表明該變量的作用域僅限於**當前cpp文件**。因此當其他cpp文件中同樣出現同名的static變量時,他們是不同的獨立的變量。 ### 靜態成員函數 與 靜態變量 延伸問題: - Q1:成員函數是否能同時修飾為static 和 const的呢? - A1:答案是**不可以**。 - C++編譯器在實現const的成員函數的時候為了確保該函數不能修改類的實例的狀態,會在函數中添加一個隱式的參數`const this*`。但當一個成員為static的時候,該函數是沒有this指針的。也就是說此時const的用法和static是衝突的。 - static的作用是表示該函數只作用在class的**靜態變量**上,與類的實例沒有關係; - const的作用是確保函數**不能修改類的實例**的狀態,與類型的靜態變量沒有關係。 - Q2: static 函數能否調用**非static** 函數呢? - A2: [Callback Function](https://www.cnblogs.com/rickyk/p/4238380.html) ## const 用法 ### Base • 修飾變量 • 指向const變量的指針 && const指針 • 指向const變量指針: ```C++ const int* ptr; int const* ptr; ``` • const指針 ```C++ int* const ptr; ``` ### 真的不能修改? ```C++ #include <stdio.h> int main() { const int a = 10; int *ptr = NULL; ptr = (int*)(&a); *ptr = 20; printf("%d \n", *ptr); // 20 printf("%d \n", a); // 10 return 0; } ``` Why? Ans: Compiler會對const做優化,const變量從符號表取值。 ```shell 20 10 ``` 改成下面這樣的代碼就沒問題了 ```diff= - const int a = 10; + const volatile int a = 10; ``` ### 修飾成員函數 對於不修改成員變量的函數,一般我們都要宣稱是const函數 ```diff class point{ public: + void GetX() const { return x; } + void GetY() const { return y; } private: int x; int y } ``` ## C++的四種cast ### reinterpret_cast reinterpret_cast:**轉換一個指針為其它類型的指針**。 它也允許從一個指針轉換為整數類型,反之亦然. 這個操作符能夠在非相關的類型之間轉換. 操作結果只是簡單的從一個指針到別的指針的 值的二進制拷貝. 在類型之間指向的內容**不做任何類型的檢查和轉換**。 ```C++ class A{}; class B{}; A* a = new A; B *b = reinterpret_cast<B*>(a); ``` ### static_cast - `static_cast`允許執行任意的**隱式轉換**和相反轉換動作(即使它是不允許隱式的) - 例如:應用到類的指針上, 意思是說它允許子類型的指針轉換為父類型的指針(這是一個有效的隱式轉換), 同時, 也能夠執行相反動作: 轉換父類為它的子類: :::info 隱式轉換:編譯器根據需要自動轉換變數型別。 顯示轉換:也稱為強制型別轉換,要定義型別轉換成要用的值的型別。 ref: https://www.itread01.com/content/1547556869.html ::: ```C++ class Base {}; class Derived: public Base{}; Base *a = new Base; Derived *b = static_cast<Derived *>(a); ``` - 當然,除了指針類型,`static_cast`也可直接應用用於類,以及基礎類型之間的轉換 ### dynamic_cast `dynamic_cast`只用於對象的指針和引用. 當用於多態類型時, 它允許任意的隱式類型轉換以及相反過程. 不過,與`static_cast`不同, 在後一種情況裡(註:即隱式轉換的相反過程),dynamic_cast會檢查操作是否有效. 也就是說, 它會檢查轉換是否會返回一個被請求的有效的完整對象。檢測在運行時進行. **如果被轉換的指針不是一個被請求的有效完整的對象指針,返回值為NULL**. 對於引用類型,會拋出bad_cast異常: 在這邊,因為derived其實記憶體空間比base還要大,所以如果想要用Derived ptr去access Base Object,這邊用Dynamic cast就會被擋下來。 ```C++ class Base {virtual dummy(){}}; class Derived: public Base {}; Base *b1 = new Derived; Base *b2 = new Base; Derived d1 = dynamic_cast<Derived *>(b1); // succeeds; Derived *d2 = dynamic_cast<Derived *>(b2); // fails, return 'NULL' ``` ### const_cast 為了要移除const屬性用的: ```c++ class C {}; const C *a = new C; C *b = const_cast<C*>(a); ``` # Vritual function > Q1 : what's your output? ```C++ class Base { public: virtual void Print() const { cout<< "Print in Base" << endl; } }; class Derive: public Base { public: virtual void Print() const { cout<< "Print in Derive" << endl; } }; void Print(const Base *base) { base->Print(); } int main() { Base b; Derive d; Print(&b); Print(&d); return 0; } ``` > A1: virtual function 可以再run time實現dynamic binding ```shell Print in Base Print in Derive ``` > Q2: 如果把`virtual` 拿掉會出現什麼? ```diff -virtual void Print() const +void Print() const ``` > A2: ```shell Print in Base Print in Base ``` - 實現動態綁定有兩個條件 - 相應成員函數為virtual function - 使用base object的**reference**或**pointer**去進行調用 ## C++ virtual function底層的實現機制 - Virtual method table - virtual table pointer > 注意:虚函数表⼀一个类有⼀一个,⽽而不是⼀一个对象⼀一个 > 64bit machine have to translate `int` to `long` ![](https://i.imgur.com/PIs9sCk.png) ```C++ using namespace std; class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } private: virtual void j() { cout << "Base::j" << endl;} }; class dev: public Base { public: virtual void k() { cout << "dev::k" << endl; } }; typedef void(*Fun)(void); //void類型的函數指針 int main() { Fun pFun = (Fun)*((long*)*(long*)(&d)+0); pFun(); // f pFun = (Fun)*((long*)*(long*)(&d)+1); pFun(); // g pFun = (Fun)*((long*)*(long*)(&d)+2); pFun(); // h } ``` ```shell= Base::f Base::g Base::h ``` ### 一般繼承(無虛函數覆蓋) ![](https://i.imgur.com/s9MUuX8.png) 在這個繼承關係中,子類沒有重載任何父類的函數。那麼,在派生類的實例中,其虛函數表如下所示: ![](https://i.imgur.com/8gM0uaG.png) > 注意:⼦子类的虚函数表是⼦类的,从⽗父类拷⻉贝⼀一份过来,并进⾏行修改 ### 一般繼承(有虛函數覆蓋) ![](https://i.imgur.com/0q8PiAZ.png) 覆蓋了父類的一個函數:`f()`。那麼,對於派生類的實例,其虛函數表會是下面的一個樣子: ![](https://i.imgur.com/XYmoatZ.png) 我們從表中可以看到下面幾點, 1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。 2)沒有被覆蓋的函數依舊。 ```C++ class dev: public Base { public: virtual void f() { cout << "dev::f" << endl; } virtual void g1() { cout << "dev::g1" << endl; } virtual void h1() { cout << "dev::h1" << endl; } }; Base *b = new dev(); b->f(); ``` ```shell= dev::f ``` ### 多重繼承(無虛函數覆蓋) ![](https://i.imgur.com/nvRpzsY.png) 我們可以看到: 1) 每個父類都有自己的虛表。 2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的) > 注意:子类的虚函数`g1()`被放在了第⼀一个⽗父类中 ![](https://i.imgur.com/IIJVVWc.png) ### 多重繼承(有虛函數覆蓋) 我們在子類中覆蓋了父類的f()函數。 ![](https://i.imgur.com/Pi9JYH8.png) 我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類 ![](https://i.imgur.com/2MEttnF.png) ```C++ class Base1 { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; class Base2 { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; class Base3 { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } }; class Derive: public Base1, public Base2, public Base3 { public: virtual void f() { cout << "Derive::f" << endl; } virtual void g1() { cout << "Derive::g1" << endl; } }; int main() { Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g() return 0; } ``` ```shell Derive::f Derive::f Derive::f Base::g Base::g Base::g ``` ## 題目 ### 为什么需要虚析构函数(virtual destructor)? > 子類的constructor沒有被調用 ```C++ #include <iostream> using namespace std; class Base{ public: Base(){cout<<" Base constructor"<<endl;} ~Base(){cout<<" Base destructor"<<endl;} }; class Derive: public Base{ public: Derive(){cout<<" Derive constructor"<<endl;} ~Derive(){cout<<" Derive destructor"<<endl;} }; int main(){ Base *b = NULL; b = new Derive(); delete b; return 0; } ``` ```shell= Base constructor Derive constructor Base destructor ``` ```diff= - ~Base(){cout<<" Base destructor"<<endl;} + virual ~Base(){cout<<" Base destructor"<<endl;} ``` ```shell= Base constructor Derive constructor Derive destructor Base destructor ``` 在《Modern C++ Design》第四章有提到,在 C++ 標準中,物件的 operator delete 有兩種的重載介面: - void operator delete(void* p) - void operator delete(void* p, std::size_t size) 當你在 delete 物件時, compiler 需要知道要釋放多少的空間,而這個資訊就是需要由 size 來提供。那麼一般在重載第一種沒有提供大小資訊的 operator delete 介面時,compiler 會依照許多種作法來幫你自動產生些許的程式碼,來提供相當於 size 的資訊。在書中有提到 compiler 可能會有下列作法: 1. 由 virtual ~Derive 來提供 sizeof(Derive) 的大小。(我需要confirm這句話) 2. 將 Derive 的大小放在 vtable 裡。 以上跟書中所提到的另外兩種作法,都是在擁有 virtual 解構子的前提下。 結論就是,若是你不宣告一個 virtual 解構子,那麼 delete Derive 只會釋出 base 大小的空間,而造成一些 Undefined Behavior。不過一般良好強大的 compiler 都會偵測到這種錯誤,而噴出 warnning(而不是 error),但也是讓你編譯過,反正就是通知的義務己盡,但是運行後所產生的 UB 就要後果自負。 ### 访问虚函数和普通函数哪个快? O(1) common functino O(n) vtable, find your virtual function, dynamic binding ### 析构函数(desctructor)⼀一定是虚函数吗? 1. 訪問效率 2. 該parent class沒有子類 ### 内联函数、构造函数、静态成员函数可以是虚函数吗?” inline: 在 compiler階段就展開了,不可能dynamic binding Constructor: 父類: 父類constructor 子類: 父類constructor + 子類constructor :::info 這種性質沒有動態綁定 ::: Static 靜態成員函數: 它是一個class所有的,所有的object都是用同一個靜態成員函數。 ### 構造函數中可以調用virtual function 嗎? 可以,但是完全沒有作用。 你沒有辦法再父類剛constructor出來的 object去調用子類的方法,因為子類還沒建構出來。 https://blog.csdn.net/K346K346/article/details/49872023 ```C++ #include <iostream> using namespace std; class A { public: virtual void show(){ cout<<"show in A"<<endl; } A(){cout<<"costructor A"<<endl;} virtual ~A(){show(); cout<<"destructor A"<<endl;} }; class B:public A { public: B(){cout<<"costructor B"<<endl;} ~B(){cout<<"destructor B"<<endl;} void show(){ cout<<"show in B"<<endl; } }; int main() { cout<<" ================= "<<endl; B *b = new B(); delete b; } ``` 在類B的對象b退出作用域時,會先調用類B的析構函數,然後調用類A的析構函數,在析構函數~A()中,調用了虛函數show()。從輸出結果來看,類A的析構函數對show()的B調用並沒有發生虛調用。 ```shell= costructor A costructor B destructor B show in A # still A destructor A ``` ref: https://blog.csdn.net/K346K346/article/details/49872023 ## 虛擬繼承相關問題 這東西其實很蛋疼,老師說非常爛。 > Q1: 什麼是**虛擬繼承**,為什麼要用它? ```c++ class Base { public: void print() { cout << "Base::f" << }; class Mid1:public Base { }; class Mid2:public Base { }; class Derive:public Mid1, public Mid2 { }; int main(){ Derive d; d.print(); // compiler error } ``` A1: 虚继承⽤用来解决这种菱形继承的⼆二义性问题 虛擬繼承(使用virtual方式繼承,為了保證繼承後父類的內存佈局只會存在一份) ```diff - class Mid1:public Base + class Mid1:virtual public Base - class Mid2:public Base + class Mid2:virtual public Base ``` - How to implement: - 和编译器⾼高度相关,不同编译器实现不同 - ⼀种思路:在Mid1对象和Mid2对象中添加虚基类指针,指向基类对象(Base),这样Mid1和Mid2中就会指向共同的基类成员,从⽽而消除了⼆义性 ![](https://i.imgur.com/u56HJWI.png) Q2: 萬惡的`sizeof()`題目,此類題目與compiler高度相關,沒有唯一答案。 ```C++ #include <iostream> using namespace std; class A { public: int a; virtual void myfunA(){}; }; class B:virtual public A { public: virtual void myfunB(){}; }; class C:virtual public A { public: virtual void myfunD(){}; }; class D: public B, public C { public: virtual void myfunD(){}; }; int main(){ cout << sizeof(A) <<endl; cout << sizeof(B) <<endl; cout << sizeof(C) <<endl; cout << sizeof(D) <<endl; } ``` A2: 大小通常由這些因素決定 - 類成員的大小 - virtual table pointer大小 - 需基類指針 ```shell= 16 24 24 32 ``` # C++ 在memory中 真實的樣子 老師推薦leveldb https://github.com/google/leveldb ## 單一繼承 ![](https://i.imgur.com/37DXgPi.png) ```C++ #include <iostream> using namespace std; class Parent { public: long iparent; Parent ():iparent (10) {} virtual void f() { cout << " Parent::f()" << endl; } virtual void g() { cout << " Parent::g()" << endl; } virtual void h() { cout << " Parent::h()" << endl; } }; class Child : public Parent { public: long ichild; Child():ichild(100) {} virtual void f() { cout << "Child::f()" << endl; } virtual void g_child() { cout << "Child::g_child()" << endl; } virtual void h_child() { cout << "Child::h_child()" << endl; } }; class GrandChild : public Child{ public: long igrandchild; GrandChild():igrandchild(1000) {} virtual void f() { cout << "GrandChild::f()" << endl; } virtual void g_child() { cout << "GrandChild::g_child()" << endl; } virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; } }; int main(){ typedef void(*Fun)(void); GrandChild gc; long** pVtab = (long**)&gc; cout << "[0] GrandChild::_vptr->" << endl; for (int i=0; (Fun)pVtab[0][i]!=NULL; i++){ Fun pFun = (Fun)pVtab[0][i]; cout << " ["<<i<<"] "; pFun(); } } ``` ```shell [0] GrandChild::_vptr-> [0] GrandChild::f() [1] Parent::g() [2] Parent::h() [3] GrandChild::g_child() [4] Child::h1() [5] GrandChild::h_grandchild() ``` ```C++ cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl; cout << "[2] Child.ichild = " << (int)pVtab[2] << endl; cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl; ``` ```shell= [1] Parent.iparent = 10 [2] Child.ichild = 100 [3] GrandChild.igrandchild = 1000 ``` > 可見以下幾個方面: 1)虛函數表在最前面的位置。 2)成員變量根據其繼承和聲明順序依次放在後面。 3)在單一的繼承中,被overwrite的虛函數在虛函數表中得到了更新。 ![](https://i.imgur.com/Ne4S2Xh.png) ## 多重繼承 假設有下面這樣一個類的繼承關係。注意:子類只overwrite了父類的`f()`函數,而還有一個是自己的函數(我們這樣做的目的是為了用`g1()`作為一個標記來標明子類的虛函數表)。而且每個類中都有一個自己的成員變量: ![](https://i.imgur.com/qpYDogO.png) ```c++ #include <cstring> #include <string.h> #include <iostream> using namespace std; class Base1 { public: long ibase1; Base1():ibase1(10) {} virtual void f() { cout << "Base1::f()" << endl; } virtual void g() { cout << "Base1::g()" << endl; } virtual void h() { cout << "Base1::h()" << endl; } }; class Base2 { public: long ibase2; Base2():ibase2(20) {} virtual void f() { cout << "Base2::f()" << endl; } virtual void g() { cout << "Base2::g()" << endl; } virtual void h() { cout << "Base2::h()" << endl; } }; class Base3 { public: long ibase3; Base3():ibase3(30) {} virtual void f() { cout << "Base3::f()" << endl; } virtual void g() { cout << "Base3::g()" << endl; } virtual void h() { cout << "Base3::h()" << endl; } }; class Derive : public Base1, public Base2, public Base3 { public: long iderive; Derive():iderive(100) {} virtual void f() { cout << "Derive::f()" << endl; } virtual void g1() { cout << "Derive::g1()" << endl; } }; int main() { typedef void(*Fun)(void); Derive d; long** pVtab = (long**)&d; cout << "[0] Base1::_vptr->" << endl; Fun pFun = (Fun)pVtab[0][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[0][1]; cout << " [1] ";pFun(); pFun = (Fun)pVtab[0][2]; cout << " [2] ";pFun(); pFun = (Fun)pVtab[0][3]; cout << " [3] "; pFun(); pFun = (Fun)pVtab[0][4]; cout << " [4] "; cout<<pFun<<endl; cout << "[1] Base1.ibase1 = " << (long)pVtab[1] << endl; // 對齊問題 long s = sizeof(Base1)/8; cout << "[" << s << "] Base2::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; cout << " [3] "; cout<<pFun<<endl; cout << "["<< s+1 <<"] Base2.ibase2 = " << (long)pVtab[s+1] << endl; // 對齊問題 s = s + sizeof(Base2)/8; cout << "[" << s << "] Base3::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; cout << " [3] "; cout<<pFun<<endl; s++; cout << "["<< s <<"] Base3.ibase3 = " << (long)pVtab[s] << endl; s++; cout << "["<< s <<"] Derive.iderive = " << (long)pVtab[s] << endl; return 0; } ``` ```shell [0] Base1::_vptr-> [0] Derive::f() [1] Base1::g() [2] Base1::h() [3] Derive::g1() [4] 1 [1] Base1.ibase1 = 10 [2] Base2::_vptr-> [0] Derive::f() [1] Base2::g() [2] Base2::h() [3] 1 [3] Base2.ibase2 = 20 [4] Base3::_vptr-> [0] Derive::f() [1] Base3::g() [2] Base3::h() [3] 0 [5] Base3.ibase3 = 30 [6] Derive.iderive = 100 ``` 我們可以看到: 1) 每個父類都有自己的虛表。 2) 子類的成員函數被放到了第一個父類的表中。 3) 內存佈局中,其父類佈局依次按聲明順序排列。 4) 每個父類的虛表中的f()函數都被overwrite成了子類的f()。這樣做就是為瞭解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。 ![](https://i.imgur.com/V56LgUr.png) ## 鑽石型重複繼承 ![](https://i.imgur.com/NiamrrS.png) ![](https://i.imgur.com/VFLTVby.png) 我們可以看見,最頂端的父類B其成員變量存在於B1和B2中,並被D給繼承下去了。而在D中,其有B1和B2的實例,於是B的成員在D的實例中存在兩份,一份是B1繼承而來的,另一份是B2繼承而來的。所以,如果我們使用以下語句,則會產生二義性編譯錯誤: ```C++ D d; d.ib = 0; //二義性錯誤 d.B1::ib = 1; //正確 d.B2::ib = 2; //正確 ``` 注意,上面例程中的最後兩條語句存取的是兩個變量。雖然我們消除了二義性的編譯錯誤,但**B類在D中還是有兩個實例**,這種繼承造成了數據的重複,我們叫這種繼承為重複繼承。重複的基類數據成員可能並不是我們想要的。所以,C++引入了虛基類的概念。 ```C++ #include <cstring> #include <string.h> #include <iostream> using namespace std; class Base1 { public: long ibase1; Base1():ibase1(10) {} virtual void f() { cout << "Base1::f()" << endl; } virtual void g() { cout << "Base1::g()" << endl; } virtual void h() { cout << "Base1::h()" << endl; } }; class Base2 { public: long ibase2; Base2():ibase2(20) {} virtual void f() { cout << "Base2::f()" << endl; } virtual void g() { cout << "Base2::g()" << endl; } virtual void h() { cout << "Base2::h()" << endl; } }; class Base3 { public: long ibase3; Base3():ibase3(30) {} virtual void f() { cout << "Base3::f()" << endl; } virtual void g() { cout << "Base3::g()" << endl; } virtual void h() { cout << "Base3::h()" << endl; } }; class Derive : public Base1, public Base2, public Base3 { public: long iderive; Derive():iderive(100) {} virtual void f() { cout << "Derive::f()" << endl; } virtual void g1() { cout << "Derive::g1()" << endl; } }; int main() { typedef void(*Fun)(void); Derive d; long** pVtab = (long**)&d; cout << "[0] Base1::_vptr->" << endl; Fun pFun = (Fun)pVtab[0][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[0][1]; cout << " [1] ";pFun(); pFun = (Fun)pVtab[0][2]; cout << " [2] ";pFun(); pFun = (Fun)pVtab[0][3]; cout << " [3] "; pFun(); pFun = (Fun)pVtab[0][4]; cout << " [4] "; cout<<pFun<<endl; cout << "[1] Base1.ibase1 = " << (long)pVtab[1] << endl; // 對齊問題 long s = sizeof(Base1)/8; cout << "[" << s << "] Base2::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; cout << " [3] "; cout<<pFun<<endl; cout << "["<< s+1 <<"] Base2.ibase2 = " << (long)pVtab[s+1] << endl; // 對齊問題 s = s + sizeof(Base2)/8; cout << "[" << s << "] Base3::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; cout << " [3] "; cout<<pFun<<endl; s++; cout << "["<< s <<"] Base3.ibase3 = " << (long)pVtab[s] << endl; s++; cout << "["<< s <<"] Derive.iderive = " << (long)pVtab[s] << endl; return 0; } ``` To hanlde casting problem ```shell g++ -fpermissive diamod_inheritance.cpp ``` ```shell= [0] D::B1::_vptr-> [0] D::f() [1] B::Bf() [2] D::f1() [3] B1::Bf1() [4] D::f2() [5] 0x1 [1] B::ib = 0 [2] B::cb = B [3] B1::ib1 = 11 [4] B1::cb1 = 1 [5] D::B2::_vptr-> [0] D::f() [1] B::Bf() [2] D::f2() [3] B2::Bf2() [4] 0x0 [6] B::ib = 0 [7] B::cb = B [8] B2::ib2 = 12 [9] B2::cb2 = 2 [10] D::id = 100 [11] D::cd = D ``` ## 鑽石型多重虛擬繼承 虛擬繼承的出現就是為瞭解決重複繼承中多個間接父類的問題的。鑽石型的結構是其最經典的結構。也是我們在這裡要討論的結構: 上述的“重複繼承”只需要把B1和B2繼承B的語法中加上**virtual** 關鍵,就成了虛擬繼承,其繼承圖如下所示: ![](https://i.imgur.com/hD1i1C7.png) ```diff= class B {……}; - class B1 : public B{……}; - class B2 : public B{……}; + class B1 : virtual public B{……}; + class B2 : virtual public B{……}; class D : public B1, public B2{ …… }; ``` 我們先看看B1 ```c++ // 單一虛擬繼承 B1 // B1 bb1; // pVtab = (int**)&bb1 [0] B1::_vptr -> [0] : B1::f() [1] : B1::f1() [2] : B1::Bf1() [3] : 0 [1] B1::ib1 : 11 [2] B1::cb1 : 1 [3] B::_vptr -> [0] : B1::f() [1] : B::Bf() [2] : 0 [4] B::ib : 0 [5] B::cb : B [6] NULL : 0 ``` 在看看D ```c++ // 鑽石型多重虛擬繼承 D // D d; // pVtab = (int**)&d; [0] B1::_vptr -> [0] : D::f() [1] : D::f1() [2] : B1::Bf1() [3] : D::f2() [4] : D::Df() [5] : 1 [1] B1::ib1 : 11 [2] B1::cb1 : 1 [3] B2::_vptr -> [0] : D::f() [1] : D::f2() [2] : B2::Bf2() [3] : 0 [4] B2::ib2 : 12 [5] B2::cb2 : 2 [6] D::id : 100 [7] D::cd : D [8] B::_vptr -> [0] : D::f() [1] : B::Bf() [2] : 0 [9] B::ib : 0 [10] B::cb : B [11] NULL : 0 ``` ![](https://i.imgur.com/HceQIs6.png) :::info ::: # Ref: - 陳皓 C++ 虛函數表解析 https://blog.csdn.net/haoel/article/details/1948051/ C++ 对象的内存布局 https://blog.csdn.net/haoel/article/details/3081328 [C++ 對象的內存佈局(上)---陳皓改進版](https://blog.csdn.net/a3192048/article/details/82259966) [C++ 對象的內存佈局(下)---陳皓改進版](https://blog.csdn.net/a3192048/article/details/82261754) - 關於virtual 2,3事 https://medium.com/theskyisblue/c-%E4%B8%AD%E9%97%9C%E6%96%BC-virtual-%E7%9A%84%E5%85%A9%E4%B8%89%E4%BA%8B-1b4e2a2dc373 ##### tags `C++` `C++ interview` `C++ virtual function` `c++ `