1. 程式人生 > >Cpp重難點問題

Cpp重難點問題

1、關於c++中鉤子的介紹與使用

鉤子故名思義,是一種連鎖的程式,將某個功能與原功能聯合起來。
鉤子程式主要用於擷取外部的輸入量,來完成內部介面的控制。

舉個例子來說:QQ登入

鉤子繫結的是鍵盤的輸入,鍵盤輸入QQ密碼,如果輸入錯誤,則鉤子繫結的介面程式,執行顯示錯誤的資訊,如果鍵盤輸入正確,則執行正常登入的程式。

那麼鉤子程式在c++中如何實現的呢?
首先得說明下c++程式中的過載和覆蓋
過載指的是不同的函式有相同的函式名,一般指的非虛擬函式,一般是靜態繫結
覆蓋指的後面的函式覆蓋前面的函式,一般是虛擬函式,一般是動態繫結

先來說覆蓋,覆蓋很簡單,虛擬函式的動態繫結的性質,虛擬函式的根據指向的物件繫結對應的函式實體
例如,初始化子類,則指向之類的覆蓋的虛擬函式實體。初始化父類,則繫結父類的虛擬化。
再來說過載,過載是靜態繫結的性質,過載繫結的函式實體對應的宣告的靜態型別。同時需要考慮的是,繼承架構中,尋找過載函式是從子類到父類頂端的過程。

舉個例子:

如果初始化定義的子類的宣告,呢麼靜態繫結子類的函式實體,如果子類的函式實體沒有找到,則向上尋找,繫結上一級父類的實體。

鉤子程式在c++中是由虛擬函式來完成的。虛擬函式進行動態繫結,如果子類有覆蓋,則鉤子對應到子類上,否則預設對應父類中。

2.再探私有/公有靜態成員變數與私有靜態成員方法

問題1: 為什麼在類內的靜態成員定義後,要到類的外部在定義和初始化?

答:首先這句話就是錯的,在類內的靜態成員變數只是一定宣告,並沒有分配相應的記憶體空間;在類外,相當於定義加上初始化,如果只是定義,也是能夠編譯成功的,因為分配了記憶體。等價於全域性變數,但只屬於類。

為什麼,因為靜態成員不屬於類的任何一個物件,所以它不是在建立類物件的時候被定義的,不是由類的建構函式初始化的。

問題2:為什麼類的靜態成員在類外部的定義只能一次?

答:好比全域性變數的多重定義
另外,任何變數都只進行一次初始化。區域性變數在程式塊結束時生存期就結束了,下次再呼叫這個程式塊時從原理上說宣告的是另一個變量了(分配到的地址也不一定一樣)。

PS:在不同編譯器的不同編譯情況時,實際的記憶體分割槽可能不同。例如TC的Small模式下堆和棧區是重合的,而Tiny模式下連靜態區域和動態區域都是重合的。

問題3:私有的靜態成員變數如何初始化,訪問許可權還是私有麼?

答:顯然,也是在類的外部進行初始化,但是訪問許可權是存在的。比如不能在程式執行的過程中直接用類名加作用域來訪問此私有的靜態成員。需要通過類公有的外部介面來訪問,包括靜態與非靜態介面。

問題4:在程式執行過程中,其他類的物件改變靜態成員變數的值,那麼相應的在建立另一個物件時的靜態成員值會是初始化的值還是上一次改變的值?

答:是上一次改變的值,因為初始化是分配記憶體,賦初值。

問題5:類的非靜態成員函式中能訪問靜態成員變數麼?

答:是可以訪問的,前提是要對靜態的進行外部的初始化。如果為初始化,則會報錯,是連結錯誤,顯然在類內部只是宣告,並沒有在類的外部進行定義與初始化,未分配記憶體。

靜態成員不屬於這個類物件,但是可以被類物件共享,屬性也保持著,可以被靜態成員函式或者非靜態成員函式訪問。

問題6:類的靜態成員函式中能訪問非靜態成員變數麼?

答:不能,解析不了。非靜態成員,不是一個特定的物件的,既沒有this指標,指向,根本找不到。

問題7:

靜態私有成員在類外不能被訪問,可通過類的靜態成員函式來訪問;
當類的建構函式是私有的時,不像普通類那樣例項化自己,只能通過靜態成員函式來呼叫建構函式。
物件之間通過類的靜態成員變數來實現資料的共享的。靜態成員變數佔有自己獨立的空間不為某個物件所私有。

3. C++中名字的查詢和繼承

我們常規的呼叫一個類函式:p->mem(),依次會進行以下4個步驟:

1.首先確定p的靜態型別,因為呼叫都 是一個成員,所以該型別必須是一個類的型別。
2.在p的靜態型別對應的類中去找,如果找不到,則依次在直接基類中不斷查詢直到繼承鏈的頂端,如果找遍了及其基類還找不到,則編譯器就報錯。如果找到了,那麼查詢過程就結束,進入型別檢查。
3.一旦找到了mem,這個時候進行常規型別檢查,確認當前找到的mem是否合法,
4.如果合法,則編譯器個根據呼叫的是否是虛擬函式而產生不同的程式碼:
——如果是虛擬函式,則產生程式碼為動態繫結
——如果不是虛擬函式,則產生一個常規的函式呼叫
注意:隱藏的時候,名字查詢優先於型別查詢,也就是說子類的同名函式會隱藏父類的同名函式,及時兩個同名函式形參列表不一致。

虛擬函式——動態繫結,根據呼叫虛擬函式的指標和引用的型別來判斷
非虛擬函式——靜態繫結,根據物件或者指標引用的型別來判斷

4.在建構函式和解構函式中是不能呼叫虛擬函式的

