記錄一下Jserv和學生陳建霖、梁穎睿合作開發的small C Compiler.
* 可輸出 ARM 架構的 ELF 執行檔
* 可編譯自己並輸出合法的執行檔 (self-compilation)
* 支援 C89 經典語法
* 支援動態連結函式庫,可很容易使用作業系統提供的服務
* 提供 JIT 編譯器支援,對於想理解 ARM 架構和虛擬機器設計者,不失是個參考
程式碼發布於 GitHub: https://github.com/jserv/amacc
2016年3月19日 星期六
2015年12月26日 星期六
Modern C - Jens Gustedt
Jserv推廌的一本電子書, 由在 INRIA (法國國立電腦科學及自動化研究院) 任職的 Jens Gustedt 撰寫. 主要是探討新的 C 語言規格和應用規範.
Modern c from Stanley Ho
2015年11月28日 星期六
sscanf in C language
這個是Jack給的一個有趣程式, 以底下的範例來說
輸入的字串可是是"
執行狀況如下:
sscanf的函式宣告型態如下
int sscanf(const char *str, const char *format, ...);
使用它時需要
輸入的字串可是是"
#include <string.h>
#include <stdio.h>
int main(int argc, char** argv){
int b[5] = {0, 0, 0, 0, 0};
int ret;
if(argc == 2){
ret = sscanf( argv[1], "%d,%d,%d,%d,%d\n", &b[0], &b[1], &b[2], &b[3], &b[4]);
printf("ret = %d, b0=%d b1=%d b2=%d b3=%d b4=%d\n", ret, b[0], b[1], b[2], b[3], b[4] );
}
}
執行狀況如下:
./test
./test 1
ret = 1, b0=1 b1=0 b2=0 b3=0 b4=0
./test 1,2
ret = 2, b0=1 b1=2 b2=0 b3=0 b4=0
./test 1,2,3
ret = 3, b0=1 b1=2 b2=3 b3=0 b4=0
./test 1,2,3,4
ret = 4, b0=1 b1=2 b2=3 b3=4 b4=0
./test 1,2,3,4,5
ret = 5, b0=1 b1=2 b2=3 b3=4 b4=5
./test 1,2,3,4,5,6
ret = 5, b0=1 b1=2 b2=3 b3=4 b4=5
sscanf的函式宣告型態如下
int sscanf(const char *str, const char *format, ...);
使用它時需要
#include <stdio.h>
2015年11月26日 星期四
宅色夫大大的 - 你所不知道的C語言beta 11/23 指標篇
你所不知道的C語言 : 指標篇
Andrew Koenig - C Traps and Pitfalls
Interactive compiler - http://gcc.godbolt.org/ , 沒有MIPS有點可惜.
Andrew Koenig - C Traps and Pitfalls
Interactive compiler - http://gcc.godbolt.org/ , 沒有MIPS有點可惜.
2010年5月5日 星期三
2010年4月28日 星期三
Learn C Programming Language by Using GDB
Learn C Programming Language by Using GDB
View more presentations from Jim Huang.
我比較感興趣的是Jersv的eserv專案.
Lightweight embedded server-side native C/C++ Web Framework
可以載tar ball,或者svn checkout http://eserv.googlecode.com/svn/trunk/ eserv-read-only
編譯完的eserv執行檔,strip過後23k,支援CGI,jquery,簡單的gallery demo,蠻有趣的.
相當適合拿來學習web server的原理唷.
只是說,怎麼沒看到Jserv demo的山寨版開心農場版本呢?
eserv Demo執行快照

