/* |
002 |
******************************************************************************************************* |
003 |
** 嵌入式程序員應知道的0x10個C語言Tips |
004 |
******************************************************************************************************* |
005 |
|
006 |
C語言是衡量嵌入式系統程序員必須並且有效的方法。這些年,我既參加也組織了許多測試,我意識到這些測試 |
007 |
能爲interviewer(面試官)和interviewee(被面試者)提供許多有用信息。此外,撇開面試的壓力不談,這種測試也是 |
008 |
至關有趣的。 |
009 |
從interviewee的角度來說,你能瞭解許多關於出題者或面試官的狀況。好比,這些測試極可能只是interviewer |
010 |
爲了彰顯其對ANSI標準細節的熟知而不是探究某些技術技巧設計出來的。再如,要你答出某個字符的ASCII值、考察 |
011 |
你關於系統調用和內存分配策略方面的能力……這標誌着出題者極可能花了大量時間在微機上而不上在嵌入式系統上。 |
012 |
若是確實是這樣的話,我得認真考慮我是否應該去作這份工做。 |
013 |
從interviewer的角度來說,一個測試也許能從多方面揭示應試者的素質:最基本的,你能瞭解應試者C語言的水 |
014 |
平。無論怎麼樣,看一下這人如何回答他不會的問題也是滿有趣的。應試者是以好的直覺作出明智的回答,仍是隻是 |
015 |
瞎蒙呢?當應試者在某個問題上卡住時是找藉口呢,仍是表現出對問題的真正的好奇心,把這當作學習的機會呢?我 |
016 |
發現這些信息與他們的測試成績同樣有用。 |
017 |
有了這些想法,我決定出一些真正針對嵌入式系統的考題,但願這些使人頭痛的考題能給應試者一點幫住。這些 |
018 |
問題都是我這些年實際遇到的。其中有些問題很難,但它們應該都能給你一點啓迪。 |
019 |
這些問題有必定的區分的度:大多數初級水平的應試者的成績會不好,而經驗豐富的程序員應該有很好的成績。 |
020 |
021 |
022 |
預處理器(Preprocessor) |
023 |
024 |
1. 請您用預處理指令#define聲明一個常數,用以代表1年中有多少秒(忽略閏年的狀況)。 |
025 |
026 |
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL |
027 |
028 |
我在這想看到幾件事情: |
029 |
•; #define語法的基本知識(例如:不能以分號結束,括號的使用,等等)。 |
030 |
•; 知道預處理器會爲你計算表達式的值的,所以,直接寫出你是如何計算一年中有多少秒而不須要計算出實際 |
031 |
的值,這樣代碼更清晰、更可讀。 |
032 |
•; 知道這個表達式將使一個16位機的整型數溢出,所以要用到長整型符號L,告訴編譯器這個常數是個長整型數。 |
033 |
•; 若是你的表達式中用到UL(表示無符號長整型),說明你有了一個好的起點。記住,第一印象很重要。 |
034 |
035 |
2. 寫一個宏定義MIN,這個宏返回兩個參數中較小(含相等的狀況)的一個。 |
036 |
037 |
#define MIN(A,B) ((A) <= (B) ? (A) : (B)) |
038 |
039 |
這個測試是爲下面的目的而設的: |
040 |
•; 標識#define在宏中應用的基本知識。這是很重要的,由於直到嵌入(inline)操做符變爲標準C的一部分,宏 |
041 |
是產生嵌入代碼的惟一方法,對於嵌入式系統來講,爲了能達到要求的性能,嵌入代碼常常是必須的方法。 |
042 |
•; 三目運算符(?:)的知識。這個操做符存在C語言中的緣由是它使得編譯器能產生比if-else更優化的代碼,了 |
043 |
解這個用法是很重要的。 |
044 |
•; 懂得在宏定義中當心地把參數用括號括起來。 |
045 |
•; 我也會用這個問題來討論宏的反作用,例如:當你寫下面的代碼時會出現什麼問題? |
046 |
047 |
least = MIN(*p++, b); |
048 |
049 |
3. 預處理指令#error的做用是什麼? |
050 |
051 |
若是你不知道答案,請看參考文獻1。這問題對區分一個正常的coder和一個書呆子是頗有用的。只有書呆子纔會 |
052 |
讀C語言課本的附錄去找出像這種問題的答案。(#error: 中止編譯並顯示錯誤信息) |
053 |
054 |
死循環(Infinite loops) |
055 |
056 |
4. 嵌入式系統中常常要用到無限循環,你怎麼樣用C編寫死循環呢? |
057 |
058 |
這個問題有幾個解決方案。個人首選方案是:while(1) {……},然而一些程序員會喜歡這個方案:for(;;) {……}。 |
059 |
這種答案讓我爲難,由於這個語法沒有確切表達到底怎麼回事。若是一個應試者給出這個做爲方案,我將用這個做爲 |
060 |
一個機會去探究他們這樣作的基本原理,顯然for(;;)比while(1)要執行更多的指令。若是他們的答案是:「我看別人 |
061 |
這樣作的,從沒有想到過爲何」,這會給我留下一個壞印象。還有一種方案是用goto語句: |
062 |
063 |
Lable: ... |
064 |
... |
065 |
goto Lable; |
066 |
067 |
應試者若是給出這種方案,這說明他多是一個彙編語言程序員(這也許是好事),或者他是一個想進入新領域的BASIC |
068 |
或FORTRAN程序員。 |
069 |
070 |
數據聲明(Data declarations) |
071 |
072 |
5. 用變量a給出下面的定義: |
073 |
074 |
a). 一個整型數 |
075 |
(An integer) |
076 |
b). 一個指向整型數的指針 |
077 |
(A pointer to an integer) |
078 |
c). 一個指向指針的指針,它指向的指針是指向一個整型數 |
079 |
(A pointer to a pointer to an integer) |
080 |
d). 一個有10個整型數的數組 |
081 |
(An array of 10 integers) |
082 |
e). 一個有10個指針的數組,指針均是指向整型數的 |
083 |
(An array of 10 pointers to integers) |
084 |
f). 一個指向有10個整型數數組的指針 |
085 |
(A pointer to an array of 10 integers) |
086 |
g). 一個指向函數的指針,該函數有一個整型參數並返回一個整型數 |
087 |
(A pointer to a function that takes an integer as an argument and returns an integer) |
088 |
h). 一個有10個指針的數組,該指針指向一個函數,該函數有一個整型參數並返回一個整型數 |
089 |
(An array of ten pointers to functions that take an integer argument and return an integer) |
090 |
091 |
答案是: |
092 |
093 |
a). int a; |
094 |
b). int *a; |
095 |
c). int **a; |
096 |
d). int a[10]; |
097 |
e). int *a[10]; |
098 |
f). int (*a)[10]; |
099 |
g). int (*a)(int); |
100 |
h). int (*a[10])(int); |
101 |
102 |
常常有人說這裏有幾個問題要翻一下書才能回答,我贊成這種說法。我寫這篇文章時,爲了肯定語法的正確性, |
103 |
個人確也查了一下書。可是當我被面試的時候,我指望被問到這些問題(或者相似的問題)。由於在被面試的這段時 |
104 |
間裏,我肯定我知道這個問題的答案。應試者若是不知道全部的答案(或至少大部分答案),那麼也就沒有爲此次面 |
105 |
試作準備,若是該面試者沒有爲此次面試作準備,那麼他又能爲何出準備呢? |
106 |
107 |
static |
108 |
109 |
6. 關鍵字static的做用是什麼? |
110 |
111 |
這個簡單的問題不多有人能回答徹底。在C語言中,關鍵字static有三個明顯的做用: |
112 |
•; static修飾局部變量時,表示但願這個局部變量的值在程序運行期間維持不變。 |
113 |
•; static修飾全局變量時,表示但願這個全局變量僅能夠在本模塊內被訪問和引用,對其餘模塊不可見。 |
114 |
•; static修飾一個函數時,表示但願這個函數僅能夠在本模塊內被調用,對其餘模塊不可見。 |
115 |
大多數應試者能正確回答第一部分,一部分能正確回答第二部分,不多的人能懂得第三部分。這是一個應試者 |
116 |
的嚴重的缺陷,由於他顯然不懂得本地化數據和代碼的好處和重要性。 |
117 |
118 |
const |
119 |
120 |
7.關鍵字const有什麼含意? |
121 |
122 |
我只要一聽到interviewee說:「const修飾的變量意味着常量」,我就知道我正在和一個業餘者打交道。去年 |
123 |
Dan Saks已經在他的文章裏徹底歸納了const的全部用法,所以ESP(譯者:Embedded Systems Programming)的每一 |
124 |
位讀者應該很是熟悉const能作什麼和不能作什麼。若是你從沒有讀到那篇文章,只要能說出const意味着「只讀」就 |
125 |
能夠了。儘管這個答案不是徹底的答案,但我接受它做爲一個正確的答案。若是你想知道更詳細的答案,仔細讀一 |
126 |
下Saks的文章吧。(譯者:const修飾的是變量而不是常量,咱們稱其爲「只讀型變量」,其初始化以後只讀不可寫)。 |
127 |
若是應試者能正確回答這個問題,我將問他一個附加的問題:下面的聲明都是什麼意思? |
128 |
129 |
const int a; |
130 |
int const a; |
131 |
const int *a; |
132 |
int * const a; |
133 |
int const * a const; |
134 |
135 |
前兩個的做用是同樣,a是一個只讀型整數。第三個意味着a是一個指向只讀型整數的指針(也就是說,整數是不 |
136 |
可修改的,但指針能夠)。第四個a是一個指向整型數的常指針(也就是說,指針指向的整型數是能夠修改的,但指針 |
137 |
不可修改)。最後一個a是一個指向常整型數的常指針(也就是說,指針指向的整型數是不可修改的,同時指針也是不 |
138 |
可修改的)。 |
139 |
若是應試者能正確回答這些問題,那麼他就給我留下了一個好印象。順帶提一句,你可能會問,即便不用關鍵字 |
140 |
const,也能夠寫出功能正確的程序,那麼我爲何還要如此重視關鍵字const呢?我有以下的幾下理由: |
141 |
•; 關鍵字const的做用是爲了給你的代碼的閱讀者傳達很是有用的信息。實際上,聲明一個參數爲常量是爲了告 |
142 |
訴了用戶這個參數的應用目的。若是你曾花不少時間清理他人留下的垃圾,你就會很快學會感謝這點多餘的 |
143 |
信息。(固然,懂得用const的程序員不多會留下垃圾讓別人來清理的。) |
144 |
•; 給優化器一些附加信息,使用關鍵字const也許能產生更緊湊的代碼。 |
145 |
•; 合理地使用關鍵字const可使編譯器很天然地保護那些不但願被修改(你極可能是無心地)的參數,防止其被 |
146 |
修改。簡而言之,這樣能夠減小bug的出現。 |
147 |
148 |
volatile |
149 |
150 |
8. 關鍵字volatile有什麼含意?並給出三個不一樣的例子。 |
151 |
152 |
一個定義爲volatile的變量是說這變量可能會被意想不到的狀況修改,告誡編譯器不要去優化和這個變量有關 |
153 |
的代碼。精確地說就是,優化器在用到這個變量時必須每次都當心地從新讀取(有時也可能須要寫入)這個變量的值, |
154 |
而不是使用其保存在寄存器裏的備份。下面是應用volatile變量的幾個例子: |
155 |
•; 硬件設備的某些寄存器(如:狀態寄存器); |
156 |
•; 某些ISR會訪問到的非自動變量(Non-automatic variables); |
157 |
•; 多線程應用中被幾個任務共享的變量; |
158 |
回答不出這個問題的人是不會被僱傭的。我認爲這是區分C程序員和嵌入式系統程序員的最基本的問題。搞嵌入式的 |
159 |
傢伙們常常同硬件、中斷、RTOS等等打交道,全部這些都會用到volatile變量。不懂volatile的用法將會帶來災難。 |
160 |
假設interviewee正確地回答了這個問題,我將稍加深究,看一下這傢伙是否是直正理解了volatile的重要用法: |
161 |
•; 一個參數既能夠是const的也能夠是volatile的嗎?解釋爲何。 |
162 |
•; 一個指針能夠是volatile的嗎?解釋爲何。 |
163 |
•; 下面的函數有什麼錯誤: |
164 |
165 |
int square(volatile int *ptr) |
166 |
{ |
167 |
return (*ptr) * (*ptr); |
168 |
} |
169 |
170 |
下面是答案: |
171 |
•; 是的。一個例子是隻讀型寄存器。它是volatile的,由於要防止它可能被意想不到地修改。它是const的,因 |
172 |
爲程序不該該試圖去修改它。 |
173 |
•; 是的。儘管這並不常見。一個例子是當一箇中服務子程序修該一個指向一個buffer的指針時,這個指正應該 |
174 |
是volatile型的。 |
175 |
•; 這段代碼有點變態。這段代碼的目的是用來返指針ptr指向值的平方。可是,因爲ptr指向一個volatile型參 |
176 |
數,編譯器將產生相似下面的代碼(關鍵要注意到,編譯器須要一次次認真地讀取ptr所指向的值): |
177 |
|
178 |
long square(volatile int *ptr) |
179 |
{ |
180 |
int a,b; |
181 |
a = *ptr; |
182 |
b = *ptr; |
183 |
184 |
return a * b; |
185 |
} |
186 |
187 |
因爲ptr指向的值可能會被意想不到地該變,所以a和b多是不一樣的。結果,這段代碼可能返回不是你所指望 |
188 |
的平方值!正確的代碼以下: |
189 |
190 |
long square(volatile int *ptr) |
191 |
{ |
192 |
int a; |
193 |
a = *ptr; |
194 |
195 |
return a * a; |
196 |
} |
197 |
198 |
位操做(Bit manipulation) |
199 |
200 |
9. 嵌入式系統老是要用戶對變量或寄存器進行位操做。 |
201 |
202 |
給定一個整型變量a,寫兩段代碼,第一個設置a的bit3,第二個清除a的bit3。在以上兩個操做中,要保持其它位 |
203 |
不變。對這個問題有三種基本的反應: |
204 |
•; 不知道如何下手。說明interviewee從沒作過嵌入式工做。(一個嵌入式編程高手也必須得是一個位操做高手。) |
205 |
•; 用bit fields。bit fields是被扔到C語言死角的東西,它保證你的代碼在不一樣編譯器之間是不可移植的,同時 |
206 |
也保證了的你的代碼是不可重用的。我最近不幸看到Infineon爲其較複雜的通訊芯片寫的驅動程序,它用到了 |
207 |
bit fields,所以徹底對我無用,由於個人編譯器用其它的方式來實現bit fields的。從道德上講:永遠不要讓 |
208 |
一個非嵌入式的傢伙沾實際硬件的邊。 |
209 |
•; 用#defines和bit masks(位掩碼)操做。這是一個有極高可移植性的方法,值得推薦。最佳的解決方案以下: |
210 |
211 |
#define BIT3 (0x1 << 3) |
212 |
213 |
static int a; |
214 |
215 |
void set_bit3(void) |
216 |
{ |
217 |
a |= BIT3; |
218 |
} |
219 |
220 |
void clear_bit3(void) |
221 |
{ |
222 |
a &= ~BIT3; |
223 |
} |
224 |
225 |
一些人喜歡爲設置和清除值而定義一個掩碼同時定義一些說明常數,這也是徹底能夠接受的。總之我但願看到幾個 |
226 |
要點:說明常數(如上述中的BIT3)、|= 和 &=~ 操做。 |
227 |
228 |
10. 嵌入式系統常常具備要求程序員去訪問某特定的內存位置的特色。 |
229 |
230 |
在某工程中,要求設置一絕對地址爲0x67a9的整型變量的值爲0xaa55。編譯器是一個純粹的ANSI編譯器。寫代碼去 |
231 |
完成這一任務。 |
232 |
這一問題測試你是否知道爲了訪問一絕對地址把一個整型數強制轉換(typecast)爲一個指針是合法的。這一問題的 |
233 |
實現方式隨着我的風格不一樣而不一樣。典型的相似代碼以下: |
234 |
235 |
int *ptr; |
236 |
ptr = (int *)0x67a9; |
237 |
*ptr = 0xaa55; |
238 |
239 |
另外一個較晦澀的方法是: |
240 |
241 |
*(int * const)(0x67a9) = 0xaa55; |
242 |