1. 程式人生 > >C語言再學習 -- 詳解C++/C 面試題 2

C語言再學習 -- 詳解C++/C 面試題 2

(經典)C語言測試:想成為嵌入式程式設計師應知道的0x10個基本問題。

1、用預處理指令#define 宣告一個常數,用以表明1年中有多少秒(忽略閏年問題) 

#define SENCONDS_PER_YEAR (60*60*24*365)UL

解答:

#define 宣告一個常量,使用計算常量表達式的值來表明一年中有多少秒,顯得就更加直觀了。再有這個表示式的值為無符號長整形,因此應使用符號 UL。 

2、寫一個“標準”巨集MIN ,這個巨集輸入兩個引數並返回較小的一個。 

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

解答:

實現輸入兩個引數並返回較小的一個,應使用三目表示式
使用必須的足夠多的圓括號來保證以正確的順序進行執行和結合。

使用#define需要注意下面幾點:

(1)巨集的名字中不能有空格,但是在替代字串中可以使用空格。ANSI C 允許在引數列表中使用空格。

(2)用圓括號括住每個引數,並括住巨集的整體定義

(3)用大寫字母表示巨集函式名,便於與變數區分。

(4)有些編譯器限制巨集只能定義一行。即使你的編譯器沒有這個限制,也應遵守這個限制。

(5)巨集的一個優點是它不檢查其中的變數型別,這是因為巨集處理字元型字串,而不是實際值。

(6)在巨集中不要使用增量或減量運算子

3、前處理器標識#error的目的是什麼?

#error  字串 => 表示產生一個錯誤資訊

解答:

#error  字串 => 表示產生一個錯誤資訊
#warning 字串 => 表示產生一個警告資訊

//#error和#warning的使用  
#include <stdio.h>  
  
#define VERSION 4  
#define VERSION 2  
#define VERSION 3  
#if(VERSION < 3)  
    #error "版本過低"  
#elif(VERSION > 3)  
    #warning "版本過高"  
#endif  
  
int main(void)  
{  
    printf("程式正常執行\n");  
    return 0;  
}  
輸出結果:  
警告: #warning "版本過高"  
//錯誤: #error "版本過低"  
//程式正常執行  

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

while (1) {...}for (;;){...}Loop; ... goto Loop解答:

while(邏輯表示式)
{
反覆執行的語句
}                            

只要邏輯表示式結果為真就反覆不停執行大括號裡的語句,直到邏輯表示式結果為假迴圈結束,只要把邏輯表示式寫成1則迴圈成為死迴圈

while 很好理解,下面講講 for 迴圈。

例如:for(num=1; num <10; num++);

在關鍵字for之後的圓括號中包含了由兩個分號分開的三個表示式:

第一個表示式進行初始化,它在for迴圈開始的時候執行一次,可以使用逗號為多個變數進行初始化。

第二個表示式是判斷條件,在每次執行之前都要對它進行求值。當表示式為假時,迴圈結束。

第三個表示式進行改變或者稱為更新,它在每次迴圈結束時進行計算。

for迴圈的靈活性

(1)可以讓一個或多個表示式為空(但是不要遺漏分號)。只須確保在迴圈中包含一些能是迴圈最終結束的語句。(2)順便說一句,中間的那個控制表示式為空會被認為是真,所以下面的迴圈會永遠執行:

for (; ;)

printf ("hello world\n");

(3)第一個表示式不必初始化一個變數,它也可是某種型別的 printf() 語句,要記住第一個表示式只在執行迴圈的其他部分之前被求值或執行一次

(4)for迴圈可使用逗號運算子把兩個表示式連結為一個表示式,並保證最左邊的表示式最先計算。

例如:

for (n = 2, m = 0; m < 1000; n *=2)  
    m +=n;  
(5)在for迴圈中使用陣列,可使用#define來指定陣列大小例如:
#define SIZE 10  
for (int n = 0; n < SIZE; n++)  
    printf ("hello world\n");  


5、用變數a給出下面的定義 

a)一個整型數int a;b)一個指向整型數的指標int *a;  c)一個指向指標的的指標,它指向的指標是指向一個整型數int **a;
d)一個有10個整型數的陣列
int a[10];  
e)一個有10個指標的陣列,該指標是指向一個整型數的。
int *a[10];
f)一個指向有10個整型數陣列的指標
int (*a)[10];
g)一個指向函式的指標,該函式有一個整型引數並返回一個整型數
int (*a)(int);
h)一個有10個指標的陣列,該指標指向一個函式,該函式有一個整型引數並返回一個整型數
int (*a[10])(int);解答:

指標陣列:首先它是一個數組,陣列的元素都是指標,例如:int *p1[10];

陣列指標:首先它是一個指標,它指向一個數組,例如:int (*p2)[10];

函式指標:首先它是一個指標,它指向一個函式,例如:int (*p3)(int);

