1. 程式人生 > >C/C++常見面試知識點總結附面試真題----20180919更新(未完)

C/C++常見面試知識點總結附面試真題----20180919更新(未完)

以下內容部分整理自網路,部分為自己面試的真題。

第一部分:計算機基礎

1. C/C++記憶體有哪幾種類型?

C中,記憶體分為5個區:堆(malloc)、棧(如區域性變數、函式引數)、程式程式碼區(存放二進位制程式碼)、全域性/靜態儲存區(全域性變數、static變數)和常量儲存區(常量)。此外,C++中有自由儲存區(new)一說。

2. 堆和棧的區別?

  • 1).堆存放動態分配的物件——即那些在程式執行時分配的物件,比如區域性變數,其生存期由程式控制;
  • 2).棧用來儲存定義在函式內的非static物件,僅在其定義的程式塊執行時才存在;
  • 3).靜態記憶體用來儲存static物件,類static資料成員以及定義在任何函式外部的變數,static物件在使用之前分配,程式結束時銷燬;
  • 4).棧和靜態記憶體的物件由編譯器自動建立和銷燬。

3. 堆和自由儲存區的區別?

總的來說,堆是C語言和作業系統的術語,是作業系統維護的一塊動態分配記憶體;自由儲存是C++中通過new與delete動態分配和釋放物件的抽象概念。他們並不是完全一樣。
從技術上來說,堆(heap)是C語言和作業系統的術語。堆是作業系統所維護的一塊特殊記憶體,它提供了動態分配的功能,當執行程式呼叫malloc()時就會從中分配,稍後呼叫free可把記憶體交還。而自由儲存是C++中通過new和delete動態分配和釋放物件的抽象概念,通過new來申請的記憶體區域可稱為自由儲存區。基本上,所有的C++編譯器預設使用堆來實現自由儲存,也即是預設的全域性運算子new和delete也許會按照malloc和free的方式來被實現,這時藉由new運算子分配的物件,說它在堆上也對,說它在自由儲存區上也正確。

4. 程式編譯的過程?

程式編譯的過程中就是將使用者的文字形式的原始碼(c/c++)轉化成計算機可以直接執行的機器程式碼的過程。主要經過四個過程:預處理、編譯、彙編和連結。具體示例如下。
一個hello.c的c語言程式如下。

#include <stdio.h>
int main()
{
    printf("happy new year!\n");
    return 0;
}

其編譯過程如下:
在這裡插入圖片描述

5. 計算機內部如何儲存負數和浮點數?

負數比較容易,就是通過一個標誌位和補碼來表示。
對於浮點型別的資料採用單精度型別(float)和雙精度型別(double)來儲存,float資料佔用32bit,double資料佔用64bit,我們在宣告一個變數float f= 2.25f的時候,是如何分配記憶體的呢?如果胡亂分配,那世界豈不是亂套了麼,其實不論是float還是double在儲存方式上都是遵從IEEE的規範的,float遵從的是IEEE R32.24 ,而double 遵從的是R64.53。更多可以參考浮點數表示。
無論是單精度還是雙精度在儲存中都分為三個部分:

  • 1). 符號位(Sign) : 0代表正,1代表為負
  • 2). 指數位(Exponent):用於儲存科學計數法中的指數資料,並且採用移位儲存
  • 3). 尾數部分(Mantissa):尾數部分
    其中float的儲存方式如下圖所示:
    在這裡插入圖片描述
    而雙精度的儲存方式如下圖:
    在這裡插入圖片描述

6. 函式呼叫的過程?

如下結構的程式碼,

int main(void)
{
  ...
  d = fun(a, b, c);
  cout<<d<<endl;
  ...
  return 0;
}

呼叫fun()的過程大致如下:

  • main()========
  • 1).引數拷貝(壓棧),注意順序是從右到左,即c-b-a;
  • 2).儲存d = fun(a, b, c)的下一條指令,即cout<<d<<endl(實際上是這條語句對應的彙編指令的起始位置);
  • 3).跳轉到fun()函式,注意,到目前為止,這些都是在main()中進行的;
  • fun()=====
  • 4).移動ebp、esp形成新的棧幀結構;
  • 5).壓棧(push)形成臨時變數並執行相關操作;
  • 6).return一個值;
  • 7).出棧(pop);
  • 8).恢復main函式的棧幀結構;
  • 9).返回main函式;
  • main()========
  • 。。。

