1. 程式人生 > >C++知識點整理

C++知識點整理

TP:6A4D8EF6

1.引用與指標有什麼區別?
1)引用必須被初始化,指標不必。(const變數也必須在一開始定義的時候就完成初始化)
2)引用初始化以後不能被改變,指標可以改變所指的物件。(也即引用初始化完成之後,就永遠指向初始化時指定的物件;但指標可以動態修改指向,使之指向其他物件.)
3)不存在指向空值的引用,但是存在指向空值的指標。

2.描述一下extern的用法
參見博文:http://www.cnblogs.com/yyxt/p/3891712.html

3.對於一個頻繁使用的短小函式,在C語言中應用什麼實現,在C++中應用什麼實現?
C用巨集定義,C++用inline;
在C++中要儘量使用inline函式代替巨集定義,具體分析參見博文:http://www.cnblogs.com/yyxt/p/4799898.html之 條款02;


4. 當一個類X 中沒有生命任何非靜態成員變數與成員函式,這時sizeof(X)的值是多少,請解釋一下?

對於C++中的一個空類

classX { }; 

事實上並不是空的,sizeof(X)並不等於0,一般的結果是1。每個X的物件都有一個隱晦的1bytes,是被編譯器安插進去的一個char,這樣可以使得這個class的兩個objects在記憶體中配置獨一無二的地址。

X作為另一個類的成員時,如:

classA
 {
 public:
     X x;
     int a;
 };
由於 X佔一個位元組,int4個位元組,再加上編譯器的alignment
調整,sizeof(Y)=8

但是當一個類繼承X時:

classY :public X
 {
 public:
      int a;
 };
這時大部分編譯器對於sizeof(Y)的結果是4,而不是8。這就是所謂的空白基類最優化在(empty baseoptimization-EBOemptybaseclassopimization-EBCO)。在空基類被繼承後由於沒有任何資料成員,所以子類優化掉基類所佔的1byteEBO並不是c++標準所規定必須的,但是大部分編譯器都會這麼做.


5. 陣列和指標有什麼區別?

(1)陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。陣列名對應著(而不是指向)一塊記憶體,其地址與容量在生命期內保持不變,只有陣列的內容可以改變。

指標可以隨時指向任意型別的記憶體塊,它的特徵是“可變”,所以我們常用指標來操作動態記憶體。指標遠比陣列靈活,但也更危險。

(2) 修改內容上的區別:

char a[] = “hello”;
a[0] = ‘X’;
char *p = “world”; // 注意p 指向常量字串
p[0] = ‘X’; // 編譯器不能發現該錯誤,執行時錯誤
(3) 用運算子sizeof 可以計算出陣列的容量(位元組數)。sizeof(p),vp 為指標得到的是一個指標變數的位元組數4bytes,而不是p 所指的記憶體容量。

(4)當陣列作為函式的引數進行傳遞時,該陣列自動退化為同類型的指標


6. strcpy()和memcpy()的區別?

strcpy和memcpy都是標準C庫函式,它們有下面的特點。
strcpy提供了字串的複製。即strcpy只用於字串複製,並且它不僅複製字串內容之外,還會複製字串的結束符。

已知strcpy函式的原型是:char* strcpy(char* dest, const char* src);
memcpy提供了一般記憶體的複製。即memcpy對於需要複製的內容沒有限制,因此用途更廣。
void *memcpy( void *dest, const void *src, size_t count );

strcpy和memcpy主要有以下3方面的區別:
1、複製的內容不同。strcpy只能複製字串,而memcpy可以複製任意內容,例如字元陣列、整型、結構體、類等。
2、複製的方法不同。strcpy不需要指定長度,它遇到被複制字元的串結束符"\0"才結束,所以容易溢位。memcpy則是根據其第3個引數決定複製的長度。
3、用途不同。通常在複製字串時用strcpy,而需要複製其他型別資料時則一般用memcpy


7. 總結static的應用和作用?
(1)函式體內static變數的作用範圍為該函式體,不同於auto變數,該變數的記憶體只被分配一次,因此其值在下次呼叫時仍維持上次的值;
(2)在模組內的static全域性變數可以被模組內所用函式訪問,但不能被模組外其它函式訪問;
(3)在模組內的static函式只可被這一模組內的其它函式呼叫,這個函式的使用範圍被限制在宣告它的模組內;
(4)在類中的static成員變數屬於整個類所擁有,對類的所有物件只有一份拷貝;
(5)在類中的static成員函式屬於整個類所擁有,這個函式不接收this指標,因而只能訪問類的static成員變數。

更多參見博文: http://www.cnblogs.com/yyxt/p/4011893.html


8. 總結const的應用和作用?

(1)欲阻止一個變數被改變,可以使用const關鍵字。在定義該const變數時,通常需要對它進行初始化,因為以後就沒有機會再去改變它了;
(2)對指標來說,可以指定指標本身為const,也可以指定指標所指的資料為const,或二者同時指定為const;
(3)在一個函式宣告中,const可以修飾形參,表明它是一個輸入引數,在函式內部不能改變其值;
(4)對於類的成員函式,若指定其為const型別,則表明其是一個常函式,不能修改類的成員變數;
(5)對於類的成員函式,有時候必須指定其返回值為const型別,以使得其返回值不為“左值”。


9. 結構(struct)和聯合(union)的區別?
1) 結構和聯合都是由多個不同的資料型別成員組成, 但在任何同一時刻, 聯合轉只存放了一個被選中的成員, 而結構的所有成員都存在。

2) 對於聯合的不同成員賦值, 將會對其它成員重寫,  原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的.


10. 類(class)與結構(struct)的區別?

(1)預設的繼承訪問許可權: struct是public的,class是private的;
(2)class是引用型別,struct是值型別;
(3)class可以繼承類、介面和被繼承,struct只能繼承介面,不能被繼承;
(4)class有預設的無參建構函式,有解構函式,struct沒有預設的無參建構函式,且只能宣告有參的建構函式,沒有解構函式;
(5)class可以使用abstract和sealed,有protected修飾符,struct不可以用abstract和sealed,沒有protected修飾符;
(6)class必須使用new初始化,結構可以不用new初始化;
(7) class例項由垃圾回收機制來保證記憶體的回收處理,而struct變數使用完後立即自動解除記憶體分配;

從職能觀點來看,class表現為行為,而struct常用於儲存資料;

作為引數傳遞時,class變數以按址方式傳遞,而struct變數是以按值方式傳遞的。

如何選擇使用結構還是類:
1).堆疊的空間有限,對於大量的邏輯的物件,建立類要比建立結構好一些
2).結構表示如點、矩形和顏色這樣的輕量物件,例如,如果宣告一個含有 1000 個點物件的陣列,則將為引用每個物件分配附加的記憶體。在此情況下,結構的成本較低
3).在表現抽象和多級別的物件層次時,類是最好的選擇
4).大多數情況下該型別只是一些資料時,結構是最佳的選擇


11. 簡述assert()用法與注意事項:
(1) 在函式開始處檢驗傳入引數的合法性;
(2) 每個assert只檢驗一個條件,因為同時檢驗多個條件時,如果斷言失敗,無法直觀的判斷是哪個條件失敗;
(3) 不能使用改變環境的語句,因為assert只在DEBUG個生效,如果這麼做,會使用程式在真正執行時遇到問題;
(4) assert和後面的語句應空一行,以形成邏輯和視覺上的一致感;
(5) 有的地方,assert不能代替條件過濾。


