面試中的C++問題

面試中的C++問題

行文開頭,希望和大家達成一個共識,有些面試的問題雖然確實很刁鑽,但是不可否認它們是在考察你對C++的一些本質特性的認識。希望大家是因標題吸引而來,看完的收穫卻不僅僅應用於面試。

1.數組指針和指針數組
首先記住後面的是主語,前面的是定語。

數組指針(也稱行指針):指向數組的指針
int (*p)[4]; //該語句是定義一個數組指針,指向含4個元素的一維數組。因爲()優先級高,優先結合成指針
所謂行指針: int a[3][4]; p = a; p++; 此時等於從a[0][]到a[1][]

指針數組:存放指針的數組
int *p[n]; // []優先級高,先與p結合成爲一個數組,再由int*說明這是一個整型指針數組

2.函數指針問題
  函數指針是指向一個函數入口的指針
  一個函數指針只能指向一種類型的函數,即具有相同的返回值和相同的參數的函數。
函數指針數組定義:void(*fun[3])(void*);
相應指向類A的成員函數的指針:void (A::*pmf)(char *, const char *);
指向外部函數的指針:void (*pf)(char *, const char *); void strcpy(char * dest, const char * source); pf=strcpy;

3.野指針成因
a.指針變量沒有被初始化。 應該在創建的時候賦NULL或者分配內存空間
char *p = NULL; char *str = (char *) malloc(100);
b.指針被delete或free之後沒有及時賦NULL
c.指針操作超越了變量的作用範圍,所指向的內存值對象生命期已經被銷燬。比如函數中聲明的指針,將其返回了

3.引用和指針的區別
1)引用必須初始化,指針則不必;
2)引用初始化以後不能改變,指針可以改變其指向的對象;
3)不存在指向空值的引用,但存在指向空值的指針;
4)引用是某個對象的別名,主要用來描述函數和參數和返回值。指針與一般的變量是一樣的,會在內存中開闢一塊內存。
5)如果函數的參數或返回值是類的對象的話,採用引用可以提高程序的效率。
6)引用可讀性更高,指針需要解引用

4.動態內存分配
I、棧區(stack)― 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。 棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限,如果申請空間超過棧剩餘空間時,將提示overflow。棧是高地址向低地址擴展的數據結構。在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。注意靜態變量是不入棧的。是一塊連續的內存的區域。
II、堆區(heap) ― 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。用malloc或new申請內存,用free或delete釋放內存。如果在申請動態內存時找不到足夠大的內存塊,malloc和new將返回NULL指針,判斷指針是否爲NULL,如果是則馬上用return語句終止本函數,或者馬上用exit(1)終止整個程序的運行,爲new和malloc設置異常處理函數(如set_new_handler允許客戶指定一個函數來處理這個異常)。堆是低地址向高地址擴展的數據結構。是不連續的內存區域。
III、全局區(靜態區)(static)―,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 - 程序結束後有系統釋放
IV、文字常量區 ―常量字符串就是放在這裏的。 程序結束後由系統釋放
V、程序代碼區―存放函數體的二進制代碼。

5.Const用法
a. char * const p; //修飾指針,指針不可改
const char* p; //修飾指針所指向的值,*p是常量字符串
const char * const p 和 char const * const p; // 內容和指針都不能改
b. const修飾函數參數是它最廣泛的一種用途,它表示函數體中不能修改參數的值, 一定要注意多用const,在引用或者指針參數的時候使用const限制是有意義的,而對於值傳遞的參數使用const則沒有意義。
c. const修飾類對象表示該對象爲常量對象,其中的任何成員都不能被修改;該對象的任何非const成員函數都不能被調用,因爲任何非const成員函數會有修改成員變量的企圖。
e.const修飾類的成員變量,表示成員常量,不能被修改,同時它只能在初始化列表中賦值。static const 的成員需在聲明的地方直接初始。
f.const修飾類的成員函數,則該成員函數不能修改類中任何非const成員。一般寫在函數的最後來修飾。
在函數實現部分也要帶const關鍵字。
g.不要輕易的將函數的返回值類型定爲const;除了重載操作符外一般不要將返回值類型定爲對某個對象的const引用。

