1. 程式人生 > >2017模擬面試題庫 —— C++相關

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。

環狀引用的本質矛盾是不能通過任何程式設計語言的方式來打破的,為了解決環狀引用,第一步首先得打破環,也就是得告訴C++,這個環上哪一個引用是最弱的,是可以被打破的,因此在一個環上只要把原來的某一個shared_ptr改成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

*/