C++進階問題-智能指針&繼承&sizeof


智能指針

請講一下智能指針原理,並實現一個簡單的智能指針

  • 智能指針其實不是一個指針。它是一個用來幫助我們管理指針的類,維護其生命周期的類。有了它,媽媽再也不用擔心我的內存泄露啦!

  • 需要解決的問題:

    • 怎么釋放內存?
    • 什么時候釋放內存?
  • 釋放內存方法一:同歸於盡! auto_ptr

  • 釋放內存方法二:引用計數!

    • 引用計數:對一個指針所指向的內存,目前有多少個對象在使用它
    • 當引用計數為0的時候,刪除對象
    • 多個智能指針共享同v 一個引用計數類
    • 在進行賦值等操作時,動態地維護引用計數
  • 實現,有兩個問題需要解決

    1. 如何對指針引用的內存進行計數
    2. 如何改進SmartPointer類使得它能夠動態地維護引用計數

Counter類實現

class Counter {
    friend class SmartPointPro;
public:
    Counter(){
        ptr = NULL;
        cnt = 0;
    }
    Counter(Object* p){
        ptr = p;
        cnt = 1;
    }
    ~Counter(){
        delete ptr;
    }

private:
    Object* ptr;
    int cnt;
};

SmartPointPro類 未考慮線程安全

  • 如何動態維護引用計數?引用計數改變發生在如下時刻:
    1. 調用構造函數時: SmartPointer p(new Object());
    2. 賦值構造函數時: SmartPointer p(const SmartPointer &p);
    3. 賦值時:SmartPointer p1(new Object()); SmartPointer p2 = p1;
class SmartPointPro {
public:
    SmartPointerPro(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.ptr_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;
};

怎么樣獲取智能指針所包裝的指針呢?