12. windows訊息系統由哪幾部分構成?
由以下3部分組成:
1. 訊息佇列:作業系統負責為程序維護一個訊息佇列,程式執行時不斷從該訊息佇列中獲取訊息、處理訊息;
2. 訊息迴圈:應用程式通過訊息迴圈不斷獲取訊息、處理訊息。
3. 訊息處理:訊息迴圈負責將訊息派發到相關的視窗上使用關聯的視窗過程函式進行處理。


13. 在標頭檔案中進行類的宣告,在對應的實現檔案中進行類的定義有什麼意義?

一般都是代表一個基本功能的原始檔引用相應的標頭檔案。一個 相關功能的模組可能有若干對原始檔和標頭檔案組成。這是基於元件程式設計的核心。c語言中標頭檔案中一般定義了函式的宣告、結構體的定義、巨集定義。(常量和全域性變數最好放到原始檔中).

優勢:

1) 從業務擴充套件性上看:標頭檔案中放函式的宣告,函式由原始檔實現,這就是將面向介面程式設計:介面和實現分開,這在面對業務變更頻繁的需求中技術實現的好處是顯而易見的: 只要定義出良好地、擴充套件性高的介面,實現是可以很方便的更換。

2) 從程式架構上看:程式碼在在大型程式中 需要分成不同的模組,單一模組中又可能分為不同的業務功能單元,他們間有很多相互的呼叫。標頭檔案中的方法宣告、結構體定義、巨集就都可以充當這部分的模組與模組間、業務功能單位間的介面呼叫。模組與模組間,功能單元與功能單元間都是面向介面的呼叫,耦合性低,這正是基於元件程式設計的核心思想。(模組的高內聚低耦合)

3) 從某些技術角度實現上看:標頭檔案可通過巨集定義(標頭檔案保護巨集)來保證類定義、結構體定義、巨集定義的唯一性。確實很方便,不容易出錯.

(4) 在編譯時,原始檔裡的實現會被編譯成臨時檔案,執行時刻程式找到標頭檔案裡的介面,根據介面找到這些臨時檔案,來呼叫它們這些實現。這樣可以提高編譯效率,因為分開的話只需要編譯一次生成對應的.obj檔案後,再次應用該類的地方,這個類就不會被再次編譯,從而大大的提高了編譯效率


14. main 函式執行之前/之後還會執行什麼程式碼?

main 函式之前執行:
    一些全域性變數、物件和靜態變數、物件的空間分配和賦初值就是在執行main函式之前;
main函式執行完後,還要去執行一些諸如釋放空間、釋放資源使用權等操作
          全域性物件的解構函式會在main函式之後執行;
          用atexit註冊的函式也會在main之後執行。



15. 將一個字串逆序?

參見博文: http://www.cnblogs.com/graphics/archive/2011/03/09/1977717.html


16. 關於陣列的幾道面試題

參見: 關於陣列的幾道面試題

17. 請簡單描述Windows記憶體管理的方法
記憶體管理是作業系統中的重要部分三言兩語講不清楚的.下面只做最簡單的描述:
當程式執行時需要從記憶體中讀出這段程式的程式碼。程式碼的位置必須在實體記憶體中才能被執行,由於現在的作業系統中有非常多的程式執行著,記憶體中不能夠完全放下,所以引出了虛擬記憶體的概念。把哪些不常用的程式片斷就放入虛擬記憶體,當需要用到它的時候在load入主存(實體記憶體)中。這個就是記憶體管理所要做的事。記憶體管理還有另外一件事需要做:計算程式片段在主存中的物理位置,以便CPU排程。
記憶體管理有塊式管理,頁式管理,段式和段頁式管理。現在常用段頁式管理.
(1) 塊式管理:把主存分為一大塊、一大塊的,當所需的程式片斷不在主存時就分配一塊主存空間,把程 序片斷load入主存,就算所需的程式片度只有幾個位元組也只能把這一塊分配給它。這樣會造成很大的浪費,平均浪費了50%的記憶體空間,但時易於管理。
(2) 頁式管理:把主存分為一頁一頁的,每一頁的空間要比一塊一塊的空間小很多,顯然這種方法的空間利用率要比塊式管理高很多。
(3) 段式管理:把主存分為一段一段的,每一段的空間又要比一頁一頁的空間小很多,這種方法在空間利用率上又比頁式管理高很多,但是也有另外一個缺點。一個程式片斷可能會被分為幾十段,這樣很多時間就會被浪費在計算每一段的實體地址上(計算機最耗時間的大家都知道是I/O吧)。
(4) 段頁式管理:結合了段式管理和頁式管理的優點。把主存分為若干頁,每一頁又分為若干段。
各種記憶體管理都有它自己的方法來計算出程式片斷在主存中的實體地址,其實都很相似. 這只是一個大概而已,不足以說明記憶體管理的皮毛。無論哪一本作業系統書上都有詳細的講解.


18.求下面函式的返回值

int func(x) 
{ 
     int countx = 0; 
     while(x) 
     { 
         countx ++; 
         x = x&(x-1); 
      } 
      return countx; 
} 

假定x = 100 答案:countx = 8

思路:將x轉化為2進位制,看含有的1的個數。


19.過載(overload)和重寫(overried,也稱“覆蓋”)的區別?

常考的題目。從定義上來說:

過載:是指允許存在多個同名函式,而這些函式的引數表不同(或許引數個數不同,或許引數型別不同,或許兩者都不同)。

重寫:是指子類重新定義父類虛擬函式的方法。

從實現原理上來說:

過載:編譯器根據函式不同的引數表,對同名函式的名稱做修飾,然後這些同名函式就成了不同的函式(至少對於編譯器來說是這樣的)。如,有兩個同名函式:function func(p:integer):integer;和function func(p:string):integer;。那麼編譯器做過修飾後的函式名稱可能是這樣的:int_func、str_func。對於這兩個函式的呼叫,在編譯器間就已經確定了,是靜態的。也就是說,它們的地址在編譯期就綁定了(早繫結),因此,過載和多型無關!

重寫:和多型真正相關。當子類重新定義了父類的虛擬函式後,父類指標根據賦給它的不同的子類指標,動態的呼叫屬於子類的該函式,這樣的函式呼叫在編譯期間是無法確定的(呼叫的子類的虛擬函式的地址無法給出)。因此,這樣的函式地址是在執行期繫結的(晚繫結)。



20. C++是不是型別安全的?
不是。兩個不同型別的指標之間可以強制轉換(用reinterpret cast)。C#是型別安全的。


21. 將程式跳轉到指定記憶體地址要對絕對地址0x100000賦值,我們可以用(unsigned int*)0x100000 = 1234;那麼要是想讓程式跳轉到絕對地址是0x100000去執行,應該怎麼做?

*((void (*)( ))0x100000 ) ( );
//首先要將0x100000強制轉換成函式指標,即:
(void (*)())0x100000
// 然後再呼叫它:
*((void (*)())0x100000)();
// 用typedef可以看得更直觀些:
typedef void(*)() voidFuncPtr;
*((voidFuncPtr)0x100000)();

22. 複雜宣告

