1. 程式人生 > >C語言經典面試題 與 C語言面試寶典

C語言經典面試題 與 C語言面試寶典

第一部分:基本概念及其它問答題

1、關鍵字static的作用是什麼?

這個簡單的問題很少有人能回答完全。在C語言中,關鍵字static有三個明顯的作用:

1). 在函式體,一個被宣告為靜態的變數在這一函式被呼叫過程中維持其值不變。

2). 在模組內(但在函式體外),一個被宣告為靜態的變數可以被模組內所用函式訪問,但不能被模組外其它函式訪問。它是一個本地的全域性變數。

3). 在模組內,一個被宣告為靜態的函式只可被這一模組內的其它函式呼叫。那就是,這個函式被限制在宣告它的模組的本地範圍內使用。

大多數應試者能正確回答第一部分,一部分能正確回答第二部分,同是很少的人能懂得第三部分。這是一個應試者的嚴重的缺點,因為他顯然不懂得本地化數

據和程式碼範圍的好處和重要性。

2、“引用”與指標的區別是什麼?

答 、1) 引用必須被初始化,指標不必。

2) 引用初始化以後不能被改變,指標可以改變所指的物件。

3) 不存在指向空值的引用,但是存在指向空值的指標。

指標通過某個指標變數指向一個物件後,對它所指向的變數間接操作。程式中使用指標,程式的可讀性差;而引用本身就是目標變數的別名,對引用的操作就是對目標變數的操作。

流操作符<<和>>、賦值操作符=的返回值、拷貝建構函式的引數、賦值操作符=的引數、其它情況都推薦使用引用

3、.h標頭檔案中的ifndef/define/endif 的作用?

答:防止該標頭檔案被重複引用。

4、#include<file.h> 與 #include "file.h"的區別?

答:前者是從Standard Library的路徑尋找和引用file.h,而後者是從當前工作路徑搜尋並引用file.h。

5、描述實時系統的基本特性

答 :在特定時間內完成特定的任務,實時性與可靠性。

6、全域性變數和區域性變數在記憶體中是否有區別?如果有,是什麼區別?

答 :全域性變數儲存在靜態資料區,區域性變數在堆疊中。

7、什麼是平衡二叉樹?

答 :左右子樹都是平衡二叉樹 且左右子樹的深度差值的絕對值不大於1。

8、堆疊溢位一般是由什麼原因導致的?

答 :1.沒有回收垃圾資源

       2.層次太深的遞迴呼叫

9、氣泡排序演算法的時間複雜度是什麼?

答 :O(n^2)

10、什麼函式不能宣告為虛擬函式?

答:constructor

11、佇列和棧有什麼區別?

答:佇列先進先出,棧後進先出

12、不能做switch()的引數型別

答 :switch的引數不能為實型。

13、區域性變數能否和全域性變數重名?

答:能,區域性會遮蔽全域性。要用全域性變數,需要使用"::"

區域性變數可以與全域性變數同名,在函式內引用這個變數時,會用到同名的區域性變數,而不會用到全域性變數。對於有些編譯器而言,在同一個函式內可以定義多個同名的區域性變數,比如在兩個迴圈體內都定義一個同名的區域性變數,而那個區域性變數的作用域就在那個迴圈體內

14、如何引用一個已經定義過的全域性變數?

答 、可以用引用標頭檔案的方式,也可以用extern關鍵字,如果用引用標頭檔案方式來引用某個在標頭檔案中宣告的全域性變數,假定你將那個變數寫錯了,那麼在編譯期間會報錯,如果你用extern方式引用時,假定你犯了同樣的錯誤,那麼在編譯期間不會報錯,而在連線期間報錯。

15、全域性變數可不可以定義在可被多個.C檔案包含的標頭檔案中?為什麼?

答 、可以,在不同的C檔案中以static形式來宣告同名全域性變數。

可以在不同的C檔案中宣告同名的全域性變數,前提是其中只能有一個C檔案中對此變數賦初值,此時連線不會出錯。

16、語句for( ;1 ;)有什麼問題?它是什麼意思?

答 、和while(1)相同,無限迴圈。

17、do……while和while……do有什麼區別?

答 、前一個迴圈一遍再判斷,後一個判斷以後再迴圈。

18、statac 全域性變數、區域性變數、函式與普通全域性變數、區域性變數、函式