6. define宏的一些問題
I.const常量與define宏定義的區別
a. 編譯器處理方式不同
define宏是在預處理階段展開,生命週期止於編譯期。#define常量存在於程序的代碼段。
const常量是編譯運行階段使用,const常量存在於程序的數據段。
b. 類型和安全檢查不同
define宏沒有類型,不做任何類型檢查,僅僅是展開。
const常量有具體的類型,在編譯階段會執行類型檢查。
c. 存儲方式不同
define宏僅僅是展開,有多少地方使用,就展開多少次,不會分配內存。但是確使代碼變長。
const常量會在內存中分配(可以是堆中也可以是棧中)

II.含參數的宏和函數的優缺點(用inline代替宏函數)
a. 函數調用時,先求出實參表達式的值,然後代入形參。而使用帶參的宏只是進行簡單的字符替換。
#define MIN(A, B) ((A) <= (B)? (A):(B))
注意i++的問題
b.函數調用是在程序運行時處理的,分配臨時的內存單元;而宏展開是在編譯時進行的,在展開時不進行
內存分配,不進行值得傳遞處理,沒有“返回值”概念
c. 對函數中的形參和實參都要定義類型,類型要求一致,如不一致則進行類型轉換。而宏不存在類型問題
d.調用函數只可得到一個返回值,而用宏則可以設法得到幾個結果
e.實用宏次數多時,宏展開後源程序變長,沒展開一次源程序增長,函數調用則不會
f.宏替換不佔用運行時間,只佔編譯時間,而函數調用佔用運行時間

7. c++的四種強制類型轉化
1). const_cast
去掉類型的const或volatile屬性,

struct SA{int k};  
const SA ra; 
ra.k = 10;   //直接修改const類型,編譯錯誤  
SA& rb =  const_cast<SA&>(ra);   
rb.k = 10;   //可以修改

2).static_cast
主要用於基本類型之間和具有繼承關係的類型之間的轉換。用於指針類型的轉換沒有太大的意義。
static_cast是無條件和靜態類型轉換,可用於基類和子類的轉換,基本類型轉換,把空指針轉換爲目標類型的空指針,把任何類型的表達式轉換成void類型,static_cast不能進行無關類型(如非基類和子類)指針之間的轉換。

int a;     
double d = static_cast<double>(a);   //基本類型轉換
int &pn = &a;     
void *p = static_cast<void*>(pn);   //任意類型轉換爲void

3). dynamic_cast
你可以用它把一個指向基類的指針或引用對象轉換成繼承類的對象;動態類型轉換,運行時類型安全檢查(轉換失敗返回NULL),基類必須有虛函數,保持多態特性才能用dynamic_cast,只能在繼承類對象的指針之間或引用之間進行類型轉換

class BaseClass{public:  int m_iNum;  virtual void foo(){};};
class DerivedClass:BaseClass{public: char* szName[100];  void bar(){};};
BaseClass* pb = new DerivedClass();

DerivedClass *p2 = dynamic_cast<DerivedClass *>(pb);
BaseClass* pParent = dynamic_cast<BaseClass*>(p2); //子類->父類,動態類型轉換,正確

4). reinterpreter_cast
轉換的類型必須是一個指針、引用、算術類型、函數指針或者成員指針。
主要是將一個類型的指針,轉換爲另一個類型的指針,最普通的用途就是在函數指針類型之間進行轉換。
簡單的將對應內存塊複製截斷。

int DoSomething(){return 0;};
typedef void(*FuncPtr)(){};
FuncPtr funcPtrArray[10];
funcPtrArray[0] = reinterpreter_cast<FuncPtr>(&DoSomething);

8. c++的空類,默認產生哪些類成員函數