7. 左值和右值

不是很嚴謹的來說,左值指的是既能夠出現在等號左邊也能出現在等號右邊的變數(或表示式),右值指的則是隻能出現在等號右邊的變數(或表示式)。舉例來說我們定義的變數 a 就是一個左值,而malloc返回的就是一個右值。或者左值就是在程式中能夠尋值的東西,右值就是一個具體的真實的值或者物件,沒法取到它的地址的東西(不完全準確),因此沒法對右值進行賦值,但是右值並非是不可修改的,比如自己定義的class, 可以通過它的成員函式來修改右值。

8. 什麼是記憶體洩漏?面對記憶體洩漏和指標越界,你有哪些方法?你通常採用哪些方法來避免和減少這類錯誤?

用動態儲存分配函式動態開闢的空間,在使用完畢後未釋放,結果導致一直佔據該記憶體單元即為記憶體洩露。

  • 1). 使用的時候要記得指標的長度.
  • 2). malloc的時候得確定在那裡free.
  • 3). 對指標賦值的時候應該注意被賦值指標需要不需要釋放.
  • 4). 動態分配記憶體的指標最好不要再次賦值.
  • 5). 在C++中應該優先考慮使用智慧指標.

第二部分:C v.s. C++

1. C和C++的區別?

  • 1). C++是C的超集;
  • 2). C是一個結構化語言,它的重點在於演算法和資料結構。C程式的設計首要考慮的是如何通過一個過程,對輸入(或環境條件)進行運算處理得到輸出(或實現過程(事務)控制),而對於C++,首要考慮的是如何構造一個物件模型,讓這個模型能夠契合與之對應的問題域,這樣就可以通過獲取物件的狀態資訊得到輸出或實現過程(事務)控制。

2. int fun() 和 int fun(void)的區別?

這裡考察的是c 中的預設型別機制。

  • 在c中,int fun() 會解讀為返回值為int(即使前面沒有int,也是如此,但是在c++中如果沒有返回型別將報錯),輸入型別和個數沒有限制, 而int fun(void)則限制輸入型別為一個void。
  • 在c++下,這兩種情況都會解讀為返回int型別,輸入void型別。

3. 在C中用const 能定義真正意義上的常量嗎?C++中的const呢?

不能。c中的const僅僅是從編譯層來限定,不允許對const 變數進行賦值操作,在執行期是無效的,所以並非是真正的常量(比如通過指標對const變數是可以修改值的),但是c++中是有區別的,c++在編譯時會把const常量加入符號表,以後(仍然在編譯期)遇到這個變數會從符號表中查詢,所以在C++中是不可能修改到const變數的。
補充:

  • 1). c中的區域性const常量儲存在棧空間,全域性const常量存在只讀儲存區,所以全域性const常量也是無法修改的,它是一個只讀變數。
  • 2). 這裡需要說明的是,常量並非僅僅是不可修改,而是相對於變數,它的值在編譯期已經決定,而不是在執行時決定。
  • 3).c++中的const 和巨集定義是有區別的,巨集是在預編譯期直接進行文字替換,而const發生在編譯期,是可以進行型別檢查和作用域檢查的。
  • 4).c語言中只有enum可以實現真正的常量。
  • 5). c++中只有用字面量初始化的const常量會被加入符號表,而變數初始化的const常量依然只是只讀變數。

下面我們通過程式碼來看看區別。
同樣一段程式碼,在c編譯器下,列印結果為*pa = 4, 4
在c++編譯下列印的結果為 *pa = 4, 8

int main(void)
{
    const int a = 8;
    int *pa = (int *)&a;
    *pa = 4;
    printf("*pa = %d, a = %d", *pa, a);
    return 0;
}

另外值得一說的是,由於c++中const常量的值在編譯期就已經決定,下面的做法是OK的,但是c中是編譯通不過的。