這裡需要明白一個符號之間優先順序的問題,"[ ]"的優先順序比"*"要高。p1 先與“ []”結合,構成一個數組的定義,陣列名為 p1, int *修飾的是陣列的內容,即陣列的每個元素。那現在我們清楚,這是一個數組,其包含 10 個指向 int 型別資料的指標,即指標陣列。

至於 p2 就更好理解了,在這裡"( )"的優先順序比"[ ]"高,"*"號和 p2 構成一個指標的定義,指標變數名為 p2, int 修飾的是陣列的內容,即陣列的每個元素。陣列在這裡並沒有名字,是個匿名陣列。那現在我們清楚 p2 是一個指 針,它指向一個包含 10 個 int 型別資料的陣列,即陣列指標。

再至於p3也不難理解,在這裡"( )"的結合方向是 從左到右,也就是說首先它是一個指標,它指向一個函式,即函式指標。

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

static 修飾全域性變數

static 修飾區域性變數

static 修飾函式

解答:

(1)static 修飾的全域性變數也叫靜態全域性變數該類具有靜態儲存時期檔案作用域內部連結僅在編譯時初始化一次如未明確初始化,它的位元組都被設定為0。static全域性變數只初使化一次是為了防止在其他檔案單元中被引用;利用這一特性可以在不同的檔案中定義同名函式和同名變數,而不必擔心命名衝突。

(2)static 修飾的區域性變數也叫靜態區域性變數,該類具有靜態儲存時期程式碼作用域空連結僅在編譯時初始化一次如未明確初始化,它的位元組都被設定為0函式呼叫結束後儲存區空間並不釋放,保留其當前值。

(3)static 修飾的函式也叫靜態函式,只可以在定義它的檔案中使用。

7、關鍵字const有什麼含意?

const 修飾的資料型別是指常型別,常型別的變數或物件的值是不能被更新的。或者說const意味著 只讀。

解答:

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

作用的話,可以保護被修改的東西,防止意外的修改,增強程式的健壯性。

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

volatile 關鍵字是一種型別修飾符volatile 的作用是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。直接讀值是指從記憶體重新裝載內容,而不是直接從暫存器拷貝內容。 volatile 使用:(1)並行裝置的硬體暫存器(如:狀態暫存器)這些暫存器裡面的值是隨時變化的。如果我們沒有將這個地址強制型別轉換成 volatile,那麼我們在使用GPC1CON 這個暫存器的時候, 會直接從 CPU 的暫存器中取值。因為之前GPC1CON  被訪問過,也就是之前就從記憶體中取出 GPC1CON 的值儲存到某個暫存器中。之所以直接從暫存器中取值,而不去記憶體中取值,是因為編譯器優化程式碼的結果(訪問 CPU暫存器比訪問 RAM 快的多)。用 volatile 關鍵字對 0xE0200080  進行強制轉換,使得每一次訪問 GPC1CON 時,執行部件都會從 0xE0200080  這個記憶體單元中取出值來賦值給 GPC1CON  。(2)一箇中斷服務子程式中會訪問到的非自動變數由於訪問暫存器的速度要快過RAM,所以編譯器一般都會作減少存取外部RAM的優化。(3)多執行緒應用中被幾個任務共享的變數當兩個執行緒都要用到某一個變數且該變數的值會被改變時,應該用 volatile 宣告,該關鍵字的作用是防止優化編譯器把變數從記憶體裝入CPU暫存器中。如果變數被裝入暫存器,那麼兩個執行緒有可能一個使用記憶體中的變數,一個使用暫存器中的變數,這會造成程式的錯誤執行。volatile的意思是讓編譯器每次操作該變數時一定要從記憶體中真正取出,而不是使用已經存在暫存器中的值。

9、嵌入式系統總是要使用者對變數或暫存器進行位操作。給定一個整型變數a,寫兩段程式碼,第一個設定a的bit 3,第二個清除a 的bit 3。在以上兩個操作中,要保持其它位不變。

#define BIT3 (0x1 << 3)
static int a;
void set_bit3 (void)
{
	a |= BIT3;
}
void clear_bit3 (void)
{
	a &= ~BIT3;
}
解答:

10、嵌入式系統經常具有要求程式設計師去訪問某特定的記憶體位置的特點。在某工程中,要求設定一絕對地址為0x67a9的整型變數的值為0xaa55。編譯器是一個純粹的ANSI編譯器。寫程式碼去完成這一任務。

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

解答:強制型別轉換。

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

__interrupt double compute_area (double radius) 
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
} 
(1)ISR 不能返回一個值。(2)ISR 不能傳遞引數(3) 在許多的處理器/編譯器中,浮點一般都是不可重入的。有些處理器/編譯器需要讓額處的暫存器入棧,有些處理器/編譯器就是不允許在ISR中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。(4)與第三點一脈相承,printf() 經常有重入和效能上的問題。

12、下面的程式碼輸出是什麼,為什麼? 