class Empty
{
 public:
    Empty();                         //缺省構造函數
    Emptyconst Empty& );           //拷貝構造函數
    ~Empty();                        //虛構函數
    Empty& operator=(const Empty& )  //賦值運算符
    Empty* operator&();              //取址運算符
    const Empty* operator&() const;  //取址運算符 const 
}

9.類成員函數的overload, override 和 隱藏的區別
a.成員函數被重載的特徵:相同的類範圍,函數名字相同,參數不同,virtual 關鍵字可有可無。
b.覆蓋指派生類的函數覆蓋基類函數,特徵是分別位於基類和派生類,函數名字相同,參數相同,基類函數必須有virtual關鍵字
c.隱藏是指派生類的函數屏蔽了與其同名的基類函數。1,派生類的函數與基類的函數同名,但是參數不同,
不論有無virtual關鍵字,基類的函數將被隱藏 2,派生類的函數與基類的函數同名,並且參數也相同,
但是基類函數沒有virtual 關鍵字。此時,基類的函數被隱藏

3種情況怎麼執行:重載:看參數;隱藏:用什麼就調用什麼;覆蓋:調用派生類 。

10.面向對象的三個基本特徵
封裝性
把客觀事物封裝成抽象的類,對自身的數據和方法進行。

繼承性
繼承概念的實現方式有三類:實現繼承、接口繼承和可視繼承。
實現繼承是指使用基類的屬性和方法而無需額外編碼的能力;
接口繼承是指僅使用屬性和方法的名稱、但是子類必須提供實現的能力;
可視繼承是指子窗體(類)使用基窗體(類)的外觀和實現代碼的能力。

多態性
多態性(polymorphisn)是允許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值之後,
父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作。允許將子類類型的指針賦值給父類類型的指針。
實現多態,有二種方式,覆蓋(子類重新定義父類的虛函數),重載(允許存在多個同名函數,參數個數,類型不同)。

11.static關鍵字
a. 函數體內作用範圍爲該函數體,該變量內存只被分配一次,具有記憶能力
b. 在模塊內的static全局變量可以被模塊內所有函數訪問,但不能被模塊外其它函數訪問;
c. 在模塊內的static函數只可被這一模塊內的其它函數調用,這個函數的使用範圍被限制在聲明它的模塊內;
d. 在類中的static成員變量屬於整個類所擁有,對類的所有對象只有一份拷貝;
e.在類中的static成員函數屬於整個類所擁有,這個函數不接收this指針,因而只能訪問類的static成員變量。

12.頭文件的作用
a. 通過頭文件來調用庫功能。在很多場合,源代碼不便(或不準)向用戶公佈,只要向用戶提供頭文件
和二進制的庫即可。用戶只需要按照頭文件中的接口聲明來調用庫功能,而不必關心接口怎麼實現的。
編譯器會從庫中提取相應的代碼。
b. 頭文件能加強類型安全檢查。如果某個接口被實現或被使用時,其方式與頭文件中的聲明不一致,
編譯器就會指出錯誤,這一簡單的規則能大大減輕程序員調試、改錯的負擔。

13.c++中哪些函數不能被聲明爲虛函數
a.普通函數(非成員函數): 虛函數用於基類和派生類
b.構造函數:虛函數可以在只知道部分信息的前提下,只需要知道接口,不需要知道具體類型的時候調用,而構造函數是要創建一個對象,勢必要知道準備類型。
c.內聯成員函數:在調用的地方直接將代碼擴展開,故不能是虛函數
d.靜態成員函數:靜態成員函數是不能被繼承的,它只屬於一個類,因此也不存在動態聯編
e.友元函數:不是類的成員函數,因此也不能被繼承

