2017模擬面試題庫 —— C++相關
Q:指標和引用的區別?
A:在x86 32位 Linux系統下,指標佔4個位元組;
從底層實現上來看:
1. 引用也是一個指標,建立一個指標和建立一個引用的彙編指令是一樣的
int a = 8; 011A5F6E mov dword ptr [a],8 int * p = &a; 011A5F75 lea eax,[a] # 將變數a地址裝入暫存器eax 011A5F78 mov dword ptr [p],eax # 將暫存器eax中的地址傳入指標p int & q = a; 011A5F7B lea eax,[a] # 將變數a地址裝入暫存器eax 011A5F7E mov dword ptr [q],eax # 將暫存器eax中的地址傳入指標q
2. 以引用 / 指標改變記憶體的彙編指令相同
*p = 10;
00F25F81 mov eax,dword ptr [p] # 將指標p中的地址傳入暫存器eax
00F25F84 mov dword ptr [eax],0Ah # 將值傳入eax中地址所指向記憶體
q = 10;
00F25F8A mov eax,dword ptr [q] # 將指標q中的地址傳入暫存器eax
00F25F8D mov dword ptr [eax],0Ah # 將值傳入eax中地址所指向記憶體
從表現形式上來看:
1. 引用自帶解引用
2. 沒有空引用,引用必須在定義的時候初始化,且引用一經初始化不能改變 *相當於 int * const p
3. 指標有多級指標 / 引用只有一級引用 *C11支援二級引用(左值、右值引用)
/*************************************************************************************/
Q:C++的函式過載?
A:C/C++ 是編譯型語言,原始檔經過編譯生成目標檔案,收集原始檔中的函式/變數生成符號表,接著在連結中進行符號解析;
符號表中的函式不允許重複定義,在C中是以 函式名 為區分函式的方式,在C++中則是以 函式名 + 引數列表 區分;(所以C中不能函式過載)
* 函式過載的前提:在同一個作用域下* 特例:對於const修飾的變數,在編譯器眼裡是相同的,不構成函式過載
(只有用 const 修飾指標 / 引用 才能構成過載)
/*************************************************************************************/
Q:volitale關鍵字是做什麼用的?
A:
1. 防止編譯器對指令順序進行調整
/* 防止Cpu調整指令順序:barrier( ) */
2. 防止多執行緒對共享變數進行快取(保證所有執行緒都能實時獲得共享變數的值)
/* 只能保證可見性,不能保證原子性(mutex、訊號量、讀寫鎖、原子鎖) */
/*************************************************************************************/
Q:記憶體洩漏
/ 資源洩漏
A:
1.呼叫 malloc/new 未 free/delete
malloc/new 後忘記 free/delete 或在執行 free/delete 之前丟擲異常
2.發生淺拷貝物件預設賦值
eg:
Class Test
{
private:
int * p;
public:
Test()
{
p = (int*)malloc(sizeof(int));
}
~Test()
{
free(p);
}
};
int main()
{
Test A;
Test B(A); // 此處發生淺拷貝,當呼叫預設拷貝建構函式時,A、B中的指標指向同一塊記憶體
// 當A、B析構時,同一塊記憶體被free兩次,程式將崩潰
Test C;
C = A; // 此處發生淺拷貝,當呼叫預設賦值函式時,A、C中的指標指向同一塊記憶體
// C中指標原指向的記憶體將丟失,發生記憶體洩漏
return 0;
}
3.基類指標指向堆區資源,而基類解構函式非虛,則派生類無法析構
基類指標實際指向的是派生類物件中基類物件的起始地址,若基類中無虛擬函式,則派生類自己生成的虛表指標在記憶體佈局中會在基類物件上方,導致 delete 時偏移量錯誤
class Base
{
private:
int * p;
public:
Base()
{
p = (int *)malloc(sizeof(int));
}
~Base() // 此處應為虛擬函式
{
cout<<"hello"<<endl;
free(p);
}
};
class Test : public Base
{
public:
Test() {}
~Test() { cout<<"world"<<endl; }
};
int main()
{
Base * pB = new Test(); // 以基類指標指向派生類物件
delete pB; // 只調用了Base的解構函式(根據指標型別靜態繫結)
return 0;
}
將基類 Base 的解構函式定義為虛擬函式後,形成動態繫結;
呼叫解構函式時查虛表,發現派生類 Test 重寫了解構函式,則呼叫 Test 的解構函式 -> 呼叫父類 Base 解構函式,物件被成功的析構掉。
4.socket / fd 未 close (fd 程序上限: 2^16 = 65535 , 即 epoll 可操作 fd 上限)
5. 產生殭屍程序,程序核心棧記憶體洩漏(8k 底端PCB)
6.new Test[100] -> delete Test
使用 new 一次生成多個物件,而沒有使用 delete [] ,多個物件只會呼叫一次解構函式
7. 建構函式中丟擲異常(new -> bad_alloc),退出時未呼叫解構函式,申請的記憶體將無法釋放
8. 智慧指標的交叉引用
class B;
class A
{
public:
shared_ptr<B> _pb;
};
class B
{
public:
shared_ptr<A> _pa;
};
int main()
{
shared_ptr<A> pa(new A); // A引用計數加一(1)
shared_ptr<B> pb(new B); // B引用計數加一(1)
pa->_pb = pb; // B引用計數加一(2)
pb->_pa = pa; // A引用計數加一(2)
return 0;
}
當函式退出時,呼叫 B 的解構函式,B 的引用計數 - 1 = 1,B 沒有析構掉;
接著呼叫 A 的解構函式,A 的引用計數 - 1 = 1,A 也沒有析構掉,A、B 的記憶體洩漏。
/*************************************************************************************/
Q:C++多型的原理?
A:C++中的多型分為靜態多型和動態多型,也稱為編譯期多型和執行期多型:
靜態多型包括:過載、模板
動態多型包括:虛擬函式
C++通過虛擬函式實現動態多型:
在基類的函式前加上 virtual 關鍵字,在派生類中重寫該函式,以指標或引用呼叫類從成員函式會發生多型,執行時根據物件的實際型別呼叫相應的函式;
原理:
對於每個定義了虛擬函式的類和它的派生類,都會產生一個虛擬函式表,這個虛擬函式表是類共享的;
每個物件前四個位元組都有一個虛表指標 vf_ptr,指向該型別的虛擬函式表vf_table(虛表存放在只讀資料段 .rodata )
動態聯編在 基類指標 指向不同的 派生類物件 時發生,若派生類物件重寫了虛擬函式,則虛表對應項將被覆蓋;
實際呼叫中根據物件的 vf_ptr 找到該類的虛表,呼叫對應的函式
擁有虛擬函式的類的建構函式過程:
push ebp # 將棧底儲存
mov ebp, esp # 將棧頂指標值賦給棧底指標
sub esp, 4ch # 開闢棧幀
ebp <-> esp 0xCCCCCCCC # 重新整理棧幀
vftable -> vfptr # 將虛表地址賦給虛表指標
/*************************************************************************************/
Q:早繫結/晚繫結
、 靜態繫結/動態繫結?
A:早繫結又稱靜態繫結,在程式編譯期發生,即編譯期就確定將要呼叫的函式地址(以物件直接呼叫成員函式);
晚繫結又稱動態繫結,在程式執行期發生,即在程式執行中確定要呼叫的函式地址(前提:以指標或引用呼叫成員函式,且類中該函式定義為虛擬函式,呼叫不在建構函式中——即該物件存在,可取地址);
p->Show(); # 靜態繫結
0139B5D8 push 0Ah # 引數壓棧
0139B5DA mov ecx,dword ptr [p]
0139B5DD call Base::Show (013916BDh) # 呼叫函式
p->Show(); # 動態繫結
009AA858 mov esi,esp
009AA85A push 0Ah # 引數壓棧
009AA85C mov eax,dword ptr [p] # 將物件前四個位元組(vfptr)放入eax
009AA85F mov edx,dword ptr [eax] # 將 vftable 地址放入edx
009AA861 mov ecx,dword ptr [p]
009AA864 mov eax,dword ptr [edx] # 查虛表得到虛擬函式地址
009AA866 call eax # 呼叫相應的虛擬函式
009AA868 cmp esi,esp
009AA86A call __RTC_CheckEsp (09A147Eh) # 調整棧平衡
/*************************************************************************************/
Q:智慧指標
A:一種確定性的通用垃圾收集機制。
智慧指標和普通指標的區別在於智慧指標加了一層封裝,目的是為了方便的管理一個物件的生命週期;
實質是一個物件,行為表現卻像一個指標;
* 智慧指標更深層的意義在於,值語義到引用語義的轉換;
C++工程實踐經驗談——陳碩:值語義與資料抽象
// TODO
shared_ptr 強智慧指標
直接持有資源,可直接修改資源,其中的計數器記錄資源有多少個強智慧指標引用
weak_ptr 弱智慧指標
不共享資源,只有資源的觀測權,通過觀測引用資源的強智慧指標計數來管理資源,它的構造不引起資源引用計數的增加
(建立物件時用強智慧指標,其他地方使用只能持有資源的弱智慧指標;避免交叉引用導致資源無法釋放)
需要特別指出的是,如果shared_ptr所表徵的引用關係中出現一個環,那麼環上所述物件的引用次數都肯定不可能減為0那麼也就不會被刪除,為了解決這個問題引入了weak_ptr。
------
例:多執行緒訪問共享物件的執行緒安全
1. 主執行緒建立物件 Test
2. 子執行緒呼叫 Test 的成員方法(Test 可能已經被析構)
解決方法:
1. 以強智慧指標建立物件
shared_ptr<Test> pa(new Test());
2. 將弱智慧指標傳入子執行緒
weak_ptr<Test> pb = *(weak_ptr<Test> *)lparg;
通過觀察強智慧指標的引用計數,來判斷該物件是否可用。
void * pthread(void * arg)
{
/* ... ... */
shared_ptr<Test> pc = pb.lock(); // 將弱智慧指標提升成強智慧指標
if(pc != NULL) // 判斷物件是否存在
{
pc->func();
}
/* ... ... */
}
智慧指標實現:https://github.com/chen892704/STL-Learning
/*************************************************************************************/
C++多繼承與虛繼承的記憶體佈局:
https://www.oschina.net/translate/cpp-virtual-inheritance?p=1#comments
繼承與多型——要點總結
繼承結構中建構函式的呼叫:
push ebp # 儲存棧底
mov ebp, esp # 將棧頂設為新的棧底
sub esp, 4ch # 開闢新的棧幀
rep stos ... # 將新的棧幀刷為0xCCCCCCCCh
vftable -> vfptr # 將當前類的虛表地址賦給建立物件的虛表指標
(每一層建構函式都會將當前類的虛擬函式表地址寫入虛表指標中)
虛擬函式表內容:包括虛擬函式指標,以及執行時的資訊;
用基類指標指向堆區派生類物件時,指標指向的位置是派生類物件中基類部分的起始位置;
若派生類實現了虛擬函式而基類沒有,則物件中的虛表指標將在派生類物件的前面,導致呼叫 delete 釋放記憶體的時候,找不到派生類物件的實際起始位置(ptr - 4byte),程式崩潰;
解決的方法是在基類中定義虛擬函式,則派生類物件中的虛表指標將從基類繼承而來,當用基類指標指向派生類物件時,基類指標將指向物件的首部。
成員能否訪問 / 訪問許可權是否正確 / 函式的預設值用哪一個 -> 在編譯期確定
包含虛擬函式的繼承結構中,呼叫哪個類的虛擬函式 -> 在執行時確定
C++型別強轉:
const_cast 去掉物件const屬性的型別轉換
static_cast 編譯器認為較安全的型別轉換
reinterpret_cast 類似於C的強轉,較底層的型別轉換
dynamic_cast 支援RTTI(run-time type identity)的型別轉換
/*
Test * ptr = dynamic_cast<Test *>(p);
如果指標p指向的物件型別為Test,則返回Test *型別的指標,否則返回NULL
*/