static全域性變數與普通的全域性變數有什麼區別?static區域性變數和普通區域性變數有什麼區別?static函式與普通函式有什麼區別?

答 、全域性變數(外部變數)的說明之前再冠以static 就構成了靜態的全域性變數。全域性變數本身就是靜態儲存方式, 靜態全域性變數當然也是靜態儲存方式。 這兩者在儲存方式上並無不同。這兩者的區別雖在於非靜態全域性變數的作用域是整個源程式, 當一個源程式由多個原始檔組成時,非靜態的全域性變數在各個原始檔中都是有效的。 而靜態全域性變數則限制了其作用域, 即只在定義該變數的原始檔內有效, 在同一源程式的其它原始檔中不能使用它。由於靜態全域性變數的作用域侷限於一個原始檔內,只能為該原始檔內的函式公用, 因此可以避免在其它原始檔中引起錯誤。

從以上分析可以看出, 把區域性變數改變為靜態變數後是改變了它的儲存方式即改變了它的生存期。把全域性變數改變為靜態變數後是改變了它的作用域, 限制了它的使用範圍。

static函式與普通函式作用域不同。僅在本檔案。只在當前原始檔中使用的函式應該說明為內部函式(static),內部函式應該在當前原始檔中說明和定義。對於可在當前原始檔以外使用的函式,應該在一個頭檔案中說明,要使用這些函式的原始檔要包含這個標頭檔案

static全域性變數與普通的全域性變數有什麼區別:static全域性變數只初使化一次,防止在其他檔案單元中被引用;

static區域性變數和普通區域性變數有什麼區別:static區域性變數只被初始化一次,下一次依據上一次結果值;

static函式與普通函式有什麼區別:static函式在記憶體中只有一份,普通函式在每個被呼叫中維持一份拷貝

19、程式的記憶體分配
答:一個由c/C++編譯的程式佔用的記憶體分為以下幾個部分
1、棧區(stack)—由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。
2、堆區(heap)—一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由OS回收。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列,呵呵。
3、全域性區(靜態區)(static)—全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域,未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域。程式結束後由系統釋放。
4、文字常量區—常量字串就是放在這裡的。程式結束後由系統釋放。
5、程式程式碼區—存放函式體的二進位制程式碼

例子程式
這是一個前輩寫的,非常詳細
//main.cpp
  int a=0;    //全域性初始化區
  char *p1;   //全域性未初始化區
  main()
  {
   intb;棧
   char s[]="abc";   //棧
   char *p2;         //棧
   char *p3="123456";   //123456\0在常量區,p3在棧上。
   static int c=0;   //全域性(靜態)初始化區
   p1 = (char*)malloc(10);
   p2 = (char*)malloc(20);   //分配得來得10和20位元組的區域就在堆區。
   strcpy(p1,"123456");   //123456\0放在常量區,編譯器可能會將它與p3所向"123456"優化成一個地方。
}

20、解釋堆和棧的區別

答:堆(heap)和棧(stack)的區別

(1)申請方式
stack:由系統自動分配。例如,宣告在函式中一個區域性變數int b;系統自動在棧中為b開闢空間
heap:需要程式設計師自己申請,並指明大小,在c中malloc函式
如p1=(char*)malloc(10);
在C++中用new運算子
如p2=(char*)malloc(10);
但是注意p1、p2本身是在棧中的。

(2)申請後系統的響應
棧:只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。
堆:首先應該知道作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,
會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣,程式碼中的delete語句才能正確的釋放本記憶體空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中。

(3)申請大小的限制
棧:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

(4)申請效率的比較:
棧:由系統自動分配,速度較快。但程式設計師是無法控制的。
堆:是由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用Virtual Alloc分配記憶體,他不是在堆,也不是在棧,而是直接在程序的地址空間中保留一塊記憶體,雖然用起來最不方便。但是速度快,也最靈活。

(5)堆和棧中的儲存內容
棧:在函式呼叫時,第一個進棧的是主函式中後的下一條指令(函式呼叫語句的下一條可執行語句)的地址,然後是函式的各個引數,在大多數的C編譯器中,引數是由右往左入棧的,然後是函式中的區域性變數。注意靜態變數是不入棧的。
當本次函式呼叫結束後,區域性變數先出棧,然後是引數,最後棧頂指標指向最開始存的地址,也就是主函式中的下一條指令,程式由該點繼續執行。
堆:一般是在堆的頭部用一個位元組存放堆的大小。堆中的具體內容由程式設計師安排。