14. main函數執行前和退出後,都會做什麼工作或執行什麼代碼
I. 執行前:
a. 運行全局構造器,全局對象的構造函數會在main函數之前執行;
b. 設置棧指針,初始化static靜態和global全局變量,即數據段的內容;
c. 將未初始化部分的賦初值:數值型short,int,long等爲0,bool爲FALSE,指針爲NULL等;
d.將main函數的參數,argc,argv等傳遞給main函數
II.退出後
a.可以用_onexit 註冊一個函數,它會在main 之後執行int fn1(void), fn2(void), fn3(void), fn4 (void)

15. 不允許重載的5個運算符

運算符 說明
.* 成員指針訪問運算符
:: 域運算符
sizeof 長度運算符
?: 條件運算符
. 成員訪問運算符

15. 動態鏈接庫和靜態鏈接庫
a. lib是編譯鏈接時用到的,dll是運行時用到的。 如果要完成源碼編譯,只需lib;要使動態鏈接的程序運行,只需dll;
b. 對於lib,那麼是靜態編譯出來的,索引和實現都在其中。使用靜態編譯的lib,程序運行時候不再需要dll,缺點是導致程序比較大,它在編譯時複製展開了。而且失去了動態庫的靈活性,發佈新版本要發佈新的應用程序纔行。
c. 對於dll,載入時動態鏈接(load-time dynamic linking),需要一個lib包含索引和入口信息,應用程序的可執行文件中,存放的不是被調用函數的代碼,而是dll中相應函數的代碼地址,從而節省了內存資源。如果不想用lib文件,運行時動態鏈接(run-time dynamic linking),可以用win32的api中LoadLibrary或LoadLibraryEx函數載入DLL,用GetProcAddress獲取DLL函數的出口地址。

16. 編譯的四個階段
預處理(Pre-Processing):處理宏定義指令#define a b,直接替代掉。 條件編譯指令#ifndef, #ifdef等,可以把不需要 代碼直接過濾。頭文件包含指令,展開頭中的定義。特殊符號的替換,如LINE,FILE等。 預處理後文件爲.i和.ii後綴
編譯(Compiling),優化: 編譯就是通過詞法分析和語法分析,在確認所有指令都符合語法規則之後,將其翻譯成等價的中間代碼或者彙編代碼。刪除公共表達式,循環優化,無用賦值刪除等。 同時有與機器硬件相關的優化。生成.s文件
彙編(Assembling):把彙編語言代碼翻譯目標機器指令的過程。目標文件包含代碼段和數據段 .o文件
鏈接(Linking):靜態鏈接和動態鏈接。 某個文件引用了另一個源文件中定義的符號;調用庫文件等。將目標文件彼此相連。可執行的exe

17. 堆棧溢出的原因
a. 沒有回收垃圾資源
b. 層次太深的遞歸調用

18.Itearator和指針的區別
I.相似點:
a. 指針和iterator都支持與整數進行+,-運算,而且其含義都是從當前位置向前或者向後移動n個位置
b. 指針和iterator都支持減法運算,指針-指針得到的是兩個指針之間的距離,迭代器-迭代器得到的是兩個迭代器之間的距離
c.通過指針或者iterator都能夠修改其指向的元素
II.不同點:
a. cout操作符可以直接輸出指針的值,但是對迭代器進行在操作的時候會報錯。通過看報錯信息和頭文件知道,迭代器返回的是對象引用而不是對象的值,所以cout只能輸出迭代器使用*取值後的值而不能直接輸出其自身。
b. 指針能指向函數而迭代器不行,迭代器只能指向容器
c. 指針是一種特殊的變量,它專門用來存放另一變量的地址,而迭代器只是參考了指針的特性進行設計的一種STL接口。

19.ifndef/define/endif和#pragma once的區別
都是爲了避免被重複包含。
 a. #pragma once用來防止同一個頭文件被多次include,這裏的“同一個文件”是指物理上的一個文件,而不是指內容相同的兩個文件。#ifndef,#define,#endif用來防止某個宏被多次定義,依賴於宏名字不能衝突。
 b. #pragma once是編譯相關,就是說這個編譯系統上能用,但在其他編譯系統不一定可以,也就是說移植性差,不過現在基本上已經是每個編譯器都有這個定義了。
 #ifndef,#define,#endif這個是C++語言相關,這是C++語言中的宏定義,通過宏定義避免文件多次編譯。所以在所有支持C++語言的編譯器上都是有效的,如果寫的程序要跨平臺,最好使用這種方式。