void * ( * (*fp1)(int))[10];
float (*(* fp2)(int,int,int))(int);
int (* ( * fp3)())[10]();
分別表示什麼意思?                                                  
1.void * ( * (*fp1)(int))[10];   fp1是一個指標,指向一個函式,這個函式的引數為int型,函式的返回值也是一個指標,這個返回指標指向一個數組,這個陣列有10個元素,每個元素是一個void*型指標。
2.float (*(* fp2)(int,int,int))(int);   fp2是一個指標,指向一個函式,這個函式的引數為3個int型,函式的返回值也是一個指標,這個返回指標指向一個函式,這個函式的引數為int型,函式的返回值是float型。
3.int (* ( * fp3)())[10]();   fp3是一個指標,指向一個函式,這個函式的引數為空,函式的返回值也是一個指標,這個返回指標指向一個數組,這個陣列有10個元素,每個元素是一個指標,指向一個函式,這個函式的引數為空,函式的返回值是int型。

C/C++經典面試題
面試題 1:變數的宣告和定義有什麼區別
為變數分配地址和儲存空間的稱為定義,不分配地址的稱為宣告。一個變數可以在多個地方宣告,但是隻在一個地方定義。加入 extern 修飾的是變數的宣告,說明此變數將在檔案以外或在檔案後面部分定義。
說明:很多時候一個變數,只是宣告不分配記憶體空間,直到具體使用時才初始化,分配記憶體空間,如外部變數。

面試題 2:寫出 bool 、int、 float、指標變數與“零值”比較的 if 語句

//bool 型資料:
if( flag )
{
	A;
}
else
{
	B;
}
// int 型資料:
if( 0 != flag )
{
	A;
}
else
{
	B;
}
// 指標型數:
if( NULL == flag )
{
	A;
}
else
{
	B;
}
//float 型資料:
if ( ( flag >= NORM ) && ( flag <= NORM ) )
{
	A;
}
注意:應特別注意在 int、指標型變數和“零值”比較的時候,把“零值”放在左邊,這樣當把“==”誤寫成“=”時,編譯器可以報錯,否則這種邏輯錯誤不容易發現,並且可能導致很嚴重的後果。

面試題 3:sizeof 和 strlen 的區別
 sizeof 和 strlen 有以下區別:
sizeof 是一個操作符,strlen 是庫函式。
 sizeof 的引數可以是資料的型別,也可以是變數,而 strlen 只能以結尾為‘\0‘的字串作引數。
 編譯器在編譯時就計算出了 sizeof 的結果。而 strlen 函式必須在執行時才能計算出來。並且 sizeof計算的是資料型別佔記憶體的大小,而 strlen 計算的是字串實際的長度。
 陣列做 sizeof 的引數不退化,傳遞給 strlen 就退化為指標了。
注意:有些是操作符看起來像是函式,而有些函式名看起來又像操作符,這類容易混淆的名稱一定要加以區分,否則遇到陣列名這類特殊資料型別作引數時就很容易出錯。最容易混淆為函式的操作符就是 sizeof。

面試題 4:C 語言的關鍵字 static 和 C++ 的關鍵字 static 有什麼區別
在 C 中 static 用來修飾區域性靜態變數和外部靜態變數、函式。而 C++中除了上述功能外,還用來定義類的成員變數和函式。即靜態成員和靜態成員函式。
注意:程式設計時 static 的記憶性,和全域性性的特點可以讓在不同時期呼叫的函式進行通訊,傳遞資訊,而 C++的靜態成員則可以在多個物件例項間進行通訊,傳遞資訊。

面試題 5:C中的 malloc 和C++中的 new 有什麼區別
malloc 和 new 有以下不同:
(1)new、delete 是操作符,可以過載,只能在 C++中使用。
(2)malloc、free 是函式,可以覆蓋,C、C++中都可以使用。
(3)new 可以呼叫物件的建構函式,對應的 delete 呼叫相應的解構函式。
(4)malloc 僅僅分配記憶體,free 僅僅回收記憶體,並不執行構造和解構函式
(5)new、delete 返回的是某種資料型別指標,malloc、free 返回的是 void 指標。
注意:malloc 申請的記憶體空間要用 free 釋放,而 new 申請的記憶體空間要用 delete 釋放,不要混用。因為兩者實現的機理不同。

面試題 6:寫一個“標準”巨集 MIN

#define min(a,b)((a)<=(b)?(a):(b))
注意:在呼叫時一定要注意這個巨集定義的副作用,如下呼叫:
((++*p)<=(x)?(++*p):(x)
p 指標就自加了兩次,違背了 MIN 的本意。

面試題 7:一個指標可以是 volatile 嗎
可以,因為指標和普通變數一樣,有時也有變化程式的不可控性。常見例:子中斷服務子程式修改一個指向一個 buffer 的指標時,必須用 volatile 來修飾這個指標。
說明:指標是一種普通的變數,從訪問上沒有什麼不同於其他變數的特性。其儲存的數值是個整型資料,和整型變數不同的是,這個整型資料指向的是一段記憶體地址。

面試題 8:a 和&a 有什麼區別
請寫出以下程式碼的列印結果,主要目的是考察 a 和&a 的區別。

#include<stdio.h>
void main( void )
{
	int a[5]={1,2,3,4,5};
	int *ptr=(int *)(&a+1);
	printf("%d,%d",*(a+1),*(ptr-1));
	return;
}
輸出結果:2,5。
注意:陣列名 a 可以作陣列的首地址,而&a 是陣列的指標。思考,將原式的 int *ptr=(int *)(&a+1);改為 int *ptr=(int *)(a+1);時輸出結果將是什麼呢?

面試題 9:簡述 C、C++程式編譯的記憶體分配情況
C、C++中記憶體分配方式可以分為三種:
(1)從靜態儲存區域分配:
記憶體在程式編譯時就已經分配好,這塊記憶體在程式的整個執行期間都存在。速度快、不容易出錯,因為有系統會善後。例如全域性變數,static 變數等。
(2)在棧上分配:
在執行函式時,函式內區域性變數的儲存單元都在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
(3)從堆上分配:
即動態記憶體分配。程式在執行的時候用 malloc 或 new 申請任意大小的記憶體,程式設計師自己負責在何時用 free 或 delete 釋放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活。如果在堆上分配了空間,就有責任回收它,否則執行的程式會出現記憶體洩漏,另外頻繁地分配和釋放不同大小的堆空間將會產生堆內碎塊。
一個 C、C++程式編譯時記憶體分為 5 大儲存區:堆區、棧區、全域性區、文字常量區、程式程式碼區。

面試題 10:簡述 strcpy、sprintf 與 memcpy 的區別
三者主要有以下不同之處:
(1)操作物件不同, strcpy 的兩個操作物件均為字串,sprintf 的操作源物件可以是多種資料型別,目的操作物件是字串, memcpy 的兩個物件就是兩個任意可操作的記憶體地址,並不限於何種資料型別。
(2)執行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)實現功能不同,strcpy 主要實現字串變數間的拷貝,sprintf 主要實現其他資料型別格式到字串的轉化,memcpy 主要是記憶體塊間的拷貝。
說明:strcpy、sprintf 與 memcpy 都可以實現拷貝的功能,但是針對的物件不同,根據實際需求,來選擇合適的函式實現拷貝功能。

面試題 11:設定地址為 0x67a9 的整型變數的值為 0xaa66

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa66;
說明:這道題就是強制型別轉換的典型例子,無論在什麼平臺地址長度和整型資料的長度是一樣的,即一個整型資料可以強制轉換成地址指標型別,只要有意義即可。