(6)存取效率的比較

char s1[]="aaaaaaaaaaaaaaa";
char *s2="bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在執行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以後的存取中,在棧上的陣列比指標所指向的字串(例如堆)快。
比如:
#include
voidmain()
{
char a=1;
char c[]="1234567890";
char *p="1234567890";
a = c[1];
a = p[1];
return;
}
對應的彙編程式碼
10:a=c[1];
004010678A4DF1movcl,byteptr[ebp-0Fh]
0040106A884DFCmovbyteptr[ebp-4],cl
11:a=p[1];
0040106D8B55ECmovedx,dwordptr[ebp-14h]
004010708A4201moval,byteptr[edx+1]
004010738845FCmovbyteptr[ebp-4],al
第一種在讀取時直接就把字串中的元素讀到暫存器cl中,而第二種則要先把指標值讀到edx中,在根據edx讀取字元,顯然慢了。

21、什麼是預編譯,何時需要預編譯?

答:預編譯又稱為預處理,是做些程式碼文字的替換工作。處理#開頭的指令,比如拷貝#include包含的檔案程式碼,#define巨集定義的替換,條件編譯等,就是為編譯做的預備工作的階段,主要處理#開始的預編譯指令,預編譯指令指示了在程式正式編譯前就由編譯器進行的操作,可以放在程式中的任何位置。

c編譯系統在對程式進行通常的編譯之前,先進行預處理。c提供的預處理功能主要有以下三種:1)巨集定義 2)檔案包含 3)條件編譯

1、 總是使用不經常改動的大型程式碼體。

2、程式由多個模組組成,所有模組都使用一組標準的包含檔案和相同的編譯選項。在這種情況下,可以將所有包含檔案預編譯為一個預編譯頭。

22、關鍵字const是什麼含意?

答:我只要一聽到被面試者說:“const意味著常數”,我就知道我正在和一個業餘者打交道。去年Dan Saks已經在他的文章裡完全概括了const的所有用法,因此ESP(譯者:Embedded Systems Programming)的每一位讀者應該非常熟悉const能做什麼和不能做什麼.如果你從沒有讀到那篇文章,只要能說出const意味著“只讀”就可以了。儘管這個答案不是完全的答案,但我接受它作為一個正確的答案。(如果你想知道更詳細的答案,仔細讀一下Saks的文章吧。)如果應試者能正確回答這個問題,我將問他一個附加的問題:下面的宣告都是什麼意思?

const int a;

int const a;

const int *a;

int * const a;

int const * a const;

前兩個的作用是一樣,a是一個常整型數。第三個意味著a是一個指向常整型數的指標(也就是,整型數是不可修改的,但指標可以)。第四個意思a是一個指向整型數的常指標(也就是說,指標指向的整型數是可以修改的,但指標是不可修改的)。最後一個意味著a是一個指向常整型數的常指標(也就是說,指標指向的整型數是不可修改的,同時指標也是不可修改的)。如果應試者能正確回答這些問題,那麼他就給我留下了一個好印象。順帶提一句,也許你可能會問,即使不用關鍵字 const,也還是能很容易寫出功能正確的程式,那麼我為什麼還要如此看重關鍵字const呢?我也如下的幾下理由:

1). 關鍵字const的作用是為給讀你程式碼的人傳達非常有用的資訊,實際上,宣告一個引數為常量是為了告訴了使用者這個引數的應用目的。如果你曾花很多時間清理其它人留下的垃圾,你就會很快學會感謝這點多餘的資訊。(當然,懂得用const的程式設計師很少會留下的垃圾讓別人來清理的。)

2). 通過給優化器一些附加的資訊,使用關鍵字const也許能產生更緊湊的程式碼。

3). 合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的引數,防止其被無意的程式碼修改。簡而言之,這樣可以減少bug的出現

23、關鍵字volatile有什麼含意 並給出三個不同的例子。

答:一個定義為volatile的變數是說這變數可能會被意想不到地改變,這樣,編譯器就不會去假設這個變數的值了。精確地說就是,優化器在用到這個變數時必須每次都小心地重新讀取這個變數的值,而不是使用儲存在暫存器裡的備份。下面是volatile變數的幾個例子:

1). 並行裝置的硬體暫存器(如:狀態暫存器)

2). 一箇中斷服務子程式中會訪問到的非自動變數(Non-automatic variables)

