1. 程式人生 > >C++ 複習要點、面試常見問題總結

C++ 複習要點、面試常見問題總結

本文總結一下C++面試時常遇到的問題。C++面試中,主要涉及的考點有

  • 關鍵字極其用法,常考的關鍵字有const, sizeof, typedef, inline, static, extern, new, delete等等
  • 語法問題
  • 型別轉換
  • 指標以及指標和引用的區別
  • 面向物件的相關問題,如虛擬函式機制等
  • 泛型程式設計的相關問題,如模板和函式的區別等
    記憶體管理,如位元組對齊(記憶體對齊)、動態記憶體管理、記憶體洩漏等
    編譯和連結
    實現函式和類

    零、序章

    0.1 C++與C的對比

  1. C++有三種程式設計方式:過程性,面向物件,泛型程式設計。
  2. C++函式符號由 函式名+引數型別 組成,C只有函式名。所以,C沒有函式過載的概念。
  3. C++ 在 C的基礎上增加了封裝、繼承、多型的概念
  4. C++增加了泛型程式設計
  5. C++增加了異常處理,C沒有異常處理
  6. C++增加了bool型
  7. C++允許無名的函式形參(如果這個形參沒有被用到的話)
  8. C允許main函式呼叫自己
  9. C++支援預設引數,C不支援
  10. C語言中,區域性變數必須在函式開頭定義,不允許類似for(int a = 0; ;;)這種定義方法。
  11. C++增加了引用
  12. C允許變長陣列,C++不允許
  13. C中函式原型可選,C++中在呼叫之前必須宣告函式原型
  14. C++增加了STL標準模板庫來支援資料結構和演算法

    一、重要的關鍵字極其用法

    1.1 const

    主要用法

    C++ 的const關鍵字的作用有很多,幾乎無處不在,面試中往往會問“說一說const有哪些用法”。下面是一些常見的const用法的總結:

image

除此以外,const的用法還有:
const引用可以引用右值,如const int& a = 1; 

注:
  • const 成員方法本質上是使得this指標是指向const物件的指標,所以在const方法內,
    const 成員函式可以被非const和const物件呼叫,而const物件只能呼叫const 成員函式。
  • 原因得從C++底層找,C++方法呼叫時,會傳一個隱形的this引數(本質上是物件的地址,形參名為this)進去,所有成員方法的第一個引數是this隱形指標。
  • const成員函式的this指標是指向const物件的const指標,當非const物件呼叫const方法時,實參this指標的型別是非const物件的const指標,賦給const物件的const指標沒有問題;但是如果const物件呼叫非const方法,此時實參this指標是指向const物件的const指標,無法賦給非const物件的const指標,所以無法呼叫。
  • 注意this實參是放在ecx暫存器中,而不是壓入棧中,這是this的特殊之處。
  • 在類的非成員函式中如果要用到類的成員變數,就可以通過訪問ecx暫存器來得到指向物件的this指標,然後再通過this指標加上成員變數的偏移量來找到相應的成員變數。http://blog.csdn.net/starlee/article/details/2062586/
  • const 指標、指向const的指標和指向const的const指標,涉及到const的特性“const左效、最左右效”
    const 全域性變數有內部連結性,即不同的檔案可以定義不同的同名const全域性變數,使用extern定義可以消除內部連結性,稱為類似全域性變數,如extern const int a = 10.另一個檔案使用extern const int a; 來引用。而且編譯器會在編譯時,將const變數替換為它的值,類似define那樣。

const 常量和define 的區別

  1. const常量有資料型別,而巨集定義沒有資料型別。編譯器可以對前者進行型別安全檢查,而對後者只進行字元替換,沒有型別安全檢查,並且在字元替換中可能會產生意想不到的錯誤(邊際效應)。
  2. 有些整合化的除錯工具可以對const常量進行除錯,但是不能對巨集定義進行除錯。
  3. 在C++程式中只使用const常量而不使用巨集常量,即const常量完全取代巨集常量。
  4. 記憶體空間的分配上。define進行巨集定義的時候,不會分配記憶體空間,編譯時會在main函式裡進行替換,只是單純的替換,不會進行任何檢查,比如型別,語句結構等,即巨集定義常量只是純粹的置放關係,如#define null 0;編譯器在遇到null時總是用0代替null它沒有資料型別.而const定義的常量具有資料型別,定義資料型別的常量便於編譯器進行資料檢查,使程式可能出現錯誤進行排查,所以const與define之間的區別在於const定義常量排除了程式之間的不安全性.
  5. const常量存在於程式的資料段,#define常量存在於程式的程式碼段
    const常量存在“常量摺疊”,在編譯器進行語法分析的時候,將常量表達式計算求值,並用求得的值來替換表示式,放入常量表,可以算作一種編譯優化。因為編譯器在優化的過程中,會把碰見的const全部以內容替換掉,類似巨集。