面試題 12:面向物件的三大特徵
面向物件的三大特徵是封裝性、繼承性和多型性:
 封裝性:將客觀事物抽象成類,每個類對自身的資料和方法實行 protection(private, protected,public)。
 繼承性:廣義的繼承有三種實現形式:實現繼承(使用基類的屬性和方法而無需額外編碼的能力)、可視繼承(子窗體使用父窗體的外觀和實現程式碼)、介面繼承(僅使用屬性和方法,實現滯後到子類實現)。

多型性:是將父類物件設定成為和一個或更多它的子物件相等的技術。用子類物件給父類物件賦值之後,父類物件就可以根據當前賦值給它的子物件的特性以不同的方式運作。
說明:面向物件的三個特徵是實現面向物件技術的關鍵,每一個特徵的相關技術都非常的複雜,程式設計師應該多看、多練。

面試題 13:C++的空類有哪些成員函式
預設建構函式。
預設拷貝建構函式。
預設解構函式。
預設賦值運算子。
預設取址運算子。
預設取址運算子 const。
注意:有些書上只是簡單的介紹了前四個函式。沒有提及後面這兩個函式。但後面這兩個函式也是空類的預設函式。另外需要注意的是,只有當實際使用這些函式的時候,編譯器才會去定義它們。

面試題 14:談談你對拷貝建構函式和賦值運算子的認識
拷貝建構函式和賦值運算子過載有以下兩個不同之處:
(1)拷貝建構函式生成新的類物件,而賦值運算子不能。
(2)由於拷貝建構函式是直接構造一個新的類物件,所以在初始化這個物件之前不用檢驗源物件是否和新建物件相同。而賦值運算子則需要這個操作,另外賦值運算中如果原來的物件中有記憶體分配要先把記憶體釋放掉.
注意:當有類中有指標型別的成員變數時,一定要重寫拷貝建構函式和賦值運算子,不要使用預設的。

面試題 15:用 C++設計一個不能被繼承的類

template <typename T> class A
{
	friend T;
	private:
	A() {}
	~A() {}
};
class B : virtual public A<B>
{
	public:
	B() {}
	~B() {}
};
class C : virtual public B
{
	public:
	C() {}
	~C() {}
};
void main( void )
{
	B b;
	//C c;
	return;
}
注意:建構函式是繼承實現的關鍵,每次子類物件構造時,首先呼叫的是父類的建構函式,然後才是自己的。

面試題 16:訪問基類的私有虛擬函式
寫出以下程式的輸出結果:

#include <iostream.h>
class A
{
	virtual void g()
	{
		cout << "A::g" << endl;
	}
	private:
		virtual void f()
		{
			cout << "A::f" << endl;
		}
};
class B : public A
{
	void g()
	{
		cout << "B::g" << endl;
	}
	virtual void h()
	{
		cout << "B::h" << endl;
	}
};
typedef void( *Fun )( void );
void main()
{
	B b;
	Fun pFun;
	for(int i = 0 ; i < 3; i++)
	{
		pFun = ( Fun )*( ( int* ) * ( int* )( &b ) + i );
		pFun();
	}
}
輸出結果:
B::g
A::f
B::h
注意:本題主要考察了面試者對虛擬函式的理解程度。一個對虛擬函式不瞭解的人很難正確的做出本題。在學習面向物件的多型性時一定要深刻理解虛擬函式表的工作原理。

面試題 17:簡述類成員函式的重寫、過載和隱藏的區別
(1)重寫和過載主要有以下幾點不同。
範圍的區別:被重寫的和重寫的函式在兩個類中,而過載和被過載的函式在同一個類中。

引數的區別:被重寫函式和重寫函式的引數列表一定相同,而被過載函式和過載函式的引數列表一定不同。

virtual 的區別:重寫的基類中被重寫的函式必須要有 virtual 修飾,而過載函式和被過載函式可以被virtual 修飾,也可以沒有。
(2)隱藏和重寫、過載有以下幾點不同。
與過載的範圍不同:和重寫一樣,隱藏函式和被隱藏函式不在同一個類中。
引數的區別:隱藏函式和被隱藏的函式的引數列表可以相同,也可不同,但是函式名肯定要相同。
當引數不相同時,無論基類中的引數是否被 virtual 修飾,基類的函式都是被隱藏,而不是被重寫。
說明:雖然過載和覆蓋都是實現多型的基礎,但是兩者實現的技術完全不相同,達到的目的也是完全不同的,覆蓋是動態態繫結的多型,而過載是靜態繫結的多型。

面試題 18:簡述多型實現的原理
編譯器發現一個類中有虛擬函式,便會立即為此類生成虛擬函式表 vtable。虛擬函式表的各表項為指向對應虛擬函式的指標。編譯器還會在此類中隱含插入一個指標 vptr(對 vc 編譯器來說,它插在類的第一個位置上)指向虛擬函式表。呼叫此類的建構函式時,在類的建構函式中,編譯器會隱含執行 vptr 與 vtable 的關聯程式碼,將 vptr 指向對應的 vtable,將類與此類的 vtable 聯絡了起來。另外在呼叫類的建構函式時,
指向基礎類的指標此時已經變成指向具體的類的 this 指標,這樣依靠此 this 指標即可得到正確的 vtable。如此才能真正與函式體進行連線,這就是動態聯編,實現多型的基本原理。
注意:一定要區分虛擬函式,純虛擬函式、虛擬繼承的關係和區別。牢記虛擬函式實現原理,因為多型C++面試的重要考點之一,而虛擬函式是實現多型的基礎。

面試題 19:連結串列和陣列有什麼區別
陣列和連結串列有以下幾點不同:
(1)儲存形式:陣列是一塊連續的空間,宣告時就要確定長度。連結串列是一塊可不連續的動態空間,長度可變,每個結點要儲存相鄰結點指標。
(2)資料查詢:陣列的線性查詢速度快,查詢操作直接使用偏移地址。連結串列需要按順序檢索結點,效率低。
(3)資料插入或刪除:連結串列可以快速插入和刪除結點,而陣列則可能需要大量資料移動。
(4)越界問題:連結串列不存在越界問題,陣列有越界問題。
說明:在選擇陣列或連結串列資料結構時,一定要根據實際需要進行選擇。陣列便於查詢,連結串列便於插入刪除。陣列節省空間但是長度固定,連結串列雖然變長但是佔了更多的儲存空間。

面試題 20:怎樣把一個單鏈表反序

// (1)反轉一個連結串列。迴圈演算法。
AList reverse(List n)
{
	if(!n) 	//判斷連結串列是否為空,為空即退出。
	{
		return n;
	}
	list cur = n.next; //儲存頭結點的下個結點
	list pre = n; 	//儲存頭結點
	list tmp;
	pre.next = null; //頭結點的指標指空,轉換後變尾結點
	while ( NULL != cur.next ) 	//迴圈直到 cur.next 為空
	{
		tmp = cur;
		tmp.next = pre
		pre = tmp;
		cur = cur.next;
	}
	return tmp; 	//f 返回頭指標
}
// (2)反轉一個連結串列。遞迴演算法。
List *reverse( List *oldList, List *newHead = NULL )
{
	List *next = oldList-> next; 	//記錄上次翻轉後的連結串列
	oldList-> next = newHead; 	//將當前結點插入到翻轉後連結串列的開頭
	newHead = oldList; 	//遞迴處理剩餘的連結串列
	
	return ( next==NULL )? newHead: reverse( t, newHead );
}
說明:迴圈演算法就是圖 10.2—圖 10.5 的移動過程,比較好理解和想到。遞迴演算法的設計雖有一點難度,但是理解了迴圈演算法,再設計遞迴演算法就簡單多了。