3). 多執行緒應用中被幾個任務共享的變數

回答不出這個問題的人是不會被僱傭的。我認為這是區分C程式設計師和嵌入式系統程式設計師的最基本的問題。嵌入式系統程式設計師經常同硬體、中斷、RTOS等等打交道,所用這些都要求volatile變數。不懂得volatile內容將會帶來災難。

假設被面試者正確地回答了這是問題(嗯,懷疑這否會是這樣),我將稍微深究一下,看一下這傢伙是不是直正懂得volatile完全的重要性。

1). 一個引數既可以是const還可以是volatile嗎?解釋為什麼。

2). 一個指標可以是volatile 嗎?解釋為什麼。

3). 下面的函式有什麼錯誤:

int square(volatile int *ptr)

{

return *ptr * *ptr;

}

下面是答案:

1). 是的。一個例子是隻讀的狀態暫存器。它是volatile因為它可能被意想不到地改變。它是const因為程式不應該試圖去修改它。

2). 是的。儘管這並不很常見。一個例子是當一箇中服務子程式修該一個指向一個buffer的指標時。

3). 這段程式碼的有個惡作劇。這段程式碼的目的是用來返指標*ptr指向值的平方,但是,由於*ptr指向一個volatile型引數,編譯器將產生類似下面的程式碼:

int square(volatile int *ptr)

{

int a,b;

a = *ptr;

b = *ptr;

return a * b;

}

由於*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結果,這段程式碼可能返不是你所期望的平方值!正確的程式碼如下:

long square(volatile int *ptr)

{

int a;

a = *ptr;

return a * a;

}

24、三種基本的資料模型

答:按照資料結構型別的不同,將資料模型劃分為層次模型、網狀模型和關係模型。

25、結構與聯合有和區別?

答:(1). 結構和聯合都是由多個不同的資料型別成員組成, 但在任何同一時刻, 聯合中只存放了一個被選中的成員(所有成員共用一塊地址空間), 而結構的所有成員都存在(不同成員的存放地址不同)。 
 (2). 對於聯合的不同成員賦值, 將會對其它成員重寫, 原來成員的值就不存在了, 而對於結構的不同成員賦值是互不影響的

26、描述記憶體分配方式以及它們的區別?

答:1) 從靜態儲存區域分配。記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在。例如全域性變數,static 變數。
2) 在棧上建立。在執行函式時,函式內區域性變數的儲存單元都可以在棧上建立,函式執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內置於處理器的指令集。
3) 從堆上分配,亦稱動態記憶體分配。程式在執行的時候用malloc 或new 申請任意多少的記憶體,程式設計師自己負責在何時用free 或delete 釋放記憶體。動態記憶體的生存期由程式設計師決定,使用非常靈活,但問題也最多

27、請說出const與#define 相比,有何優點?

答:Const作用:定義常量、修飾函式引數、修飾函式返回值三個作用。被Const修飾的東西都受到強制保護,可以預防意外的變動,能提高程式的健壯性。

1) const 常量有資料型別,而巨集常量沒有資料型別。編譯器可以對前者進行型別安全檢查。而對後者只進行字元替換,沒有型別安全檢查,並且在字元替換可能會產生意料不到的錯誤。
      2) 有些整合化的除錯工具可以對const 常量進行除錯,但是不能對巨集常量進行除錯。

28、簡述陣列與指標的區別?

答:陣列要麼在靜態儲存區被建立(如全域性陣列),要麼在棧上被建立。指標可以隨時指向任意型別的記憶體塊。
    (1)修改內容上的差別
      char a[] = “hello”;
      a[0] = ‘X’;
      char *p = “world”; // 注意p 指向常量字串
      p[0] = ‘X’; // 編譯器不能發現該錯誤,執行時錯誤
   (2) 用運算子sizeof 可以計算出陣列的容量(位元組數)。sizeof(p),p 為指標得到的是一個 指標變數的位元組數,而不是p 所指的記憶體容量。C++/C 語言沒有辦法知道指標所指的記憶體容量,除非在申請記憶體時記住它。注意當陣列作為函式的引數進行傳遞時,該陣列自動退化為同類型的指標。
     char a[] = "hello world";
     char *p = a;
     cout<< sizeof(a) << endl; // 12 位元組
     cout<< sizeof(p) << endl; // 4 位元組
     計算陣列和指標的記憶體容量
     void Func(char a[100])
     {
      cout<< sizeof(a) << endl; // 4 位元組而不是100 位元組
     }