1.2 sizeof

  1. sizeof關鍵字不會計算表示式的值,而只會根據型別推斷大小。
  2. sizeof() 的括號可以省略, 如 sizeof a ;
  3. 類A的大小是 所有非靜態成員變數大小之和+虛擬函式指標大小

    1.3 static 

    static的用法有:
    (1)宣告靜態全域性變數,如static int a; 靜態全域性變數的特點:
    該變數在全域性資料區分配記憶體; 未經初始化的靜態全域性變數會被程式自動初始化為0(自動變數的值是隨機的,除非它被顯式初始化); 
    靜態全域性變數在宣告它的整個檔案都是可見的,而在檔案之外是不可見的;  
    (2)宣告靜態區域性變數,即在函式內部宣告的,靜態區域性變數的特點:
    該變數在全域性資料區分配記憶體; 
    靜態區域性變數在程式執行到該物件的宣告處時被首次初始化,即以後的函式呼叫不再進行初始化; 
    靜態區域性變數一般在宣告處初始化,如果沒有顯式初始化,會被程式自動初始化為0; 
    它始終駐留在全域性資料區,直到程式執行結束。但其作用域為區域性作用域,當定義它的函式或語句塊結束時,其作用域隨之結束;
    (3)宣告靜態函式,限定函式的區域性訪問性,僅在檔案內部可見
    (4)類的靜態資料成員,與全域性變數相比,靜態資料成員的好處有:
    靜態資料成員沒有進入程式的全域性名字空間,因此不存在與程式中其它全域性名字衝突的可能性; 
    可以實現資訊隱藏。靜態資料成員可以是private成員,而全域性變數不能;
    (5)類的靜態方法

1.4 typedef 

typedef 用來定義新的型別,類似的還有#define 和 using (C++11) (應該儘可能用using ,比如 using AAA = int64_t; )

與巨集定義的對比
  • define 在預處理階段進行簡單替換,不做型別檢查;

  • typedef在編譯階段處理,在作用域內給型別一個別名。
  • typedef 是一個語句,結尾有分號;#define是一個巨集指令,結尾沒有分號
    typedef int* pInt; 和 #define pInt int* 不等價,前者定義 pInt a, b;會定義兩個指標,後者是一個指標,一個int。
  • 不能宣告為inline的函式
  • 包含了遞迴、迴圈等結構的函式一般不會被內聯。
  • 虛擬函式一般不會內聯,但是如果編譯器能在編譯時確定具體的呼叫函式,那麼仍然會就地展開該函式。
  • 如果通過函式指標呼叫行內函數,那麼該函式將不會內聯而是通過call進行呼叫。
  • 構造和解構函式一般會生成大量程式碼,因此一般也不適合內聯。
  • 如果行內函數呼叫了其他函式也不會被內聯。
    1.5 inline
    inline用來向編譯器請求宣告為行內函數,編譯器有權拒絕。
    與巨集函式的對比
    行內函數在執行時可除錯,而巨集定義不可以;
    編譯器會對行內函數的引數型別做安全檢查或自動型別轉換(同普通函式),而巨集定義則不會;
    行內函數可以訪問類的成員變數,巨集定義則不能;
    在類中宣告同時定義的成員函式,自動轉化為行內函數
    巨集只是預定義的函式,在編譯階段不進行型別安全性檢查,在編譯的時候將對應函式用巨集命令替換。對程式效能無影響

    1.6 static const  const  static 

  1. static const 
    static const 資料成員可以在類內初始化 也可以在類外,不能在建構函式中初始化,也不能在建構函式的初始化列表中初始化
  2. static
    static資料成員只能在類外,即類的實現檔案中初始化,也不能在建構函式中初始化,不能在建構函式的初始化列表中初始化;
  3. const
    const資料成員只能在建構函式的初始化列表中初始化;

    1.7 explicit 

    explicit禁止了隱式轉換型別,用來修飾建構函式。原則上應該在所有的建構函式前加explicit關鍵字,當你有心利用隱式轉換的時候再去解除explicit,這樣可以大大減少錯誤的發生。
如果一個建構函式 Foo(int) ;則下面的語句是合法的:
Foo f; 
f = 12; // 發生了隱式轉換,先呼叫Foo(int)用12構建了一個臨時物件,然後呼叫賦值運算子複製到 f 中
如果給建構函式加了explicit,即 explicit Foo(int);就只能進行顯示轉換,無法進行隱式轉換了:
f = 12; // 非法,隱式轉換
f = Foo(12); // 合法,顯示轉換
f = (Foo)12;//合法,顯示轉換,C風格