int main(void)
{
    const int a = 8;
    const int b = 2;
    int array[a+b] = {0};
    return 0;
}

4. 巨集和內聯(inline)函式的比較?

  • 1). 首先巨集是C中引入的一種預處理功能;
  • 2). 內聯(inline)函式是C++中引用的一個新的關鍵字;C++中推薦使用行內函數來替代巨集程式碼片段;
  • 3). 行內函數將函式體直接擴充套件到呼叫行內函數的地方,這樣減少了引數壓棧,跳轉,返回等過程;
  • 4). 由於內聯發生在編譯階段,所以內聯相較巨集,是有引數檢查和返回值檢查的,因此使用起來更為安全;
  • 5). 需要注意的是, inline會向編譯期提出內聯請求,但是是否內聯由編譯期決定(當然可以通過設定編譯器,強制使用內聯);
  • 6). 由於內聯是一種優化方式,在某些情況下,即使沒有顯示的宣告內聯,比如定義在class內部的方法,編譯器也可能將其作為行內函數。
  • 7). 行內函數不能過於複雜,最初C++限定不能有任何形式的迴圈,不能有過多的條件判斷,不能對函式進行取地址操作等,但是現在的編譯器幾乎沒有什麼限制,基本都可以實現內聯。
    更多請參考inline關鍵字

5. C++中有了malloc / free , 為什麼還需要 new / delete?

  • 1). malloc與free是C++/C語言的標準庫函式,new/delete是C++的運算子。它們都可用於申請動態記憶體和釋放記憶體。
  • 2). 對於非內部資料型別的物件而言,光用maloc/free無法滿足動態物件的要求。物件在建立的同時要自動執行建構函式,物件在消亡之前要自動執行解構函式。
    由於malloc/free是庫函式而不是運算子,不在編譯器控制權限之內,不能夠把執行建構函式和解構函式的任務強加於malloc/free。因此C++語言需要一個能完成動態記憶體分配和初始化工作的運算子new,以一個能完成清理與釋放記憶體工作的運算子delete。注意new/delete不是庫函式。
    最後補充一點體外話,new 在申請記憶體的時候就可以初始化(如下程式碼), 而malloc是不允許的。另外,由於malloc是庫函式,需要相應的庫支援,因此某些簡易的平臺可能不支援,但是new就沒有這個問題了,因為new是C++語言所自帶的運算子。
int *p = new int(1);

6. C和C++中的強制型別轉換?

C中是直接在變數或者表示式前面加上(小括號括起來的)目標型別來進行轉換,一招走天下,操作簡單,但是由於太過直接,缺少檢查,因此容易發生編譯檢查不到錯誤,而人工檢查又及其難以發現的情況;而C++中引入了下面四種轉換:

  • 1). static_cast
    a. 用於基本型別間的轉換
    b. 不能用於基本型別指標間的轉換
    c. 用於有繼承關係類物件間的轉換和類指標間的轉換
  • 2). dynamic_cast
    a. 用於有繼承關係的類指標間的轉換
    b. 用於有交叉關係的類指標間的轉換
    c. 具有型別檢查的功能
    d. 需要虛擬函式的支援
  • 3). reinterpret_cast
    a. 用於指標間的型別轉換
    b. 用於整數和指標間的型別轉換
  • 4). const_cast
    a. 用於去掉變數的const屬性
    b. 轉換的目標型別必須是指標或者引用

7. const 有什麼用途

主要有三點:

  • 1).定義只讀變數,即常量;
  • 2).修飾函式的引數和函式的返回值;
  • 3).修飾函式的定義體,這裡的函式為類的成員函式,被const修飾的成員函式代表不修改成員變數的值.
class Screen {
public:
char get() const;
};

8. static 有什麼用途

  • 1). 靜態(區域性/全域性)變數
  • 2). 靜態函式
  • 3). 類的靜態資料成員
  • 4). 類的靜態成員函式

9. 在C++程式中呼叫被C編譯器編譯後的函式,為什麼要加extern“C”?

C++語言支援函式過載,C語言不支援函式過載,函式被C++編譯器編譯後在庫中的名字與C語言的不同,假設某個函式原型為:

          void foo(int x, int y);