29、分別寫出BOOL,int,float,指標型別的變數a 與“零”的比較語句。

答:BOOL :    if ( !a ) or if(a)
int :     if ( a == 0)
float :   const EXPRESSION EXP = 0.000001
          if ( a < EXP && a >-EXP)
pointer : if ( a != NULL) or if(a == NULL)

30、如何判斷一段程式是由C 編譯程式還是由C++編譯程式編譯的?

答:#ifdef __cplusplus
cout<<"c++";
#else
cout<<"c";
#endif

31、論述含引數的巨集與函式的優缺點

答:        帶參巨集                  函式

處理時間    編譯時               程式執行時

引數型別   沒有引數型別問題      定義實參、形參型別

處理過程   不分配記憶體            分配記憶體

程式長度   變長                  不變

執行速度   不佔執行時間          呼叫和返回佔用時間

32、用兩個棧實現一個佇列的功能?要求給出演算法和思路!

答 、設2個棧為A,B, 一開始均為空.

入隊:

將新元素push入棧A;

出隊:

(1)判斷棧B是否為空;

(2)如果不為空,則將棧A中所有元素依次pop出並push到棧B;

(3)將棧B的棧頂元素pop出;

這樣實現的佇列入隊和出隊的平攤複雜度都還是O(1), 比上面的幾種方法要好

33、嵌入式系統中經常要用到無限迴圈,你怎麼樣用C編寫死迴圈呢?

答:這個問題用幾個解決方案。我首選的方案是:

while(1)

{

}

一些程式設計師更喜歡如下方案:

for(;;)

{

}

這個實現方式讓我為難,因為這個語法沒有確切表達到底怎麼回事。如果一個應試者給出這個作為方案,我將用這個作為一個機會去探究他們這樣做的

基本原理。如果他們的基本答案是:“我被教著這樣做,但從沒有想到過為什麼。”這會給我留下一個壞印象。

第三個方案是用 goto

Loop:

...

goto Loop;

應試者如給出上面的方案,這說明或者他是一個組合語言程式設計師(這也許是好事)或者他是一個想進入新領域的BASIC/FORTRAN程式設計師。

34、位操作(Bit manipulation)

答: 嵌入式系統總是要使用者對變數或暫存器進行位操作。給定一個整型變數a,寫兩段程式碼,第一個設定a的bit 3,第二個清除a 的bit 3。在以上兩個操作中,要保持其它位不變。
對這個問題有三種基本的反應
1)不知道如何下手。該被面者從沒做過任何嵌入式系統的工作。
2) 用bit fields。Bit fields是被扔到C語言死角的東西,它保證你的程式碼在不同編譯器之間是不可移植的,同時也保證了的你的程式碼是不可重用的。我最近不幸看到 Infineon為其較複雜的通訊晶片寫的驅動程式,它用到了bit fields因此完全對我無用,因為我的編譯器用其它的方式來實現bit fields的。從道德講:永遠不要讓一個非嵌入式的傢伙粘實際硬體的邊。
3) 用 #defines 和 bit masks 操作。這是一個有極高可移植性的方法,是應該被用到的方法。最佳的解決方案如下:
#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void) 
{
    a |= BIT3;
}
void clear_bit3(void) 
{
    a &= ~BIT3;
}
    一些人喜歡為設定和清除值而定義一個掩碼同時定義一些說明常數,這也是可以接受的。我希望看到幾個要點:說明常數、|=和&=~操作。

35、訪問固定的記憶體位置(Accessing fixed memory locations)

答:嵌入式系統經常具有要求程式設計師去訪問某特定的記憶體位置的特點。在某工程中,要求設定一絕對地址為0x67a9的整型變數的值為0xaa66。編譯器是一個純粹的ANSI編譯器。寫程式碼去完成這一任務。
這一問題測試你是否知道為了訪問一絕對地址把一個整型數強制轉換(typecast)為一指標是合法的。這一問題的實現方式隨著個人風格不同而不同。典型的類似程式碼如下:
    int *ptr;
    ptr = (int *)0x67a9;
    *ptr = 0xaa66;
 A more obscure approach is: 
一個較晦澀的方法是:
 *(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二種方案,但我建議你在面試時使用第一種方案。

36、中斷(Interrupts)

答: 中斷是嵌入式系統中重要