1.虛擬函式的作用是什麼?是實現部分或預設的功能,而且該功能可以被子類所修改。如果父類的建構函式設定成虛擬函式,那麼子類的建構函式會直接覆蓋掉父類的建構函式。而父類的建構函式就失去了一些初始化的功能。這與子類的構造需要先完成父類的構造的流程相違背了。而這個後果會相當嚴重。

2.虛擬函式的呼叫是需要通過“虛擬函式表”來進行的,而虛擬函式表也需要在物件例項化之後才能夠進行呼叫。在構造物件的過程中,還沒有為“虛擬函式表”分配記憶體。所以,這個呼叫也是違背先例項化後呼叫的準則。

3.虛擬函式的呼叫是由父類指標進行完成的,而物件的構造則是由編譯器完成的,由於在建立一個物件的過程中,涉及到資源的建立,型別的確定,而這些是無法在執行過程中確定的,需要在編譯的過程中就確定下來。而多型是在執行過程中體現出來的,所以是不能夠通過虛擬函式來建立建構函式的,與例項化的次序不同也有關係。

那麼虛夠函式為什麼可以設計成虛擬函式呢?由於虛擬函式是釋放物件的時候才執行的,所以一開始也就無法確定析夠函式的。而去由於析構的過程中,是先析構子類物件,後析構父類物件。所以,需要通過虛擬函式來指引子類物件。所以,如果不設定成虛擬函式的話,解構函式是無法執行子類的解構函式的。

5.智慧指標

1.make_shared函式

#include <memory>

make_shared是一個非成員函式,具有給共享物件分配記憶體,並且只分配一次記憶體的優點,和顯式通過建構函式初始化(new)的shared_ptr相比較,後者需要至少兩次分配記憶體。這些額外的開銷有可能會導致記憶體溢位的問題。
最安全的使用動態記憶體的方法是使用一個make_shared的函式。

此函式在動態記憶體中分配一個物件並初始化,返回指向此物件的shared_ptr。
我們可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數,無論我們拷貝一個share_ptr,計數器都會遞增。

當我們給一個shared_ptr賦值或者shared被銷燬,計數器就會遞減。
當用一個shared_ptr初始化另外一個shared_ptr,或將它作為引數傳遞給一個函式以及作為函式的返回值(賦值給其他的),計數器都會遞增,一旦一個share_ptr的計數器變為0,它就會釋放自己所管理的物件。

!注意標準庫是用計數器還是其他資料結構來記錄有多少個指標共享物件由標準庫來決定,關鍵是智慧指標類能記錄有多少個shared_ptr指向相同的物件,並能在恰當的時候自動釋放物件。

2.shared_ptr 自動銷燬所管理的物件

當指向物件的最後一個shared_ptr 被銷燬時,shared_ptr 類會自動銷燬此物件。它是通過特殊的成員函式解構函式來控制物件銷燬時做什麼操作。

shared_ptr 的解構函式會遞減它所指向的物件的引用計數,如果引用計數變為0,shared_ptr 的函式就會銷燬物件,並釋放它佔用的資源。

對於一塊記憶體,shared_ptr 類保證只要有任何shared_ptr 物件引用它,它就不會被釋放
如果我們忘記了銷燬程式不再需要的shared_ptr,程式仍然會正確執行,但會浪費記憶體

注意!:如果你將shared_ptr存放於一個容器中,而後不在需要全部元素,而只使用其中的一部分,要記得呼叫erase刪除不再需要的那些元素。

注意!:將一個shared_ptr 賦予另一個shared_ptr 會遞增賦值號右側的shared_ptr 的引用計數,而遞減左側shared_ptr 的引用計數,如果一個shared_ptr 引用技術
變為0時,它所指向的物件會被自動銷燬。

6. C++隱式轉換

C++提供了關鍵字explicit,可以阻止不應該允許的經過轉換建構函式進行的隱式轉換的發生,宣告為explicit的建構函式不能在隱式轉換中使用。

C++中, 一個引數的建構函式(或者除了第一個引數外其餘引數都有預設值的多參建構函式), 承擔了兩個角色。
1 是個構造;2 是個預設且隱含的型別轉換操作符。

所以, 有時候在我們寫下如 AAA = XXX, 這樣的程式碼, 且恰好XXX的型別正好是AAA單引數構造器的引數型別, 這時候編譯器就自動呼叫這個構造器, 建立一個AAA的物件。

這樣看起來好象很酷, 很方便。 但在某些情況下, 卻違背了程式設計師的本意。 這時候就要在這個構造器前面加上explicit修飾, 指定這個構造器只能被明確的呼叫/使用, 不能作為型別轉換操作符被隱含的使用。

解析:explicit建構函式是用來防止隱式轉換的。請看下面的程式碼:

  1. #include <iostream>  
  2. using namespace std;  
  3. class Test1  
  4. {  
  5. public :  
  6.     Test1(int num):n(num){}  
  7. private:  
  8.     int n;  
  9. };  
  10. class Test2  
  11. {  
  12. public :  
  13.     explicit Test2(int num):n(num){}  
  14. private:  
  15.     int n;  
  16. };  
  17.   
  18. int main()  
  19. {  
  20.     Test1 t1 = 12;  
  21.     Test2 t2(13);  
  22.     Test2 t3 = 14;  
  23.           
  24.     return 0;  
  25. }  

編譯時,會指出 t3那一行error:無法從“int”轉換為“Test2”。而t1卻編譯通過。註釋掉t3那行,除錯時,t1已被賦值成功。
注意:當類的宣告和定義分別在兩個檔案中時,explicit只能寫在在宣告中,不能寫在定義中。