  • method1: 寫GetPtr(), GetObject()等函數
  • method2: 重載指針的操作符,->/*

STL中的四種智能指針

  • auto_ptr: 弱版本的智能指針,賦值會有問題
  • shared_ptr: 引用計數版本
  • weak_ptr: 解決循環引用的問題
  • unique_ptr: auto_ptr的改進版本,不允許直接賦值,可以移動

單例模式

//機智的Meyers寫法,優雅而線程安全

class Singleton{
private:
    Singleton(){}
    Singleton(const Singleton& s){}
    Singleton& operator=(const Singleton& s){}
public:
    static Singleton* GetInstance() {
        static Singleton instance;
        return &instance;
    }
}

struct對齊問題

  • 筆試、面試中的超高頻問題:
struct Q{
    char c;
    int num1;
    double num2;
};
  • 問上述代碼中sizeof(Q)為多少? 16
  • 實際上這是考察struct結構地址對齊的問題
  • struct的對其系數和以下幾個關系有關
    1. 元素大小
    2. 元素順序
    3. #pargma pack參數

對齊規則

  • struct中成員在內存中按順序排列,在沒有#pargma pack(n)的情況下,各個成員的對齊系數為自己的長度
  • 在有#pargma pack(n)的情況下,各個成員的對齊系數為min(自己的長度,n)
  • struct整體的對齊系數為對齊系數中最大的
  • 依次排列時要滿足地址對對齊系數取模為0

C++虛函數相關問題

C++中為什么要使用虛函數

  • Base類和派生類都有相同的函數接口,但是考慮到不同的派生類的特性,可以用虛函數來使用不同的實現
  • 虛函數實現動態綁定,提高程序的靈活性
  • 實現動態綁定的兩個條件
    1. 相應成員函數為虛函數
    2. 使用基類對象的引用指針進行調用

請說明C++虛函數的底層實現機制

出現頻率: 5星 題目難度: 4星 使用范圍: 所有公司

  • 虛函數表: 一個類的虛函數的地址表,所有對象都是通過它來找到合適的虛函數. 他就是這個類的"四十二章經"

  • 虛函數表指針: 每個類的對象實例都擁有一個指針指向這張虛函數表(一般在對象實例的最前面),它幫助對象找到這張表的地址,然后就可以遍歷其中的函數指針,調用相應的函數

  • 注意:虛函數表一個類有一個,而不是一個對象有一個

  • 子類的虛函數表是子類的,從父類拷貝一份過來,並進行修改. 和父類並不是共享

  • 那么發生繼承覆蓋時發生了什么呢?

    1. 單繼承沒有覆蓋: 拷貝一份父類的虛函數表,然后添上自己的函數
    2. 單繼承有覆蓋: 拷貝一份父類的虛函數表,有重載的將其替換
    3. 多繼承無覆蓋: 子類的虛函數被放在了第一個父類中
    4. 多繼承有覆蓋: 覆蓋的部分就修改,三個父類拷貝過來的虛函數表都進行了覆蓋
  • 普通函數不進入虛函數表

  • C++本身是沒有類地址的,虛函數表放在代碼段或者數據段的

  • 如果沒有在子類中重載,調用的就還是父類的虛函數實現.

國內的面試題和虛函數有關的題

題目1:為什么需要虛析構函數?

  • 若析構函數非虛,用基類指針去new一個派生類對象時,調用delete時,只會析構派生類對象的基類部分
  • 如果子類有資源需要釋放,則析構函數一定要寫成虛函數

題目2:析構函數一定是虛函數嗎?

  • 不一定
  • 在沒有資源需要釋放,或者final類型的類也不需要

題目3:訪問虛函數和普通函數哪個快?

  • 顯然是普通函數
  • 普通函數在編譯階段就確定了,訪問時可以直接調到對應的地址執行.
  • 虛函數需要在虛函數表中查找,有一定的耗時
  • 此外,由於有虛函數表的存在,因此會有一定的內存占用

題目4:內聯函數/構造函數/靜態成員函數可以是虛函數嗎

  • 否 否 否
  • 內聯函數是編譯時展開,虛函數是運行時綁定,存在沖突,不能是虛函數
  • 構造函數,子類在構造過程中,先構造父類,此時還沒有生成子類成員,此時無法動態綁定
  • 靜態成員函數不存在this指針

題目5: 構造函數中可以調用虛函數嗎?

  • 不可以. 調用構造函數后如果有多層繼承關系,實際上會從最頂層的基類從上往下構造,如果此時調用了一個子類的虛函數,其尚未被構造出來,這樣的函數行為就是完全不可預測的。

C++虛繼承相關問題

  • 解決菱形繼承帶來的二義性的問題
  • 中間層繼承基類方式為虛繼承, virtual public Base
  • 實現原理和編譯器高度相關,VC++實現思路如下:
    • 在mid1和mid2對象中添加虛基類指針,指向基類對象Base,這樣Mid1和Mid2中就會指向共同的基類成員,從而消除了二義性
  • 在開發中應該盡量避免

C++虛繼承中的sizeof

  • 遇到這樣的題,只能說明面試官在耍流氓,因為都是和編譯器高度相關的,沒有唯一答案
  • 但是我們要知道類的大小取決於哪些因素:
    1. 類成員的大小
    2. 虛函數表指針大小(是否和父類共享)
    3. 虛基類指針

先看下面的一段代碼

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 myfunC(){}
};

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;
    return 0;
}
  • 在VC++下,答案為: 8, 16, 16, 24

  • 在g++ 32位機器下,答案為: 8, 12, 12, 16

  • 分析VC++

    • 對A, int a(4B) + vptr(4B) = 8 Bytes
    • 對B與C, A的內容(8B) + vptrOfSelf(4B) + 指向Base的虛基類指針(4B) = 16Bytes
    • B + C - A = 16 + 16 - 8 = 24 (? 存疑 !)

C++對象內存模型

  • 單一的一般繼承
    [圖片上傳失敗...(image-5efc6-1516850329498)]

  • 多重繼承
    [圖片上傳失敗...(image-9653a5-1516850329498)]

  • 鑽石形重復繼承--沒有使用虛繼承
    [圖片上傳失敗...(image-322c8a-1516850329498)]

  • 鑽石形多重虛擬繼承,下面給出gcc的結果
    [圖片上傳失敗...(image-80a507-1516850329498)]


免責聲明!

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



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