18.容器
STL是一個標準的C++庫,容器只是其中一個重要的組成部分,有順序容器和關聯容器
a. 順序容器: 指的是一組具有相同類型T的對象,以嚴格的線性形式組織在一起
vector:底層數據結構爲數組 ,支持快速隨機訪問
deque:底層數據結構爲一箇中央控制器和多個緩衝區,支持首尾(中間不能)快速增刪,也支持隨機訪問
list:底層數據結構爲雙向鏈表,支持快速增刪
stack:底層一般用23實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時
queue:底層一般用23實現,封閉頭部即可,不用vector的原因應該是容量大小有限制,擴容耗時
priority_queue: 的底層數據結構一般爲vector爲底層容器,堆heap爲處理規則來管理底層容器實現
b. 關聯容器,提供一個key實現對對象的隨機訪問,其特點是key是有序的元素是按照預定義的鍵順序插入的
set 底層數據結構爲紅黑樹,有序,不重複
multiset 底層數據結構爲紅黑樹,有序,可重複
map 底層數據結構爲紅黑樹,有序,不重複
multimap 底層數據結構爲紅黑樹,有序,可重複
hash_set 底層數據結構爲hash表,無序,不重複
hash_multiset 底層數據結構爲hash表,無序,可重複
hash_map 底層數據結構爲hash表,無序,不重複
hash_multimap 底層數據結構爲hash表,無序,可重複

vector、list和deque提供給程序員不同的複雜度,因此應該這麼用:vector是一種可以默認使用的序列類型,當很頻繁地對序列中部進行插入和刪除時應該用list,當大部分插入和刪除發生在序列的頭或尾時可以選擇deque這種數據結構。

你需要“可以在容器的任意位置插入一個新元素”的能力嗎?如果是,你需要序列容器,關聯容器做不到。

你關心元素在容器中的順序嗎?如果不,散列容器就是可行的選擇。否則,你要避免使用散列容器。

你需要哪一類迭代器?如果必須是隨機訪問迭代器,在技術上你就只能限於vector、deque和string。

當插入或者刪除數據時,是否非常在意容器內現有元素的移動?如果是,你就必須放棄連續內存容器。

容器中的數據的內存佈局需要兼容C嗎?如果是,你就只能用vector。

查找速度很重要嗎?如果是,你就應該看看散列容器(優於)排序的vector(優於)標準的關聯容器大概是這個順序。

你需要插入和刪除的事務性語義嗎?也就是說,你需要有可靠地回退插入和刪除的能力嗎?如果是,你就需要使用基於節點的容器。如果你需要多元素插入的事務性語義,你就應該選擇list,因爲list是唯一提供多元素插入事務性語義的標準容器。事務性語義對於有興趣寫異常安全代碼的程序員來說非常重要。

你要把迭代器、指針和引用的失效次數減到最少嗎?如果是,你就應該使用基於節點的容器,因爲在這些容器上進行插入和刪除不會使迭代器、指針和引用失效(除非它們指向你刪除的元素)。一般來說,在連續內存容器上插入和刪除會使所有指向容器的迭代器、指針和引用失效。

你需要具有以下特性的序列容器嗎:1)可以使用隨機訪問迭代器;2)只要沒有刪除而且插入只發生在容器結尾,指針和引用的數據就不會失效?這個一個非常特殊的情況,但如果你遇到這種情況,deque就是你夢想的容器。(有趣的是,當插入只在容器結尾時,deque的迭代器也可能會失效,deque是唯一一個“在迭代器失效時不會使它的指針和引用失效”的標準STL容器。)

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