面試題 21:簡述佇列和棧的異同
佇列和棧都是線性儲存結構,但是兩者的插入和刪除資料的操作不同,佇列是“先進先出”,棧是“後進先出”。
注意:區別棧區和堆區。堆區的存取是“順序隨意”,而棧區是“後進先出”。棧由編譯器自動分配釋放 ,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。堆一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由 OS 回收。分配方式類似於連結串列。它與本題中的堆和棧是兩回事。堆疊只是一種資料結構,而堆區和棧區是程式的不同記憶體儲存區域。

面試題 22:能否用兩個棧實現一個佇列的功能

// 結點結構體:
typedef struct node
{
	int data;
	node *next;
}node,*LinkStack;
//建立空棧:
LinkStack CreateNULLStack( LinkStack &S)
{
	S = (LinkStack)malloc( sizeof( node ) );	//申請新結點
	if( NULL == S)
	{
		printf("Fail to malloc a new node.\n");
		return NULL;
	}
	S->data = 0; 	//初始化新結點
	S->next = NULL;
	return S;
}
// 棧的插入函式:
LinkStack Push( LinkStack &S, int data)
{
	if( NULL == S) 	//檢驗棧
	{
		printf("There no node in stack!");
		return NULL;
	}
	LinkStack p = NULL;
	p = (LinkStack)malloc( sizeof( node ) ); 	//申請新結點
	if( NULL == p)
	{
		printf("Fail to malloc a new node.\n");
		return S;
	}
	if( NULL == S->next)
	{
		p->next = NULL;
	}
	else
	{
		p->next = S->next;
	}
	p->data = data; 		//初始化新結點
	S->next = p; 	//插入新結點
	return S;
}
// 出棧函式:
node Pop( LinkStack &S)
{
	node temp;
	temp.data = 0;
	temp.next = NULL;
	if( NULL == S) 	//檢驗棧
	{
		printf("There no node in stack!");
		return temp;
	}
	temp = *S;
	if( S->next == NULL )
	{
		printf("The stack is NULL,can't pop!\n");
		return temp;
	}
	LinkStack p = S ->next; 	//節點出棧
	S->next = S->next->next;
	temp = *p;
	free( p );
	p = NULL;
	return temp;
}
// 雙棧實現佇列的入隊函式:
LinkStack StackToQueuPush( LinkStack &S, int data)
{
	node n;
	LinkStack S1 = NULL;
	CreateNULLStack( S1 );	//建立空棧
	while( NULL != S->next ) 	//S 出棧入 S1
	{
		n = Pop( S );
		Push( S1, n.data );
	}
	Push( S1, data );	//新結點入棧
	while( NULL != S1->next ) 	//S1 出棧入 S
	{
		n = Pop( S1 );
		Push( S, n.data );
	}
	return S;
}
說明:用兩個棧能夠實現一個佇列的功能,那用兩個佇列能否實現一個佇列的功能呢?結果是否定的,因為棧是先進後出,將兩個棧連在一起,就是先進先出。而佇列是現先進先出,無論多少個連在一起都是先進先出,而無法實現先進後出。

面試題 23:計算一顆二叉樹的深度

// 深度的計算函式:
int depth(BiTree T)
{
	if(!T) return 0;
	//判斷當前結點是否為葉子結點
	int d1= depth(T->lchild);//求當前結點的左孩子樹的深度
	int d2= depth(T->rchild);//求當前結點的右孩子樹的深度
	return (d1>d2?d1:d2)+1;
}
注意:根據二叉樹的結構特點,很多演算法都可以用遞迴演算法來實現。

面試題 24:編碼實現直接插入排序

// 直接插入排序程式設計實現如下:
#include<iostream.h>
void main( void )
{
	int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 };
	int i,j;
	for( i = 0; i < 10; i++)
	{
		cout<<ARRAY[i]<<" ";
	}
	cout<<endl;
	for( i = 2; i <= 10; i++ )	//將 ARRAY[2],...,ARRAY[n]依次按序插入
	{
		if(ARRAY[i] < ARRAY[i-1])	//如果 ARRAY[i]大於一切有序的數值,
					//ARRAY[i]將保持原位不動
		{
			ARRAY[0] = ARRAY[i];	//將 ARRAY[0]看做是哨兵,是 ARRAY[i]的副本
			j = i - 1;
			do{
							//從右向左在有序區 ARRAY[1..i-1]中
							//查詢 ARRAY[i]的插入位置
				ARRAY[j+1] = ARRAY[j]; //將數值大於 ARRAY[i]記錄後移
				j-- ;
			}while( ARRAY[0] < ARRAY[j] );
			ARRAY[j+1]=ARRAY[0]; 	//ARRAY[i]插入到正確的位置上
		}
	}
	for(i = 0; i < 10; i++)
	{
		cout<<ARRAY[i]<<" ";
	}
	cout<<endl;
}
注意:所有為簡化邊界條件而引入的附加結點(元素)均可稱為哨兵。引入哨兵後使得查詢迴圈條件的時間大約減少了一半,對於記錄數較大的檔案節約的時間就相當可觀。類似於排序這樣使用頻率非常高的演算法,要儘可能地減少其執行時間。所以不能把上述演算法中的哨兵視為雕蟲小技。

面試題 25:編碼實現氣泡排序

// 氣泡排序程式設計實現如下:
#include <stdio.h>
#define LEN 10 //陣列長度
void main( void )
{
	int ARRAY[10] = { 0, 6, 3, 2, 7, 5, 4, 9, 1, 8 }; //待排序陣列
	printf( "\n" );
	for( int a = 0; a < LEN; a++ )	//列印陣列內容
	{
		printf( "%d ", ARRAY[a] );
	}
	int i = 0;
	int j = 0;
	bool isChange; //設定交換標誌
	for( i = 1; i < LEN; i++ )
	{	//最多做 LEN-1 趟排序
		isChange = 0;		//本趟排序開始前,交換標誌應為假
		for( j = LEN-1; j >= i; j-- ) 		//對當前無序區 ARRAY[i..LEN]自下向上掃描
		{
			if( ARRAY[j+1] < ARRAY[j] )
			{	//交換記錄
				ARRAY[0] = ARRAY[j+1]; 	//ARRAY[0]不是哨兵,僅做暫存單元
				ARRAY[j+1] = ARRAY[j];
				ARRAY[j] = ARRAY[0];
				isChange = 1;  //發生了交換,故將交換標誌置為真
			}
		}
		printf( "\n" );
		for( a = 0; a < LEN; a++)
		//列印本次排序後陣列內容
		{
			printf( "%d ", ARRAY[a] );
		}
		if( !isChange )
		{
			break; 	//本趟排序未發生交換,提前終止
		}
	}
	printf( "\n" );
	return;
}

26:編碼實現直接選擇排序