void foo(void) 
{ 
unsigned int a = 6; 
int b = -20; 
(a+b > 6) ? puts("> 6") : puts("<= 6"); 
} 
當表示式中存在有符號型別和無符號型別時所有的運算元都自動轉換為無符號型別。因此-20變成了一個非常大的正整數,所以該表示式計算出的結果大於6。
解答:除了有符號型別和無符號型別混合使用時自動轉換為無符號型別較小的型別和較大的型別混合使用會被轉換成較大的型別,防止資料丟失。
#include <stdio.h>  
#include <string.h>  
#include <stdlib.h>  
  
int main (void)  
{  
    int a = 10;  
    printf ("sizeof ((a > 5) ? 4 : 8.0) = %d\n", sizeof ((a > 5) ? 4 : 8.0));  
    return 0;  
}  
輸出結果:  
sizeof ((a > 5) ? 4 : 8.0) = 8  

13、評價下面的程式碼片斷: 

unsigned int zero = 0; 
unsigned int compzero = 0xFFFF;  
/*1's complement of zero */ 
對於一個int型不是16位的處理器為說,上面的程式碼是不正確的。應編寫如下: 
unsigned int compzero = ~0; 

14、儘管不像非嵌入式計算機那麼常見,嵌入式系統還是有從堆(heap)中動態分配記憶體的過程的。那麼嵌入式系統中,動態分配記憶體可能發生的問題是什麼? 

(1)越界,理論上可以申請4G,但超過程式記憶體的大小就會返回空指標,所以要檢查返回的指標是否為空。 (2)用完記得釋放,以免記憶體洩漏。 還有使用的過程中不要指標越界,這樣會導致致命錯誤。(3)使用 free 或 delete 釋放了記憶體後,沒有將指標設定為 NULL。導致產生“野指標”解答:

函式用法:

type *p;
p = (type*)malloc(n * sizeof(type));
if(NULL == p)
/*請使用if來判斷,這是有必要的*/
{
    perror("error...");
    exit(1);
}
.../*其它程式碼*/
free(p);
p = NULL;/*請加上這句*/

函式使用需要注意的地方:

1、malloc 函式返回的是 void * 型別,必須通過 (type *) 來將強制型別轉換

2、malloc 函式的實參為 sizeof(type),用於指明一個整型資料需要的大小。

3、申請記憶體空間後,必須檢查是否分配成功

4、當不需要再使用申請的記憶體時,記得釋放,而且只能釋放一次。如果把指標作為引數呼叫free函式釋放,則函式結束後指標成為野指標(如果一個指標既沒有捆綁過也沒有記錄空地址則稱為野指標),所以釋放後應該把指向這塊記憶體的指標指向NULL,防止程式後面不小心使用了它。

5、要求malloc和free符合一夫一妻制,如果申請後不釋放就是記憶體洩漏,如果無故釋放那就是什麼也沒做。釋放只能一次,如果釋放兩次及兩次以上會出現錯誤(釋放空指標例外,釋放空指標其實也等於啥也沒做,所以釋放空指標釋放多少次都沒有問題)。

15、Typedef 在C語言中頻繁用以宣告一個已經存在的資料型別的同義字。也可以用前處理器做類似的事。例如,思考一下下面的例子: 
#define dPS struct s * 
typedef struct s * tPS; 
以上兩種情況的意圖都是要定義dPS 和 tPS 作為一個指向結構s指標。哪種方法更好呢?(如果有的話)為什麼? 

typedef更好
解答:首先你要了解 typedef 和 define 的區別,巨集定義只是簡單的字串代換,是在預處理完成的typedef是在編譯時處理的,它不是作簡單的代換,而是對型別說明符重新命名。被命名的識別符號具有型別定義說明的功能。上面兩種情況,從形式上看這兩者相似,但在實際使用中卻不相同。dPS p1,p2;在巨集代換後變成: struct s* p1, p2;  定義p1為一個指向結構的指標,p2為一個實際的結構。
tPS p3,p4; 
而typedef代換後,正確地定義了p3 和p4 兩個指標。

總結,typedef和#define的不同之處:

1、與#define不同,typedef 給出的符號名稱僅限於對型別,而不是對值。

2、typedef 的解釋由編譯器,而不是是處理器執行。

3、雖然它的範圍有限,但在其受限範圍內,typedef 比 #define 更靈活。

16、C語言同意一些令人震驚的結構,下面的結構是合法的嗎,如果是它做些什麼? 
int a = 5, b = 7, c; 
c = a+++b; 

上面的例子是完全合乎語法的。解答:
#include <stdio.h>

int main (void)
{
	int a = 5,b = 7,c;
	c  = a+++b;
	printf ("c = %d\n", c);
	return 0;
}
輸出結果:
c = 12
c = a+++b;可以看做:
c  = a++ + b;
或者c  = a + ++b;
兩者結果是不同的。++ 自增運算子 為單目運算子 結合方向 是從右到左;+ 加 雙目運算子 結合方向 從左到右++ 優先順序 高於 +上面的程式碼被編譯器處理成:
c = a++ + b;