參考資料:
以 GDB 重新學習 C 語言程式設計
Jserv 以 GDB 重新學習 C 語言程式設計 (成功大學)
2008年8月28日 星期四
利用 序列比對演算法 辨識 抄襲之C程式(轉貼)
剛剛看到的,覺得還滿有趣的。這是暨南大學的碩士論文 (原網頁)。
通常改人家的作業,最簡單的就是換變數;再好一點,就是會改一下相似的迴圈,例如 if else 和 switch、for 和 while 等;或是把 Type 改一改:然後自愛一點的改法,可能還會認真看看,改一下輸出輸入的語法等。
這個論文主要跟傳統 Unix 下的 diff 最大的不同,就是會去檢查上述的這些情況 - 變數替換、函式互換、變數型態替換等等等。詳細可以看看論文,寫的很清楚,滿有趣的。
轉貼資料來源:
http://twntwn.info/blog/ajer001/archives/764
通常改人家的作業,最簡單的就是換變數;再好一點,就是會改一下相似的迴圈,例如 if else 和 switch、for 和 while 等;或是把 Type 改一改:然後自愛一點的改法,可能還會認真看看,改一下輸出輸入的語法等。
這個論文主要跟傳統 Unix 下的 diff 最大的不同,就是會去檢查上述的這些情況 - 變數替換、函式互換、變數型態替換等等等。詳細可以看看論文,寫的很清楚,滿有趣的。
轉貼資料來源:
http://twntwn.info/blog/ajer001/archives/764
2007年4月26日 星期四
C/C++語言struct深層探索
C/C++語言struct深層探索
1. struct的巨大作用
面對一個人的大型C/C++程式時,只看其對struct的使用情況我們就可以對其編寫者的編程經驗進行評估。因為一個大型的C/C++程式,勢必要涉及一些(甚至大量)進行資料組合的結構體,這些結構體可以將原本意義屬於一個整體的資料組合在一起。從某種程度上來說,會不會用struct,怎樣用struct是區別一個開發人員是否具備豐富開發經歷的標誌。
在網路協定、通信控制、嵌入式系統的C/C++編程中,我們經常要傳送的不是簡單的位元組流(char型陣列),而是多種資料組合起來的一個整體,其表現形式是一個結構體。
經驗不足的開發人員往往將所有需要傳送的內容依順序保存在char型陣列中,通過指標偏移的方法傳送網路報文等資訊。這樣做編程複雜,易出錯,而且一旦控制方式及通信協定有所變化,程式就要進行非常細緻的修改。
一個有經驗的開發者則靈活運用結構體,舉一個例子,假設網路或控制協定中需要傳送三種報文,其格式分別為packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
優秀的程式設計者這樣設計傳送的報文:
struct CommuPacket
{
int iPacketType; //報文類型標誌
union //每次傳送的是三種報文中的一種,使用union
{
struct structA packetA;
struct structB packetB;
struct structC packetC;
}
};
在進行報文傳送時,直接傳送struct CommuPacket一個整體。
假設發送函數的原形如下:
// pSendData:發送位元組流的首位址,iLen:要發送的長度
Send(char * pSendData, unsigned int iLen);
發送方可以直接進行如下調用發送struct CommuPacket的一個實例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假設接收函數的原形如下:
// pRecvData:發送位元組流的首位址,iLen:要接收的長度
//返回值:實際接收到的位元組數
unsigned int Recv(char * pRecvData, unsigned int iLen);
接收方可以直接進行如下調用將接收到的資料保存在struct CommuPacket的一個實例recvCommuPacket中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接著判斷報文類型進行相應處理:
switch(recvCommuPacket. iPacketType)
{
case PACKET_A:
… //A類報文處理
break;
case PACKET_B:
… //B類報文處理
break;
case PACKET_C:
… //C類報文處理
break;
}
以上程式中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的強制類型轉換:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再轉化為char型指標,這樣就可以直接利用處理位元組流的函數。
利用這種強制類型轉化,我們還可以方便程式的編寫,例如要對sendCommuPacket所處記憶體初始化為0,可以這樣調用標準庫函數memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
2. struct的成員對齊
Intel、微軟等公司曾經出過一道類似的面試題:
1. #include
2. #pragma pack(8)
3. struct example1
4. {
5. short a;
6. long b;
7. };
8. struct example2
9. {
10. char c;
11. example1 struct1;
12. short e;
13. };
14. #pragma pack()
15. int main(int argc, char* argv[])
16. {
17. example2 struct2;
18. cout << sizeof(example1) << endl;
19. cout << sizeof(example2) << endl;
20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2)
<< endl;
21. return 0;
22. }
問程式的輸入結果是什麼?
答案是:
8
16
4
不明白?還是不明白?下麵一一道來:
2.1 自然對界
struct是一種複合資料類型,其構成元素既可以是基本資料類型(如int、long、float等)的變數,也可以是一些複合資料類型(如array、struct、union等)的資料單元。對於結構體,編譯器會自動進行成員變數的對齊,以提高運算效率。缺省情況下,編譯器為結構體的每個成員按其自然對界(natural alignment)條件分配空間。各個成員按照它們被聲明的順序在記憶體中順序存儲,第一個成員的位址和整個結構的位址相同。
自然對界(natural alignment)即默認對齊方式,是指按結構體的成員中size最大的成員對齊。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述結構體中,size最大的是short,其長度為2位元組,因而結構體中的char成員a、c都以2為單位對齊,sizeof(naturalalign)的結果等於6;
如果改為:
struct naturalalign
{
char a;
int b;
char c;
};
其結果顯然為12。
2.2指定對界
一般地,可以通過下面的方法來改變缺省的對界條件:
• 使用虛擬指令#pragma pack (n),編譯器將按照n個位元組對齊;
• 使用虛擬指令#pragma pack (),取消自定義位元組對齊方式。
注意:如果#pragma pack (n)中指定的n大於結構體中最大成員的size,則其不起作用,結構體仍然按照size最大的成員進行對界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
當n為4、8、16時,其對齊方式均一樣,sizeof(naturalalign)的結果都等於12。而當n為2時,其發揮了作用,使得sizeof(naturalalign)的結果為8。
在VC++ 6.0編譯器中,我們可以指定其對界方式,其操作方式為依次選擇projetct > setting > C/C++功能表,在struct member alignment中指定你要的對界方式。
另外,通過__attribute((aligned (n)))也可以讓所作用的結構體成員對齊在n位元組邊界上,但是它較少被使用,因而不作詳細講解。
2.3 面試題的解答
至此,我們可以對Intel、微軟的面試題進行全面的解答。
程式中第2行#pragma pack (8)雖然指定了對界為8,但是由於struct example1中的成員最大size為4(long變數size為4),故struct example1仍然按4位元組對界,struct example1的size為8,即第18行的輸出結果;
struct example2中包含了struct example1,其本身包含的簡單資料成員的最大size為2(short變數e),但是因為其包含了struct example1,而struct example1中的最大成員size為4,struct example2也應以4對界,#pragma pack (8)中指定的對界對struct example2也不起作用,故19行的輸出結果為16;
由於struct example2中的成員以4為單位對界,故其char變數c後應補充3個空,其後才是成員struct1的記憶體空間,20行的輸出結果為4。
3. C和C++間struct的深層區別
在C++語言中struct具有了“類” 的功能,其與關鍵字class的區別在於struct中成員變數和函數的默認訪問許可權為public,而class的為private。
例如,定義struct類和class類:
struct structA
{
char a;
…
}
class classB
{
char a;
…
}
則:
struct A a;
a.a = 'a'; //訪問public成員,合法
classB b;
b.a = 'a'; //訪問private成員,不合法
許多文獻寫到這裏就認為已經給出了C++中struct和class的全部區別,實則不然,另外一點需要注意的是:
C++中的struct保持了對C中struct的全面相容(這符合C++的初衷——“a better c”),因而,下面的操作是合法的:
//定義struct
struct structA
{
char a;
char b;
int c;
};
structA a = {'a' , 'a' ,1}; // 定義時直接賦初值
即struct可以在定義的時候直接以{ }對其成員變數賦初值,而class則不能,在經典書目《thinking C++ 2nd edition》中作者對此點進行了強調。
4. struct編程注意事項
看看下面的程式:
1. #include
2. struct structA
3. {
4. int iMember;
5. char *cMember;
6. };
7. int main(int argc, char* argv[])
8. {
9. structA instant1,instant2;
10.char c = 'a';
11. instant1.iMember = 1;
12. instant1.cMember = &c;
13.instant2 = instant1;
14.cout << *(instant1.cMember) << endl;
15.*(instant2.cMember) = 'b';
16. cout << *(instant1.cMember) << endl;
17. return 0;
}
14行的輸出結果是:a
16行的輸出結果是:b
Why?我們在15行對instant2的修改改變了instant1中成員的值!
原因在於13行的instant2 = instant1賦值語句採用的是變數逐個拷貝,這使得instant1和instant2中的cMember指向了同一片記憶體,因而對instant2的修改也是對instant1的修改。
在C語言中,當結構體中存在指標型成員時,一定要注意在採用賦值語句時是否將2個實例中的指標型成員指向了同一片記憶體。
在C++語言中,當結構體中存在指標型成員時,我們需要重寫struct的拷貝構造函數並進行“=”操作符重載。
轉載自CSDN
1. struct的巨大作用
面對一個人的大型C/C++程式時,只看其對struct的使用情況我們就可以對其編寫者的編程經驗進行評估。因為一個大型的C/C++程式,勢必要涉及一些(甚至大量)進行資料組合的結構體,這些結構體可以將原本意義屬於一個整體的資料組合在一起。從某種程度上來說,會不會用struct,怎樣用struct是區別一個開發人員是否具備豐富開發經歷的標誌。
在網路協定、通信控制、嵌入式系統的C/C++編程中,我們經常要傳送的不是簡單的位元組流(char型陣列),而是多種資料組合起來的一個整體,其表現形式是一個結構體。
經驗不足的開發人員往往將所有需要傳送的內容依順序保存在char型陣列中,通過指標偏移的方法傳送網路報文等資訊。這樣做編程複雜,易出錯,而且一旦控制方式及通信協定有所變化,程式就要進行非常細緻的修改。
一個有經驗的開發者則靈活運用結構體,舉一個例子,假設網路或控制協定中需要傳送三種報文,其格式分別為packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
優秀的程式設計者這樣設計傳送的報文:
struct CommuPacket
{
int iPacketType; //報文類型標誌
union //每次傳送的是三種報文中的一種,使用union
{
struct structA packetA;
struct structB packetB;
struct structC packetC;
}
};
在進行報文傳送時,直接傳送struct CommuPacket一個整體。
假設發送函數的原形如下:
// pSendData:發送位元組流的首位址,iLen:要發送的長度
Send(char * pSendData, unsigned int iLen);
發送方可以直接進行如下調用發送struct CommuPacket的一個實例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假設接收函數的原形如下:
// pRecvData:發送位元組流的首位址,iLen:要接收的長度
//返回值:實際接收到的位元組數
unsigned int Recv(char * pRecvData, unsigned int iLen);
接收方可以直接進行如下調用將接收到的資料保存在struct CommuPacket的一個實例recvCommuPacket中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接著判斷報文類型進行相應處理:
switch(recvCommuPacket. iPacketType)
{
case PACKET_A:
… //A類報文處理
break;
case PACKET_B:
… //B類報文處理
break;
case PACKET_C:
… //C類報文處理
break;
}
以上程式中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的強制類型轉換:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再轉化為char型指標,這樣就可以直接利用處理位元組流的函數。
利用這種強制類型轉化,我們還可以方便程式的編寫,例如要對sendCommuPacket所處記憶體初始化為0,可以這樣調用標準庫函數memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
2. struct的成員對齊
Intel、微軟等公司曾經出過一道類似的面試題:
1. #include
2. #pragma pack(8)
3. struct example1
4. {
5. short a;
6. long b;
7. };
8. struct example2
9. {
10. char c;
11. example1 struct1;
12. short e;
13. };
14. #pragma pack()
15. int main(int argc, char* argv[])
16. {
17. example2 struct2;
18. cout << sizeof(example1) << endl;
19. cout << sizeof(example2) << endl;
20. cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2)
<< endl;
21. return 0;
22. }
問程式的輸入結果是什麼?
答案是:
8
16
4
不明白?還是不明白?下麵一一道來:
2.1 自然對界
struct是一種複合資料類型,其構成元素既可以是基本資料類型(如int、long、float等)的變數,也可以是一些複合資料類型(如array、struct、union等)的資料單元。對於結構體,編譯器會自動進行成員變數的對齊,以提高運算效率。缺省情況下,編譯器為結構體的每個成員按其自然對界(natural alignment)條件分配空間。各個成員按照它們被聲明的順序在記憶體中順序存儲,第一個成員的位址和整個結構的位址相同。
自然對界(natural alignment)即默認對齊方式,是指按結構體的成員中size最大的成員對齊。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述結構體中,size最大的是short,其長度為2位元組,因而結構體中的char成員a、c都以2為單位對齊,sizeof(naturalalign)的結果等於6;
如果改為:
struct naturalalign
{
char a;
int b;
char c;
};
其結果顯然為12。
2.2指定對界
一般地,可以通過下面的方法來改變缺省的對界條件:
• 使用虛擬指令#pragma pack (n),編譯器將按照n個位元組對齊;
• 使用虛擬指令#pragma pack (),取消自定義位元組對齊方式。
注意:如果#pragma pack (n)中指定的n大於結構體中最大成員的size,則其不起作用,結構體仍然按照size最大的成員進行對界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
當n為4、8、16時,其對齊方式均一樣,sizeof(naturalalign)的結果都等於12。而當n為2時,其發揮了作用,使得sizeof(naturalalign)的結果為8。
在VC++ 6.0編譯器中,我們可以指定其對界方式,其操作方式為依次選擇projetct > setting > C/C++功能表,在struct member alignment中指定你要的對界方式。
另外,通過__attribute((aligned (n)))也可以讓所作用的結構體成員對齊在n位元組邊界上,但是它較少被使用,因而不作詳細講解。
2.3 面試題的解答
至此,我們可以對Intel、微軟的面試題進行全面的解答。
程式中第2行#pragma pack (8)雖然指定了對界為8,但是由於struct example1中的成員最大size為4(long變數size為4),故struct example1仍然按4位元組對界,struct example1的size為8,即第18行的輸出結果;
struct example2中包含了struct example1,其本身包含的簡單資料成員的最大size為2(short變數e),但是因為其包含了struct example1,而struct example1中的最大成員size為4,struct example2也應以4對界,#pragma pack (8)中指定的對界對struct example2也不起作用,故19行的輸出結果為16;
由於struct example2中的成員以4為單位對界,故其char變數c後應補充3個空,其後才是成員struct1的記憶體空間,20行的輸出結果為4。
3. C和C++間struct的深層區別
在C++語言中struct具有了“類” 的功能,其與關鍵字class的區別在於struct中成員變數和函數的默認訪問許可權為public,而class的為private。
例如,定義struct類和class類:
struct structA
{
char a;
…
}
class classB
{
char a;
…
}
則:
struct A a;
a.a = 'a'; //訪問public成員,合法
classB b;
b.a = 'a'; //訪問private成員,不合法
許多文獻寫到這裏就認為已經給出了C++中struct和class的全部區別,實則不然,另外一點需要注意的是:
C++中的struct保持了對C中struct的全面相容(這符合C++的初衷——“a better c”),因而,下面的操作是合法的:
//定義struct
struct structA
{
char a;
char b;
int c;
};
structA a = {'a' , 'a' ,1}; // 定義時直接賦初值
即struct可以在定義的時候直接以{ }對其成員變數賦初值,而class則不能,在經典書目《thinking C++ 2nd edition》中作者對此點進行了強調。
4. struct編程注意事項
看看下面的程式:
1. #include
2. struct structA
3. {
4. int iMember;
5. char *cMember;
6. };
7. int main(int argc, char* argv[])
8. {
9. structA instant1,instant2;
10.char c = 'a';
11. instant1.iMember = 1;
12. instant1.cMember = &c;
13.instant2 = instant1;
14.cout << *(instant1.cMember) << endl;
15.*(instant2.cMember) = 'b';
16. cout << *(instant1.cMember) << endl;
17. return 0;
}
14行的輸出結果是:a
16行的輸出結果是:b
Why?我們在15行對instant2的修改改變了instant1中成員的值!
原因在於13行的instant2 = instant1賦值語句採用的是變數逐個拷貝,這使得instant1和instant2中的cMember指向了同一片記憶體,因而對instant2的修改也是對instant1的修改。
在C語言中,當結構體中存在指標型成員時,一定要注意在採用賦值語句時是否將2個實例中的指標型成員指向了同一片記憶體。
在C++語言中,當結構體中存在指標型成員時,我們需要重寫struct的拷貝構造函數並進行“=”操作符重載。
轉載自CSDN
2007年4月6日 星期五
SUN公司的考試題目
據說是一道SUN公司的考試題目
題目如下:
寫出一個帶參數的巨集#define get_struct_addr_from_member_addr(p,stru,m)
這個巨集能根據任意的結構實體的某一個成員位址,算出該結構實體的位址
其中參數p是指向該成員的指標,stru結構實體,m是該成員名稱
本題答案:
這一題的答案,最怪的部份就是((stru*)0)->m了,居然有這種寫法@@
因為照理說根本不可能在位址為0的地方找到m的位址啊!
可是實際上是..編譯器在進行((stru*)0)->m的運算時,要先取得stru的位置才能取到m的位址,
而在m不存在時,位置就不會改變,於是結果就得到了stru的位置了@@
======================================================
//--winnt.h
// Calculate the address of the base of the structure given its type, and an
// address of a field within the structure.
//
題目如下:
寫出一個帶參數的巨集#define get_struct_addr_from_member_addr(p,stru,m)
這個巨集能根據任意的結構實體的某一個成員位址,算出該結構實體的位址
其中參數p是指向該成員的指標,stru結構實體,m是該成員名稱
本題答案:
#define get_struct_addr_from_member_addr(p, stru, m) \
(stru*)( (char*)p - (char*)&(((stru*)0)->m) )
這一題的答案,最怪的部份就是((stru*)0)->m了,居然有這種寫法@@
因為照理說根本不可能在位址為0的地方找到m的位址啊!
可是實際上是..編譯器在進行((stru*)0)->m的運算時,要先取得stru的位置才能取到m的位址,
而在m不存在時,位置就不會改變,於是結果就得到了stru的位置了@@
======================================================
//--winnt.h
// Calculate the address of the base of the structure given its type, and an
// address of a field within the structure.
//
#define CONTAINING_RECORD(address, type, field) ((type *)( \
(PCHAR)(address) - \
(UINT_PTR)(&((type *)0)->field)))
有趣的C程式
#include <stdio.h>有沒有看懂這段程式在做什麼呀?想一下吧^^
int main(void)
{
char *c="#include <stdio.h>%cint main(void)%c{%cchar *c=%c%s%c;%cprintf(c,10,10,10,34,c,34,10,10);%c}";
printf(c,10,10,10,34,c,34,10,10);
}
變數的資料型態種類
C語言變數的資料型態種類
類別 | 符號位元 | 位元長 | 表示法 | 數值範圍 |
整數 | 有 | 16 | int(short) | -32768->32767 |
32 | long | -2147483648->2147483647 | ||
無 | 16 | unsigned int | 0->65535 | |
16 | unsigned short | 0->65535 | ||
32 | unsigned long | 0->4294967295 | ||
浮點數 | 有 | 32 | float | 10^-38->10^38 |
64 | double | 10^-308-->10^308 | ||
字元 | 無 | 8 | char | 0->255 |
2007年4月4日 星期三
深入淺出 Hello World Part
一個 hello world 是很多人寫程序的起點,那麼關於這個hello world 你又究竟理解有多深呢?
你敢說自己很熟悉這個簡單的不起眼的 hello world 程序背後的方方面面嗎?
看看來自台灣的 Jserv 展示的關於 hello world 的你未曾想過的不一樣的世界!
「深入淺出 Hello World Part I/II (台北場次)」簡報上線
http://blog.linux.org.tw/~jserv/archives/001844.html
「深入淺出 Hello World Part I/II (台北場次)」錄影
ftp://tnlug.linux.org.tw/video/TnLUG/2007-07-22_HelloWorld
你敢說自己很熟悉這個簡單的不起眼的 hello world 程序背後的方方面面嗎?
看看來自台灣的 Jserv 展示的關於 hello world 的你未曾想過的不一樣的世界!
「深入淺出 Hello World Part I/II (台北場次)」簡報上線
http://blog.linux.org.tw/~jserv/archives/001844.html
「深入淺出 Hello World Part I/II (台北場次)」錄影
ftp://tnlug.linux.org.tw/video/TnLUG/2007-07-22_HelloWorld
C語言巨集定義技巧
轉載自:
http://miaozl.spaces.live.com/?_c11_blogpart_blogpart=blogview&_c=blogpart&_c02_owner=1&partqs=amonth%3d2%26ayear%3d2007
C語言巨集定義技巧
1,防止一個頭檔被重複包含
2,重新定義一些類型,防止由於各種平臺和編譯器的不同,而產生的類型位元組數差異,方便移植。
3,得到指定位址上的一個位元組或字
4,求最大值和最小值
5,得到一個field在結構體(struct)中的偏移量
6,得到一個結構體中field所佔用的位元組數
7,按照LSB格式把兩個位元組轉化為一個Word
8,按照LSB格式把一個Word轉化為兩個位元組
9,得到一個變數的位址(word寬度)
10,得到一個字的高位和低位元位元組
11,返回一個比X大的最接近的8的倍數
12,將一個字母轉換為大寫
13,判斷字元是不是10進值的數字
14,判斷字元是不是16進值的數字
15,防止溢出的一個方法
16,返回陣列元素的個數
17,返回一個無符號數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)
18,對於IO空間映射在存儲空間的結構,輸入輸出處理
19,使用一些宏跟蹤調試
A N S I標準說明了五個預定義的宏名。它們是:
如果編譯不是標準的,則可能僅支援以上宏名中的幾個,或根本不支持。記住編譯程序
也許還提供其他預定義的宏名。
_ L I N E _及_ F I L E _巨集指令在有關# l i n e的部分中已討論,這裏討論其餘的宏名。
_ D AT E _巨集指令含有形式為月/日/年的串,表示原始檔案被翻譯到代碼時的日期。
源代碼翻譯到目標代碼的時間作為串包含在_ T I M E _中。串形式為時:分:秒。
如果實現是標準的,則宏_ S T D C _含有十進位常量1。如果它含有任何其他數,則實現是
非標準的。
可以定義宏,例如:
當定義了_DEBUG,輸出資料資訊和所在檔所在行
20,巨集定義防止使用是錯誤
用小括弧包含。
例如:#define ADD(a,b) (a+b)
用do{}while(0)語句包含多語句防止錯誤
例如:#difne DO(a,b) a+b;\
a++;
應用時:if(….)
DO(a,b); //產生錯誤
else
解決方法: #difne DO(a,b) do{a+b;\
a++;}while(0)
宏中"#"和"##"的用法
一、一般用法
我們使用#把巨集引數變為一個字串,用##把兩個巨集引數貼合在一起.
用法:
二、當巨集引數是另一個宏的時候
需要注意的是凡巨集定義裏有用'#'或'##'的地方巨集引數是不會再展開.
1, 非'#'和'##'的情況
#define TOW (2)
#define MUL(a,b) (a*b)
printf("%d*%d=%d\n", TOW, TOW, MUL(TOW,TOW));
這行的宏會被展開為:
printf("%d*%d=%d\n", (2), (2), ((2)*(2)));
MUL裏的參數TOW會被展開為(2).
2, 當有'#'或'##'的時候
#define A (2)
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
printf("int max: %s\n", STR(INT_MAX)); // INT_MAX #i nclude
這行會被展開為:
printf("int max: %s\n", "INT_MAX");
printf("%s\n", CONS(A, A)); // compile error
這一行則是:
printf("%s\n", int(AeA));
INT_MAX和A都不會再被展開, 然而解決這個問題的方法很簡單. 加多一層中間轉換宏.
加這層巨集的用意是把所有宏的參數在這層裏全部展開, 那麼在轉換巨集裏的那一個巨集(_STR)就能得到正確的巨集引數.
#define A (2)
#define _STR(s) #s
#define STR(s) _STR(s) // 轉換宏
#define _CONS(a,b) int(a##e##b)
#define CONS(a,b) _CONS(a,b) // 轉換宏
printf("int max: %s\n", STR(INT_MAX)); // INT_MAX,int型的最大值,為一個變數 #i nclude
輸出為: int max: 0x7fffffff
STR(INT_MAX) --> _STR(0x7fffffff) 然後再轉換成字串;
printf("%d\n", CONS(A, A));
輸出為:200
CONS(A, A) --> _CONS((2), (2)) --> int((2)e(2))
三、'#'和'##'的一些應用特例
1、合併匿名變數名
#define ___ANONYMOUS1(type, var, line) type var##line
#define __ANONYMOUS0(type, line) ___ANONYMOUS1(type, _anonymous, line)
#define ANONYMOUS(type) __ANONYMOUS0(type, __LINE__)
例:ANONYMOUS(static int); 即: static int _anonymous70; 70表示該行行號;
第一層:ANONYMOUS(static int); --> __ANONYMOUS0(static int, __LINE__);
第二層: --> ___ANONYMOUS1(static int, _anonymous, 70);
第三層: --> static int _anonymous70;
即每次只能解開當前層的宏,所以__LINE__在第二層才能被解開;
2、填充結構
#define FILL(a) {a, #a}
enum IDD{OPEN, CLOSE};
typedef struct MSG{
IDD id;
const char * msg;
}MSG;
MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};
相當於:
MSG _msg[] = {{OPEN, "OPEN"},
{CLOSE, "CLOSE"}};
3、記錄檔案名
#define _GET_FILE_NAME(f) #f
#define GET_FILE_NAME(f) _GET_FILE_NAME(f)
static char FILE_NAME[] = GET_FILE_NAME(__FILE__);
4、得到一個數值類型所對應的字串緩衝大小
#define _TYPE_BUF_SIZE(type) sizeof #type
#define TYPE_BUF_SIZE(type) _TYPE_BUF_SIZE(type)
char buf[TYPE_BUF_SIZE(INT_MAX)];
--> char buf[_TYPE_BUF_SIZE(0x7fffffff)];
--> char buf[sizeof "0x7fffffff"];
這裏相當於:
char buf[11];
http://miaozl.spaces.live.com/?_c11_blogpart_blogpart=blogview&_c=blogpart&_c02_owner=1&partqs=amonth%3d2%26ayear%3d2007
C語言巨集定義技巧
1,防止一個頭檔被重複包含
#ifndef COMDEF_H
#define COMDEF_H
//頭檔內容
#endif
2,重新定義一些類型,防止由於各種平臺和編譯器的不同,而產生的類型位元組數差異,方便移植。
typedef unsigned char boolean; /* Boolean value type. */
typedef unsigned long int uint32; /* Unsigned 32 bit value */
typedef unsigned short uint16; /* Unsigned 16 bit value */
typedef unsigned char uint8; /* Unsigned 8 bit value */
typedef signed long int int32; /* Signed 32 bit value */
typedef signed short int16; /* Signed 16 bit value */
typedef signed char int8; /* Signed 8 bit value */
//下面的不建議使用
typedef unsigned char byte; /* Unsigned 8 bit value type. */
typedef unsigned short word; /* Unsinged 16 bit value type. */
typedef unsigned long dword; /* Unsigned 32 bit value type. */
typedef unsigned char uint1; /* Unsigned 8 bit value type. */
typedef unsigned short uint2; /* Unsigned 16 bit value type. */
typedef unsigned long uint4; /* Unsigned 32 bit value type. */
typedef signed char int1; /* Signed 8 bit value type. */
typedef signed short int2; /* Signed 16 bit value type. */
typedef long int int4; /* Signed 32 bit value type. */
typedef signed long sint31; /* Signed 32 bit value */
typedef signed short sint15; /* Signed 16 bit value */
typedef signed char sint7; /* Signed 8 bit value */
3,得到指定位址上的一個位元組或字
#define MEM_B( x ) ( *( (byte *) (x) ) )
#define MEM_W( x ) ( *( (word *) (x) ) )
4,求最大值和最小值
#define MAX( x, y ) ( ((x) > (y)) ? (x) : (y) )
#define MIN( x, y ) ( ((x) < (y)) ? (x) : (y) )
5,得到一個field在結構體(struct)中的偏移量
#define FPOS( type, field ) \
/*lint -e545 */ ( (dword) &(( type *) 0)-> field ) /*lint +e545 */
6,得到一個結構體中field所佔用的位元組數
#define FSIZ( type, field ) sizeof( ((type *) 0)->field )
7,按照LSB格式把兩個位元組轉化為一個Word
#define FLIPW( ray ) ( (((word) (ray)[0]) * 256) + (ray)[1] )
8,按照LSB格式把一個Word轉化為兩個位元組
#define FLOPW( ray, val ) \
(ray)[0] = ((val) / 256); \
(ray)[1] = ((val) & 0xFF)
9,得到一個變數的位址(word寬度)
#define B_PTR( var ) ( (byte *) (void *) &(var) )
#define W_PTR( var ) ( (word *) (void *) &(var) )
10,得到一個字的高位和低位元位元組
#define WORD_LO(xxx) ((byte) ((word)(xxx) & 255))
#define WORD_HI(xxx) ((byte) ((word)(xxx) >> 8))
11,返回一個比X大的最接近的8的倍數
#define RND8( x ) ((((x) + 7) / 8 ) * 8 )
12,將一個字母轉換為大寫
#define UPCASE( c ) ( ((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c) )
13,判斷字元是不是10進值的數字
#define DECCHK( c ) ((c) >= '0' && (c) <= '9')
14,判斷字元是不是16進值的數字
#define HEXCHK( c ) ( ((c) >= '0' && (c) <= '9') ||\
((c) >= 'A' && (c) <= 'F') ||\
((c) >= 'a' && (c) <= 'f') )
15,防止溢出的一個方法
#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))
16,返回陣列元素的個數
#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )
17,返回一個無符號數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)
#define MOD_BY_POWER_OF_TWO( val, mod_by ) \
( (dword)(val) & (dword)((mod_by)-1) )
18,對於IO空間映射在存儲空間的結構,輸入輸出處理
#define inp(port) (*((volatile byte *) (port)))
#define inpw(port) (*((volatile word *) (port)))
#define inpdw(port) (*((volatile dword *)(port)))
#define outp(port, val) (*((volatile byte *) (port)) = ((byte) (val)))
#define outpw(port, val) (*((volatile word *) (port)) = ((word) (val)))
#define outpdw(port, val) (*((volatile dword *) (port)) = ((dword) (val)))
19,使用一些宏跟蹤調試
A N S I標準說明了五個預定義的宏名。它們是:
_ L I N E _
_ F I L E _
_ D A T E _
_ T I M E _
_ S T D C _
如果編譯不是標準的,則可能僅支援以上宏名中的幾個,或根本不支持。記住編譯程序
也許還提供其他預定義的宏名。
_ L I N E _及_ F I L E _巨集指令在有關# l i n e的部分中已討論,這裏討論其餘的宏名。
_ D AT E _巨集指令含有形式為月/日/年的串,表示原始檔案被翻譯到代碼時的日期。
源代碼翻譯到目標代碼的時間作為串包含在_ T I M E _中。串形式為時:分:秒。
如果實現是標準的,則宏_ S T D C _含有十進位常量1。如果它含有任何其他數,則實現是
非標準的。
可以定義宏,例如:
當定義了_DEBUG,輸出資料資訊和所在檔所在行
#ifdef _DEBUG
#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
#else
#define DEBUGMSG(msg,date)
#endif
20,巨集定義防止使用是錯誤
用小括弧包含。
例如:#define ADD(a,b) (a+b)
用do{}while(0)語句包含多語句防止錯誤
例如:#difne DO(a,b) a+b;\
a++;
應用時:if(….)
DO(a,b); //產生錯誤
else
解決方法: #difne DO(a,b) do{a+b;\
a++;}while(0)
宏中"#"和"##"的用法
一、一般用法
我們使用#把巨集引數變為一個字串,用##把兩個巨集引數貼合在一起.
用法:
#i nclude
#i nclude
using namespace std;
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
int main()
{
printf(STR(vck)); // 輸出字串"vck"
printf("%d\n", CONS(2,3)); // 2e3 輸出:2000
return 0;
}
二、當巨集引數是另一個宏的時候
需要注意的是凡巨集定義裏有用'#'或'##'的地方巨集引數是不會再展開.
1, 非'#'和'##'的情況
#define TOW (2)
#define MUL(a,b) (a*b)
printf("%d*%d=%d\n", TOW, TOW, MUL(TOW,TOW));
這行的宏會被展開為:
printf("%d*%d=%d\n", (2), (2), ((2)*(2)));
MUL裏的參數TOW會被展開為(2).
2, 當有'#'或'##'的時候
#define A (2)
#define STR(s) #s
#define CONS(a,b) int(a##e##b)
printf("int max: %s\n", STR(INT_MAX)); // INT_MAX #i nclude
這行會被展開為:
printf("int max: %s\n", "INT_MAX");
printf("%s\n", CONS(A, A)); // compile error
這一行則是:
printf("%s\n", int(AeA));
INT_MAX和A都不會再被展開, 然而解決這個問題的方法很簡單. 加多一層中間轉換宏.
加這層巨集的用意是把所有宏的參數在這層裏全部展開, 那麼在轉換巨集裏的那一個巨集(_STR)就能得到正確的巨集引數.
#define A (2)
#define _STR(s) #s
#define STR(s) _STR(s) // 轉換宏
#define _CONS(a,b) int(a##e##b)
#define CONS(a,b) _CONS(a,b) // 轉換宏
printf("int max: %s\n", STR(INT_MAX)); // INT_MAX,int型的最大值,為一個變數 #i nclude
輸出為: int max: 0x7fffffff
STR(INT_MAX) --> _STR(0x7fffffff) 然後再轉換成字串;
printf("%d\n", CONS(A, A));
輸出為:200
CONS(A, A) --> _CONS((2), (2)) --> int((2)e(2))
三、'#'和'##'的一些應用特例
1、合併匿名變數名
#define ___ANONYMOUS1(type, var, line) type var##line
#define __ANONYMOUS0(type, line) ___ANONYMOUS1(type, _anonymous, line)
#define ANONYMOUS(type) __ANONYMOUS0(type, __LINE__)
例:ANONYMOUS(static int); 即: static int _anonymous70; 70表示該行行號;
第一層:ANONYMOUS(static int); --> __ANONYMOUS0(static int, __LINE__);
第二層: --> ___ANONYMOUS1(static int, _anonymous, 70);
第三層: --> static int _anonymous70;
即每次只能解開當前層的宏,所以__LINE__在第二層才能被解開;
2、填充結構
#define FILL(a) {a, #a}
enum IDD{OPEN, CLOSE};
typedef struct MSG{
IDD id;
const char * msg;
}MSG;
MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};
相當於:
MSG _msg[] = {{OPEN, "OPEN"},
{CLOSE, "CLOSE"}};
3、記錄檔案名
#define _GET_FILE_NAME(f) #f
#define GET_FILE_NAME(f) _GET_FILE_NAME(f)
static char FILE_NAME[] = GET_FILE_NAME(__FILE__);
4、得到一個數值類型所對應的字串緩衝大小
#define _TYPE_BUF_SIZE(type) sizeof #type
#define TYPE_BUF_SIZE(type) _TYPE_BUF_SIZE(type)
char buf[TYPE_BUF_SIZE(INT_MAX)];
--> char buf[_TYPE_BUF_SIZE(0x7fffffff)];
--> char buf[sizeof "0x7fffffff"];
這裏相當於:
char buf[11];
2007年4月3日 星期二
編程秘笈篇:C語言高效編程的四大絕招
編程秘笈篇:C語言高效編程的四大絕招
編寫高效簡潔的C語言代碼,是許多軟體工程師追求的目標。本文就是針對編程工作中的一些體會和經驗做相關的闡述。
第一招:以空間換時間
電腦程式中最大的矛盾是空間和時間的矛盾,那麼,從這個角度出發逆向思維來考慮程式的效率問題,我們就有了解決問題的第1招--以空間換時間。比如說字串的賦值:
方法A:通常的辦法
方法B:
使用的時候可以直接用指標來操作。
從上面的例子可以看出,A和B的效率是不能比的。在同樣的存儲空間下,B直接使用指標就可以操作了,而A需要調用兩個字元函數才能完成。B的缺點在於靈活性沒有A好。在需要頻繁更改一個字串內容的時候,A具有更好的靈活性;如果採用方法B,則需要預存許多字串,雖然佔用了大量的記憶體,但是獲得了程式執行的高效率。
如果系統的即時性要求很高,記憶體還有一些,那我推薦你使用該招數。該招數的變招--使用巨集函數而不是函數。舉例如下:
方法C:
方法D:
函數和巨集函數的區別就在於,巨集函數佔用了大量的空間,而函數佔用了時間。大家要知道的是,函數調用是要使用系統的棧來保存資料的,如果編譯器裏有棧檢查選項,一般在函數的頭會嵌入一些彙編語句對當前棧進行檢查;同時,CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操作,所以,函數調用需要一些CPU時間。
而巨集函數不存在這個問題。巨集函數僅僅作為預先寫好的代碼嵌入到當前程式,不會產生函數調用,所以僅僅是佔用了空間,在頻繁調用同一個巨集函數的時候,該現象尤其突出。
D方法是我看到的最好的置位元操作函數,是ARM公司源碼的一部分,在短短的三行內實現了很多功能,幾乎涵蓋了所有的位操作功能。C方法是其變體,其中滋味還需大家仔細體會。
第二招:數學方法解決問題
現在我們演繹高效C語言編寫的第二招--採用數學方法來解決問題。數學是電腦之母,沒有數學的依據和基礎,就沒有電腦的發展,所以在編寫程式的時候,採用一些數學方法會對程式的執行效率有數量級的提高。舉例如下,求 1~100的和。
方法E:
方法F
這個例子是我印象最深的一個數學用例,是我的電腦啟蒙老師考我的。當時我只有小學三年級,可惜我當時不知道用公式 N×(N+1)/ 2 來解決這個問題。方法E迴圈了100次才解決問題,也就是說最少用了100個賦值,100個判斷,200個加法(I和j);而方法F僅僅用了1個加法,1次乘法,1次除法。效果自然不言而喻。所以,現在我在編程式的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提高程式運行的效率。
第三招:使用位操作
實現高效的C語言編寫的第三招——使用位操作。減少除法和取模的運算。在電腦程式中資料的位元是可以操作的最小資料單位,理論上可以用"位運算"來完成所有的運算和操作。一般的位元操作是用來控制硬體的,或者做資料變換使用,但是,靈活的位操作可以有效地提高程式運行的效率。舉例如下:
方法G
方法H
在字面上好像H比G麻煩了好多,但是,仔細查看產生的彙編代碼就會明白,方法G調用了基本的取模函數和除法函數,既有函數調用,還有很多彙編代碼和寄存器參與運算;而方法H則僅僅是幾句相關的彙編,代碼更簡潔,效率更高。當然,由於編譯器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 來看,效率的差距還是不小。相關彙編代碼就不在這裏列舉了。
運用這招需要注意的是,因為CPU的不同而產生的問題。比如說,在PC上用這招編寫的程式,並在PC上調試通過,在移植到一個16位機平臺上的時候,可能會產生代碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。
第四招:彙編嵌入
高效C語言編程的必殺技,第四招——嵌入彙編。"在熟悉組合語言的人眼裏,C語言編寫的程式都是垃圾"。這種說法雖然偏激了一些,但是卻有它的道理。組合語言是效率最高的電腦語言,但是,不可能靠著它來寫一個作業系統吧?所以,為了獲得程式的高效率,我們只好採用變通的方法--嵌入彙編,混合編程。舉例如下,將陣列一賦值給陣列二,要求每一位元組都相符。
方法I
方法J
方法I是最常見的方法,使用了1024次迴圈;方法J則根據平臺不同做了區分,在ARM平臺下,用嵌入彙編僅用128次迴圈就完成了同樣的操作。這裏有朋友會說,為什麼不用標準的記憶體拷貝函數呢?這是因為在源資料裏可能含有資料為0的位元組,這樣的話,標準庫函數會提前結束而不會完成我們要求的操作。這個常式典型應用於LCD資料的拷貝過程。根據不同的CPU,熟練使用相應的嵌入彙編,可以大大提高程式執行的效率。
雖然是必殺技,但是如果輕易使用會付出慘重的代價。這是因為,使用了嵌入彙編,便限制了程式的可攜性,使程式在不同平臺移植的過程中,臥虎藏龍,險象環生!同時該招數也與現代軟體工程的思想相違背,只有在迫不得已的情況下才可以採用。
編寫高效簡潔的C語言代碼,是許多軟體工程師追求的目標。本文就是針對編程工作中的一些體會和經驗做相關的闡述。
第一招:以空間換時間
電腦程式中最大的矛盾是空間和時間的矛盾,那麼,從這個角度出發逆向思維來考慮程式的效率問題,我們就有了解決問題的第1招--以空間換時間。比如說字串的賦值:
方法A:通常的辦法
#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,"This is a example!!");
方法B:
const char string2[LEN] ="This is a example!";
char * cp;
cp = string2 ;
使用的時候可以直接用指標來操作。
從上面的例子可以看出,A和B的效率是不能比的。在同樣的存儲空間下,B直接使用指標就可以操作了,而A需要調用兩個字元函數才能完成。B的缺點在於靈活性沒有A好。在需要頻繁更改一個字串內容的時候,A具有更好的靈活性;如果採用方法B,則需要預存許多字串,雖然佔用了大量的記憶體,但是獲得了程式執行的高效率。
如果系統的即時性要求很高,記憶體還有一些,那我推薦你使用該招數。該招數的變招--使用巨集函數而不是函數。舉例如下:
方法C:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK(int __bf)
{
return ((1U << (bw ## __bf)) - 1)
<< (bs ## __bf);
}
void SET_BITS(int __dst,
int __bf, int __val)
{
__dst = ((__dst) & ~(BIT_MASK(__bf))) |
\
(((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
}
SET_BITS(MCDR2, MCDR2_ADDRESS,
RegisterNumber);
方法D:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK(MCDR2_ADDRESS)
#define BIT_MASK(__bf)
(((1U << (bw ## __bf)) - 1)
<< (bs ## __bf))
#define SET_BITS(__dst, __bf, __val)
\
((__dst) = ((__dst) & ~(BIT_MASK(__bf)))
| \
(((__val) << (bs ## __bf))
& (BIT_MASK(__bf))))
SET_BITS(MCDR2, MCDR2_ADDRESS,
RegisterNumber);
函數和巨集函數的區別就在於,巨集函數佔用了大量的空間,而函數佔用了時間。大家要知道的是,函數調用是要使用系統的棧來保存資料的,如果編譯器裏有棧檢查選項,一般在函數的頭會嵌入一些彙編語句對當前棧進行檢查;同時,CPU也要在函數調用時保存和恢復當前的現場,進行壓棧和彈棧操作,所以,函數調用需要一些CPU時間。
而巨集函數不存在這個問題。巨集函數僅僅作為預先寫好的代碼嵌入到當前程式,不會產生函數調用,所以僅僅是佔用了空間,在頻繁調用同一個巨集函數的時候,該現象尤其突出。
D方法是我看到的最好的置位元操作函數,是ARM公司源碼的一部分,在短短的三行內實現了很多功能,幾乎涵蓋了所有的位操作功能。C方法是其變體,其中滋味還需大家仔細體會。
第二招:數學方法解決問題
現在我們演繹高效C語言編寫的第二招--採用數學方法來解決問題。數學是電腦之母,沒有數學的依據和基礎,就沒有電腦的發展,所以在編寫程式的時候,採用一些數學方法會對程式的執行效率有數量級的提高。舉例如下,求 1~100的和。
方法E:
int I , j;
for (I = 1 ;I<=100; I ++)
{
j += I;
}
方法F
int I;
I = (100 * (1+100)) / 2
這個例子是我印象最深的一個數學用例,是我的電腦啟蒙老師考我的。當時我只有小學三年級,可惜我當時不知道用公式 N×(N+1)/ 2 來解決這個問題。方法E迴圈了100次才解決問題,也就是說最少用了100個賦值,100個判斷,200個加法(I和j);而方法F僅僅用了1個加法,1次乘法,1次除法。效果自然不言而喻。所以,現在我在編程式的時候,更多的是動腦筋找規律,最大限度地發揮數學的威力來提高程式運行的效率。
第三招:使用位操作
實現高效的C語言編寫的第三招——使用位操作。減少除法和取模的運算。在電腦程式中資料的位元是可以操作的最小資料單位,理論上可以用"位運算"來完成所有的運算和操作。一般的位元操作是用來控制硬體的,或者做資料變換使用,但是,靈活的位操作可以有效地提高程式運行的效率。舉例如下:
方法G
int I,J;
I = 257 /8;
J = 456 % 32;
方法H
int I,J;
I = 257 >>3;
J = 456 - (456 >> 4 << 4);
在字面上好像H比G麻煩了好多,但是,仔細查看產生的彙編代碼就會明白,方法G調用了基本的取模函數和除法函數,既有函數調用,還有很多彙編代碼和寄存器參與運算;而方法H則僅僅是幾句相關的彙編,代碼更簡潔,效率更高。當然,由於編譯器的不同,可能效率的差距不大,但是,以我目前遇到的MS C ,ARM C 來看,效率的差距還是不小。相關彙編代碼就不在這裏列舉了。
運用這招需要注意的是,因為CPU的不同而產生的問題。比如說,在PC上用這招編寫的程式,並在PC上調試通過,在移植到一個16位機平臺上的時候,可能會產生代碼隱患。所以只有在一定技術進階的基礎下才可以使用這招。
第四招:彙編嵌入
高效C語言編程的必殺技,第四招——嵌入彙編。"在熟悉組合語言的人眼裏,C語言編寫的程式都是垃圾"。這種說法雖然偏激了一些,但是卻有它的道理。組合語言是效率最高的電腦語言,但是,不可能靠著它來寫一個作業系統吧?所以,為了獲得程式的高效率,我們只好採用變通的方法--嵌入彙編,混合編程。舉例如下,將陣列一賦值給陣列二,要求每一位元組都相符。
char string1[1024],string2[1024];
方法I
int I;
for (I =0 ;I<1024;I++)
*(string2 + I) = *(string1 + I)
方法J
#ifdef _PC_
int I;
for (I =0 ;I<1024;I++)
*(string2 + I) = *(string1 + I);
#else
#ifdef _ARM_
__asm
{
MOV R0,string1
MOV R1,string2
MOV R2,#0
loop:
LDMIA R0!, [R3-R11]
STMIA R1!, [R3-R11]
ADD R2,R2,#8
CMP R2, #400
BNE loop
}
#endif
方法I是最常見的方法,使用了1024次迴圈;方法J則根據平臺不同做了區分,在ARM平臺下,用嵌入彙編僅用128次迴圈就完成了同樣的操作。這裏有朋友會說,為什麼不用標準的記憶體拷貝函數呢?這是因為在源資料裏可能含有資料為0的位元組,這樣的話,標準庫函數會提前結束而不會完成我們要求的操作。這個常式典型應用於LCD資料的拷貝過程。根據不同的CPU,熟練使用相應的嵌入彙編,可以大大提高程式執行的效率。
雖然是必殺技,但是如果輕易使用會付出慘重的代價。這是因為,使用了嵌入彙編,便限制了程式的可攜性,使程式在不同平臺移植的過程中,臥虎藏龍,險象環生!同時該招數也與現代軟體工程的思想相違背,只有在迫不得已的情況下才可以採用。
A*尋徑演算法
作者:yuki
原帖及討論:http://www.bc-cn.net/bbs/dispbbs.asp?BoardID=5&ID=82148
貼個小東西,也許是許多遊戲開發愛好者都想要獲得演算法。
下面我來說說我理解的A*演算法的原理:
A*演算法是一個求最短路徑的函數,為許多即時戰略遊戲所用刀(或許人家大型的即時戰略遊戲筆者演算法更好,不管它)。它由兩個函數組成,一個是評估函數,也就是確定人物移動的下一個位置必須離目標位置最近,評估函數評估的結果越精確,則尋徑的速度越快;另一個就是尋徑函數,也就根據評估的結果做出回應,然後從新位置繼續評估下一個位置,若無路可走(四周都是障礙什麼的),那麼折回一個路徑節點,嘗試其他方向,這個演算法有個缺點,隨著遊戲中人物增多,相應的處理節點就增多了,會影響處理速度,而且佔用大量的記憶體。
有興趣的朋友可以改成動態的尋徑,就是當入口和出口位置都在變化的時候進行尋徑,這個代碼也只有200多行。
我的演算法還不能算是最優的,因為評估函數只不過是簡單的測試兩點距離(這會帶來誤差),選擇離出口最短的且非障礙物的方向,進行下一個路徑節點的移動。
這裏說一句,我希望大家將我的代碼用於學習目的,不希望看見是為了交作業而拷貝過去,我會很傷心的。
原帖及討論:http://www.bc-cn.net/bbs/dispbbs.asp?BoardID=5&ID=82148
貼個小東西,也許是許多遊戲開發愛好者都想要獲得演算法。
下面我來說說我理解的A*演算法的原理:
A*演算法是一個求最短路徑的函數,為許多即時戰略遊戲所用刀(或許人家大型的即時戰略遊戲筆者演算法更好,不管它)。它由兩個函數組成,一個是評估函數,也就是確定人物移動的下一個位置必須離目標位置最近,評估函數評估的結果越精確,則尋徑的速度越快;另一個就是尋徑函數,也就根據評估的結果做出回應,然後從新位置繼續評估下一個位置,若無路可走(四周都是障礙什麼的),那麼折回一個路徑節點,嘗試其他方向,這個演算法有個缺點,隨著遊戲中人物增多,相應的處理節點就增多了,會影響處理速度,而且佔用大量的記憶體。
有興趣的朋友可以改成動態的尋徑,就是當入口和出口位置都在變化的時候進行尋徑,這個代碼也只有200多行。
我的演算法還不能算是最優的,因為評估函數只不過是簡單的測試兩點距離(這會帶來誤差),選擇離出口最短的且非障礙物的方向,進行下一個路徑節點的移動。
這裏說一句,我希望大家將我的代碼用於學習目的,不希望看見是為了交作業而拷貝過去,我會很傷心的。
/* AStar.cpp */
/* 設計者: yuki */
typedef unsigned char byte_t;
typedef unsigned int uint_t;
/* 路徑節點 */
typedef struct footprint {
/* 存放在陣列中的位置 */
uint_t pos;
/* 存放方向信號量 */
byte_t direct;
struct footprint *next;
struct footprint *prev;
} path_t;
/*
方向信號量查詢表
0x01(0000 0001) : 上
0x02(0000 0010) : 下
0x04(0000 0100) : 左
0x08(0000 1000) : 右
*/
static byte_t d_signal[4] = {0x01, 0x02, 0x04, 0x08};
/*
方向信號量使用表
如果指定方向已經走過,那麼使用“與”運算去處該方向
0x0E(0000 1110) : 上
0x0D(0000 1101) : 下
0x0B(0000 1011) : 左
0x07(0000 0111) : 右
*/
static byte_t d_spend[4] = {0x0E, 0x0D, 0x0B, 0x07};
/* 指定方向移動偏量 */
static int move[4][2] = { {0, -1}, {0, 1}, {-1, 0}, {1, 0} };
/* 列印迷宮用的符號 */
static byte_t symbolic[3] = {'#',0x20,'*'};
/* 求兩點間的距離 */
inline uint_t
distance( uint_t pos1X, uint_t pos1Y, uint_t pos2X, uint_t pos2Y ) {
uint_t ret = 0;
/* 距離公式 */
ret = (uint_t)sqrt((pow((double)((int)pos1X - (int)pos2X),2) + pow((double)((int)pos1Y - (int)pos2Y),2)));
return ret;
}
/* 壓縮座標 */
inline uint_t
create_pos( uint_t pX, uint_t pY ) {
uint_t ret = 0;
/* 將pX賦給ret,這樣pX座標在ret的低八位 */
ret = pX;
/* 將pX座標移到高八位去,這樣低位就能存放pY */
ret <<= 8;
/* 將pY存放放到ret的低八位,並保持高八位元的資料不變 */
ret |= pY;
return ret;
}
/*
== 估計函數 ===========================================
-p : 當前移動到的節點指針
-quit_x
-quit_y : quit_x 和 quit_y表示迷宮出口座標
-maze : 迷宮矩陣
=======================================================
*/
inline path_t *
evaluate( path_t *p, uint_t quit_x, uint_t quit_y, byte_t maze[MAZE_HEIGHT][MAZE_WIDTH] ) {
uint_t pX, pY;
/* 用於接收四個方向離開出口的距離,以便選擇最近的方向移動 */
int dis[4];
int minDis = 32767;
int minId = -1;
path_t *pnode = (path_t *)0;
register int i;
/* 計算當前節點的座標 */
pX = p->pos >> 8;
pY = p->pos & 0x00FF;
memset(dis, (int)-1, sizeof(int)*4);
/* 計算每個方向離開出口的距離,一次存放到dis陣列,若沒有i方向,則dis[i]任保留-1 */
for( i = 0; i < 4; ++i ) {
if( (p->direct & d_signal[i]) >> i == 0x01 )
dis[i] =(int)distance(pX + move[i][0], pY + move[i][1], quit_x, quit_y);
}
/* 獲得最短距離的方向 */
for(i = 0; i < 4; ++i) {
if(dis[i] != -1 && dis[i] < minDis) {
minId = i;
minDis = dis[i];
}
}
/* 若沒有可用的方向,則通知尋徑函數折回 */
if(minId == -1)
return (path_t *)0;
/* 用去最近距離方向的信號量 */
p->direct &= d_spend[minId];
/* 在移動到新位置之前,在舊位置處留下足跡 */
maze[pY][pX] = (byte_t)PATH_FOOTPRINT;
/* 構建新的路徑節點 */
pnode = (path_t *)malloc(sizeof(path_t));
assert(pnode);
/* 計算下一個位置的座標 */
pX += move[minId][0];
pY += move[minId][1];
pnode->pos = create_pos(pX, pY);
pnode->prev = p;
pnode->next = (path_t *)0;
pnode->direct = 0;
/* 在新位置處,計算下一個位置可用的移動方向 */
for(i = 0; i < 4; ++i) {
if(maze[pY + move[i][1]][pX + move[i][0]] != PATH_BLOCK && maze[pY + move[i][1]][pX + move[i][0]] != PATH_FOOTPRINT) {
/* 若嘗試的下一個位置不是障礙物或自己走過的足跡,則視為可用方向,獲得該方向的信號量 */
pnode->direct |= d_signal[i];
}
}
return pnode;
}
/*
== A*演算法尋徑函數 ===========================================
-eX
-eY :入口座標
-qX
-qY :出口座標
-maze :迷宮矩陣
=============================================================
*/
inline path_t *
AStar(uint_t eX, uint_t eY, uint_t qX, uint_t qY, byte_t maze[MAZE_HEIGHT][MAZE_WIDTH]) {
register int i;
/* 壓縮座標 */
uint_t quit_pos = create_pos(qX, qY);
/* 構建入口路徑節點,視為路徑鏈的頭 */
path_t *head = (path_t *)malloc(sizeof(path_t));
path_t *p = (path_t *)0;
path_t *back = (path_t *)0;
assert(head);
p = head;
p->direct = 0;
p->pos = create_pos(eX,eY);
p->next = (path_t *)0;
p->prev = (path_t *)0;
/* 創建入口處的可用方向 */
for(i = 0; i < 4; ++i) {
if(maze[eY + move[i][1]][eX + move[i][0]] != PATH_BLOCK)
/* 若無障礙物則獲得該方向的信號量 */
p->direct |= d_signal[i];
}
do {
/* 獲得下個路徑的節點指針 */
back = evaluate(p, qX, qY, maze);
if(back) {
p->next = back;
p = p->next;
}
/* 無路可走則折回 */
if(p->direct == 0 && p != head && p->pos != quit_pos) {
back = p->prev;
back->next = (path_t *)0;
/* 清楚腳印 */
maze[p->pos & 0x00FF][p->pos >> 8] = (byte_t)PATH_WALKON;
free(p);
p = back;
}
/* 若走不出迷宮,即折回到入口,且入口處的可用方向全部嘗試過 */
if(p == head && p->direct == 0) {
free(head);
return (path_t *)0;
}
} while( p->pos != quit_pos );
/* 在出口處留下腳印,便於列印 */
maze[p->pos & 0x00FF][p->pos >> 8] = (byte_t)PATH_FOOTPRINT;
return head;
}
/* AStar.h */
/* 設計者: yuki */
#ifndef __ASTAR_H
#define __ASTAR_H
#define MAZE_WIDTH 10 /* 迷宮寬度 */
#define MAZE_HEIGHT 10 /* 迷宮高度 */
#define PATH_BLOCK 0 /* 障礙物 */
#define PATH_WALKON 1 /* 可行走 */
#define PATH_FOOTPRINT 2 /* 腳印 */
#include "AStar.cpp"
#endif
/* main.cpp */
/* 設計者: yuki */
#include
#include
#include
#include
#include
#include
#include "AStar.h"
static byte_t maze[MAZE_HEIGHT][MAZE_WIDTH] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 1, 0, 1, 1, 1, 0, 1, 0,
0, 1, 1, 0, 1, 1, 1, 0, 1, 0,
0, 1, 1, 1, 1, 0, 0, 0, 1, 0,
0, 1, 0, 0, 0, 1, 1, 0, 1, 0,
0, 1, 1, 1, 0, 1, 1, 0, 1, 0,
0, 1, 0, 1, 1, 1, 0, 1, 1, 0,
0, 1, 0, 0, 0, 1, 0, 0, 1, 0,
0, 0, 1, 1, 1, 1, 1, 1, 1, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0
};
int main() {
register int i,j;
path_t *pHead = AStar((uint_t)1,(uint_t)1,(uint_t)2,(uint_t)8,maze);
path_t *p = pHead;
path_t *bak;
if(p) {
bak = p->next;
printf("(%u,%u)",p->pos >> 8, p->pos & 0x00FF);
free(p);
p = bak;
while(p) {
bak = p->next;
printf("->(%u,%u)",p->pos >> 8, p->pos & 0x00FF);
free(p);
p = bak;
}
printf("\n");
}
else
printf("No path to get out of the maze\n");
pHead = p = bak = (path_t *)0;
/* 列印迷宮 */
for(i = 0; i < MAZE_HEIGHT; ++i) {
for(j = 0; j < MAZE_WIDTH; ++j)
printf("%c",symbolic[maze[i][j]]);
printf("\n");
}
getch();
return 0;
}
C語言嵌入式系統編程修煉之記憶體操作
C語言嵌入式系統編程修煉之記憶體操作
作者:佚名
數據指針
在嵌入式系統的編程中,常常要求在特定的記憶體單元讀寫內容,彙編有對應的MOV指令,而除C/C++以外的其他編程語言基本沒有直接訪問絕對位址的能力。在嵌入式系統的實際調試中,多借助C語言指針所具有的對絕對位址單元內容的讀寫能力。以指針直接操作記憶體多發生在如下幾種情況:
(1) 某I/O晶片被定位在CPU的存儲空間而非I/O空間,而且寄存器對應於某特定位址;
(2) 兩個CPU之間以雙埠RAM通信,CPU需要在雙埠RAM的特定單元(稱為mail box)書寫內容以在對方CPU產生中斷;
(3) 讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。
譬如:
以上程式的意義為在絕對位址0xF0000+0xFF00(80186使用16位段位址和16位偏移位址)寫入11。
在使用絕對位址指針時,要注意指針自增自減操作的結果取決於指針指向的數據類別。上例中p++後的結果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的結果等同於:p = p+sizeof(int),而p-(或-p)的結果是p = p-sizeof(int)。
同理,若執行:
long int *p = (long int *)0xF000FF00;
則p++(或++p)的結果等同於:p = p+sizeof(long int) ,而p-(或-p)的結果是p = p-sizeof(long int)。
記住:CPU以位元組為單位編址,而C語言指針以指向的資料類型長度作自增和自減。理解這一點對於以指針直接操作記憶體是相當重要的。
函數指針
首先要理解以下三個問題:
(1)C語言中函數名直接對應於函數生成的指令代碼在記憶體中的位址,因此函數名可以直接賦給指向函數的指針;
(2)調用函數實際上等同於"調轉指令+參數傳遞處理+回歸位置入棧",本質上最核心的操作是將函數生成的目標代碼的首位址賦給CPU的PC寄存器;
(3)因為函數調用的本質是跳轉到某一個位址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:
請拿出你可以獲得的任何一本大學《微型電腦原理》教材,書中講到,186 CPU啟動後跳轉至絕對位址0xFFFF0(對應C語言指針是0xF000FFF0,0xF000為段位址,0xFFF0為段內偏移)執行,請看下面的代碼:
在以上的程式中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟重啟"的作用,跳轉到CPU啟動後第一條要執行的指令的位置。
記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個位址開始執行指令!
陣列vs.動態申請
在嵌入式系統中動態記憶體申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的記憶體空間往往是十分有限的,不經意的記憶體洩露會很快導致系統的崩潰。
所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程式:
上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即"誰申請,就由誰釋放"原則。不滿足這個原則,會導致代碼的耦合度增大,因為用戶在調用function函數時需要知道其內部細節!
正確的做法是在調用處申請記憶體,並傳入function函數,如下:
而函數function則接收參數p,如下:
基本上,動態申請記憶體方式可以用較大的陣列替換。對於編程新手,筆者推薦你儘量採用陣列!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻範政治錯誤走反革命道路的楊康。
給出原則:
(1)盡可能的選用陣列,陣列不能越界訪問(真理越過一步就是謬誤,陣列越過界限就光榮地成全了一個混亂的嵌入式系統);
(2)如果使用動態申請,則申請後一定要判斷是否申請成功了,並且malloc和free應成對出現!
關鍵字const
const意味著"唯讀"。區別如下代碼的功能非常重要,也是老生長歎,如果你還不知道它們的區別,而且已經在程式界摸爬滾打多年,那只能說這是一個悲哀:
(1) 關鍵字const的作用是為給讀你代碼的人傳達非常有用的資訊。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬於"輸入參數"。在有多個形參的時候,函數的調用者可以憑藉參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。
(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。
const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:"只能讀的普通變數",可以稱其為"不能改變的變數"(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define巨集定義!故在C語言中如下程式是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:編譯階段不能用到變數 */
關鍵字volatile
C語言編譯器會對用戶書寫的代碼進行優化,譬如如下代碼:
很可能被編譯器優化為:
但是這樣的優化結果可能導致錯誤,如果I/O空間0x100埠的內容在執行第一次讀操作後被其他程式寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變數a的定義前加上volatile關鍵字可以防止編譯器的類似優化,正確的做法是:
volatile int a;
volatile變數可能用於如下幾種情況:
(1) 並行設備的硬體寄存器(如:狀態寄存器,例中的代碼屬於此類);
(2) 一個中斷服務副程式中會訪問到的非自動變數(也就是總體變數);
(3) 多線程應用中被幾個任務共用的變數。
CPU字長與記憶體位寬不一致處理
在背景篇中提到,本文特意選擇了一個與CPU字長不一致的存儲晶片,就是為了進行本節的討論,解決CPU字長與記憶體位元寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫位元組、字的介面,如下:
子貢問曰:Why偏移要乘以2?
子曰:請看圖1,16位80186與8位NVRAM之間互連只能以位址線A1對其A0,CPU本身的A0與NVRAM不連接。因此,NVRAM的位址只能是偶數位址,故每次以0x10為單位前進!
C語言嵌入式系統編程修煉之記憶體操作(2) src="/Article/UploadFDL04/200602/20060204204039446.jpg" border=0>
圖1 CPU與NVRAM位址線連接
子貢再問:So why 80186的位址線A0不與NVRAM的A0連接?
子曰:請看《IT論語》之《微機原理篇》,那裏面講述了關於計算機組成的聖人之道。
總結
本篇主要講述了嵌入式系統C編程中記憶體操作的相關技巧。掌握並深入理解關於資料指針、函數指針、動態申請記憶體、const及volatile關鍵字等的相關知識,是一個優秀的C語言程式設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在記憶體操作中體現。
我們之所以在嵌入式系統中使用C語言進行程式設計,99%是因為其強大的記憶體操作能力!
如果你愛編程,請你愛C語言;
如果你愛C語言,請你愛指針;
如果你愛指針,請你愛指針的指針!
作者:佚名
數據指針
在嵌入式系統的編程中,常常要求在特定的記憶體單元讀寫內容,彙編有對應的MOV指令,而除C/C++以外的其他編程語言基本沒有直接訪問絕對位址的能力。在嵌入式系統的實際調試中,多借助C語言指針所具有的對絕對位址單元內容的讀寫能力。以指針直接操作記憶體多發生在如下幾種情況:
(1) 某I/O晶片被定位在CPU的存儲空間而非I/O空間,而且寄存器對應於某特定位址;
(2) 兩個CPU之間以雙埠RAM通信,CPU需要在雙埠RAM的特定單元(稱為mail box)書寫內容以在對方CPU產生中斷;
(3) 讀取在ROM或FLASH的特定單元所燒錄的漢字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;
以上程式的意義為在絕對位址0xF0000+0xFF00(80186使用16位段位址和16位偏移位址)寫入11。
在使用絕對位址指針時,要注意指針自增自減操作的結果取決於指針指向的數據類別。上例中p++後的結果是p= 0xF000FF01,若p指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的結果等同於:p = p+sizeof(int),而p-(或-p)的結果是p = p-sizeof(int)。
同理,若執行:
long int *p = (long int *)0xF000FF00;
則p++(或++p)的結果等同於:p = p+sizeof(long int) ,而p-(或-p)的結果是p = p-sizeof(long int)。
記住:CPU以位元組為單位編址,而C語言指針以指向的資料類型長度作自增和自減。理解這一點對於以指針直接操作記憶體是相當重要的。
函數指針
首先要理解以下三個問題:
(1)C語言中函數名直接對應於函數生成的指令代碼在記憶體中的位址,因此函數名可以直接賦給指向函數的指針;
(2)調用函數實際上等同於"調轉指令+參數傳遞處理+回歸位置入棧",本質上最核心的操作是將函數生成的目標代碼的首位址賦給CPU的PC寄存器;
(3)因為函數調用的本質是跳轉到某一個位址單元的code去執行,所以可以"調用"一個根本就不存在的函數實體,暈?請往下看:
請拿出你可以獲得的任何一本大學《微型電腦原理》教材,書中講到,186 CPU啟動後跳轉至絕對位址0xFFFF0(對應C語言指針是0xF000FFF0,0xF000為段位址,0xFFF0為段內偏移)執行,請看下面的代碼:
typedef void (*lpFunction) ( ); /* 定義一個無參數、無返回類型的 */
/* 函數指針類型 */
lpFunction lpReset = (lpFunction)0xF000FFF0; /* 定義一個函數指針,指向*/
/* CPU啟動後所執行第一條指令的位置 */
lpReset(); /* 調用函數 */
在以上的程式中,我們根本沒有看到任何一個函數實體,但是我們卻執行了這樣的函數調用:lpReset(),它實際上起到了"軟重啟"的作用,跳轉到CPU啟動後第一條要執行的指令的位置。
記住:函數無它,唯指令集合耳;你可以調用一個沒有函數體的函數,本質上只是換一個位址開始執行指令!
陣列vs.動態申請
在嵌入式系統中動態記憶體申請存在比一般系統編程時更嚴格的要求,這是因為嵌入式系統的記憶體空間往往是十分有限的,不經意的記憶體洩露會很快導致系統的崩潰。
所以一定要保證你的malloc和free成對出現,如果你寫出這樣的一段程式:
char * function(void)
{
char *p;
p = (char *)malloc(…);
if(p==NULL)
…;
… /* 一系列針對p的操作 */
return p;
}
在某處調用function(),用完function中動態申請的記憶體後將其free,如下:
char *q = function();
…
free(q);
上述代碼明顯是不合理的,因為違反了malloc和free成對出現的原則,即"誰申請,就由誰釋放"原則。不滿足這個原則,會導致代碼的耦合度增大,因為用戶在調用function函數時需要知道其內部細節!
正確的做法是在調用處申請記憶體,並傳入function函數,如下:
char *p=malloc(…);
if(p==NULL)
…;
function(p);
…
free(p);
p=NULL;
而函數function則接收參數p,如下:
void function(char *p)
{
… /* 一系列針對p的操作 */
}
基本上,動態申請記憶體方式可以用較大的陣列替換。對於編程新手,筆者推薦你儘量採用陣列!嵌入式系統可以以博大的胸襟接收瑕疵,而無法"海納"錯誤。畢竟,以最笨的方式苦練神功的郭靖勝過機智聰明卻範政治錯誤走反革命道路的楊康。
給出原則:
(1)盡可能的選用陣列,陣列不能越界訪問(真理越過一步就是謬誤,陣列越過界限就光榮地成全了一個混亂的嵌入式系統);
(2)如果使用動態申請,則申請後一定要判斷是否申請成功了,並且malloc和free應成對出現!
關鍵字const
const意味著"唯讀"。區別如下代碼的功能非常重要,也是老生長歎,如果你還不知道它們的區別,而且已經在程式界摸爬滾打多年,那只能說這是一個悲哀:
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
(1) 關鍵字const的作用是為給讀你代碼的人傳達非常有用的資訊。例如,在函數的形參前添加const關鍵字意味著這個參數在函數體內不會被修改,屬於"輸入參數"。在有多個形參的時候,函數的調用者可以憑藉參數前是否有const關鍵字,清晰的辨別哪些是輸入參數,哪些是可能的輸出參數。
(2)合理地使用關鍵字const可以使編譯器很自然地保護那些不希望被改變的參數,防止其被無意的代碼修改,這樣可以減少bug的出現。
const在C++語言中則包含了更豐富的含義,而在C語言中僅意味著:"只能讀的普通變數",可以稱其為"不能改變的變數"(這個說法似乎很拗口,但卻最準確的表達了C語言中const的本質),在編譯階段需要的常數仍然只能以#define巨集定義!故在C語言中如下程式是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:編譯階段不能用到變數 */
關鍵字volatile
C語言編譯器會對用戶書寫的代碼進行優化,譬如如下代碼:
int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100埠的內容存入a變數*/
b = a;
a = inWord (0x100); /*再次讀取I/O空間0x100埠的內容存入a變數*/
c = a;
很可能被編譯器優化為:
int a,b,c;
a = inWord(0x100); /*讀取I/O空間0x100埠的內容存入a變數*/
b = a;
c = a;
但是這樣的優化結果可能導致錯誤,如果I/O空間0x100埠的內容在執行第一次讀操作後被其他程式寫入新值,則其實第2次讀操作讀出的內容與第一次不同,b和c的值應該不同。在變數a的定義前加上volatile關鍵字可以防止編譯器的類似優化,正確的做法是:
volatile int a;
volatile變數可能用於如下幾種情況:
(1) 並行設備的硬體寄存器(如:狀態寄存器,例中的代碼屬於此類);
(2) 一個中斷服務副程式中會訪問到的非自動變數(也就是總體變數);
(3) 多線程應用中被幾個任務共用的變數。
CPU字長與記憶體位寬不一致處理
在背景篇中提到,本文特意選擇了一個與CPU字長不一致的存儲晶片,就是為了進行本節的討論,解決CPU字長與記憶體位元寬不一致的情況。80186的字長為16,而NVRAM的位寬為8,在這種情況下,我們需要為NVRAM提供讀寫位元組、字的介面,如下:
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函數功能:讀NVRAM中位元組
* 參數:wOffset,讀取位置相對NVRAM基底位址的偏移
* 返回:讀取到的位元組值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼偏移要×2? */
return *lpAddr;
}
/* 函數功能:讀NVRAM中字
* 參數:wOffset,讀取位置相對NVRAM基底位址的偏移
* 返回:讀取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
WORD wTmp = 0;
LPBYTE lpAddr;
/* 讀取高位位元組 */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 為什麼偏移要×2? */
wTmp += (*lpAddr)*256;
/* 讀取低位元位元組 */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 為什麼偏移要×2? */
wTmp += *lpAddr;
return wTmp;
}
/* 函數功能:向NVRAM中寫一個位元組
*參數:wOffset,寫入位置相對NVRAM基底位址的偏移
* byData,欲寫入的位元組
*/
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{
…
}
/* 函數功能:向NVRAM中寫一個字 */
*參數:wOffset,寫入位置相對NVRAM基底位址的偏移
* wData,欲寫入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{
…
}
子貢問曰:Why偏移要乘以2?
子曰:請看圖1,16位80186與8位NVRAM之間互連只能以位址線A1對其A0,CPU本身的A0與NVRAM不連接。因此,NVRAM的位址只能是偶數位址,故每次以0x10為單位前進!
C語言嵌入式系統編程修煉之記憶體操作(2) src="/Article/UploadFDL04/200602/20060204204039446.jpg" border=0>
圖1 CPU與NVRAM位址線連接
子貢再問:So why 80186的位址線A0不與NVRAM的A0連接?
子曰:請看《IT論語》之《微機原理篇》,那裏面講述了關於計算機組成的聖人之道。
總結
本篇主要講述了嵌入式系統C編程中記憶體操作的相關技巧。掌握並深入理解關於資料指針、函數指針、動態申請記憶體、const及volatile關鍵字等的相關知識,是一個優秀的C語言程式設計師的基本要求。當我們已經牢固掌握了上述技巧後,我們就已經學會了C語言的99%,因為C語言最精華的內涵皆在記憶體操作中體現。
我們之所以在嵌入式系統中使用C語言進行程式設計,99%是因為其強大的記憶體操作能力!
如果你愛編程,請你愛C語言;
如果你愛C語言,請你愛指針;
如果你愛指針,請你愛指針的指針!
C語言嵌入式系統編程修煉之六:性能優化
C語言嵌入式系統編程修煉之六:性能優化
作者:宋寶華 更新日期:2005-07-22
使用巨集定義
在C語言中,巨集是產生內嵌代碼的唯一方法。對於嵌入式系統而言,為了能達到性能要求,巨集是一種很好的代替函數的方法。
寫一個"標準"巨集MIN ,這個巨集輸入兩個參數並返回較小的一個:
錯誤做法:
正確做法:
對於巨集,我們需要知道三點:
(1)巨集定義"像"函數;
(2)巨集定義不是函數,因而需要括上所有"參數";
(3)巨集定義可能產生副作用。
下面的代碼:
將被替換為:
發生的事情無法預料。
因而不要給巨集定義傳入有副作用的"參數"。
使用寄存器變數
當對一個變數頻繁被讀寫時,需要反復訪問記憶體,從而花費大量的存取時間。為此,C語言提供了一種變數,即寄存器變數。這種變數存放在CPU的寄存器中,使用時,不需要訪問記憶體,而直接從寄存器中讀寫,從而提高效率。寄存器變數的說明符是register。對於迴圈次數較多的迴圈控制變數及循環體內反復使用的變數均可定義為寄存器變數,而迴圈計數是應用寄存器變數的最好候選者。
(1) 只有局部自動變數和形參才可以定義為寄存器變數。因為寄存器變數屬於動態存儲方式,凡需要採用靜態存儲方式的量都不能定義為寄存器變數,包括:模組間總體變數、模組內總體變數、局部static變數;
(2) register是一個"建議"型關鍵字,意指程式建議該變數放在寄存器中,但最終該變數可能因為條件不滿足並未成為寄存器變數,而是被放在了記憶體中,但編譯器中並不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。
下面是一個採用寄存器變數的例子:
本程式迴圈n次,i和s都被頻繁使用,因此可定義為寄存器變數。
內嵌彙編
程式中對時間要求苛刻的部分可以用內嵌彙編來重寫,以帶來速度上的顯著提高。但是,開發和測試彙編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用彙編的部分。
在程式中,存在一個80-20原則,即20%的程式消耗了80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。
嵌入式C程式中主要使用線上彙編,即在C程式中直接插入_asm{ }內嵌彙編語句:
利用硬體特性
首先要明白CPU對各種記憶體的訪問速度,基本上是:
CPU內部RAM > 外部同步RAM > 外部非同步RAM > FLASH/ROM
對於程式碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啟動後將FLASH或ROM中的目標代碼拷貝入RAM中後再執行以提高取指令速度;
對於UART等設備,其內部有一定容量的接收BUFFER,我們應儘量在BUFFER被占滿後再向CPU提出中斷。例如電腦終端在向目的機通過RS-232傳遞資料時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;
如果對某設備能採取DMA方式讀取,就採用DMA讀取,DMA讀取方式在讀取目標中包含的存儲資訊較大時效率較高,其資料傳輸的基本單位是塊,而所傳輸的資料是從設備直接送入記憶體的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了CPU與外設的平行作業程度。
活用位操作
使用C語言的位元操作可以減少除法和取模的運算。在電腦程式中資料的位元是可以操作的最小資料單位,理論上可以用"位運算"來完成所有的運算和操作,因而,靈活的位操作可以有效地提高程式運行的效率。舉例如下:
對於以2的指數次方為"*"、"/"或"%"因數的數學運算,轉化為移位運算"<< >>"通常可以提高演算法效率。因為乘除運算指令週期通常比移位元運算大。
C語言位元運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關係。我們通常要對硬體寄存器進行位元設置,譬如,我們通過將AM186ER型80186處理器的中斷遮罩控制寄存器的第低6位設置為0(開中斷2),最通用的做法是:
而將該位設置為1的做法是:
判斷該位是否為1的做法是:
上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握。
總結
在性能優化方面永遠注意80-20準備,不要優化程式中開銷不大的那80%,這是勞而無功的。
巨集定義是C語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但巨集在本質上不是函數,因而要防止巨集展開後出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準C至今沒有包括C++中inline函數的功能,inline函數兼具無調用開銷和安全的優點。
使用寄存器變數、內嵌彙編和活用位元操作也是提高程式效率的有效方法。
除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬體設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用DMA傳輸方式等。
作者:宋寶華 更新日期:2005-07-22
使用巨集定義
在C語言中,巨集是產生內嵌代碼的唯一方法。對於嵌入式系統而言,為了能達到性能要求,巨集是一種很好的代替函數的方法。
寫一個"標準"巨集MIN ,這個巨集輸入兩個參數並返回較小的一個:
錯誤做法:
#define MIN(A,B) ( A <= B ? A : B )
正確做法:
#define MIN(A,B) ((A)<= (B) ? (A) : (B) )
對於巨集,我們需要知道三點:
(1)巨集定義"像"函數;
(2)巨集定義不是函數,因而需要括上所有"參數";
(3)巨集定義可能產生副作用。
下面的代碼:
least = MIN(*p++, b);
將被替換為:
( (*p++) <= (b) ?(*p++):(b) )
發生的事情無法預料。
因而不要給巨集定義傳入有副作用的"參數"。
使用寄存器變數
當對一個變數頻繁被讀寫時,需要反復訪問記憶體,從而花費大量的存取時間。為此,C語言提供了一種變數,即寄存器變數。這種變數存放在CPU的寄存器中,使用時,不需要訪問記憶體,而直接從寄存器中讀寫,從而提高效率。寄存器變數的說明符是register。對於迴圈次數較多的迴圈控制變數及循環體內反復使用的變數均可定義為寄存器變數,而迴圈計數是應用寄存器變數的最好候選者。
(1) 只有局部自動變數和形參才可以定義為寄存器變數。因為寄存器變數屬於動態存儲方式,凡需要採用靜態存儲方式的量都不能定義為寄存器變數,包括:模組間總體變數、模組內總體變數、局部static變數;
(2) register是一個"建議"型關鍵字,意指程式建議該變數放在寄存器中,但最終該變數可能因為條件不滿足並未成為寄存器變數,而是被放在了記憶體中,但編譯器中並不報錯(在C++語言中有另一個"建議"型關鍵字:inline)。
下面是一個採用寄存器變數的例子:
/* 求1+2+3+….+n的值 */
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i<=n;i++)
{
s=s+i;
}
return s;
}
本程式迴圈n次,i和s都被頻繁使用,因此可定義為寄存器變數。
內嵌彙編
程式中對時間要求苛刻的部分可以用內嵌彙編來重寫,以帶來速度上的顯著提高。但是,開發和測試彙編代碼是一件辛苦的工作,它將花費更長的時間,因而要慎重選擇要用彙編的部分。
在程式中,存在一個80-20原則,即20%的程式消耗了80%的運行時間,因而我們要改進效率,最主要是考慮改進那20%的代碼。
嵌入式C程式中主要使用線上彙編,即在C程式中直接插入_asm{ }內嵌彙編語句:
/* 把兩個輸入參數的值相加,結果存放到另外一個總體變數中 */
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}
利用硬體特性
首先要明白CPU對各種記憶體的訪問速度,基本上是:
CPU內部RAM > 外部同步RAM > 外部非同步RAM > FLASH/ROM
對於程式碼,已經被燒錄在FLASH或ROM中,我們可以讓CPU直接從其中讀取代碼執行,但通常這不是一個好辦法,我們最好在系統啟動後將FLASH或ROM中的目標代碼拷貝入RAM中後再執行以提高取指令速度;
對於UART等設備,其內部有一定容量的接收BUFFER,我們應儘量在BUFFER被占滿後再向CPU提出中斷。例如電腦終端在向目的機通過RS-232傳遞資料時,不宜設置UART只接收到一個BYTE就向CPU提中斷,從而無謂浪費中斷處理時間;
如果對某設備能採取DMA方式讀取,就採用DMA讀取,DMA讀取方式在讀取目標中包含的存儲資訊較大時效率較高,其資料傳輸的基本單位是塊,而所傳輸的資料是從設備直接送入記憶體的(或者相反)。DMA方式較之中斷驅動方式,減少了CPU 對外設的干預,進一步提高了CPU與外設的平行作業程度。
活用位操作
使用C語言的位元操作可以減少除法和取模的運算。在電腦程式中資料的位元是可以操作的最小資料單位,理論上可以用"位運算"來完成所有的運算和操作,因而,靈活的位操作可以有效地提高程式運行的效率。舉例如下:
/* 方法1 */
int i,j;
i = 879 / 16;
j = 562 % 32;
/* 方法2 */
int i,j;
i = 879 >> 4;
j = 562 - (562 >> 5 << 5);
對於以2的指數次方為"*"、"/"或"%"因數的數學運算,轉化為移位運算"<< >>"通常可以提高演算法效率。因為乘除運算指令週期通常比移位元運算大。
C語言位元運算除了可以提高運算效率外,在嵌入式系統的編程中,它的另一個最典型的應用,而且十分廣泛地正在被使用著的是位間的與(&)、或(|)、非(~)操作,這跟嵌入式系統的編程特點有很大關係。我們通常要對硬體寄存器進行位元設置,譬如,我們通過將AM186ER型80186處理器的中斷遮罩控制寄存器的第低6位設置為0(開中斷2),最通用的做法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);
而將該位設置為1的做法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);
判斷該位是否為1的做法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp & INT_I2_MASK)
{
… /* 該位為1 */
}
上述方法在嵌入式系統的編程中是非常常見的,我們需要牢固掌握。
總結
在性能優化方面永遠注意80-20準備,不要優化程式中開銷不大的那80%,這是勞而無功的。
巨集定義是C語言中實現類似函數功能而又不具函數調用和返回開銷的較好方法,但巨集在本質上不是函數,因而要防止巨集展開後出現不可預料的結果,對宏的定義和使用要慎而處之。很遺憾,標準C至今沒有包括C++中inline函數的功能,inline函數兼具無調用開銷和安全的優點。
使用寄存器變數、內嵌彙編和活用位元操作也是提高程式效率的有效方法。
除了編程上的技巧外,為提高系統的運行效率,我們通常也需要最大可能地利用各種硬體設備自身的特點來減小其運轉開銷,例如減小中斷次數、利用DMA傳輸方式等。
Embedded C Language Porblems
embedded c language problems
英文原文見:http://www.embedded.com/2000/0005/0005feat2.htm
英文標題:A 'C' Test: The 0x10 Best Questions for Would-be Embedded Programmers
作者:Nigel Jones 譯者:RTOSFUN E-Mail:rtosfun@gmail.com
各位,拿出鉛筆。這個測試將分辯出你是否有潛力成為一個嵌入式程式師,或者你是不是一個有潛力的嵌入式程式師。
C語言測試是招聘嵌入式系統程式師過程中必須而且有效的方法。這些年,我既參加也組織了許多這種測試,在這過程中我意識到這些測試能為帶面試者和被面試者提供許多有用資訊,此外,撇開面試的壓力不談,這種測試也是相當有趣的。從被面試者的角度來講,你能瞭解許多關於出題者或監考者的情況。這個測試只是出題者為顯示其對ANSI標準細節的知識而不是技術秘決而設計嗎?這個愚蠢的問題嗎?如要你答出某個字元的ASCII值。這些問題著重考察你的系統調用和記憶體分配策略方面的能力嗎?這標誌著出題者也許花時間在微機上而不上在嵌入式系統上。如果上述任何問題的答案是“是”的話,那麼我知道我得認真考慮我是否應該去做這份工作。從面試者的角度來講,一個測試也許能從多方面揭示應試者的素質:最基本的,你能瞭解應試者C語言的水準。不管怎麼樣,看一下這人如何回答他不會的問題也是滿有趣。應試者是以好的直覺做出明智的選擇,還是只是瞎蒙呢?當應試者在某個問題上卡住時是找藉口呢,還是表現出對問題的真正的好奇心,把這看成學習的機會呢?我發現這些資訊與他們的測試成績一樣有用。有了這些想法,我決定出一些真正針對嵌入式系統的考題,希望這些令人頭痛的考題能給正在找工作的人一點幫住。這些問題都是我這些年實際碰到的。其中有些題很難,但它們應該都能給你一點啟迪。這個測試適於不同水準的應試者,大多數初級水準的應試者的成績會很差,經驗豐富的程式師應該有很好的成績。為了讓你能自己決定某些問題的偏好,每個問題沒有分配分數,如果選擇這些考題為你所用,請自行按你的意思分配分數。
[B]預處理器(Preprocessor)[/b]
1. 用預處理指令#define 聲明一個常數,用以表明1年中有多少秒(忽略閏年問題)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在這想看到幾件事情:
1). #define 語法的基本知識(例如:不能以分號結束,括弧的使用,等等)
2). 懂得預處理器將為你計算常數運算式的值,因此,直接寫出你是如何計算一年中有多少秒而不是計算出實際的值,是更清晰而沒有代價的。
3). 意識到這個運算式將使一個16位機的整型數溢出-因此要用到長整型符號L,告訴編譯器這個常數是的長整型數。
4). 如果你在你的運算式中用到UL(表示無符號長整型),那麼你有了一個好的起點。記住,第一印象很重要。
2. 寫一個“標準”巨集MIN,這個巨集輸入兩個參數並返回較小的一個。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
這個測試是為下面的目的而設的:
1). 標識#define在巨集中應用的基本知識。這是很重要的,因為直到嵌入(inline)操作符變為標準C的一部分,宏是方便產生嵌入代碼的唯一方法,對於嵌入式系統來說,為了能達到要求的性能,嵌入代碼經常是必須的方法。
2). 三重條件操作符的知識。這個操作符存在C語言中的原因是它使得編譯器能產生比if-then-else更優化的代碼,瞭解這個用法是很重要的。
3). 懂得在宏中小心地把參數用括弧括起來
4). 我也用這個問題開始討論宏的副作用,例如:當你寫下面的代碼時會發生什麼事?
least = MIN(*p++, b);
3. 預處理器標識#error的目的是什麼?
編譯程序時,只要遇到 #error 就會跳出一個編譯錯誤,既然是編譯錯誤,要它幹嘛呢?其目的就是保證程式是按照你所設想的那樣進行編譯的。下面舉個例子:程式中往往有很多的預處理指令#ifdef XXX...#else#endif當程式比較大時,往往有些巨集定義是在外部指定的(如makefile),或是在系統頭檔中指定的,當你不太確定當前是否定義了 XXX 時,就可以改成如下這樣進行編譯:#ifdef XXX...#error "XXX has been defined"#else#endif這樣,如果編譯時出現錯誤,輸出了XXX has been defined,表明宏XXX已經被定義了。用處就是這樣,是不是感覺很沒有用處?
閉環(Infinite loops)
4. 嵌入式系統中經常要用到無限迴圈,你怎麼樣用C編寫閉環呢?
這個問題用幾個解決方案。我首選的方案是:
while(1)
{
?}
一些程式師更喜歡如下方案:
for(;;)
{
?}
這個實現方式讓我為難,因為這個語法沒有確切表達到底怎麼回事。如果一個應試者給出這個作為方案,我將用這個作為一個機會去探究他們這樣做的
基本原理。如果他們的基本答案是:“我被教著這樣做,但從沒有想到過為什麼。”這會給我留下一個壞印象。
第三個方案是用 goto
Loop:
...
goto Loop;
應試者如給出上面的方案,這說明或者他是一個組合語言程式師(這也許是好事)或者他是一個想進入新領域的BASIC/FORTRAN程式師。
數據聲明(Data declarations)
5. 用變數a給出下面的定義
a) 一個整型數(An integer)
b) 一個指向整型數的指標(A pointer to an integer)
c) 一個指向指標的的指標,它指向的指標是指向一個整型數(A pointer to a pointer to an integer)
d) 一個有10個整型數的陣列(An array of 10 integers)
e) 一個有10個指標的陣列,該指標是指向一個整型數的(An array of 10 pointers to integers)
f) 一個指向有10個整型數陣列的指標(A pointer to an array of 10 integers)
g) 一個指向函數的指標,該函數有一個整型參數並返回一個整型數(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一個有10個指標的陣列,該指標指向一個函數,該函數有一個整型參數並返回一個整型數( An array of ten pointers to functions that take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人們經常聲稱這裏有幾個問題是那種要翻一下書才能回答的問題,我同意這種說法。當我寫這篇文章時,為了確定語法的正確性,我的確查了一下書。
但是當我被面試的時候,我期望被問到這個問題(或者相近的問題)。因為在被面試的這段時間裏,我確定我知道這個問題的答案。應試者如果不知道
所有的答案(或至少大部分答案),那麼也就沒有為這次面試做準備,如果該面試者沒有為這次面試做準備,那麼他又能為什麼出準備呢?
Static
6. 關鍵字static的作用是什麼?
這個簡單的問題很少有人能回答完全。在C語言中,關鍵字static有三個明顯的作用:
1). 在函數體,一個被聲明為靜態的變數在這一函數被調用過程中維持其值不變。
2). 在模組內(但在函數體外),一個被聲明為靜態的變數可以被模組內所用函數訪問,但不能被模組外其他函數訪問。它是一個本地的總體變數。
3). 在模組內,一個被聲明為靜態的函數只可被這一模組內的其他函數調用。那就是,這個函數被限制在聲明它的模組的本地範圍內使用。
大多數應試者能正確回答第一部分,一部分能正確回答第二部分,同是很少的人能懂得第三部分。這是一個應試者的嚴重的缺點,因為他顯然不懂得本地化資料和代碼範圍的好處和重要性。
Const
7.關鍵字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的出現。
Volatile
8. 關鍵字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;
}
位操作(Bit manipulation)
9. 嵌入式系統總是要用戶對變數或寄存器進行位元操作。給定一個整型變數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;
}
一些人喜歡為設置和清除值而定義一個遮罩同時定義一些說明常數,這也是可以接受的。我希望看到幾個要點:說明常數、|=和&=~操作。
訪問固定的記憶體位置(Accessing fixed memory locations)
10. 嵌入式系統經常具有要求程式師去訪問某特定的記憶體位置的特點。在某工程中,要求設置一絕對位址為0x67a9的整型變數的值為0xaa66。編譯器是一個純粹的ANSI編譯器。寫代碼去完成這一任務。
這一問題測試你是否知道為了訪問一絕對位址把一個整型數強制轉換(typecast)為一指針是合法的。這一問題的實現方式隨著個人風格不同而不同。典型的類似代碼如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
一個較晦澀的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二種方案,但我建議你在面試時使用第一種方案。
中斷(Interrupts)
11. 中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴展—讓標準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中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。
4). 與第三點一脈相承,printf()經常有重入和性能上的問題。如果你丟掉了第三和第四點,我不會太為難你的。不用說,如果你能得到後兩點,那麼你的被雇用前景越來越光明了。
代碼例子(Code examples)
12 . 下面的代碼輸出是什麼,為什麼?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
這個問題測試你是否懂得C語言中的整數自動轉換原則,我發現有些開發者懂得極少這些東西。不管如何,這無符號整型問題的答案是輸出是“>6”。原因是當運算式中存在有符號類型和無符號類型時所有的運算元都自動轉換為無符號類型。 因此-20變成了一個非常大的正整數,所以該運算式計算出的結果大於6。這一點對於應當頻繁用到無符號資料類型的嵌入式系統來說是豐常重要的。如果你答錯了這個問題,你也就到了得不到這份工作的邊緣。
13. 評價下面的代碼片斷:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
對於一個int型不是16位的處理器為說,上面的代碼是不正確的。應編寫如下:
unsigned int compzero = ~0;
這一問題真正能揭露出應試者是否懂得處理器字長的重要性。在我的經驗裏,好的嵌入式程式師非常準確地明白硬體的細節和它的局限,然而PC機程式往往把硬體作為一個無法避免的煩惱。
到了這個階段,應試者或者完全垂頭喪氣了或者信心滿滿志在必得。如果顯然應試者不是很好,那麼這個測試就在這裏結束了。但如果顯然應試者做得不錯,那麼我就扔出下面的追加問題,這些問題是比較難的,我想僅僅非常優秀的應試者能做得不錯。提出這些問題,我希望更多看到應試者應付問題的方法,而不是答案。不管如何,你就當是這個娛樂吧…
動態記憶體分配(Dynamic memory allocation)
14. 儘管不像非嵌入式電腦那麼常見,嵌入式系統還是有從堆(heap)中動態分配記憶體的過程的。那麼嵌入式系統中,動態分配記憶體可能發生的問題是什麼?
這裏,我期望應試者能提到記憶體碎片,碎片收集的問題,變數的持行時間等等。這個主題已經在ESP雜誌中被廣泛地討論過了(主要是 P.J. Plauger, 他的解釋遠遠超過我這裏能提到的任何解釋),所有回過頭看一下這些雜誌吧!讓應試者進入一種虛假的安全感覺後,我拿出這麼一個小節目:下面的代碼片段的輸出是什麼,為什麼?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Got a null pointer");
else
puts("Got a valid pointer");
這是一個有趣的問題。最近在我的一個同事不經意把0值傳給了函數malloc,得到了一個合法的指標之後,我才想到這個問題。這就是上面的代碼,該代碼的輸出是“Got a valid pointer”。我用這個來開始討論這樣的一問題,看看被面試者是否想到庫常式這樣做是正確。得到正確的答案固然重要,但解決問題的方法和你做決定的基本原理更重要些。
Typedef
15. Typedef 在C語言中頻繁用以聲明一個已經存在的資料類型的同義字。也可以用預處理器做類似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上兩種情況的意圖都是要定義dPS 和 tPS 作為一個指向結構s指標。哪種方法更好呢?(如果有的話)為什麼?
這是一個非常微妙的問題,任何人答對這個問題(正當的原因)是應當被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一個擴展為
struct s * p1, p2;
上面的代碼定義p1為一個指向結構的指,p2為一個實際的結構,這也許不是你想要的。第二個例子正確地定義了p3 和p4 兩個指針。
晦澀的語法
16. C語言同意一些令人震驚的結構,下面的結構是合法的嗎,如果是它做些什麼?
int a = 5, b = 7, c;
c = a+++b;
這個問題將做為這個測驗的一個愉快的結尾。不管你相不相信,上面的例子是完全合乎語法的。問題是編譯器如何處理它?水準不高的編譯作者實際上會爭論這個問題,根據最處理原則,編譯器應當能處理盡可能所有合法的用法。因此,上面的代碼被處理成:
c = a++ + b;
因此, 這段代碼持行後a = 6, b = 7, c = 12。
如果你知道答案,或猜出正確答案,做得好。如果你不知道答案,我也不把這個當作問題。我發現這個問題的最大好處是:這是一個關於代碼編寫風格,代碼的可讀性,代碼的可修改性的好的話題。
好了,夥計們,你現在已經做完所有的測試了。這就是我出的C語言測試題,我懷著愉快的心情寫完它,希望你以同樣的心情讀完它。如果是認為這是一個好的測試,那麼儘量都用到你的找工作的過程中去吧。天知道也許過個一兩年,我就不做現在的工作,也需要找一個。
Nigel Jones 是一個顧問,現在住在Maryland,當他不在水下時,你能在多個範圍的嵌入項目中找到他。他很高興能收到讀者的來信,他的email地址是: NAJones@compuserve.com。
References
• Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
• Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.
英文原文見:http://www.embedded.com/2000/0005/0005feat2.htm
英文標題:A 'C' Test: The 0x10 Best Questions for Would-be Embedded Programmers
作者:Nigel Jones 譯者:RTOSFUN E-Mail:rtosfun@gmail.com
各位,拿出鉛筆。這個測試將分辯出你是否有潛力成為一個嵌入式程式師,或者你是不是一個有潛力的嵌入式程式師。
C語言測試是招聘嵌入式系統程式師過程中必須而且有效的方法。這些年,我既參加也組織了許多這種測試,在這過程中我意識到這些測試能為帶面試者和被面試者提供許多有用資訊,此外,撇開面試的壓力不談,這種測試也是相當有趣的。從被面試者的角度來講,你能瞭解許多關於出題者或監考者的情況。這個測試只是出題者為顯示其對ANSI標準細節的知識而不是技術秘決而設計嗎?這個愚蠢的問題嗎?如要你答出某個字元的ASCII值。這些問題著重考察你的系統調用和記憶體分配策略方面的能力嗎?這標誌著出題者也許花時間在微機上而不上在嵌入式系統上。如果上述任何問題的答案是“是”的話,那麼我知道我得認真考慮我是否應該去做這份工作。從面試者的角度來講,一個測試也許能從多方面揭示應試者的素質:最基本的,你能瞭解應試者C語言的水準。不管怎麼樣,看一下這人如何回答他不會的問題也是滿有趣。應試者是以好的直覺做出明智的選擇,還是只是瞎蒙呢?當應試者在某個問題上卡住時是找藉口呢,還是表現出對問題的真正的好奇心,把這看成學習的機會呢?我發現這些資訊與他們的測試成績一樣有用。有了這些想法,我決定出一些真正針對嵌入式系統的考題,希望這些令人頭痛的考題能給正在找工作的人一點幫住。這些問題都是我這些年實際碰到的。其中有些題很難,但它們應該都能給你一點啟迪。這個測試適於不同水準的應試者,大多數初級水準的應試者的成績會很差,經驗豐富的程式師應該有很好的成績。為了讓你能自己決定某些問題的偏好,每個問題沒有分配分數,如果選擇這些考題為你所用,請自行按你的意思分配分數。
[B]預處理器(Preprocessor)[/b]
1. 用預處理指令#define 聲明一個常數,用以表明1年中有多少秒(忽略閏年問題)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在這想看到幾件事情:
1). #define 語法的基本知識(例如:不能以分號結束,括弧的使用,等等)
2). 懂得預處理器將為你計算常數運算式的值,因此,直接寫出你是如何計算一年中有多少秒而不是計算出實際的值,是更清晰而沒有代價的。
3). 意識到這個運算式將使一個16位機的整型數溢出-因此要用到長整型符號L,告訴編譯器這個常數是的長整型數。
4). 如果你在你的運算式中用到UL(表示無符號長整型),那麼你有了一個好的起點。記住,第一印象很重要。
2. 寫一個“標準”巨集MIN,這個巨集輸入兩個參數並返回較小的一個。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
這個測試是為下面的目的而設的:
1). 標識#define在巨集中應用的基本知識。這是很重要的,因為直到嵌入(inline)操作符變為標準C的一部分,宏是方便產生嵌入代碼的唯一方法,對於嵌入式系統來說,為了能達到要求的性能,嵌入代碼經常是必須的方法。
2). 三重條件操作符的知識。這個操作符存在C語言中的原因是它使得編譯器能產生比if-then-else更優化的代碼,瞭解這個用法是很重要的。
3). 懂得在宏中小心地把參數用括弧括起來
4). 我也用這個問題開始討論宏的副作用,例如:當你寫下面的代碼時會發生什麼事?
least = MIN(*p++, b);
3. 預處理器標識#error的目的是什麼?
編譯程序時,只要遇到 #error 就會跳出一個編譯錯誤,既然是編譯錯誤,要它幹嘛呢?其目的就是保證程式是按照你所設想的那樣進行編譯的。下面舉個例子:程式中往往有很多的預處理指令#ifdef XXX...#else#endif當程式比較大時,往往有些巨集定義是在外部指定的(如makefile),或是在系統頭檔中指定的,當你不太確定當前是否定義了 XXX 時,就可以改成如下這樣進行編譯:#ifdef XXX...#error "XXX has been defined"#else#endif這樣,如果編譯時出現錯誤,輸出了XXX has been defined,表明宏XXX已經被定義了。用處就是這樣,是不是感覺很沒有用處?
閉環(Infinite loops)
4. 嵌入式系統中經常要用到無限迴圈,你怎麼樣用C編寫閉環呢?
這個問題用幾個解決方案。我首選的方案是:
while(1)
{
?}
一些程式師更喜歡如下方案:
for(;;)
{
?}
這個實現方式讓我為難,因為這個語法沒有確切表達到底怎麼回事。如果一個應試者給出這個作為方案,我將用這個作為一個機會去探究他們這樣做的
基本原理。如果他們的基本答案是:“我被教著這樣做,但從沒有想到過為什麼。”這會給我留下一個壞印象。
第三個方案是用 goto
Loop:
...
goto Loop;
應試者如給出上面的方案,這說明或者他是一個組合語言程式師(這也許是好事)或者他是一個想進入新領域的BASIC/FORTRAN程式師。
數據聲明(Data declarations)
5. 用變數a給出下面的定義
a) 一個整型數(An integer)
b) 一個指向整型數的指標(A pointer to an integer)
c) 一個指向指標的的指標,它指向的指標是指向一個整型數(A pointer to a pointer to an integer)
d) 一個有10個整型數的陣列(An array of 10 integers)
e) 一個有10個指標的陣列,該指標是指向一個整型數的(An array of 10 pointers to integers)
f) 一個指向有10個整型數陣列的指標(A pointer to an array of 10 integers)
g) 一個指向函數的指標,該函數有一個整型參數並返回一個整型數(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一個有10個指標的陣列,該指標指向一個函數,該函數有一個整型參數並返回一個整型數( An array of ten pointers to functions that take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人們經常聲稱這裏有幾個問題是那種要翻一下書才能回答的問題,我同意這種說法。當我寫這篇文章時,為了確定語法的正確性,我的確查了一下書。
但是當我被面試的時候,我期望被問到這個問題(或者相近的問題)。因為在被面試的這段時間裏,我確定我知道這個問題的答案。應試者如果不知道
所有的答案(或至少大部分答案),那麼也就沒有為這次面試做準備,如果該面試者沒有為這次面試做準備,那麼他又能為什麼出準備呢?
Static
6. 關鍵字static的作用是什麼?
這個簡單的問題很少有人能回答完全。在C語言中,關鍵字static有三個明顯的作用:
1). 在函數體,一個被聲明為靜態的變數在這一函數被調用過程中維持其值不變。
2). 在模組內(但在函數體外),一個被聲明為靜態的變數可以被模組內所用函數訪問,但不能被模組外其他函數訪問。它是一個本地的總體變數。
3). 在模組內,一個被聲明為靜態的函數只可被這一模組內的其他函數調用。那就是,這個函數被限制在聲明它的模組的本地範圍內使用。
大多數應試者能正確回答第一部分,一部分能正確回答第二部分,同是很少的人能懂得第三部分。這是一個應試者的嚴重的缺點,因為他顯然不懂得本地化資料和代碼範圍的好處和重要性。
Const
7.關鍵字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的出現。
Volatile
8. 關鍵字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;
}
位操作(Bit manipulation)
9. 嵌入式系統總是要用戶對變數或寄存器進行位元操作。給定一個整型變數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;
}
一些人喜歡為設置和清除值而定義一個遮罩同時定義一些說明常數,這也是可以接受的。我希望看到幾個要點:說明常數、|=和&=~操作。
訪問固定的記憶體位置(Accessing fixed memory locations)
10. 嵌入式系統經常具有要求程式師去訪問某特定的記憶體位置的特點。在某工程中,要求設置一絕對位址為0x67a9的整型變數的值為0xaa66。編譯器是一個純粹的ANSI編譯器。寫代碼去完成這一任務。
這一問題測試你是否知道為了訪問一絕對位址把一個整型數強制轉換(typecast)為一指針是合法的。這一問題的實現方式隨著個人風格不同而不同。典型的類似代碼如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
一個較晦澀的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二種方案,但我建議你在面試時使用第一種方案。
中斷(Interrupts)
11. 中斷是嵌入式系統中重要的組成部分,這導致了很多編譯開發商提供一種擴展—讓標準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中做浮點運算。此外,ISR應該是短而有效率的,在ISR中做浮點運算是不明智的。
4). 與第三點一脈相承,printf()經常有重入和性能上的問題。如果你丟掉了第三和第四點,我不會太為難你的。不用說,如果你能得到後兩點,那麼你的被雇用前景越來越光明了。
代碼例子(Code examples)
12 . 下面的代碼輸出是什麼,為什麼?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
這個問題測試你是否懂得C語言中的整數自動轉換原則,我發現有些開發者懂得極少這些東西。不管如何,這無符號整型問題的答案是輸出是“>6”。原因是當運算式中存在有符號類型和無符號類型時所有的運算元都自動轉換為無符號類型。 因此-20變成了一個非常大的正整數,所以該運算式計算出的結果大於6。這一點對於應當頻繁用到無符號資料類型的嵌入式系統來說是豐常重要的。如果你答錯了這個問題,你也就到了得不到這份工作的邊緣。
13. 評價下面的代碼片斷:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
對於一個int型不是16位的處理器為說,上面的代碼是不正確的。應編寫如下:
unsigned int compzero = ~0;
這一問題真正能揭露出應試者是否懂得處理器字長的重要性。在我的經驗裏,好的嵌入式程式師非常準確地明白硬體的細節和它的局限,然而PC機程式往往把硬體作為一個無法避免的煩惱。
到了這個階段,應試者或者完全垂頭喪氣了或者信心滿滿志在必得。如果顯然應試者不是很好,那麼這個測試就在這裏結束了。但如果顯然應試者做得不錯,那麼我就扔出下面的追加問題,這些問題是比較難的,我想僅僅非常優秀的應試者能做得不錯。提出這些問題,我希望更多看到應試者應付問題的方法,而不是答案。不管如何,你就當是這個娛樂吧…
動態記憶體分配(Dynamic memory allocation)
14. 儘管不像非嵌入式電腦那麼常見,嵌入式系統還是有從堆(heap)中動態分配記憶體的過程的。那麼嵌入式系統中,動態分配記憶體可能發生的問題是什麼?
這裏,我期望應試者能提到記憶體碎片,碎片收集的問題,變數的持行時間等等。這個主題已經在ESP雜誌中被廣泛地討論過了(主要是 P.J. Plauger, 他的解釋遠遠超過我這裏能提到的任何解釋),所有回過頭看一下這些雜誌吧!讓應試者進入一種虛假的安全感覺後,我拿出這麼一個小節目:下面的代碼片段的輸出是什麼,為什麼?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Got a null pointer");
else
puts("Got a valid pointer");
這是一個有趣的問題。最近在我的一個同事不經意把0值傳給了函數malloc,得到了一個合法的指標之後,我才想到這個問題。這就是上面的代碼,該代碼的輸出是“Got a valid pointer”。我用這個來開始討論這樣的一問題,看看被面試者是否想到庫常式這樣做是正確。得到正確的答案固然重要,但解決問題的方法和你做決定的基本原理更重要些。
Typedef
15. Typedef 在C語言中頻繁用以聲明一個已經存在的資料類型的同義字。也可以用預處理器做類似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上兩種情況的意圖都是要定義dPS 和 tPS 作為一個指向結構s指標。哪種方法更好呢?(如果有的話)為什麼?
這是一個非常微妙的問題,任何人答對這個問題(正當的原因)是應當被恭喜的。答案是:typedef更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一個擴展為
struct s * p1, p2;
上面的代碼定義p1為一個指向結構的指,p2為一個實際的結構,這也許不是你想要的。第二個例子正確地定義了p3 和p4 兩個指針。
晦澀的語法
16. C語言同意一些令人震驚的結構,下面的結構是合法的嗎,如果是它做些什麼?
int a = 5, b = 7, c;
c = a+++b;
這個問題將做為這個測驗的一個愉快的結尾。不管你相不相信,上面的例子是完全合乎語法的。問題是編譯器如何處理它?水準不高的編譯作者實際上會爭論這個問題,根據最處理原則,編譯器應當能處理盡可能所有合法的用法。因此,上面的代碼被處理成:
c = a++ + b;
因此, 這段代碼持行後a = 6, b = 7, c = 12。
如果你知道答案,或猜出正確答案,做得好。如果你不知道答案,我也不把這個當作問題。我發現這個問題的最大好處是:這是一個關於代碼編寫風格,代碼的可讀性,代碼的可修改性的好的話題。
好了,夥計們,你現在已經做完所有的測試了。這就是我出的C語言測試題,我懷著愉快的心情寫完它,希望你以同樣的心情讀完它。如果是認為這是一個好的測試,那麼儘量都用到你的找工作的過程中去吧。天知道也許過個一兩年,我就不做現在的工作,也需要找一個。
Nigel Jones 是一個顧問,現在住在Maryland,當他不在水下時,你能在多個範圍的嵌入項目中找到他。他很高興能收到讀者的來信,他的email地址是: NAJones@compuserve.com。
References
• Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
• Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.
訂閱:
文章 (Atom)
-
昨天差點昏倒, 因為Chrome Browser一開啟後居然一片白畫面. 雖然還有IE可以用, 但就是習慣Chrome了啊, 然後開始Google別人如何解決, 不過看起來不少人遇到相同的問題, 但都沒什麼解決方法. 什麼掃毒啦, 重新安裝Chrome啦, 砍掉Default啦....
-
I/O mapped I/O(port-mapped I/O或Direct I/O) I/O與memory均擁有自己的記憶體空間 需要特別的指令來處理I/O 好處是完全不用考慮記憶體空間被I/O佔用,缺點需要額外的指令專門處理I/O存取。 Memory Mapped I/O I/...
-
好像不少人會找這個Sample Code, 小修改一下好了. 先前的Code有不少的Warning出現而且會Crash耶! 底下分別列出UDP Server及Client的範例程式. UDP Server (udp-server.c) 利用 socket 介面設計網路應用程...
一個小故事讓我們明白資金流通的意義
“又是炎熱小鎮慵懶的一天。太陽高掛,街道無人,每個人都債台高築,靠信用度日。這時,從外地來了一位有錢的旅客,他進了一家旅館,拿出一張1000 元鈔票放在櫃檯,說想先看看房間,挑一間合適的過夜,就在此人上樓的時候---- 店主抓了這張1000 元鈔,跑到隔壁屠戶那裡支付了他欠的肉錢...