該函式被C編譯器編譯後在庫中的名字為 _foo, 而C++編譯器則會產生像: _foo_int_int 之類的名字。為了解決此類名字匹配的問題,C++提供了C連結交換指定符號 extern “C”。

10. 標頭檔案中的 ifndef/define/endif 是幹什麼用的? 該用法和 program once 的區別?

相同點:
它們的作用是防止標頭檔案被重複包含。
不同點

  • 1). ifndef 由語言本身提供支援,但是 program once 一般由編譯器提供支援,也就是說,有可能出現編譯器不支援的情況(主要是比較老的編譯器)。
  • 2). 通常執行速度上 ifndef 一般慢於 program once,特別是在大型專案上, 區別會比較明顯,所以越來越多的編譯器開始支援 program once。
  • 3). ifndef 作用於某一段被包含(define 和 endif 之間)的程式碼, 而 program once 則是針對包含該語句的檔案, 這也是為什麼 program once 速度更快的原因。
  • 4). 如果用 ifndef 包含某一段巨集定義,當這個巨集名字出現“撞車”時,可能會出現這個巨集在程式中提示巨集未定義的情況(在編寫大型程式時特性需要注意,因為有很多程式設計師在同時寫程式碼)。相反由於program once 針對整個檔案, 因此它不存在巨集名字“撞車”的情況, 但是如果某個標頭檔案被多次拷貝,program once 無法保證不被多次包含,因為program once 是從物理上判斷是不是同一個標頭檔案,而不是從內容上。

11. 當i是一個整數的時候++i和i++那個更快一點?i++和++i的區別是什麼?

答:理論上++i更快,實際與編譯器優化有關,通常幾乎無差別。

//i++實現程式碼為:
int operator++(int)
{
    int temp = *this;
    ++*this;
    return temp;
}//返回一個int型的物件本身

// ++i實現程式碼為:
int& operator++()
{
    *this += 1;
    return *this;
}//返回一個int型的物件引用

i++和++i的考點比較多,簡單來說,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一個確定的值,是一個可修改的左值,如下使用:

cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;

可以不停的巢狀++i。
這裡有很多的經典筆試題,一起來觀摩下:

int main()
{
    int i = 1;
    printf("%d,%d\n", ++i, ++i);    //3,3
    printf("%d,%d\n", ++i, i++);    //5,3
    printf("%d,%d\n", i++, i++);    //6,5
    printf("%d,%d\n", i++, ++i);    //8,9
    system("pause");
    return 0;
}

首先是函式的引數入棧順序從右向左入棧的,計算順序也是從右往左計算的,不過都是計算完以後再進行的壓棧操作:
對於第1個printf,首先執行++i,返回值是i,這時i的值是2,再次執行++i,返回值是i,得到i=3,將i壓入棧中,此時i為3,也就是壓入3,3;
對於第2個printf,首先執行i++,返回值是原來的i,也就是3,再執行++i,返回值是i,依次將3,5壓入棧中得到輸出結果
對於第3個printf,首先執行i++,返回值是5,再執行i++返回值是6,依次將5,6壓入棧中得到輸出結果
對於第4個printf,首先執行++i,返回i,此時i為8,再執行i++,返回值是8,此時i為9,依次將i,8也就是9,8壓入棧中,得到輸出結果。
上面的分析也是基於VS搞的,不過準確來說函式多個引數的計算順序是未定義的(the order of evaluation of function arguments are undefined)。筆試題目的執行結果隨不同的編譯器而異。

第三部分:陣列、指標 & 引用

1. 指標和引用的區別?

相同點:

  • 1). 都是地址的概念;
  • 2). 都是“指向”一塊記憶體。指標指向一塊記憶體,它的內容是所指記憶體的地址;而引用則是某塊記憶體的別名;
  • 3). 引用在內部實現其實是藉助指標來實現的,一些場合下引用可以替代指標,比如作為函式形參。
