智能指針
請講一下智能指針原理,並實現一個簡單的智能指針
-
智能指針其實不是一個指針。它是一個用來幫助我們管理指針的類,維護其生命周期的類。有了它,媽媽再也不用擔心我的內存泄露啦!
-
需要解決的問題:
- 怎么釋放內存?
- 什么時候釋放內存?
-
釋放內存方法一:同歸於盡! auto_ptr
-
釋放內存方法二:引用計數!
- 引用計數:對一個指針所指向的內存,目前有多少個對象在使用它
- 當引用計數為0的時候,刪除對象
- 多個智能指針共享同v 一個引用計數類
- 在進行賦值等操作時,動態地維護引用計數
-
實現,有兩個問題需要解決
- 如何對指針引用的內存進行計數
- 如何改進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類 未考慮線程安全
- 如何動態維護引用計數?引用計數改變發生在如下時刻:
- 調用構造函數時: SmartPointer p(new Object());
- 賦值構造函數時: SmartPointer p(const SmartPointer &p);
- 賦值時: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的對其系數和以下幾個關系有關
- 元素大小
- 元素順序
- #pargma pack參數
對齊規則
- struct中成員在內存中按順序排列,在沒有#pargma pack(n)的情況下,各個成員的對齊系數為自己的長度
- 在有#pargma pack(n)的情況下,各個成員的對齊系數為min(自己的長度,n)
- struct整體的對齊系數為對齊系數中最大的
- 依次排列時要滿足地址對對齊系數取模為0
C++虛函數相關問題
C++中為什么要使用虛函數
- Base類和派生類都有相同的函數接口,但是考慮到不同的派生類的特性,可以用虛函數來使用不同的實現
- 虛函數實現動態綁定,提高程序的靈活性
- 實現動態綁定的兩個條件
- 相應成員函數為虛函數
- 使用基類對象的引用或指針進行調用
請說明C++虛函數的底層實現機制
出現頻率: 5星 題目難度: 4星 使用范圍: 所有公司
-
虛函數表: 一個類的虛函數的地址表,所有對象都是通過它來找到合適的虛函數. 他就是這個類的"四十二章經"
-
虛函數表指針: 每個類的對象實例都擁有一個指針指向這張虛函數表(一般在對象實例的最前面),它幫助對象找到這張表的地址,然后就可以遍歷其中的函數指針,調用相應的函數
-
注意:虛函數表一個類有一個,而不是一個對象有一個
-
子類的虛函數表是子類的,從父類拷貝一份過來,並進行修改. 和父類並不是共享
-
那么發生繼承覆蓋時發生了什么呢?
- 單繼承沒有覆蓋: 拷貝一份父類的虛函數表,然后添上自己的函數
- 單繼承有覆蓋: 拷貝一份父類的虛函數表,有重載的將其替換
- 多繼承無覆蓋: 子類的虛函數被放在了第一個父類中
- 多繼承有覆蓋: 覆蓋的部分就修改,三個父類拷貝過來的虛函數表都進行了覆蓋
-
普通函數不進入虛函數表
-
C++本身是沒有類地址的,虛函數表放在代碼段或者數據段的
-
如果沒有在子類中重載,調用的就還是父類的虛函數實現.
國內的面試題和虛函數有關的題
題目1:為什么需要虛析構函數?
- 若析構函數非虛,用基類指針去new一個派生類對象時,調用delete時,只會析構派生類對象的基類部分
- 如果子類有資源需要釋放,則析構函數一定要寫成虛函數
題目2:析構函數一定是虛函數嗎?
- 不一定
- 在沒有資源需要釋放,或者final類型的類也不需要
題目3:訪問虛函數和普通函數哪個快?
- 顯然是普通函數
- 普通函數在編譯階段就確定了,訪問時可以直接調到對應的地址執行.
- 虛函數需要在虛函數表中查找,有一定的耗時
- 此外,由於有虛函數表的存在,因此會有一定的內存占用
題目4:內聯函數/構造函數/靜態成員函數可以是虛函數嗎
- 否 否 否
- 內聯函數是編譯時展開,虛函數是運行時綁定,存在沖突,不能是虛函數
- 構造函數,子類在構造過程中,先構造父類,此時還沒有生成子類成員,此時無法動態綁定
- 靜態成員函數不存在this指針
題目5: 構造函數中可以調用虛函數嗎?
- 不可以. 調用構造函數后如果有多層繼承關系,實際上會從最頂層的基類從上往下構造,如果此時調用了一個子類的虛函數,其尚未被構造出來,這樣的函數行為就是完全不可預測的。
C++虛繼承相關問題
- 解決菱形繼承帶來的二義性的問題
- 中間層繼承基類方式為虛繼承, virtual public Base
- 實現原理和編譯器高度相關,VC++實現思路如下:
- 在mid1和mid2對象中添加虛基類指針,指向基類對象Base,這樣Mid1和Mid2中就會指向共同的基類成員,從而消除了二義性
- 在開發中應該盡量避免
C++虛繼承中的sizeof
- 遇到這樣的題,只能說明面試官在耍流氓,因為都是和編譯器高度相關的,沒有唯一答案
- 但是我們要知道類的大小取決於哪些因素:
- 類成員的大小
- 虛函數表指針大小(是否和父類共享)
- 虛基類指針
先看下面的一段代碼
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)]