26:編碼實現直接選擇排序
#include"stdio.h"
#define LEN 9
void main( void )
{
	int ARRAY[LEN]={ 5, 6, 8, 2, 4, 1, 9, 3, 7 };	//待序陣列
	printf("Before sorted:\n");
	for( int m = 0; m < LEN; m++ )	//列印排序前陣列
	{
		printf( "%d", ARRAY[m] );
	}
	for (int i = 1; i <= LEN - 1; i++) //選擇排序
	{
		int t = i - 1;
		int temp = 0;
		for (int j = i; j < LEN; j++)
		{
			if (ARRAY[j] < ARRAY[t])
			{
				t = j;
			}
		}
		if (t != (i - 1))
		{
			temp = ARRAY[i - 1];
			ARRAY[i - 1] = ARRAY[t];
			ARRAY[t] = temp;
		}
	}
	printf( "\n" );
	printf("After sorted:\n");
	for( i = 0; i < LEN; i++ ) 	//列印排序後陣列
	{
		printf( "%d	", ARRAY[i] );
	}
	printf( "\n" );
}
注意:在直接選擇排序中,具有相同關鍵碼的物件可能會顛倒次序,因而直接選擇排序演算法是一種不穩定的排序方法。在本例中只是例舉了簡單的整形陣列排序,肯定不會有什麼問題。但是在複雜的資料元素序列組合中,只是根據單一的某一個關鍵值排序,直接選擇排序則不保證其穩定性,這是直接選擇排序的一個弱點。

面試題 27:程式設計實現堆排序

// 堆排序程式設計實現:
#include <stdio.h>
void createHeep(int ARRAY[],int sPoint, int Len) 	//生成大根堆
{
	while( ( 2 * sPoint + 1 ) < Len )
	{
		int mPoint = 2 * sPoint + 1 ;
		if( ( 2 * sPoint + 2 ) < Len )
		{
			if(ARRAY[ 2 * sPoint + 1 ] < ARRAY[ 2 * sPoint + 2 ] )
			{
				mPoint = 2*sPoint+2;
			}
		}
		if(ARRAY[ sPoint ] < ARRAY[ mPoint ]) 	//堆被破壞,需要重新調整
		{
			int tmpData= ARRAY[ sPoint ];	//交換 sPoint 與 mPoint 的資料
			ARRAY[ sPoint ] = ARRAY[ mPoint ];
			ARRAY[ mPoint ] = tmpData;
			sPoint = mPoint ;
		}
		else
		{
			break; 	//堆未破壞,不再需要調整
		}
	}
	return;
}
void heepSort( int ARRAY[], int Len )	//堆排序
{
	int i=0;
	for ( i = ( Len / 2 - 1 ); i >= 0; i-- )	//將 Hr[0,Lenght-1]建成大根堆
	{
		createHeep(ARRAY, i, Len);
	}
	for ( i = Len - 1; i > 0; i-- )
	{
		int tmpData = ARRAY[0];	//與最後一個記錄交換
		ARRAY[0] = ARRAY[i];
		ARRAY[i] = tmpData;
		createHeep( ARRAY, 0, i ); 	//將 H.r[0..i]重新調整為大根堆
	}
	return;
}
int main( void )
{
	int ARRAY[] ={ 5, 4, 7, 3, 9, 1, 6, 8, 2};
	printf("Before sorted:\n");	//列印排序前陣列內容
	for ( int i = 0; i < 9; i++ )
	{
		printf("%d ", ARRAY[i]);
	}
	printf("\n");
	heepSort( ARRAY, 9 ); //堆排序
	printf("After sorted:\n"); //列印排序後陣列內容
	for( i = 0; i < 9; i++ )
	{
		printf( "%d ", ARRAY[i] );
	}
	printf( "\n" );
	return 0;
}
說明:堆排序,雖然實現複雜,但是非常的實用。另外讀者可是自己設計實現小堆排序的演算法。雖然和大堆排序的實現過程相似,但是卻可以加深對堆排序的記憶和理解。

面試題 28:程式設計實現基數排序

#include <stdio.h>
#include <malloc.h>
#define LEN 8
typedef struct node 	//佇列結點
{
	int data;
	struct node * next;
}node,*QueueNode;
typedef struct Queue 	//佇列
{
	QueueNode front;
	QueueNode rear;
}Queue,*QueueLink;
QueueLink CreateNullQueue( QueueLink &Q) 	//建立空佇列
{
	Q = NULL;
	Q = ( QueueLink )malloc( sizeof( Queue ) );
	if( NULL == Q )
	{
		printf("Fail to malloc null queue!\n");
		return NULL;
	}
	15Q->front = ( QueueNode )malloc( sizeof( node ) );
	Q->rear = ( QueueNode )malloc( sizeof( node ) );
	if( NULL == Q->front || NULL == Q->rear )
	{
		printf("Fail to malloc a new queue's fornt or rear!\n");
		return NULL;
	}
	Q->rear = NULL;
	Q->front->next= Q->rear;
	return Q;
}
int lenData( node data[], int len) 	//計算佇列中各結點的資料的最大位數
{
	int m = 0;
	int temp = 0;
	int d;
	for( int i = 0; i < len; i++)
	{
		d = data[i].data;
		while( d > 0)
		{
			d /= 10;
			temp ++;
		}
		if( temp > m )
		{
			m = temp;
		}
		temp = 0;
	}
	return m;
}

QueueLink Push( QueueLink &Q , node node )	//將資料壓入佇列
{
	QueueNode p1,p;
	p =( QueueNode )malloc( sizeof( node ) );
	if( NULL == p )
	{
		printf("Fail to malloc a new node!\n");
		return NULL;
	}
	p1 = Q->front;
	while(p1->next != NULL)
	{
		p1 = p1->next;
	}
	p->data = node.data;
	p1->next = p;
	p->next = Q->rear;
	return NULL;
}
node Pop( QueueLink &Q) 	//資料出佇列
{
	node temp;
	temp.data = 0;
	temp.next = NULL;
	QueueNode p;
	p = Q->front->next;
	if( p != Q->rear )
	{
		temp = *p;
		Q->front->next = p->next;
		free( p );
		p = NULL;
	}
	return temp;
}

int IsEmpty( QueueLink Q)
{
	if( Q->front->next == Q->rear )
	{
		return 0;
	}
	return 1;
}

int main( void )
	{
	int i = 0;
	int Max = 0;	//記錄結點中資料的最大位數
	int d = 10;
	int power = 1;
	int k = 0;
	node Array[LEN] ={{450, NULL}, {32,NULL}, { 781,NULL}, { 57 ,NULL},{ 145,NULL},{ 613,NULL},{ 401,NULL},{ 594,NULL}}; 	//佇列結點陣列
	QueueLink Queue[10];
	for( i = 0; i < 10; i++)
	{
		CreateNullQueue( Queue[i]);	//初始化佇列陣列
	}
	for( i = 0; i < LEN; i++)
	{
		printf("%d ",Array[i].data);
	}
	printf("\n");
	Max = lenData( Array, LEN );	//計算陣列中關鍵字的最大位數
	printf("%d\n",Max);
	for(int j = 0; j < Max; j++)	//按位排序
	{
		if(j == 0) power = 1;
		else power = power *d;
		for(i = 0; i < LEN; i++)
		{
			k = Array[i].data /power - (Array[i].data/(power * d)) * d;
			Push( Queue[k], Array[i] );
		}
		for(int l = 0, k = 0; l < d; l++)	//排序後出佇列重入陣列
		{
			while( IsEmpty( Queue[l] ) )
			{
				Array[k++] = Pop( Queue[l] );
			}
		}
		for( int t = 0; t < LEN; t++)
		{
			printf("%d ",Array[t].data);
		}
		printf("\n");
	}
	return 0;
}
說明:佇列為基數排序的實現提供了很大的方便,適當的資料機構可以減少演算法的複雜度,讓更多的演算法實現更容易。

