【Notes】C/C++面試知識點總結

文章目錄


1.計算機基礎

1.1 C/C++內存有哪幾種類型?

C中,內存分爲5個區:(malloc)、(如局部變量、函數參數)、程序代碼區(存放二進制代碼)、全局/靜態存儲區(全局變量、static變量)和常量存儲區(常量)。此外,C++中有自由存儲區(new)一說。全局變量、static變量會初始化爲零,而堆和棧上的變量是隨機的,不確定的。

1.2 堆和棧的區別?

(1)堆存放動態分配的對象——即那些在程序運行時分配的對象,比如局部變量,其生存期由程序控制;
(2)棧用來保存定義在函數內的非static對象,僅在其定義的程序塊運行時才存在;
(3)靜態內存用來保存static對象,類static數據成員以及定義在任何函數外部的變量,static對象在使用之前分配,程序結束時銷燬;
(4)棧和靜態內存的對象由編譯器自動創建和銷燬。

1.3 堆和自由存儲區的區別?

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

1.4 程序編譯的過程?

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

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

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

1.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的存儲方式如下圖所示:
在這裏插入圖片描述
而雙精度的存儲方式如下圖:
在這裏插入圖片描述

1.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()========

1.7 左值和右值

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

1.8 什麼是內存泄漏?面對內存泄漏和指針越界,你有哪些方法?你通常採用哪些方法來避免和減少這類錯誤?

用動態存儲分配函數動態開闢的空間,在使用完畢後未釋放,結果導致一直佔據該內存單元即爲內存泄露。
(1)使用的時候要記得指針的長度
(2)malloc的時候得確定在那裏free
(3)對指針賦值的時候應該注意被賦值指針需要不需要釋放
(4)動態分配內存的指針最好不要再次賦值
(5)在C++中應該優先考慮使用智能指針

2.C與C++

2.1 C和C++的區別?

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

2.2 int fun()和 int fun(void)的區別?

這裏考察的是c 中的默認類型機制。
(1)在c中,int fun() 會解讀爲返回值爲int(即使前面沒有int,也是如此,但是在c++中如果沒有返回類型將報錯),輸入類型和個數沒有限制, 而int fun(void)則限制輸入類型爲一個void。
(2)在c++下,這兩種情況都會解讀爲返回int類型,輸入void類型。

2.3 const有什麼用途

(1)定義只讀變量,或者常量(只讀變量和常量的區別參考下面一條);
(2)修飾函數的參數和函數的返回值;
(3)修飾函數的定義體,這裏的函數爲類的成員函數,被const修飾的成員函數代表不能修改成員變量的值,因此const成員函數只能調用const成員函數;
(4)只讀對象。只讀對象只能調用const成員函數。

class Screen {
public:
const char cha; //const成員變量
char get() const; //const成員函數
};
const Screen screen; //只讀對象

2.4 在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常量依然只是只讀變量。
(6)c++中const成員爲只讀變量,可以通過指針修改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;
}

2.5 宏和內聯(inline)函數的比較?

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

2.6 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);

特別的,在C++中,如下的代碼,用new創建一個對象(new 會觸發構造函數, delete會觸發析構函數),但是malloc僅僅申請了一個空間,所以在C++中引入new和delete來支持面向對象。

#include <cstdlib>
class Test
{
    ...
}
Test* pn = new Test;
Test* pm = (Test*)malloc(sizeof(Test));

2.7 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. 轉換的目標類型必須是指針或者引用
在C++中,普通類型可以通過類型轉換構造函數轉換爲類類型,那麼類可以轉換爲普通類型嗎?答案是肯定的。但是在工程應用中一般不用類型轉換函數,因爲無法抑制隱式的調用類型轉換函數(類型轉換構造函數可以通過explicit來抑制其被隱式的調用),而隱式調用經常是bug的來源。實際工程中替代的方式是定義一個普通函數,通過顯式的調用來達到類型轉換的目的。

class test{
    int m_value;
    ...
public:
    operator int()  //類型轉換函數
    {
        return m_value;
    }

    int toInt() //顯示調用普通函數來實現類型轉換
    {
        return m_value
    }
}int main()
{
    ...
    test a(5);
    int i = a;
    ...
    return 0;
}

2.8 static 有什麼用途

(1)靜態(局部/全局)變量
(2)靜態函數
(3)類的靜態數據成員
(4)類的靜態成員函數

2.9 類的靜態成員變量和靜態成員函數各有哪些特性?

靜態成員變量

(1)靜態成員變量需要在類內聲明(加static),在類外初始化(不能加static),如下例所示;
(2)靜態成員變量在類外單獨分配存儲空間,位於全局數據區,因此靜態成員變量的生命週期不依賴於類的某個對象,而是所有類的對象共享靜態成員變量;
(3)可以通過對象名直接訪問公有靜態成員變量;
(4)可以通過類名直接調用公有靜態成員變量,即不需要通過對象,這一點是普通成員變量所不具備的。

