參考:C/C++ 面試題
作者:zhaouc
釋出時間: 2015-02-15 15:51:00
網址:https://blog.csdn.net/zhaouc/article/details/43835667
參考:C/C++常見面試知識點總結附面試真題----20210529更新
作者:kuweicai
釋出時間: 2018-09-19 22:47:57
網址:https://blog.csdn.net/kuweicai/article/details/82779648?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-2.nonecase
目錄
-
- 1. C中static有什麼作用
- 2.C++中const有什麼用?
- 3. C與C++各自是如何定義常量的?有什麼不同?
- 4. 既然C++中有更好的const為什麼還要使用宏?
- 5. C++中引用和指針的差別?
- 6. 說一說C與C++的記憶體配置設定方式?
- 7. new/delete 與 malloc()/free() 的差別?
- 8. #include<a.h>和#include"a.h" 有什麼差別?
- 9. 在C++ 程式中調用被 C編譯器編譯後的函數,為什麼要加 extern "C"?
- 10. C++中的什麼是多态性? 是如何實作的?
- 11. 什麼是動态特性?
- 12.什麼是封裝?C++中是如何實作的?
- 13. 什麼是RTTI?
- 14. 什麼是拷貝構造函數?
- 15. 什麼是深淺拷貝?
- 16.面向對象程式設計的優點?
以下内容部分整理自網絡,部分為自己面試的真題。
1. C中static有什麼作用
(1)隐藏。 當我們同時編譯多個檔案時,所有未加static字首的全局變量和函數都具有全局可見性,故使用static在不同的檔案中定義同名函數和同名變量,而不必擔心命名沖突。
(2)static的第二個作用是保持變量内容的持久。存儲在靜态資料區的變量會在程式剛開始運作時就完成初始化,也是唯一的一次初始化。共有兩種變量存儲在靜态存儲區:全局變量和static變量。
(3)static的第三個作用是預設初始化為0.其實全局變量也具備這一屬性,因為全局變量也存儲在靜态資料區。在靜态資料區,記憶體中所有的位元組預設值都是0×00,某些時候這一特點可以減少程式員的工作量。
2.C++中const有什麼用?
不要一聽到const就說是常量,這樣給考官一種在和一個外行交談的感覺。應該說const修飾的内容不可改變就行了, 定義常量隻是一種使用方式而已,還有const資料成員,const參數, const傳回值, const成員函數等, 被const修飾的東西都受到強制保護,可以預防意外的變動,能提高程式的健壯性。
3. C與C++各自是如何定義常量的?有什麼不同?
C中是使用宏#define定義, C++使用更好的const來定義。
差別:
1)const是有資料類型的常量,而宏常量沒有,編譯器可以對前者進行靜态類型安全檢查,對後者僅是字元替換,沒有類型安全檢查,而且在字元替換時可能會産生意料不到的錯誤(邊際效應)。
2)有些編譯器可以對const常量進行調試, 不能對宏調試。
4. 既然C++中有更好的const為什麼還要使用宏?
const無法代替宏作為衛哨來防止檔案的重複包含。
5. C++中引用和指針的差別?
引用是對象的别名, 操作引用就是操作這個對象, 必須在建立的同時有效得初始化(引用一個有效的對象, 不可為NULL), 初始化完畢就再也不可改變, 引用具有指針的效率, 又具有變量使用的友善性和直覺性, 在語言層面上引用和對象的用法一樣, 在二進制層面上引用一般都是通過指針來實作的, 隻是編譯器幫我們完成了轉換。 之是以使用引用是為了用适當的工具做恰如其分的事, 展現了最小特權原則。
6. 說一說C與C++的記憶體配置設定方式?
1)從靜态存儲區域配置設定。記憶體在程式編譯的時候就已經配置設定好,這塊記憶體在程式的整個運作期間都存在,如全局變量,static變量。
2)在棧上建立。在執行函數時,函數内局部變量的存儲單元都可以在棧上建立,函數執行結束時這些存儲單元自動被釋放。棧記憶體配置設定運算内置于處理器的指令集中,效率很高,但是配置設定的記憶體容量有限。
3)從堆上配置設定(動态記憶體配置設定)程式在運作的時候用malloc或new申請任意多少的記憶體,程式員負責在何時用free或delete釋放記憶體。動态記憶體的生存期自己決定,使用非常靈活。
7. new/delete 與 malloc()/free() 的差別?
malloc() 與 free() 是C語言的标準庫函數, new/delete 是C++的運算符, 他們都可以用來申請和釋放記憶體, malloc()和free()不在編譯器控制權限之内, 不能把構造函數和析構函數的任務強加給他們。www.cdtarena.com
8. #include<a.h>和#include"a.h" 有什麼差別?
答:對于#include <a.h> ,編譯器從标準庫路徑開始搜尋 a.h對于#include "a.h" ,編譯器從使用者的工作路徑開始搜尋 a.h
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. C++中的什麼是多态性? 是如何實作的?
多态性是面向對象程式設計語言繼資料抽象和繼承之後的第三個基本特征。它是在運作時出現的多态性通過派生類和虛函數實作。基類和派生類中使用同樣的函數名, 完成不同的操作具體實作相隔離的另一類接口,即把" w h a t"從"h o w"分離開來。多态性提高了代碼的組織性和可讀性,虛函數則根據類型的不同來進行不同的隔離。
11. 什麼是動态特性?
在絕大多數情況下, 程式的功能是在編譯的時候就确定下來的, 我們稱之為靜态特性。 反之, 如果程式的功能是在運作時刻才能确定下來的, 則稱之為動态特性。C++中, 虛函數,抽象基類, 動态綁定和多态構成了出色的動态特性。
12.什麼是封裝?C++中是如何實作的?
封裝來源于資訊隐藏的設計理念, 是通過特性和行為的組合來建立新資料類型讓接口與具體實作相隔離。C++中是通過類來實作的, 為了盡量避免某個子產品的行為幹擾同一系統中的其它子產品,應該讓子產品僅僅公開必須讓外界知道的接口。
13. 什麼是RTTI?
RTTI事指運作時類型識别(Run-time type identification)在隻有一個指向基類的指針或引用時确定一個對象的準确類型。
14. 什麼是拷貝構造函數?
它是單個參數的構造函數,其參數是與它同屬一類的對象的(常)引用;類定義中,如果未提供自己的拷貝構造函數,C++提供一個預設拷貝構造函數,該預設拷貝構造函數完成一個成員到一個成員的拷貝
15. 什麼是深淺拷貝?
淺拷貝是建立了一個對象用一個現成的對象初始化它的時候隻是複制了成員(簡單指派)而沒有拷貝配置設定給成員的資源(如給其指針變量成員配置設定了動态記憶體); 深拷貝是當一個對象建立時,如果配置設定了資源,就需要定義自己的拷貝構造函數,使之不但拷貝成員也拷貝配置設定給它的資源。
16.面向對象程式設計的優點?
開發時間短, 效率高, 可靠性高。面向對象程式設計的編碼具有高可重用性,可以在應用程式中大量采用成熟的類庫(如STL),進而雖短了開發時間,軟體易于維護和更新。
第一部分:計算機基礎
1. C/C++記憶體有哪幾種類型?
C中,記憶體分為5個區:堆(malloc)、棧(如局部變量、函數參數)、程式代碼區(存放二進制代碼)、全局/靜态存儲區(全局變量、static變量)和常量存儲區(常量)。此外,C++中有自由存儲區(new)一說。
全局變量、static變量會初始化為預設值,而堆和棧上的變量是随機的,不确定的。
2. 堆和棧的差別?
- 1).堆存放動态配置設定的對象——即那些在程式運作時動态配置設定的對象,比如 new 出來的對象,其生存期由程式控制;
- 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;
}
- 1
- 2
- 3
- 4
- 5
- 6
其編譯過程如下:
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
調用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. const 有什麼用途
主要有三點:
- 1).定義隻讀變量,或者常量(隻讀變量和常量的差別參考下面一條);
- 2).修飾函數的參數和函數的傳回值;
- 3).修飾函數的定義體,這裡的函數為類的成員函數,被const修飾的成員函數代表不能修改成員變量的值,是以const成員函數隻能調用const成員函數;
- 4).隻讀對象。隻讀對象隻能調用const成員函數。
class Screen {
public:
const char cha; //const成員變量
char get() const; //const成員函數
};
const Screen screen; //隻讀對象
- 1
- 2
- 3
- 4
- 5
- 6
- 7
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
另外值得一說的是,由于c++中const常量的值在編譯期就已經決定,下面的做法是OK的,但是c中是編譯通不過的。
int main(void)
{
const int a = 8;
const int b = 2;
int array[a+b] = {0};
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
5. 宏和内聯(inline)函數的比較?
- 1). 首先宏是C中引入的一種預處理功能;
- 2). 内聯(inline)函數是C++中引用的一個新的關鍵字;C++中推薦使用内聯函數來替代宏代碼片段;
- 3). 内聯函數将函數體直接擴充到調用内聯函數的地方,這樣減少了參數壓棧,跳轉,傳回等過程;
- 4). 由于内聯發生在編譯階段,是以内聯相較宏,是有參數檢查和傳回值檢查的,是以使用起來更為安全;
- 5). 需要注意的是, inline會向編譯期提出内聯請求,但是是否内聯由編譯期決定(當然可以通過設定編譯器,強制使用内聯);
- 6). 由于内聯是一種優化方式,在某些情況下,即使沒有顯示的聲明内聯,比如定義在class内部的方法,編譯器也可能将其作為内聯函數。
-
7). 内聯函數不能過于複雜,最初C++限定不能有任何形式的循環,不能有過多的條件判斷,不能對函數進行取位址操作等,但是現在的編譯器幾乎沒有什麼限制,基本都可以實作内聯。
更多請參考inline關鍵字
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);
- 1
特别的,在C++中,如下的代碼,用new建立一個對象(new 會觸發構造函數, delete會觸發析構函數),但是malloc僅僅申請了一個空間,是以在C++中引入new和delete來支援面向對象。
#include <cstdlib>
class Test
{
...
}
Test* pn = new Test;
Test* pm = (Test*)malloc(sizeof(Test));
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
8. static 有什麼用途
- 1). 靜态(局部/全局)變量
- 2). 靜态函數
- 3). 類的靜态資料成員
- 4). 類的靜态成員函數
9. 類的靜态成員變量和靜态成員函數各有哪些特性?
靜态成員變量
- 1). 靜态成員變量需要在類内聲明(加static),在類外初始化(不能加static),如下例所示;
- 2). 靜态成員變量在類外單獨配置設定存儲空間,位于全局資料區,是以靜态成員變量的生命周期不依賴于類的某個對象,而是所有類的對象共享靜态成員變量;
- 3). 可以通過對象名直接通路公有靜态成員變量;
- 4). 可以通過類名直接調用公有靜态成員變量,即不需要通過對象,這一點是普通成員變量所不具備的。
class example{
public:
static int m_int; //static成員變量
};
int example::m_int = 0; //沒有static
cout<<example::m_int; //可以直接通過類名調用靜态成員變量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
靜态成員函數
- 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(); //可以直接通過類名調用靜态成員變量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
10. 在C++程式中調用被C編譯器編譯後的函數,為什麼要加extern“C”?
C++語言支援函數重載,C語言不支援函數重載,函數被C++編譯器編譯後在庫中的名字與C語言的不同,假設某個函數原型為:
void foo(int x, int y);
- 1
該函數被C編譯器編譯後在庫中的名字為 _foo, 而C++編譯器則會産生像: _foo_int_int 之類的名字。為了解決此類名字比對的問題,C++提供了C連結交換指定符号 extern “C”。
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 是從實體上判斷是不是同一個頭檔案,而不是從内容上。
12. 當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型的對象引用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
i++和++i的考點比較多,簡單來說,就是i++傳回的是i的值,而++i傳回的是i+1的值。也就是++i是一個确定的值,是一個可修改的左值,如下使用:
cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;
- 1
- 2
可以不停的嵌套++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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
首先是函數的參數入棧順序從右向左入棧的,計算順序也是從右往左計算的,不過都是計算完以後再進行的壓棧操作:
對于第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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
3. 三目運算符
在C中三目運算符(? :)的結果僅僅可以作為右值,比如如下的做法在C編譯器下是會報錯的,但是C++中卻是可以是通過的。這個進步就是通過引用來實作的,因為下面的三目運算符的傳回結果是一個引用,然後對引用進行指派是允許的。
int main(void)
{
int a = 8;
int b = 6;
(a>b ? a : b) = 88;
cout<<a; //output 88
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
4. 指針數組和數組指針的差別
數組指針,是指向數組的指針,而指針數組則是指該數組的元素均為指針。
- 數組指針,是指向數組的指針,其本質為指針,形式如下。如 int (*p)[10],p即為指向數組的指針,()優先級高,首先說明p是一個指針,指向一個整型的一維數組,這個一維數組的長度是n,也可以說是p的步長。也就是說執行p+1時,p要跨過n個整型資料的長度。數組指針是指向數組首元素的位址的指針,其本質為指針,可以看成是二級指針。
類型名 (*數組辨別符)[數組長度]
- 1
- 指針數組,在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的首位址的值。
類型名 *數組辨別符[數組長度]
- 1
5. 左值引用與右值引用
該部分主要摘自:c++ 學習筆記
左值引用就是我們通常所說的引用,如下所示。左值引用通常可以看作是變量的别名。
type-id & cast-expression
// demo
int a = 10
int &b = a
int &c = 10 // 錯誤,無所對一個立即數做引用
const int &d = 10 // 正确, 常引用引用常數量是ok的,其等價于 const int temp = 10; const int &d = temp
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
右值引用是 C++11 新增的特性,其形式如下所示。右值引用用來綁定到右值,綁定到右值以後本來會被銷毀的右值的生存期會延長至與綁定到它的右值引用的生存期。
type-id && cast-expression
// demo
int &&var = 10; // ok
int a = 10
int &&b = a // 錯誤, a 為左值
int &&c = var // 錯誤,var 為左值
int &&d = move(a) // ok, 通過move得到左值的右值引用
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在彙編層面右值引用做的事情和常引用是相同的,即産生臨時量來存儲常量。但是,唯一 一點的差別是,右值引用可以進行讀寫操作,而常引用隻能進行讀操作。
6. 右值引用的意義
-
右值引用支援移動語義的實作,可以減少拷貝,提升程式的執行效率。
下面的代碼時沒有采用右值引用時的實作。
class Stack
{
public:
// 構造
Stack(int size = 1000)
:msize(size), mtop(0)
{
cout << "Stack(int)" << endl;
mpstack = new int[size];
}
// 析構
~Stack()
{
cout << "~Stack()" << endl;
delete[]mpstack;
mpstack = nullptr;
}
// 拷貝構造
Stack(const Stack &src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(const Stack&)" << endl;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
}
// 指派重載
Stack& operator=(const Stack &src)
{
cout << "operator=" << endl;
if (this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
return *this;
}
int getSize()
{
return msize;
}
private:
int *mpstack;
int mtop;
int msize;
};
Stack GetStack(Stack &stack)
{
Stack tmp(stack.getSize());
return tmp;
}
int main()
{
Stack s;
s = GetStack(s);
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
運作結果如下。
Stack(int) // 構造s
Stack(int) // 構造tmp
Stack(const Stack&) // tmp拷貝構造main函數棧幀上的臨時對象
~Stack() // tmp析構
operator= // 臨時對象指派給s
~Stack() // 臨時對象析構
~Stack() // s析構
- 1
- 2
- 3
- 4
- 5
- 6
- 7
執行代碼的過程中調用拷貝構造,将記憶體中的内容逐個拷貝,在 C++ 11 中可以借助右值引用實作移動拷貝構造和移動指派來解決這個問題。
Stack(Stack &&src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(Stack&&)" << endl;
/*此處沒有重新開辟記憶體拷貝資料,把src的資源直接給目前對象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
}
// 帶右值引用參數的指派運算符重載函數
Stack& operator=(Stack &&src)
{
cout << "operator=(Stack&&)" << endl;
if(this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
/*此處沒有重新開辟記憶體拷貝資料,把src的資源直接給目前對象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
return *this;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
執行結果如下。可以看到,在有拷貝構造和移動拷貝構造函數的時候,優先調用了移動拷貝構造和移動指派。在移動拷貝構造和移動指派中直接把資源所有權進行了轉移,而非拷貝,這就大大提高了執行效率。
Stack(int) // 構造s
Stack(int) // 構造tmp
Stack(Stack&&) // 調用帶右值引用的拷貝構造函數,直接将tmp的資源給臨時對象
~Stack() // tmp析構
operator=(Stack&&) // 調用帶右值引用的指派運算符重載函數,直接将臨時對象資源給s
~Stack() // 臨時對象析構
~Stack() // s析構
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 右值引用在可以使重載函數變得更加簡潔。右值引用可以适用 const T& 和 T& 形式的參數。
-
struct W { W(int&, int&) {} }; struct X { X(const int&, int&) {} }; struct Y { Y(int&, const int&) {} }; struct Z { Z(const int&, const int&) {} }; template <typename T, typename A1, typename A2> T* factory(A1& a1, A2& a2) { return new T(a1, a2); } template <typename T, typename A1, typename A2> T* factory_new(A1&& a1, A2&& a2) { return new T(std::forward<A1>(a1), std::forward<A2>(a2)); } // demo int a = 2; int b = 2; W* c = factory<w>(a, b); // ok Z* d = factory<Z>(2, 2); // 錯誤,2 是右值 W* pw = factory_new<W>(a, b); // ok X* px = factory_new<X>(2, b); // ok Y* py = factory_new<Y>(a, 2); // ok Z* e = factory_new<Z>(2, 2); // ok W* f = factory_new<W>(2, 2); // 錯誤,
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
第四部分:C++特性
1. 什麼是面向對象(OOP)?面向對象的意義?
Object Oriented Programming, 面向對象是一種對現實世界了解和抽象的方法、思想,通過将需求要素轉化為對象進行問題處理的一種思想。其核心思想是資料抽象、繼承和動态綁定(多态)。
面向對象的意義在于:将日常生活中習慣的思維方式引入程式設計中;将需求中的概念直覺的映射到解決方案中;以子產品為中心建構可複用的軟體系統;提高軟體産品的可維護性和可擴充性。
2. 解釋下封裝、繼承和多态?
-
1). 封裝:
封裝是實作面向對象程式設計的第一步,封裝就是将資料或函數等集合在一個個的單元中(我們稱之為類)。
封裝的意義在于保護或者防止代碼(資料)被我們無意中破壞。
從封裝的角度看,public, private 和 protected 屬性的特點如下。
- 不管那種屬性,内類都是可以通路的
- public 是一種暴露的手段,比如暴露接口,類的對象可以通路
- private 是一種隐藏的手段,類的對象不能通路
- protected 成員:
- 和 public 一樣可以被子類繼承
- 和 private 一樣不能在類外被直接調用
- 特例:在衍生類中可以通過衍生類對象通路,如下代碼所示
class Base { public: Base(){}; virtual ~Base(){}; protected: int int_pro; }; class A : public Base { public: A(){}; A(int da){int_pro = da;} // 通過 obj 對象直接通路 protected 成員 void Set(A &obj){obj.int_pro = 24;} void PrintPro(){cout << "The proteted data is " << int_pro <<endl;} };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
-
2). 繼承:
繼承主要實作重用代碼,節省開發時間。
子類可以繼承父類的一些東西。
- a.**公有繼承(public)**公有繼承的特點是基類的公有成員和保護成員作為派生類的成員時,它們都保持原有的狀态(基類的私有成員仍然是私有的,不能被這個派生類的子類所通路)。
- b.**私有繼承(private)**私有繼承的特點是基類的公有成員和保護成員都作為派生類的私有成員(并且不能被這個派生類的子類所通路)。
- c.**保護繼承(protected)**保護繼承的特點是基類的所有公有成員和保護成員都成為派生類的保護成員(并且隻能被它的派生類成員函數或友元通路,基類的私有成員仍然是私有的)。
3).多态:
多态是指通過基類的指針或者引用,在運作時動态調用實際綁定對象函數的行為。與之相對應的編譯時綁定函數稱為靜态綁定。多态是設計模式的基礎,多态是架構的基礎。
3. 什麼時候生成預設構造函數(無參構造函數)?什麼時候生成預設拷貝構造函數?什麼是深拷貝?什麼是淺拷貝?預設拷貝構造函數是哪種拷貝?什麼時候用深拷貝?
- 1). 沒有任何構造函數時,編譯器會自動生成預設構造函數,也就是無參構造函數;當類沒有拷貝構造函數時,會生成預設拷貝構造函數。
- 2). 深拷貝是指拷貝後對象的邏輯狀态相同,而淺拷貝是指拷貝後對象的實體狀态相同;預設拷貝構造函數屬于淺拷貝。
-
3). 當系統中有成員指代了系統中的資源時,需要深拷貝。比如指向了動态記憶體空間,打開了外存中的檔案或者使用了系統中的網絡接口等。如果不進行深拷貝,比如動态記憶體空間,可能會出現多次被釋放的問題。是否需要定義拷貝構造函數的原則是,是類是否有成員調用了系統資源,如果定義拷貝構造函數,一定是定義深拷貝,否則沒有意義。
更多可以參考下面的代碼,比較容易混淆的是指派操作符,其實區分很簡單,在出現等号的時候,如果有構造新的對象時調用的就是構造,不然就是指派操作符。
class A { public: A() { m = new int[4]{ 1,2,3,4 }; std::cout << "constructor" << std::endl; } ~A() { if (m != nullptr) { delete[] m; } } A(const A& a) { this->m = new int[4]; memcpy(a.m, this->m, this->len * sizeof(int)); std::cout << "copy constructor" << std::endl; } // 移動構造 A(A&& a) : m(a.m) { a.m = nullptr; std::cout << "move constructor" << std::endl; } // 指派操作符重載 A& operator= (const A& a) { memcpy(a.m, this->m, this->len * sizeof(int)); std::cout << "operator" << std::endl; return *this; } private: int len = 4; int* m = nullptr; }; A getA(A a) { return a; } int main(void) { A a; // construct A b = a; // copy construct A c(a); // copy construct A d; // construct d = a; // operate A e = getA(a); // construct, move construct return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
4. 構造函數和析構函數的執行順序?
構造函數
- 1). 首先調用父類的構造函數;
- 2). 調用成員變量的構造函數;
- 3). 調用類自身的構造函數。
析構函數
對于棧對象或者全局對象,調用順序與構造函數的調用順序剛好相反,也即後構造的先析構。對于堆對象,析構順序與delete的順序相關。5. 虛析構函數的作用?
基類采用虛析構函數可以防止記憶體洩漏。比如下面的代碼中,如果基類 A 中不是虛析構函數,則 B 的析構函數不會被調用,是以會造成記憶體洩漏。class A{ public: A(){} //~A(){} virtual ~A(){} // 虛析構 }; class B : public A{ public: B(){ // new memory } ~B(){ // delete memory } }; int main(int argc, char *argv) { A *p = new B; // some operations // ... delete p; // 由于基類中是虛析構,這裡會先調用B的析構函數,然後調用A的析構函數 return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
6. 細看拷貝構造函數
對于 class A,它的拷貝構造函數如下:A::A(const A &a){}
- 1
1) 為什麼必須是目前類的引用呢?
循環調用。如果拷貝構造函數的參數不是目前類的引用,而是目前類的對象,那麼在調用拷貝構造函數時,會将另外一個對象直接傳遞給形參,這本身就是一次拷貝,會再次調用拷貝構造函數,然後又将一個對象直接傳遞給了形參,将繼續調用拷貝構造函數……這個過程會一直持續下去,沒有盡頭,陷入死循環。
隻有當參數是目前類的引用時,才不會導緻再次調用拷貝構造函數,這不僅是邏輯上的要求,也是 C++ 文法的要求。
2) 為什麼是 const 引用呢?
拷貝構造函數的目的是用其它對象的資料來初始化目前對象,并沒有期望更改其它對象的資料,添加 const 限制後,這個含義更加明确了。
另外一個原因是,添加 const 限制後,可以将 const 對象和非 const 對象傳遞給形參了,因為非 const 類型可以轉換為 const 類型。如果沒有 const 限制,就不能将 const 對象傳遞給形參,因為 const 類型不能轉換為非 const 類型,這就意味着,不能使用 const 對象來初始化目前對象了。
7. C++的編譯環境
如下圖所示,C++的編譯環境由如下幾部分構成:C++标準庫、C語言相容庫、編譯器擴充庫及編譯子產品。#include<iostream> //C++标準庫,不帶".h" #include<string.h> //C語言相容庫,由編譯器廠商提供
- 1
- 2
8. Most vexing parse
直接上代碼吧。下面 f 和 g 是有問題的,這種情況就稱為 Most vexing parse。class A { public: A() { cout << "const without param" << endl; } A(int a) { cout << "const with param" << endl; } A(const A& b) { cout << "copy construct" << endl; } }; int main(void) { A a; // const without param A b(10); // const with param A c = A(); // const without param A d = A(10); // const with param A e(d); // copy construct A f(); A g(A()); A h{}; // const without param A i{A{}}; // const without param return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
A f(); // 這個是不是可以看做聲明了一個傳回值為A的函數,函數名為 f,參數無 A g(A()); // 這個是不是可以看做聲明了一個傳回值為A的函數,函數名為 g, 參數類型為函數指針,這個函數指針的傳回值類型為A,參數無
- 1
- 2