面試題 29:談談你對程式設計規範的理解或認識
程式設計規範可總結為:程式的可行性,可讀性、可移植性以及可測試性。
說明:這是程式設計規範的總綱目,面試者不一定要去背誦上面給出的那幾個例子,應該去理解這幾個例子說明的問題,想一想,自己如何解決可行性、可讀性、可移植性以及可測試性這幾個問題,結合以上幾個例子和自己平時的程式設計習慣來回答這個問題。

面試題 30:short i = 0; i = i + 1L;這兩句有錯嗎
程式碼一是錯的,程式碼二是正確的。
說明:在資料安全的情況下大型別的資料向小型別的資料轉換一定要顯示的強制型別轉換。

面試題 31:&&和&、||和|有什麼區別
(1)&和|對運算元進行求值運算,&&和||只是判斷邏輯關係。
(2)&&和||在在判斷左側運算元就能確定結果的情況下就不再對右側運算元求值。
注意:在程式設計的時候有些時候將&&或||替換成&或|沒有出錯,但是其邏輯是錯誤的,可能會導致不可預想的後果(比如當兩個運算元一個是 1 另一個是 2 時)。

面試題 32:C++的引用和 C 語言的指標有什麼區別
指標和引用主要有以下區別:
(1)引用必須被初始化,但是不分配儲存空間。指標不宣告時初始化,在初始化的時候需要分配儲存空間。
(2)引用初始化以後不能被改變,指標可以改變所指的物件。
(3)不存在指向空值的引用,但是存在指向空值的指標。
注意:引用作為函式引數時,會引發一定的問題,因為讓引用作引數,目的就是想改變這個引用所指向地址的內容,而函式呼叫時傳入的是實參,看不出函式的引數是正常變數,還是引用,因此可能會引發錯誤。所以使用時一定要小心謹慎。

面試題 33:在二元樹中找出和為某一值的所有路徑
輸入一個整數和一棵二元樹。從樹的根結點開始往下訪問,一直到葉結點所經過的所有結點形成一條路徑。打印出和與輸入整數相等的所有路徑。例如,輸入整數 9 和如下二元樹:
           3
        /     \
      2        6
    / \
  5   4
則打印出兩條路徑:3,6 和 3,2,4。
【答案】

typedef struct path
{
	BiTNode* tree;//結點資料成員
	struct path* next;//結點指標成員
}PATH,*pPath;
// 初始化樹的結點棧:
void init_path( pPath* L )
{
	*L = ( pPath )malloc( sizeof( PATH ) );	//建立空樹
	( *L )->next = NULL;
}
// 樹結點入棧函式:
void push_path(pPath H, pBTree T)
{
	pPath p = H->next;
	pPath q = H;
	while( NULL != p )
	{
		q = p;
		p = p->next;
	}
	p = ( pPath )malloc( sizeof( PATH ) ); 	//申請新結點
	p->next = NULL; 	//初始化新結點
	p->tree = T;
	q->next = p;	//新結點入棧
}
// 樹結點列印函式:
void print_path( pPath L )
{
	pPath p = L->next;
	while( NULL != p )	//列印當前棧中所有資料
	{
		printf("%d, ", p->tree->data);
		p = p->next;
	}
}
// 樹結點出棧函式:
void pop_path( pPath H )
{
	pPath p = H->next;
	pPath q = H;
	if( NULL == p )	//檢驗當前棧是否為空
	{
		printf("Stack is null!\n");
		return;
	}
	p = p->next;
	while( NULL != p )	//出棧
	{
		q = q->next;
		p = p->next;
	}
	free( q->next );	//釋放出棧結點空間
	q->next = NULL;
}
// 判斷結點是否為葉子結點:
int IsLeaf(pBTree T)
{
	return ( T->lchild == NULL )&&( T->rchild==NULL );
}

// 查詢符合條件的路徑:
int find_path(pBTree T, int sum, pPath L)
{
	push_path( L, T);
	record += T->data;
	if( ( record == sum ) && ( IsLeaf( T ) ) )	//列印符合條件的當前路徑
	{
		print_path( L );
		printf( "\n" );
	}
	if( T->lchild != NULL )	//遞迴查詢當前節點的左孩子
	{
		find_path( T->lchild, sum, L);
	}
	if( T->rchild != NULL )	//遞迴查詢當前節點的右孩子
	{
		find_path( T->rchild, sum, L);
	}
	record -= T->data;
	pop_path(L);
	return 0;
}
注意:資料結構一定要活學活用,例如本題,把所有的結點都壓入棧,而不符合條件的結點彈出棧,很容易實現了有效路徑的查詢。雖然用連結串列也可以實現,但是用棧更利於理解這個問題,即適當的資料結構為更好的演算法設計提供了有利的條件。

面試題 35:typedef 和 define 有什麼區別
(1)用法不同:typedef 用來定義一種資料型別的別名,增強程式的可讀性。define 主要用來定義常量,以及書寫複雜使用頻繁的巨集。
(2)執行時間不同:typedef 是編譯過程的一部分,有型別檢查的功能。define 是巨集定義,是預編譯的部分,其發生在編譯之前,只是簡單的進行字串的替換,不進行型別的檢查。
(3)作用域不同:typedef 有作用域限定。define 不受作用域約束,只要是在 define 聲明後的引用都是正確的。
(4)對指標的操作不同:typedef 和 define 定義的指標時有很大的區別。
注意:typedef 定義是語句,因為句尾要加上分號。而 define 不是語句,千萬不能在句尾加分號。

面試題 36:關鍵字 const 是什麼?
const 用來定義一個只讀的變數或物件。主要優點:便於型別檢查、同巨集定義一樣可以方便地進行引數的修改和調整、節省空間,避免不必要的記憶體分配、可為函式過載提供參考。
說明:const 修飾函式引數,是一種程式設計規範的要求,便於閱讀,一看即知這個引數不能被改變,實現時不易出錯。

面試題 37:static 有什麼作用
static 在 C 中主要用於定義全域性靜態變數、定義區域性靜態變數、定義靜態函式。在 C++中新增了兩種作用:定義靜態資料成員、靜態函式成員。
注意:因為 static 定義的變數分配在靜態區,所以其定義的變數的預設值為 0,普通變數的預設值為隨機數,在定義指標變數時要特別注意。

面試題 38:extern 有什麼作用
extern 標識的變數或者函式宣告其定義在別的檔案中,提示編譯器遇到此變數和函式時在其它模組中尋找其定義。

面試題 39:流操作符過載為什麼返回引用
在程式中,流操作符>>和<<經常連續使用。因此這兩個操作符的返回值應該是一個仍舊支援這兩個操作符的流引用。其他的資料型別都無法做到這一點。
注意:除了在賦值操作符和流操作符之外的其他的一些操作符中,如+、-、*、/等卻千萬不能返回引用。因為這四個操作符的物件都是右值,因此,它們必須構造一個物件作為返回值。