class example{
private:
static int m_int; //static成員變量
};
int example::m_int = 0; //沒有static
cout<<example::m_int; //可以直接通過類名調用靜態成員變量

靜態成員函數

(1)靜態成員函數是類所共享的;
(2)靜態成員函數可以訪問靜態成員變量,但是不能直接訪問普通成員變量(需要通過對象來訪問);需要注意的是普通成員函數既可以訪問普通成員變量,也可以訪問靜態成員變量;
(3)可以通過對象名直接訪問公有靜態成員函數;
(4)可以通過類名直接調用公有靜態成員函數,即不需要通過對象,這一點是普通成員函數所不具備的。

class example{
private:
static int m_int_s; //static成員變量
int m_int;
static int getI() //靜態成員函數在普通成員函數前加static即可
{
  return m_int_s; //如果返回m_int則報錯,但是可以return d.m_int是合法的
}
};
cout<<example::getI(); //可以直接通過類名調用靜態成員變量

2.10 在C++程序中調用被C編譯器編譯後的函數,爲什麼要加extern“C”?

C++語言支持函數重載,C語言不支持函數重載,函數被C++編譯器編譯後在庫中的名字與C語言的不同,假設某個函數原型爲:

void foo(int x, int y);

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

2.11 頭文件中的 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 是從物理上判斷是不是同一個頭文件,而不是從內容上。

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

理論上++i更快,實際與編譯器優化有關,通常幾乎無差別。簡單來說,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++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壓入棧中,得到輸出結果。

3.數組,指針&引用

3.1 指針和引用的區別?

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

3.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.3 三目運算符

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

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

3.4 指針數組和數組指針的區別

數組指針,是指向數組的指針,而指針數組則是指該數組的元素均爲指針
1.數組指針,是指向數組的指針,其本質爲指針,形式如下。如 int (*p)[10],p即爲指向數組的指針,()優先級高,首先說明p是一個指針,指向一個整型的一維數組,這個一維數組的長度是n,也可以說是p的步長。也就是說執行p+1時,p要跨過n個整型數據的長度。數組指針是指向數組首元素的地址的指針,其本質爲指針,可以看成是二級指針。

類型名 (*數組標識符)[數組長度]

2.指針數組,在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的首地址的值。

類型名 *數組標識符[數組長度]

4.C++特性

4.1 什麼是面向對象(OOP)?面向對象的意義?

Object Oriented Programming, 面向對象是一種對現實世界理解和抽象的方法、思想,通過將需求要素轉化爲對象進行問題處理的一種思想。其核心思想是數據抽象、繼承和動態綁定(多態)。
面向對象的意義在於:將日常生活中習慣的思維方式引入程序設計中;將需求中的概念直觀的映射到解決方案中;以模塊爲中心構建可複用的軟件系統;提高軟件產品的可維護性和可擴展性。

4.2 解釋下封裝、繼承和多態?

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

4.3 什麼時候生成默認構造函數(無參構造函數)?什麼時候生成默認拷貝構造函數?什麼是深拷貝?什麼是淺拷貝?默認拷貝構造函數是哪種拷貝?什麼時候用深拷貝?

(1)沒有任何構造函數時,編譯器會自動生成默認構造函數,也就是無參構造函數;當類沒有拷貝構造函數時,會生成默認拷貝構造函數。
(2)深拷貝是指拷貝後對象的邏輯狀態相同,而淺拷貝是指拷貝後對象的物理狀態相同;默認拷貝構造函數屬於淺拷貝
(3)當系統中有成員指代了系統中的資源時,需要深拷貝。比如指向了動態內存空間,打開了外存中的文件或者使用了系統中的網絡接口等。如果不進行深拷貝,比如動態內存空間,可能會出現多次被釋放的問題。是否需要定義拷貝構造函數的原則是,是類是否有成員調用了系統資源,如果定義拷貝構造函數,一定是定義深拷貝,否則沒有意義。

4.4 構造函數和析構函數的執行順序?

構造函數

(1)首先調用父類的構造函數;
(2)調用成員變量的構造函數;
(3)調用類自身的構造函數。

析構函數

對於棧對象或者全局對象,調用順序與構造函數的調用順序剛好相反,也即後構造的先析構。對於堆對象,析構順序與delete的順序相關。

4.5 C++的編譯環境

如下圖所示,C++的編譯環境由如下幾部分構成:C++標準庫、C語言兼容庫、編譯器擴展庫及編譯模塊。
在這裏插入圖片描述

#include<iostream>  //C++標準庫,不帶".h"
#include<string.h>  //C語言兼容庫,由編譯器廠商提供

值得注意的是,C語言兼容庫功能上跟C++標準庫中的C語言子庫相同,它的存在主要爲了兼容C語言編譯器,也就是說如果一個文件只包含C語言兼容庫(不包含C++標準庫),那麼它在C語言編譯器中依然可以編譯通過。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章