不同點:
  • 1). 指標是一個實體,而引用(看起來,這點很重要)僅是個別名;
  • 2). 引用只能在定義時被初始化一次,之後不可變;指標可變;引用“從一而終”,指標可以“見異思遷”;
  • 3). 引用不能為空,指標可以為空;
  • 4). “sizeof 引用”得到的是所指向的變數(物件)的大小,而“sizeof 指標”得到的是指標本身的大小;
  • 5). 指標和引用的自增(++)運算意義不一樣;
  • 6). 引用是型別安全的,而指標不是 (引用比指標多了型別檢查)
  • 7). 引用具有更好的可讀性和實用性。

2. 引用佔用記憶體空間嗎?

如下程式碼中對引用取地址,其實是取的引用所對應的記憶體空間的地址。這個現象讓人覺得引用好像並非一個實體。但是引用是佔用記憶體空間的,而且其佔用的記憶體和指標一樣,因為引用的內部實現就是通過指標來完成的。

比如 Type& name; <===> Type* const name。

int main(void)
{
        int a = 8;
        const int &b = a;
        int *p = &a;
        *p = 0;
        cout<<a; //output 0
    return 0;
}

3. 三目運算子

在C中三目運算子(? :)的結果僅僅可以作為左值,比如如下的做法在C編譯器下是會報錯的,但是C++中卻是可以是通過的。這個進步就是通過引用來實現的,因為下面的三目運算子的返回結果是一個引用,然後對引用進行賦值是允許的。

int main(void)
{
        int a = 8;
        int b = 6;
        (a>b ? a : b) = 88;
        cout<<a; //output 88
    return 0;
}

4. 指標陣列和陣列指標的區別

陣列指標,是指向陣列的指標,而指標陣列則是指該陣列的元素均為指標。

  • 陣列指標,是指向陣列的指標,其本質為指標,形式如下。如 int (*p)[10],p即為指向陣列的指標,()優先順序高,首先說明p是一個指標,指向一個整型的一維陣列,這個一維陣列的長度是n,也可以說是p的步長。也就是說執行p+1時,p要跨過n個整型資料的長度。陣列指標是指向陣列首元素的地址的指標,其本質為指標,可以看成是二級指標。
型別名 (*陣列識別符號)[陣列長度]
  • 指標陣列,在C語言和C++中,陣列元素全為指標的陣列稱為指標陣列,其中一維指標陣列的定義形式如下。指標陣列中每一個元素均為指標,其本質為陣列。如 int *p[n], []優先順序高,先與p結合成為一個數組,再由int*說明這是一個整型指標陣列,它有n個指標型別的陣列元素。這裡執行p+1時,則p指向下一個陣列元素,這樣賦值是錯誤的:p=a;因為p是個不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它們分別是指標變數可以用來存放變數地址。但可以這樣 *p=a; 這裡*p表示指標陣列第一個元素的值,a的首地址的值。
型別名 *陣列識別符號[陣列長度]

第四部分:C++特性

1. 什麼是面向物件(OOP)?

Object Oriented Programming, 面向物件是一種對現實世界理解和抽象的方法、思想,通過將需求要素轉化為物件進行問題處理的一種思想。其核心思想是資料抽象、繼承和動態繫結(多型)。

2. 解釋下封裝、繼承和多型?

  • 1). 封裝:
    封裝是實現面向物件程式設計的第一步,封裝就是將資料或函式等集合在一個個的單元中(我們稱之為類)。
    封裝的意義在於保護或者防止程式碼(資料)被我們無意中破壞。
  • 2). 繼承:
    繼承主要實現重用程式碼,節省開發時間。
    子類可以繼承父類的一些東西。
    a. 公有繼承(public)
    公有繼承的特點是基類的公有成員和保護成員作為派生類的成員時,它們都保持原有的狀態,而基類的私有成員仍然是私有的,不能被這個派生類的子類所訪問。
    b. 私有繼承(private)
    私有繼承的特點是基類的公有成員和保護成員都作為派生類的私有成員,並且不能被這個派生類的子類所訪問。
    c. 保護繼承(protected)
    保護繼承的特點是基類的所有公有成員和保護成員都成為派生類的保護成員,並且只能被它的派生類成員函式或友元訪問,基類的私有成員仍然是私有的。