面試題 40:簡述指標常量與常量指標區別
指標常量是指定義了一個指標,這個指標的值只能在定義時初始化,其他地方不能改變。常量指標是指定義了一個指標,這個指標指向一個只讀的物件,不能通過常量指標來改變這個物件的值。指標常量強調的是指標的不可改變性,而常量指標強調的是指標對其所指物件的不可改變性。
注意:無論是指標常量還是常量指標,其最大的用途就是作為函式的形式引數,保證實參在被呼叫函式中的不可改變特性。

面試題 41:陣列名和指標的區別
請寫出以下程式碼的列印結果:

#include <iostream.h>
#include <string.h>
void main(void)
{
	char str[13]="Hello world!";
	char *pStr="Hello world!";
	cout<<sizeof(str)<<endl;
	cout<<sizeof(pStr)<<endl;
	cout<<strlen(str)<<endl;
	cout<<strlen(pStr)<<endl;
	return;
}
【答案】
列印結果:
13
4
12
12
注意:一定要記得陣列名並不是真正意義上的指標,它的內涵要比指標豐富的多。但是當陣列名當做引數傳遞給函式後,其失去原來的含義,變作普通的指標。另外要注意 sizeof 不是函式,只是操作符。

面試題 42:如何避免“野指標”
“野指標”產生原因及解決辦法如下:
(1)指標變數宣告時沒有被初始化。解決辦法:指標宣告時初始化,可以是具體的地址值,也可讓它指向 NULL。
(2)指標 p 被 free 或者 delete 之後,沒有置為 NULL。解決辦法:指標指向的記憶體空間被釋放後指標應該指向 NULL。
(3)指標操作超越了變數的作用範圍。解決辦法:在變數的作用域結束前釋放掉變數的地址空間並且讓指標指向 NULL。
注意:
“野指標”的解決方法也是程式設計規範的基本原則,平時使用指標時一定要避免產生“野指標”,
在使用指標前一定要檢驗指標的合法性。

面試題 43:常引用有什麼作用
常引用的引入主要是為了避免使用變數的引用時,在不知情的情況下改變變數的值。常引用主要用於定義一個普通變數的只讀屬性的別名、作為函式的傳入形參,避免實參在呼叫函式中被意外的改變。

說明:很多情況下,需要用常引用做形參,被引用物件等效於常物件,不能在函式中改變實參的值,這樣的好處是有較高的易讀性和較小的出錯率。

面試題 44:編碼實現字串轉化為數字
編碼實現函式 atoi(), 設計一個程式,把一個字串轉化為一個整型數值。例如數字:“5486321”,
轉化成字元:5486321。
【答案】

int myAtoi(const char * str)
{
	int num = 0;//儲存轉換後的數值
	int isNegative = 0; 	//記錄字串中是否有負號
	int n =0;
	char *p = str;
	if(p == NULL) //判斷指標的合法性
	{
		return -1;
	}
	while(*p++ != '\0')	//計算數字符串度
	{
		n++;
	}
	p = str;
	if(p[0] == '-')	//判斷陣列是否有負號
	{
		isNegative = 1;
	}
	char temp = '0';
	for(int i = 0 ; i < n; i++)
	{
		char temp = *p++;
		if(temp > '9' ||temp < '0')	//濾除非數字字元
		{
			continue;
		}
		if(num !=0 || temp != '0')	//濾除字串開始的 0 字元
		{
			temp -= 0x30;	//將數字字元轉換為數值
			num += temp *int( pow(10 , n - 1 -i) );
		}
	}
	if(isNegative)	//如果字串中有負號,將數值取反
	{
		return (0 - num);
	}
	else
	{
		return num;	//返回轉換後的數值
	}
}
注意:此段程式碼只是實現了十進位制字串到數字的轉化,讀者可以自己去實現 2 進位制,8 進位制,10進位制,16 進位制的轉化。

面試題 45:簡述 strcpy、sprintf 與 memcpy 的區別
三者主要有以下不同之處:
(1)操作物件不同, strcpy 的兩個操作物件均為字串,sprintf 的操作源物件可以是多種資料型別,目的操作物件是字串, memcpy 的兩個物件就是兩個任意可操作的記憶體地址,並不限於何種資料型別。
(2)執行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。
(3)實現功能不同,strcpy 主要實現字串變數間的拷貝,sprintf 主要實現其他資料型別格式到字串的轉化,memcpy 主要是記憶體塊間的拷貝。
說明:strcpy、sprintf 與 memcpy 都可以實現拷貝的功能,但是針對的物件不同,根據實際需求,來
選擇合適的函式實現拷貝功能。

面試題 46:用 C 編寫一個死迴圈程式

while(1)
{ }
說明:很多種途徑都可實現同一種功能,但是不同的方法時間和空間佔用度不同,特別是對於嵌入式軟體,處理器速度比較慢,儲存空間較小,所以時間和空間優勢是選擇各種方法的首要考慮條件。

面試題 47:編碼實現某一變數某位清 0 或置 1
給定一個整型變數 a,寫兩段程式碼,第一個設定 a 的 bit 3,第二個清 a 的 bit 3,在以上兩個操作中,要保持其他位不變。
【答案】

#define BIT3 (0x1 << 3 )
Satic int a;
//設定 a 的 bit 3:
void set_bit3( void )
{
    a |= BIT3;//將 a 第 3 位置 1
}
//清 a 的 bit 3
void set_bit3( void )
{
    a &= ~BIT3;  //將 a 第 3 位清零
}
說明:在置或清變數或暫存器的某一位時,一定要注意不要影響其他位。所以用加減法是很難實現的。

面試題 48:評論下面這個中斷函式
中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴充套件——讓標準 C 支援中斷。具體代表事實是,產生了一個新的關鍵字__interrupt。下面的程式碼就使用了__interrupt 關鍵字去定義一箇中斷服務子程式(ISR),請評論以下這段程式碼。

__interrupt double compute_area (double radius)
{
    double area = PI * radius * radius;
    printf(" Area = %f", area);
    return area;
}
【答案】
這段中斷服務程式主要有以下四個問題:
(1)ISR 不能返回一個值。
(2)ISR 不能傳遞引數。
(3)在 ISR 中做浮點運算是不明智的。
(4)printf()經常有重入和效能上的問題。
注意:本題的第三個和第四個問題雖不是考察的重點,但是如果能提到這兩點可給面試官留下一個好印象。

面試題 49:建構函式能否為虛擬函式
建構函式不能是虛擬函式。而且不能在建構函式中呼叫虛擬函式,因為那樣實際執行的是父類的對應函式,因為自己還沒有構造好。解構函式可以是虛擬函式,而且,在一個複雜類結構中,這往往是必須的。解構函式也可以是純虛擬函式,但純虛解構函式必須有定義體,因為解構函式的呼叫是在子類中隱含的。說明:虛擬函式的動態繫結特性是實現過載的關鍵技術,動態繫結根據實際的呼叫情況查詢相應類的虛擬函式表,呼叫相應的虛擬函式。

面試題 50:談談你對面向物件的認識
面向物件可以理解成對待每一個問題,都是首先要確定這個問題由幾個部分組成,而每一個部分其實就是一個物件。然後再分別設計這些物件,最後得到整個程式。傳統的程式設計多是基於功能的思想來進行考慮和設計的,而面向物件的程式設計則是基於物件的角度來考慮問題。這樣做能夠使得程式更加的簡潔清晰。
說明:程式設計中接觸最多的“面向物件程式設計