1.8 extern 

  • extern可以置於變數或者函式前,以標示變數或者函式的定義在別的檔案中,提示編譯器遇到此變數和函式時在其他模組中尋找其定義。此外extern也可用來進行連結指定。

    二、語法問題

    2.1 a++ 與 ++a的區別

  • a++ 返回加之前的值,++a返回加之後的a變數
  • a++返回的是一個臨時變數,是右值,無法賦值;++a返回的是變數a,是左值

    2.2 switch語句

  • switch語句的表示式必須是整型int , char, short等。

    2.3 函式呼叫過程

http://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601204.html

https://zhuanlan.zhihu.com/p/25816426

  • 執行到函式呼叫指令時:
    +++++++++ 入棧 ++++++++++++
    將實參從右向左壓入棧
    壓入返回地址
    壓入主調函式的基地址
    跳到被呼叫函式的地址,執行函式程式碼,區域性變數按宣告順序依次壓入棧
    將返回值放入暫存器eax(累加器)中
    +++++++++ 出棧 ++++++++++++
    區域性變數全部出棧
    返回地址出棧,找到原執行地址
    形參出棧
    賦值操作將暫存器中的返回值賦給左值(如果有的話)

    2.4 左值與右值

  • 判斷左值和右值的標準是是否可以取地址。右值和左值不同,有可能存在於暫存器中,無法取地址,無法被賦值,臨時變數就是右值,存放在暫存器中,被賦給左值後被釋放。

    2.5 C語言識別符號

  • 關鍵字、預定義識別符號、使用者識別符號(不能以數字開頭)
    a123 // 合法
    _a // 合法
    _0 // 合法
    0asdasd// 非法

    2.6 全域性變數的優缺點

    優點:
    (1)可以減少變數的個數
    (2)減少由於實際引數和形式引數的資料傳遞帶來的時間消耗。
    缺點:
    (1)全域性變數儲存在靜態存貯區,程式開始執行時為其分配記憶體,程式結束釋放該記憶體。與區域性變數的動態分配、動態釋放相比,生存期比較長,因此過多的全域性變數會佔用較多的記憶體單元。
    (2)全域性變數破壞了函式的封裝效能。前面的章節曾經講過,函式象一個黑匣子,一般是通過函式引數和返回值進行輸入輸出,函式內部實現相對獨立。但函式中 如果使用了全域性變數,那麼函式體內的語句就可以繞過函式引數和返回值進行存取,這種情況破壞了函式的獨立性,使函式對全域性變數產生依賴。同時,也降低了該 函式的可移植性。
    (3)全域性變數使函式的程式碼可讀性降低。由於多個函式都可能使用全域性變數,函式執行時全域性變數的值可能隨時發生變化,對於程式的查錯和除錯都非常不利。

    2.7 複合型別有哪些?

    6個
    class, struct, union, enum, 陣列,指標

    2.8 運算子優先順序和結合性?

    結合性有兩種,一種是自左至右,另一種是自右至左,大部分運算子的結合性是自左至右,只有單目運算子、三目運算子的賦值運算子的結合性自右至左。
    優先順序有15種。記憶方法如下:
    記住一個最高的:構造型別的元素或成員以及小括號。
    記住一個最低的:逗號運算子。
    剩餘的是一、二、三、賦值。
    意思是單目、雙目、三目和賦值運算子。
    在諸多運算子中,又分為:
    算術、關係、邏輯。
    兩種位操作運算子中,移位運算子在算術運算子後邊,邏輯位運算子在邏輯運算子的前面。再細分如下:
    算術運算子分 *,/,%高於+,-。
    關係運算符中,〉,〉=,<,<=高於==,!=。
    邏輯運算子中,除了邏輯求反(!)是單目外,邏輯與(&&)高於邏輯或(||)。
    邏輯位運算子中,除了邏輯按位求反(~)外,按位與(&)高於按位半加(^),高於按位或(|)。
    這樣就將15種優先順序都記住了,再將記憶方法總結如下:
    去掉一個最高的,去掉一個最低的,剩下的是一、二、三、賦值。雙目運算子中,順序為
    算術、移位、關係(>,<,==)、邏輯位和邏輯(&& ||)。

    2.9 using 宣告和using 編譯指令的區別?哪個更好?

    using宣告是指類似using std::vector;這種的,using編譯指令是指using namespace std;這種的。區別:
    (1)using宣告使特定的標示符可用,using編譯指令使整個名稱空間可用。
    (2)假設名稱空間和宣告區域定義了相同的名稱。如果試圖使用using宣告將名稱空間的名稱匯入該宣告區域,則這兩個名稱會發生衝突,從而出錯。如果使用using編譯指令將該名稱空間的名稱匯入該宣告區域,則區域性版本將隱藏名稱空間版本。
    (3)一般來說,使用using宣告比使用using編譯指令更安全,這是由於它只匯入指定的名稱。如果該名稱與區域性名稱發生衝突,編譯器將付出指示。using編譯指令匯入所有名稱,包括可能並不需要的名稱。如果與區域性名稱發生衝突,則區域性名稱將覆蓋名稱空間版本,而編譯器並不會發出警告。

    2.10 for迴圈的效率問題

    http://blog.sina.com.cn/s/blog_549941cb01013qgx.html
  1. 最長迴圈放到內部可以提高I cache的效率,降低因為迴圈跳轉造成cache的miss以及流水線flush造成的延時
  2. 多次相同迴圈後也能提高跳轉預測的成功率,提高流水線效率
  3. 編譯器會自動展開迴圈提高效率, 這個不一定是必然有效的
    但不是絕對正確的,比如:
1 int x[1000][100];
2 for(i=0;i<1000;i++)
3 for(j=0;j<100;j++)
4 {
5 //access x[i][j]
6 }
7 
8 int x[1000][100];
9 for(j=0;j<100;j++)
10 for(i=0;i=1000;i++)
11 {
12 //access x[i][j]
13 }
14

這時候第一個的效率就比第二個的高,原因嘛和硬體也有一些關係,CPU對於記憶體的訪問都是通過資料快取(cache)來進行的。

三、型別轉換

3.1 四種類型強制轉換

dynamic_cast:該轉換符用於將一個指向派生類的基類指標或引用轉換為派生類的指標或引用。
const_cast:最常用的用途就是刪除const屬性。
static_cast:static_cast本質上是傳統c語言強制轉換的替代品,比C型別轉換更嚴格, 該操作符用於非多型型別的轉換,任何標準轉換都可以使用他,即static_cast可以把int轉換為double,但不能把兩個不相關的類物件進行轉換,比如類A不能轉換為一個不相關的類B型別。static_cast在類物件和基礎型別轉換中,會呼叫類的建構函式,和型別轉換運算子比如operator int(),來進行顯示轉換。
reinterpret_cast:該操作符用於將一種型別轉換為另一種不同的型別,比如可以把一個整型轉換為一個指標,或把一個指標轉換為一個整型,因此使用該操作符的危險性較高,一般不應使用該操作符。

四、指標

4.1 指標與引用的區別

  1. 指標是一個變數,引用只是別名
  2. 指標需要解引用才能訪問物件,引用不需要
  3. 引用在定義時必須初始化,且以後不可轉移引用的物件,指標可以
    引用沒有const,即int& const a ;沒有;而指標有const指標,即int* const ptr; 
  4. 引用不可以為空;而指標可以
  5. 指標變數需要分配棧空間;而引用不需要,僅僅是個別名
  6. sizeof(引用)得到對應物件的大小;sizeof(指標)得到指標大小
  7. 指標加法和引用加法不一樣
  8. 引用不需要釋放記憶體空間,在編譯時就會優化掉

    4.2 指標與陣列名的區別

  9. 陣列名不是指標,對陣列名取地址,得到整個陣列的地址
  10. 陣列名 + 1會跳過整個陣列的大小,指標+1只會跳過一個元素的大小
  11. 陣列名作為函式引數傳遞時,退化為指標
  12. sizeof(陣列名)返回整個陣列的大小,sizeof(指標)返回指標大小
  13. 陣列名無法修改值,是常量
  14. int (*p)[] = &arr; 才是正確的陣列指標寫法

    4.3 野指標、空指標的概念

  15. 野指標是指指向無效記憶體的指標,不能對野指標取內容,delete
  16. 空指標是指置為0\NULL\nullptr的指標,可以對空指標delete多次
    五、面向物件

    5.1 面向物件的三大特性

    三大特性:封裝,繼承,多型  
  17. 封裝:封裝是實現面向物件程式設計的第一步,封裝就是將資料或函式等集合在一個個的單元中(我們稱之為類)。封裝的意義在於保護或者防止程式碼(資料)被我們無意中破壞。
  18. 繼承:繼承主要實現重用程式碼,節省開發時間。子類可以繼承父類的一些東西。
  19. 多型:同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。分為編譯時多型和執行時多型。

轉自:
https://blog.csdn.net/csdn_chai/article/details/78041050