天天看點

[轉帖]想成為嵌入式程式員應知道的16個基本問題

  這是嵌入式C程式員的基本知識。作者在Embedded Systems Programming雜志上發表了很多嵌入式系統開發方面的文章。

  C語言測試是招聘嵌入式系統程式員過程中必須而且有效的方法。這些年,我既參加也組織了許多這種測試,在這過程中我意識到這些測試能為面試者和被面試者提供許多有用資訊,此外,撇開面試的壓力不談,這種測試也是相當有趣的。

  從被面試者的角度來講,你能了解許多關于出題者或監考者的情況。這個測試隻是出題者為顯示其對ANSI标準細節的知識而不是技術技巧而設計嗎?這是個愚蠢的問題嗎?如要你答出某個字元的ASCII值。這些問題着重考察你的系統調用和記憶體配置設定政策方面的能力嗎?這标志着出題者也許花時間在微機上而不是在嵌入式系統上。如果上述任何問題的答案是"是"的話,那麼我知道我得認真考慮我是否應該去做這份工作。

從面試者的角度來講,一個測試也許能從多方面揭示應試者的素質:最基本的,你能了解應試者C語言的水準。不管怎麼樣,看一下這人如何回答他不會的問題也是滿有趣。應試者是以好的直覺做出明智的選擇,還是隻是瞎蒙呢?當應試者在某個問題上卡住時是找借口呢,還是表現出對問題的真正的好奇心,把這看成學習的機會呢?我發現這些資訊與他們的測試成績一樣有用。

  有了這些想法,我決定出一些真正針對嵌入式系統的考題,希望這些令人頭痛的考題能給正在找工作的人一點幫助。這些問題都是我這些年實際碰到的。其中有些題很難,但它們應該都能給你一點啟迪。

這個測試适于不同水準的應試者,大多數初級水準的應試者的成績會很差,經驗豐富的程式員應該有很好的成績。為了讓你能自己決定某些問題的偏好,每個問題沒有配置設定分數,如果選擇這些考題為你所用,請自行按你的意思配置設定分數。

預處理器(Preprocessor)

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的目的是什麼?

  如果你不知道答案,請看參考文獻1。這問題對區分一個正常的夥計和一個書呆子是很有用的。隻有書呆子才會讀C語言課本的附錄去找出象這種問題的答案。當然如果你不是在找一個書呆子,那麼應試者最好希望自己不要知道答案。

死循環(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 intege)r

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;

A more obscure approach is:

一個較晦澀的方法是:

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

即使你的品味更接近第二種方案,但我建議你在面試時使用第一種方案。

中斷(Interrupts)

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()經常有重入和性能上的問題。如果你丢掉了第三和第四點,我不會太為難你的。不用說,如果你能得到後兩點,那麼你的被雇用前景越來越光明了。

代碼例子(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;

對于一個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語言測試題,我懷着愉快的心情寫完它,希望你以同樣的心情讀完它。如果是認為這是一個好的測試,那麼盡量都用到你的找工作的過程中去吧。天知道也許過個一兩年,我就不做現在的工作,也需要找一個。

參考文獻

1) Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.

2) Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.

如何優化C語言代碼(程式員必讀)

AlexanderWu 發表于2006 2月, 11 12:37 [C資料庫]

1、選擇合适的算法和資料結構

應該熟悉算法語言,知道各種算法的優缺點,具體資料請參見相應的參考資料,有

很多計算機書籍上都有介紹。将比較慢的順序查找法用較快的二分查找或亂序查找

法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,都可以大大

提高程式執行的效率。.選擇一種合适的資料結構也很重要,比如你在一堆随機存

放的數中使用了大量的插入和删除指令,那使用連結清單要快得多。

數組與指針語句具有十分密碼的關系,一般來說,指針比較靈活簡潔,而數組則比

較直覺,容易了解。對于大部分的編譯器,使用指針比使用數組生成的代碼更短,

執行效率更高。但是在Keil中則相反,使用數組比使用的指針生成的代碼更短。。

3、使用盡量小的資料類型

能夠使用字元型(char)定義的變量,就不要使用整型(int)變量來定義;能夠使用

整型變量定義的變量就不要用長整型(long int),能不使用浮點型(float)變量就

不要使用浮點型變量。當然,在定義變量後不要超過變量的作用範圍,如果超過變

量的範圍指派,C編譯器并不報錯,但程式運作結果卻錯了,而且這樣的錯誤很難

發現。

在ICCAVR中,可以在Options中設定使用printf參數,盡量使用基本型參數(%c、

%d、%x、%X、%u和%s格式說明符),少用長整型參數(%ld、%lu、%lx和%lX格式說明

符),至于浮點型的參數(%f)則盡量不要使用,其它C編譯器也一樣。在其它條件不

變的情況下,使用%f參數,會使生成的代碼的數量增加很多,執行速度降低。

4、使用自加、自減指令

通常使用自加、自減指令和複合指派表達式(如a-=1及a+=1等)都能夠生成高品質的

程式代碼,編譯器通常都能夠生成inc和dec之類的指令,而使用a=a+1或a=a-1之類

的指令,有很多C編譯器都會生成二到三個位元組的指令。在AVR單片适用的ICCAVR、

GCCAVR、IAR等C編譯器以上幾種書寫方式生成的代碼是一樣的,也能夠生成高品質

的inc和dec之類的的代碼。

5、減少運算的強度

可以使用運算量小但功能相同的表達式替換原來複雜的的表達式。如下:

(1)、求餘運算。

a=a%8;

可以改為:

a=a&7;

說明:位操作隻需一個指令周期即可完成,而大部分的C編譯器的“%”運算均是調

用子程式來完成,代碼長、執行速度慢。通常,隻要求是求2n方的餘數,均可使用

位操作的方法來代替。

(2)、平方運算

a=pow(a,2.0);

可以改為:

a=a*a;

說明:在有内置硬體乘法器的單片機中(如51系列),乘法運算比求平方運算快得多

,因為浮點數的求平方是通過調用子程式來實作的,在自帶硬體乘法器的AVR單片

機中,如ATMega163中,乘法運算隻需2個時鐘周期就可以完成。既使是在沒有内置

硬體乘法器的AVR單片機中,乘法運算的子程式比平方運算的子程式代碼短,執行

速度快。

如果是求3次方,如:

a=pow(a,3.0);

更改為:

a=a*a*a;

則效率的改善更明顯。

(3)、用移位實作乘除法運算

a=a*4;

b=b/4;

可以改為:

a=a<<2;

b=b>>2;

說明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR中,如果

乘以2n,都可以生成左移的代碼,而乘以其它的整數或除以任何數,均調用乘除法

子程式。用移位的方法得到代碼比調用乘除法子程式生成的代碼效率高。實際上,

隻要是乘以或除以一個整數,均可以用移位的方法得到結果,如:

a=a*9

可以改為:

a=(a<<3)+a

6、循環

(1)、循環語

對于一些不需要循環變量參加運算的任務可以把它們放到循環外面,這裡的任務包

括表達式、函數的調用、指針運算、數組通路等,應該将沒有必要執行多次的操作

全部集合在一起,放到一個init的初始化程式中進行。

(2)、延時函數:

通常使用的延時函數均采用自加的形式:

void delay (void)

{

unsigned int i;

for (i=0;i<1000;i++)

;

}

将其改為自減延時函數:

void delay (void)

{

unsigned int i;

for (i=1000;i>0;i--)

;

}

兩個函數的延時效果相似,但幾乎所有的C編譯對後一種函數生成的代碼均比前一

種代碼少1~3個位元組,因為幾乎所有的MCU均有為0轉移的指令,采用後一種方式能

夠生成這類指令。

在使用while循環時也一樣,使用自減指令控制循環會比使用自加指令控制循環生

成的代碼更少1~3個字母。

但是在循環中有通過循環變量“i”讀寫數組的指令時,使用預減循環時有可能使

數組超界,要引起注意。

(3)while循環和do…while循環

用while循環時有以下兩種循環形式:

unsigned int i;

i=0;

while (i<1000)

{

i++;

//使用者程式

}

或:

unsigned int i;

i=1000;

do

i--;

//使用者程式

while (i>0);

在這兩種循環中,使用do…while循環編譯後生成的代碼的長度短于while循環。

7、查表

在程式中一般不進行非常複雜的運算,如浮點數的乘除及開方等,以及一些複雜的

數學模型的插補運算,對這些即消耗時間又消費資源的運算,應盡量使用查表的方

式,并且将資料表置于程式存儲區。如果直接生成所需的表比較困難,也盡量在啟

了,減少了程式執行過程中重複計算的工作量。

8、其它

比如使用線上彙編及将字元串和一些常量儲存在程式存儲器中,均有利于優化

-- C語言的檔案操作

檔案的基本概念

  所謂“檔案”是指一組相關資料的有序集合。 這個資料集有一個名稱,叫做檔案名。 實際上在前面的各章中我們已經多次使用了檔案,例如源程式檔案、目标檔案、可執行檔案、庫檔案 (頭檔案)等。檔案通常是駐留在外部媒體(如磁盤等)上的, 在使用時才調入記憶體中來。從不同的角度可對檔案作不同的分類。從使用者的角度看,檔案可分為普通檔案和裝置檔案兩種。

  普通檔案是指駐留在磁盤或其它外部媒體上的一個有序資料集,可以是源檔案、目标檔案、可執行程式; 也可以是一組待輸入處理的原始資料,或者是一組輸出的結果。對于源檔案、目标檔案、 可執行程式可以稱作程式檔案,對輸入輸出資料可稱作資料檔案。

  裝置檔案是指與主機相聯的各種外部裝置,如顯示器、列印機、鍵盤等。在作業系統中,把外部裝置也看作是一個檔案來進行管理,把它們的輸入、輸出等同于對磁盤檔案的讀和寫。 通常把顯示器定義為标準輸出檔案, 一般情況下在螢幕上顯示有關資訊就是向标準輸出檔案輸出。如前面經常使用的printf,putchar 函數就是這類輸出。鍵盤通常被指定标準的輸入檔案, 從鍵盤上輸入就意味着從标準輸入檔案上輸入資料。scanf,getchar函數就屬于這類輸入。

  從檔案編碼的方式來看,檔案可分為ASCII碼檔案和二進制碼檔案兩種。

  ASCII檔案也稱為文本檔案,這種檔案在磁盤中存放時每個字元對應一個位元組,用于存放對應的ASCII碼。例如,數5678的存儲形式為:

ASC碼:  00110101 00110110 00110111 00111000

     ↓     ↓    ↓    ↓

十進制碼: 5     6    7    8 共占用4個位元組。ASCII碼檔案可在螢幕上按字元顯示, 例如源程式檔案就是ASCII檔案,用DOS指令TYPE可顯示檔案的内容。 由于是按字元顯示,是以能讀懂檔案内容。

  二進制檔案是按二進制的編碼方式來存放檔案的。 例如, 數5678的存儲形式為: 00010110 00101110隻占二個位元組。二進制檔案雖然也可在螢幕上顯示, 但其内容無法讀懂。C系統在處理這些檔案時,并不區分類型,都看成是字元流,按位元組進行處理。 輸入輸出字元流的開始和結束隻由程式控制而不受實體符号(如回車符)的控制。 是以也把這種檔案稱作“流式檔案”。

  本章讨論流式檔案的打開、關閉、讀、寫、 定位等各種操作。檔案指針在C語言中用一個指針變量指向一個檔案, 這個指針稱為檔案指針。通過檔案指針就可對它所指的檔案進行各種操作。 定義說明檔案指針的一般形式為: FILE* 指針變量辨別符; 其中FILE應為大寫,它實際上是由系統定義的一個結構, 該結構中含有檔案名、檔案狀态和檔案目前位置等資訊。 在編寫源程式時不必關心FILE結構的細節。例如:FILE *fp; 表示fp是指向FILE結構的指針變量,通過fp 即可找存放某個檔案資訊的結構變量,然後按結構變量提供的資訊找到該檔案, 實施對檔案的操作。習慣上也籠統地把fp稱為指向一個檔案的指針。檔案的打開與關閉檔案在進行讀寫操作之前要先打開,使用完畢要關閉。 所謂打開檔案,實際上是建立檔案的各種有關資訊, 并使檔案指針指向該檔案,以便進行其它操作。關閉檔案則斷開指針與檔案之間的聯系,也就禁止再對該檔案進行操作。

  在C語言中,檔案操作都是由庫函數來完成的。 在本章内将介紹主要的檔案操作函數。

檔案打開函數fopen

  fopen函數用來打開一個檔案,其調用的一般形式為: 檔案指針名=fopen(檔案名,使用檔案方式) 其中,“檔案指針名”必須是被說明為FILE 類型的指針變量,“檔案名”是被打開檔案的檔案名。 “使用檔案方式”是指檔案的類型和操作要求。“檔案名”是字元串常量或字元串數組。例如:

FILE *fp;

fp=("file a","r");

其意義是在目前目錄下打開檔案file a, 隻允許進行“讀”操作,并使fp指向該檔案。

FILE *fphzk

fphzk=("c:hzk16',"rb")

其意義是打開C驅動器磁盤的根目錄下的檔案hzk16, 這是一個二進制檔案,隻允許按二進制方式進行讀操作。兩個反斜線“ ”中的第一個表示轉義字元,第二個表示根目錄。使用檔案的方式共有12種,下面給出了它們的符号和意義。

檔案使用方式        意 義

“rt”      隻讀打開一個文本檔案,隻允許讀資料

“wt”      隻寫打開或建立一個文本檔案,隻允許寫資料

“at”      追加打開一個文本檔案,并在檔案末尾寫資料

“rb”      隻讀打開一個二進制檔案,隻允許讀資料

“wb”       隻寫打開或建立一個二進制檔案,隻允許寫資料

“ab”       追加打開一個二進制檔案,并在檔案末尾寫資料

“rt+”      讀寫打開一個文本檔案,允許讀和寫

“wt+”      讀寫打開或建立一個文本檔案,允許讀寫

“at+”      讀寫打開一個文本檔案,允許讀,或在檔案末追加數 據

“rb+”      讀寫打開一個二進制檔案,允許讀和寫

“wb+”      讀寫打開或建立一個二進制檔案,允許讀和寫

“ab+”      讀寫打開一個二進制檔案,允許讀,或在檔案末追加資料

對于檔案使用方式有以下幾點說明:

1. 檔案使用方式由r,w,a,t,b,+六個字元拼成,各字元的含義是:

r(read): 讀

w(write): 寫

a(append): 追加

t(text): 文本檔案,可省略不寫

b(banary): 二進制檔案

+: 讀和寫

2. 凡用“r”打開一個檔案時,該檔案必須已經存在, 且隻能從該檔案讀出。

3. 用“w”打開的檔案隻能向該檔案寫入。 若打開的檔案不存在,則以指定的檔案名建立該檔案,若打開的檔案已經存在,則将該檔案删去,重建一個新檔案。

4. 若要向一個已存在的檔案追加新的資訊,隻能用“a ”方式打開檔案。但此時該檔案必須是存在的,否則将會出錯。

5. 在打開一個檔案時,如果出錯,fopen将傳回一個空指針值NULL。在程式中可以用這一資訊來判别是否完成打開檔案的工作,并作相應的處理。是以常用以下程式段打開檔案:

if((fp=fopen("c:hzk16","rb")==NULL)

{

printf("nerror on open c:hzk16 file!");

getch();

exit(1);

}

  這段程式的意義是,如果傳回的指針為空,表示不能打開C槽根目錄下的hzk16檔案,則給出提示資訊“error on open c: hzk16file!”,下一行getch()的功能是從鍵盤輸入一個字元,但不在螢幕上顯示。在這裡,該行的作用是等待, 隻有當使用者從鍵盤敲任一鍵時,程式才繼續執行, 是以使用者可利用這個等待時間閱讀出錯提示。敲鍵後執行exit(1)退出程式。

6. 把一個文本檔案讀入記憶體時,要将ASCII碼轉換成二進制碼, 而把檔案以文本方式寫入磁盤時,也要把二進制碼轉換成ASCII碼,是以文本檔案的讀寫要花費較多的轉換時間。對二進制檔案的讀寫不存在這種轉換。

7. 标準輸入檔案(鍵盤),标準輸出檔案(顯示器 ),标準出錯輸出(出錯資訊)是由系統打開的,可直接使用。檔案關閉函數fclose檔案一旦使用完畢,應用關閉檔案函數把檔案關閉, 以避免檔案的資料丢失等錯誤。

fclose函數

調用的一般形式是: fclose(檔案指針); 例如:

fclose(fp); 正常完成關閉檔案操作時,fclose函數傳回值為0。如傳回非零值則表示有錯誤發生。檔案的讀寫對檔案的讀和寫是最常用的檔案操作。

在C語言中提供了多種檔案讀寫的函數:

·字元讀寫函數 :fgetc和fputc

·字元串讀寫函數:fgets和fputs

·資料塊讀寫函數:freed和fwrite

·格式化讀寫函數:fscanf和fprinf

  下面分别予以介紹。使用以上函數都要求包含頭檔案stdio.h。字元讀寫函數fgetc和fputc字元讀寫函數是以字元(位元組)為機關的讀寫函數。 每次可從檔案讀出或向檔案寫入一個字元。

一、讀字元函數fgetc

  fgetc函數的功能是從指定的檔案中讀一個字元,函數調用的形式為: 字元變量=fgetc(檔案指針); 例如:ch=fgetc(fp);其意義是從打開的檔案fp中讀取一個字元并送入ch中。

  對于fgetc函數的使用有以下幾點說明:

1. 在fgetc函數調用中,讀取的檔案必須是以讀或讀寫方式打開的。

2. 讀取字元的結果也可以不向字元變量指派,例如:fgetc(fp);但是讀出的字元不能儲存。

3. 在檔案内部有一個位置指針。用來指向檔案的目前讀寫位元組。在檔案打開時,該指針總是指向檔案的第一個位元組。使用fgetc 函數後, 該位置指針将向後移動一個位元組。 是以可連續多次使用fgetc函數,讀取多個字元。 應注意檔案指針和檔案内部的位置指針不是一回事。檔案指針是指向整個檔案的,須在程式中定義說明,隻要不重新指派,檔案指針的值是不變的。檔案内部的位置指針用以訓示檔案内部的目前讀寫位置,每讀寫一次,該指針均向後移動,它不需在程式中定義說明,而是由系統自動設定的。

[例10.1]讀入檔案e10-1.c,在螢幕上輸出。

#include<stdio.h>

main()

{

FILE *fp;

char ch;

if((fp=fopen("e10_1.c","rt"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

ch=fgetc(fp);

while (ch!=EOF)

{

putchar(ch);

ch=fgetc(fp);

}

fclose(fp);

}

  本例程式的功能是從檔案中逐個讀取字元,在螢幕上顯示。 程式定義了檔案指針fp,以讀文本檔案方式打開檔案“e10_1.c”, 并使fp指向該檔案。如打開檔案出錯, 給出提示并退出程式。程式第12行先讀出一個字元,然後進入循環, 隻要讀出的字元不是檔案結束标志(每個檔案末有一結束标志EOF)就把該字元顯示在螢幕上,再讀入下一字元。每讀一次,檔案内部的位置指針向後移動一個字元,檔案結束時,該指針指向EOF。執行本程式将顯示整個檔案。

二、寫字元函數fputc

  fputc函數的功能是把一個字元寫入指定的檔案中,函數調用的 形式為: fputc(字元量,檔案指針); 其中,待寫入的字元量可以是字元常量或變量,例如:fputc('a',fp);其意義是把字元a寫入fp所指向的檔案中。

  對于fputc函數的使用也要說明幾點:

1. 被寫入的檔案可以用、寫、讀寫,追加方式打開,用寫或讀寫方式打開一個已存在的檔案時将清除原有的檔案内容,寫入字元從檔案首開始。如需保留原有檔案内容,希望寫入的字元以檔案末開始存放,必須以追加方式打開檔案。被寫入的檔案若不存在,則建立該檔案。

2. 每寫入一個字元,檔案内部位置指針向後移動一個位元組。

3. fputc函數有一個傳回值,如寫入成功則傳回寫入的字元, 否則傳回一個EOF。可用此來判斷寫入是否成功。

[例10.2]從鍵盤輸入一行字元,寫入一個檔案, 再把該檔案内容讀出顯示在螢幕上。

#include<stdio.h>

main()

{

FILE *fp;

char ch;

if((fp=fopen("string","wt+"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

printf("input a string:n");

ch=getchar();

while (ch!='n')

{

fputc(ch,fp);

ch=getchar();

}

rewind(fp);

ch=fgetc(fp);

while(ch!=EOF)

{

putchar(ch);

ch=fgetc(fp);

}

printf("n");

fclose(fp);

}

  程式中第6行以讀寫文本檔案方式打開檔案string。程式第13行從鍵盤讀入一個字元後進入循環,當讀入字元不為回車符時, 則把該字元寫入檔案之中,然後繼續從鍵盤讀入下一字元。 每輸入一個字元,檔案内部位置指針向後移動一個位元組。寫入完畢, 該指針已指向檔案末。如要把檔案從頭讀出,須把指針移向檔案頭, 程式第19行rewind函數用于把fp所指檔案的内部位置指針移到檔案頭。 第20至25行用于讀出檔案中的一行内容。

[例10.3]把指令行參數中的前一個檔案名辨別的檔案, 複制到後一個檔案名辨別的檔案中, 如指令行中隻有一個檔案名則把該檔案寫到标準輸出檔案(顯示器)中。

#include<stdio.h>

main(int argc,char *argv[])

{

FILE *fp1,*fp2;

char ch;

if(argc==1)

{

printf("have not enter file name strike any key exit");

getch();

exit(0);

}

if((fp1=fopen(argv[1],"rt"))==NULL)

{

printf("Cannot open %sn",argv[1]);

getch();

exit(1);

}

if(argc==2) fp2=stdout;

else if((fp2=fopen(argv[2],"wt+"))==NULL)

{

printf("Cannot open %sn",argv[1]);

getch();

exit(1);

}

while((ch=fgetc(fp1))!=EOF)

fputc(ch,fp2);

fclose(fp1);

fclose(fp2);

}

  本程式為帶參的main函數。程式中定義了兩個檔案指針 fp1 和fp2,分别指向指令行參數中給出的檔案。如指令行參數中沒有給出檔案名,則給出提示資訊。程式第18行表示如果隻給出一個檔案名,則使fp2指向标準輸出檔案(即顯示器)。程式第25行至28行用循環語句逐個讀出檔案1中的字元再送到檔案2中。再次運作時,給出了一個檔案名(由例10.2所建立的檔案), 故輸出給标準輸出檔案stdout,即在顯示器上顯示檔案内容。第三次運作,給出了二個檔案名,是以把string中的内容讀出,寫入到OK之中。可用DOS指令type顯示OK的内容:字元串讀寫函數fgets和fputs

一、讀字元串函數fgets函數的功能是從指定的檔案中讀一個字元串到字元數組中,函數調用的形式為: fgets(字元數組名,n,檔案指針); 其中的n是一個正整數。表示從檔案中讀出的字元串不超過 n-1個字元。在讀入的最後一個字元後加上串結束标志''。例如:fgets(str,n,fp);的意義是從fp所指的檔案中讀出n-1個字元送入字元數組str中。

[例10.4]從e10_1.c檔案中讀入一個含10個字元的字元串。

#include<stdio.h>

main()

{

FILE *fp;

char str[11];

if((fp=fopen("e10_1.c","rt"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

fgets(str,11,fp);

printf("%s",str);

fclose(fp);

}

  本例定義了一個字元數組str共11個位元組,在以讀文本檔案方式打開檔案e101.c後,從中讀出10個字元送入str數組,在數組最後一個單元内将加上'',然後在螢幕上顯示輸出str數組。輸出的十個字元正是例10.1程式的前十個字元。

  對fgets函數有兩點說明:

1. 在讀出n-1個字元之前,如遇到了換行符或EOF,則讀出結束。

2. fgets函數也有傳回值,其傳回值是字元數組的首位址。

二、寫字元串函數fputs

fputs函數的功能是向指定的檔案寫入一個字元串,其調用形式為: fputs(字元串,檔案指針) 其中字元串可以是字元串常量,也可以是字元數組名, 或指針 變量,例如:

fputs(“abcd“,fp);

其意義是把字元串“abcd”寫入fp所指的檔案之中。[例10.5]在例10.2中建立的檔案string中追加一個字元串。

#include<stdio.h>

main()

{

FILE *fp;

char ch,st[20];

if((fp=fopen("string","at+"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

printf("input a string:n");

scanf("%s",st);

fputs(st,fp);

rewind(fp);

ch=fgetc(fp);

while(ch!=EOF)

{

putchar(ch);

ch=fgetc(fp);

}

printf("n");

fclose(fp);

}

  本例要求在string檔案末加寫字元串,是以,在程式第6行以追加讀寫文本檔案的方式打開檔案string 。 然後輸入字元串, 并用fputs函數把該串寫入檔案string。在程式15行用rewind函數把檔案内部位置指針移到檔案首。 再進入循環逐個顯示目前檔案中的全部内容。

資料塊讀寫函數fread和fwrite

  C語言還提供了用于整塊資料的讀寫函數。 可用來讀寫一組資料,如一個數組元素,一個結構變量的值等。讀資料塊函數調用的一般形式為: fread(buffer,size,count,fp); 寫資料塊函數調用的一般形式為: fwrite(buffer,size,count,fp); 其中buffer是一個指針,在fread函數中,它表示存放輸入資料的首位址。在fwrite函數中,它表示存放輸出資料的首位址。 size 表示資料塊的位元組數。count 表示要讀寫的資料塊塊數。fp 表示檔案指針。

例如:

fread(fa,4,5,fp); 其意義是從fp所指的檔案中,每次讀4個位元組(一個實數)送入實數組fa中,連續讀5次,即讀5個實數到fa中。

[例10.6]從鍵盤輸入兩個學生資料,寫入一個檔案中, 再讀出這兩個學生的資料顯示在螢幕上。

#include<stdio.h>

struct stu

{

char name[10];

int num;

int age;

char addr[15];

}boya[2],boyb[2],*pp,*qq;

main()

{

FILE *fp;

char ch;

int i;

pp=boya;

qq=boyb;

if((fp=fopen("stu_list","wb+"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

printf("ninput datan");

for(i=0;i<2;i++,pp++)

scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);

pp=boya;

fwrite(pp,sizeof(struct stu),2,fp);

rewind(fp);

fread(qq,sizeof(struct stu),2,fp);

printf("nnnametnumber age addrn");

for(i=0;i<2;i++,qq++)

printf("%st%5d%7d%sn",qq->name,qq->num,qq->age,qq->addr);

fclose(fp);

}

  本例程式定義了一個結構stu,說明了兩個結構數組boya和 boyb以及兩個結構指針變量pp和qq。pp指向boya,qq指向boyb。程式第16行以讀寫方式打開二進制檔案“stu_list”,輸入二個學生資料之後,寫入該檔案中, 然後把檔案内部位置指針移到檔案首,讀出兩塊學生資料後,在螢幕上顯示。

格式化讀寫函數fscanf和fprintf

fscanf函數,fprintf函數與前面使用的scanf和printf 函數的功能相似,都是格式化讀寫函數。 兩者的差別在于 fscanf 函數和fprintf函數的讀寫對象不是鍵盤和顯示器,而是磁盤檔案。這兩個函數的調用格式為: fscanf(檔案指針,格式字元串,輸入表列); fprintf(檔案指針,格式字元串,輸出表列); 例如:

fscanf(fp,"%d%s",&i,s);

fprintf(fp,"%d%c",j,ch);

用fscanf和fprintf函數也可以完成例10.6的問題。修改後的程式如例10.7所示。

[例10.7]

#include<stdio.h>

struct stu

{

char name[10];

int num;

int age;

char addr[15];

}boya[2],boyb[2],*pp,*qq;

main()

{

FILE *fp;

char ch;

int i;

pp=boya;

qq=boyb;

if((fp=fopen("stu_list","wb+"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

printf("ninput datan");

for(i=0;i<2;i++,pp++)

scanf("%s%d%d%s",pp->name,&pp->num,&pp->age,pp->addr);

pp=boya;

for(i=0;i<2;i++,pp++)

fprintf(fp,"%s %d %d %sn",pp->name,pp->num,pp->age,pp->

addr);

rewind(fp);

for(i=0;i<2;i++,qq++)

fscanf(fp,"%s %d %d %sn",qq->name,&qq->num,&qq->age,qq->addr);

printf("nnnametnumber age addrn");

qq=boyb;

for(i=0;i<2;i++,qq++)

printf("%st%5d %7d %sn",qq->name,qq->num, qq->age,

qq->addr);

fclose(fp);

}

  與例10.6相比,本程式中fscanf和fprintf函數每次隻能讀寫一個結構數組元素,是以采用了循環語句來讀寫全部數組元素。 還要注意指針變量pp,qq由于循環改變了它們的值,是以在程式的25和32行分别對它們重新賦予了數組的首位址。

檔案的随機讀寫

  前面介紹的對檔案的讀寫方式都是順序讀寫, 即讀寫檔案隻能從頭開始,順序讀寫各個資料。 但在實際問題中常要求隻讀寫檔案中某一指定的部分。 為了解決這個問題可移動檔案内部的位置指針到需要讀寫的位置,再進行讀寫,這種讀寫稱為随機讀寫。 實作随機讀寫的關鍵是要按要求移動位置指針,這稱為檔案的定位。檔案定位移動檔案内部位置指針的函數主要有兩個, 即 rewind 函數和fseek函數。

  rewind函數前面已多次使用過,其調用形式為: rewind(檔案指針); 它的功能是把檔案内部的位置指針移到檔案首。 下面主要介紹

fseek函數。

  fseek函數用來移動檔案内部位置指針,其調用形式為: fseek(檔案指針,位移量,起始點); 其中:“檔案指針”指向被移動的檔案。 “位移量”表示移動的位元組數,要求位移量是long型資料,以便在檔案長度大于64KB 時不會出錯。當用常量表示位移量時,要求加字尾“L”。“起始點”表示從何處開始計算位移量,規定的起始點有三種:檔案首,目前位置和檔案尾。

其表示方法如表10.2。

起始點    表示符号    數字表示

——————————————————————————

檔案首    SEEK—SET    0

目前位置   SEEK—CUR    1

檔案末尾   SEEK—END     2

例如:

fseek(fp,100L,0);其意義是把位置指針移到離檔案首100個位元組處。還要說明的是fseek函數一般用于二進制檔案。在文本檔案中由于要進行轉換,故往往計算的位置會出現錯誤。檔案的随機讀寫在移動位置指針之後, 即可用前面介紹的任一種讀寫函數進行讀寫。由于一般是讀寫一個資料據塊,是以常用fread和fwrite函數。下面用例題來說明檔案的随機讀寫。

[例10.8]在學生檔案stu list中讀出第二個學生的資料。

#include<stdio.h>

struct stu

{

char name[10];

int num;

int age;

char addr[15];

}boy,*qq;

main()

{

FILE *fp;

char ch;

int i=1;

qq=&boy;

if((fp=fopen("stu_list","rb"))==NULL)

{

printf("Cannot open file strike any key exit!");

getch();

exit(1);

}

rewind(fp);

fseek(fp,i*sizeof(struct stu),0);

fread(qq,sizeof(struct stu),1,fp);

printf("nnnametnumber age addrn");

printf("%st%5d %7d %sn",qq->name,qq->num,qq->age,

qq->addr);

}

  檔案stu_list已由例10.6的程式建立,本程式用随機讀出的方法讀出第二個學生的資料。程式中定義boy為stu類型變量,qq為指向boy的指針。以讀二進制檔案方式打開檔案,程式第22行移動檔案位置指針。其中的i值為1,表示從檔案頭開始,移動一個stu類型的長度, 然後再讀出的資料即為第二個學生的資料。

檔案檢測函數

C語言中常用的檔案檢測函數有以下幾個。

一、檔案結束檢測函數feof函數調用格式: feof(檔案指針);

功能:判斷檔案是否處于檔案結束位置,如檔案結束,則傳回值為1,否則為0。

二、讀寫檔案出錯檢測函數ferror函數調用格式: ferror(檔案指針);

功能:檢查檔案在用各種輸入輸出函數進行讀寫時是否出錯。 如ferror傳回值為0表示未出錯,否則表示有錯。

三、檔案出錯标志和檔案結束标志置0函數clearerr函數調用格式: clearerr(檔案指針);

功能:本函數用于清除出錯标志和檔案結束标志,使它們為0值。

C庫檔案

C系統提供了豐富的系統檔案,稱為庫檔案,C的庫檔案分為兩類,一類是擴充名為".h"的檔案,稱為頭檔案, 在前面的包含指令中我們已多次使用過。在".h"檔案中包含了常量定義、 類型定義、宏定義、函數原型以及各種編譯選擇設定等資訊。另一類是函數庫,包括了各種函數的目标代碼,供使用者在程式中調用。 通常在程式中調用一個庫函數時,要在調用之前包含該函數原型所在的".h" 檔案。

在附錄中給出了全部庫函數。

ALLOC.H    說明記憶體管理函數(配置設定、釋放等)。

ASSERT.H    定義 assert調試宏。

BIOS.H     說明調用IBM—PC ROM BIOS子程式的各個函數。

CONIO.H    說明調用DOS控制台I/O子程式的各個函數。

CTYPE.H    包含有關字元分類及轉換的名類資訊(如 isalpha和toascii等)。

DIR.H     包含有關目錄和路徑的結構、宏定義和函數。

DOS.H     定義和說明MSDOS和8086調用的一些常量和函數。

ERRON.H    定義錯誤代碼的助記符。

FCNTL.H    定義在與open庫子程式連接配接時的符号常量。

FLOAT.H    包含有關浮點運算的一些參數和函數。

GRAPHICS.H   說明有關圖形功能的各個函數,圖形錯誤代碼的常量定義,正對不同驅動程式的各種顔色值,及函數用到的一些特殊結構。

IO.H      包含低級I/O子程式的結構和說明。

LIMIT.H    包含各環境參數、編譯時間限制、數的範圍等資訊。

MATH.H     說明數學運算函數,還定了 HUGE VAL 宏, 說明了matherr和matherr子程式用到的特殊結構。

MEM.H     說明一些記憶體操作函數(其中大多數也在STRING.H 中說明)。

PROCESS.H   說明程序管理的各個函數,spawn…和EXEC …函數的結構說明。

SETJMP.H    定義longjmp和setjmp函數用到的jmp buf類型, 說明這兩個函數。

SHARE.H    定義檔案共享函數的參數。

SIGNAL.H    定義SIG[ZZ(Z] [ZZ)]IGN和SIG[ZZ(Z] [ZZ)]DFL常量,說明rajse和signal兩個函數。

STDARG.H    定義讀函數參數表的宏。(如vprintf,vscarf函數)。

STDDEF.H    定義一些公共資料類型和宏。

STDIO.H    定義Kernighan和Ritchie在Unix System V 中定義的标準和擴充的類型和宏。還定義标準I/O 預定義流:stdin,stdout和stderr,說明 I/O流子程式。

STDLIB.H    說明一些常用的子程式:轉換子程式、搜尋/ 排序子程式等。

STRING.H    說明一些串操作和記憶體操作函數。

SYSSTAT.H   定義在打開和建立檔案時用到的一些符号常量。

SYSTYPES.H  說明ftime函數和timeb結構。

SYSTIME.H   定義時間的類型time[ZZ(Z] [ZZ)]t。

TIME.H     定義時間轉換子程式asctime、localtime和gmtime的結構,ctime、 difftime、 gmtime、 localtime和stime用到的類型,并提供這些函數的原型。

VALUE.H    定義一些重要常量, 包括依賴于機器硬體的和為與Unix System V相相容而說明的一些常量,包括浮點和雙精度值的範圍。

補充:在Unix系統的文本檔案中,是用換行符(ASCII 10)作為行結束标記。在Macintosh系統中,是用回車符(ASCII 13)作為行結束标記。而Windows系統則是沿用了DOS系統的标準,換行符和回車符都用來作為行結束标記。

信威筆試

綜合能力測試:

3、172,84,40,18 ?

4、8 5 2

4 5 0

+C C C +E E E

A-I分别表示0-9的數字,問A的值

6-9 圖形題

2、男醫生多于男護士

4、至少有一個女醫生

問“我”的性别和職位

11、四個人坐在方桌旁,兩個女士A和B,兩個男士C和D,四個人分别是遊泳,滑冰,體操和

網球運動員

1、遊泳在A左邊

2、體操在C對面

3、B和D相鄰

4、一位女生在滑冰的左邊

問誰是網球運動員

12、上司從博物館難走一塊明朝城牆的磚,如何要回來

硬體筆試題

  揭露華為、大唐等企業硬體筆試題 [專題文章]

漢王筆試   

下面是一些基本的數字電路知識問題,請簡要回答之。

  

a) 什麼是Setup 和Holdup時間?

b) 什麼是競争與冒險現象?怎樣判斷?如何消除?

c) 請畫出用D觸發器實作2倍分頻的邏輯電路?

d) 什麼是"線與"邏輯,要實作它,在硬體特性上有什麼具體要求?

e) 什麼是同步邏輯和異步邏輯?

f) 請畫出微機接口電路中,典型的輸入裝置與微機接口邏輯示意圖(資料接口、控制接口、所存器/緩沖器)。

g) 你知道那些常用邏輯電平?TTL與COMS電平可以直接互連嗎?

  

2、 可程式設計邏輯器件在現代電子設計中越來越重要,請問:

a) 你所知道的可程式設計邏輯器件有哪些?

b) 試用VHDL或VERILOG、ABLE描述8位D觸發器邏輯。

  

3、 設想你将設計完成一個電子電路方案。請簡述用EDA軟體(如PROTEL)進行設計(包括原理圖和PCB圖)到調試出樣機的整個過程。在各環節應注意哪些問題?

  

  

飛利浦-大唐筆試歸來

  

1,用邏輯們和cmos電路實作ab cd

2. 用一個二選一mux和一個inv實作異或

3. 給了reg的setup,hold時間,求中間組合邏輯的delay範圍。 Setup/hold time 是測試晶片對輸入信号和時鐘信号之間的時間要求。建立時間是指觸發器的時鐘信号上升沿到來以前,資料穩定不變的時間。輸入信号應提前時鐘上升沿(如上升沿有效)T時間到達晶片,這個T就是建立時間-Setup time.如不滿足setup time,這個資料就不能被這一時鐘打入觸發器,隻有在下一個時鐘上升沿,資料才能被打入觸發器。 保持時間是指觸發器的時鐘信号上升沿到來以後,資料穩定不變的時間。時hold time不夠,資料同樣不能被打入觸發器。

4. 如何解決亞穩态

5. 用verilog/vhdl寫一個fifo控制器

6. 用verilog/vddl檢測stream中的特定字元串

    

信威dsp軟體面試題

  

1)DSP和通用處理器在結構上有什麼不同,請簡要畫出你熟悉的一種DSP結構圖

  

2)說說定點DSP和浮點DSP的定義(或者說出他們的差別)

  

3)說說你對循環尋址和位反序尋址的了解

  

4)請寫出【-8,7】的二進制補碼,和二進制偏置碼。 用Q15表示出0.5和-0.5

  

揚智電子筆試

  

第一題:用mos管搭出一個二輸入與非門。

第二題:內建電路前段設計流程,寫出相關的工具。

第三題:名詞IRQ,BIOS,USB,VHDL,SDR

第四題:unix 指令cp -r, rm,uname

第五題:用波形表示D觸發器的功能

第六題:寫異步D觸發器的verilog module

第七題:What is PC Chipset?

第八題:用傳輸門和倒向器搭一個邊沿觸發器

第九題:畫狀态機,接受1,2,5分錢的賣報機,每份報紙5分錢。

  

華為面題 (硬體)

全都是幾本模電數電信号單片機題目

1.用與非門等設計全加法器

2.給出兩個門電路讓你分析異同

3.名詞:sram,ssram,sdram

4.信号與系統:在時域與頻域關系

5.信号與系統:和4題差不多

6.晶體振蕩器,好像是給出振蕩頻率讓你求周期(應該是單片機的,12分之一周期....)

7.串行通信與同步通信異同,特點,比較

8.RS232c高電平脈沖對應的TTL邏輯是?(負邏輯?)

9.延時問題,判錯

10.史密斯特電路,求回差電壓

11.VCO是什麼,什麼參數(壓控振蕩器?)

12. 用D觸發器做個二分颦的電路.又問什麼是狀态圖

13. 什麼耐奎斯特定律,怎麼由模拟信号轉為數字信号

14. 用D觸發器做個4進制的計數

15.那種排序方法最快?

  

一、 研發(軟體)

用C語言寫一個遞歸算法求N!;

給一個C的函數,關于字元串和數組,找出錯誤;

防火牆是怎麼實作的?

你對哪方面程式設計熟悉?

  

新太硬體面題

(1)d觸發器和d鎖存器的差別

(2)有源濾波器和無源濾波器的原理及差別

(3)sram,falsh memory,及dram的差別?

(4)iir,fir濾波器的異同

(5)冒泡排序的原理

(6)作業系統的功能

(7)學過的計算機語言及開發的系統

(8)拉氏變換和傅立葉變換的表達式及聯系。

C語言變量和資料存儲

C語言的強大功能之一是可以靈活地定義資料的存儲方式。C語言從兩個方面控制變量的性質:作用域(scope)和生存期(lifetime)。作用域是指可以存取變量的代碼範圍,生存期是指可以存取變量的時間範圍。

作用域有三種:

1. extern(外部的) 這是在函數外部定義的變量的預設存儲方式。extern變量的作用域是整個程式。

2.static(靜态的) 在函數外部說明為static的變量的作用域為從定義點到該檔案尾部;在函數内部說明為static的變量的作用域為從定義點到該局部程式塊尾部。

3.auto(自動的) 這是在函數内部說明的變量的預設存儲方式。auto變量的作用域為從定義點到該局部程式塊尾部。

變量的生存期也有三種,但它們不象作用域那樣有預定義的關鍵字名稱。第一種是extern和static變量的生存期,它從main()函數被調用之前開始,到程式退出時為止。第二種是函數參數和auto變量的生存期,它從函數調用時開始,到函數傳回時為止。第三種是動态配置設定的資料的生存期,它從程式調用malloc()或calloc()為資料配置設定存儲空間時開始,到程式調用free()或程式退出時為止。

變量可以存儲在記憶體中的不同地方,這依賴于它們的生存期。在函數外部定義的變量(全局變量或靜态外部變量)和在函數内部定義的static變量,其生存期就是程式運作的全過程,這些變量被存儲在資料段(datasegment)中。資料段是在記憶體中為這些變量留出的一段大小固定的空間,它分為兩部分,一部分用來存放初始化變量,另一部分用來存放未初始化變量。

在函數内部定義的auto變量(沒有用關鍵字static定義的變量)的生存期從程式開始執行其所在的程式塊代碼時開始,到程式離開該程式塊時為止。作為函數參數的變量隻在調用該函數期間存在。這些變量被存儲在棧(stack)中。棧是記憶體中的一段空間,開始很小,以後逐漸自動增大,直到達到某個預定義的界限。在象DOS這樣的沒有虛拟記憶體(virtual memory)的系統中,這個界限由系統決定,并且通常非常大,是以程式員不必擔心用盡棧空間。關于虛拟記憶體 的讨論,請參見2.3。

第三種(也是最後一種)記憶體空間實際上并不存儲變量,但是可以用來存儲變量所指向的資料。如果把調用malloc()函數的結果賦給一個指針變量,那麼這個指針變量将包含一塊動态配置設定的記憶體的位址,這塊記憶體位于一段名為“堆(heap)”的記憶體空間中。堆開始時也很小,但當程式員調用malloc()或calloc()等記憶體配置設定函數時它就會增大。堆可以和資料段或棧共用一個記憶體段(memorysegment),也可以有它自己的記憶體段,這完全取決于編譯選項和作業系統。

與棧相似,堆也有一個增長界限,并且決定這個界限的規則與棧相同。

請參見:

1.1 什麼是局部程式塊(10calblock)?

2.2 變量必須初始化嗎?

2.3 什麼是頁抖動(pagethrashing)?

7.20 什麼是棧(stack)?

7.21 什麼是堆(heap)7 .

不。使用變量之前應該給變量一個值,一個好的編譯程式将幫助你發現那些還沒有被給定一個值就被使用的變量。不過,變量不一定需要初始化。在函數外部定義的變量或者在函數内部用static關鍵字定義的變量(被定義在資料段中的那些變量,見2.1)在沒有明确地被程式初始化之前都已被系統初始化為0了。在函數内部或程式塊内部定義的不帶static關鍵字的變量都是自動變量,如果你沒有明确地初始化這些變量,它們就會具有未定義值。如果你沒有初始化一個自動變量,在使用它之前你就必須保證先給它指派。

調用malloc()函數從堆中配置設定到的空間也包含未定義的資料,是以在使用它之前必須先進行初始化,但調用calloc()函數配置設定到的空間在配置設定時就已經被初始化為0了。

請參見:

1.1 什麼是局部程式塊(10calblock)?

7.20 什麼是棧(stack)?

7.21 什麼是堆(heap)?

有些作業系統(如UNIX和增強模式下的Windows)使用虛拟記憶體,這是一種使機器的作業位址空間大于實際記憶體的技術,它是通過用磁盤空間模拟RAM(random—access memory)來實作的。

在80386和更進階的Intel CPU晶片中,在現有的大多數其它微處理器(如Motorola 68030,sparc和Power PC)中,都有一個被稱為記憶體管理單元(Memory Management Unit,縮寫為MMU)的器件。MMU把記憶體看作是由一系列“頁(page)”組成的來處理。一頁記憶體是指一個具有一定大小的連續的記憶體塊,通常為4096或8192位元組。作業系統為每個正在運作的程式建立并維護一張被稱為程序記憶體映射(Process Memory Map,縮與為PMM)的表,表中記錄了程式可以存取的所有記憶體頁以及它們的實際位置。

每當程式存取一塊記憶體時,它會把相應的位址(虛拟位址,virtualaddress)傳送給MMU,MMU會在PMM中查找這塊記憶體的實際位置(實體位址,physical address),實體位址可以是由作業系統指定的在記憶體中或磁盤上的任何位置。如果程式要存取的位置在磁盤上,就必須把包含該位址的頁從磁盤上讀到記憶體中,并且必須更新PMM以反映這個變化(這被稱為pagefault,即頁錯)。

希望你繼續讀下去,因為下面就要介紹其中的難點了。存取磁盤比存取RAM要慢得多,是以作業系統會試圖在RAM中保持盡量多的虛拟記憶體。如果你在運作一個非常大的程式(或者同時運作幾個小程式),那麼可能沒有足夠的RAM來承擔程式要使用的全部記憶體,是以必須把一些頁從RAM中移到磁盤上(這被為pagingout,即頁出)。

作業系統會試圖去判斷哪些頁可能暫時不會被使用(通常基于過去使用記憶體的情況),如果它判斷錯了,或者程式正在很多地方存取很多記憶體,那麼為了讀入已調出的頁,就會産生大量頁錯動作。因為RAM已被全部使用,是以為了調入要存取的一頁,必須調出另一頁,而這将導緻更多的頁錯動作,因為此時不同的一頁已被移到磁盤上。在短時間内出現大量頁錯動作的情形被稱為頁抖動,它将大大降低系統的執行效率。

頻繁存取記憶體中大量散布的位置的程式更容易在系統中造成頁抖動。如果同時運作許多小程式,而實際上已經不再使用這些程式,也很容易造成頁抖動。為了減少頁抖動,你應該減少同時運作的程式的數目。對于大的程式,你應該改變它的工作方式,以盡量使作業系統能準确地判斷出哪些頁不再需要。為此,你可以使用高速緩沖存儲技術,或者改變用于大型資料結構的查找算法,或者使用效率更高的malloc()函數。當然,你也可以考慮增加系統的RAM,以減少頁出動作。

請參見:

7.17 怎樣說明一個大于640KB的數組?

7.21 什麼是堆(heap)?

18.14 怎樣才能使DOS程式獲得超過64KB的可用記憶體?

21.31 Windows是怎樣組織記憶體的?

如果希望一個變量在被初始化後其值不會被修改,程式員就會通過cons,修飾符和編譯程式達成默契。編譯程式會努力去保證這種默契——它将禁止程式中出現對說明為const的變量進行修改的代碼。

const指針的準确提法應該是指向const資料的指針,即它所指向的資料不能被修改。隻要在指針說明的開頭加入const修飾符,就可說明一個cosnt指針。盡管const指針所指向的資料不能被修改,但cosnt指針本身是可以修改的。下面給出了const指針的一些合法和非法的用法例子:

const char *str="hello";

char c=*str;

str++;

*str='a';

str[1]='b';

前兩條語句是合法的,因為它們沒有修改str所指向的資料;後兩條語句是非法的,因為它們要修改str所指向的資料。

在說明函數參數時,常常要使用const指針。例如,一個計算字元串長度的函數不必改變字元串内容,它可以寫成這樣:

my_strlen(const char *str)

{

int count=0;

while ( * str++)

{

count ++;

}

return count;

}

注意,如果有必要,一個非const指針可以被隐式地轉換為const指針,但一個const指針不能被轉換成非const指針。這就是說,在調用my_strlen()時,它的參數既可以是一個const指針,也可以是一個非const指針。

請參見:

2.7 一個變量可以同時被說明為const和volatile嗎?

2.8 什麼時候應該使用const修飾符?

2.14 什麼時候不應該使用類型強制轉換(type cast)?

2. 18 用const說明常量有什麼好處?

register修飾符暗示編譯程式相應的變量将被頻繁使用,如果可能的話,應将其儲存在CPU的寄存器中,以加快其存取速度。但是,使用register修飾符有幾點限制。

首先,register變量必須是能被CPU寄存器所接受的類型。這通常意味着register變量必須是一個單個的值,并且其長度應小于或等于整型的長度。但是,有些機器的寄存器也能存放浮點數。

其次,因為register變量可能不存放在記憶體中,是以不能用取址運算符“&”來擷取register變量的位址。如果你試圖這樣做,編譯程式就會報告這是一個錯誤。

register修飾符的用處有多大還受其它一些規則的影響。因為寄存器的數量是有限的,而且某些寄存器隻能接受特定類型的資料(如指針和浮點數),是以,真正能起作用的register修飾符的數目和類型都依賴于運作程式的機器,而任何多餘的register修飾符都将被編譯程式所忽略。

在某些情況下,把變量儲存在寄存器中反而會降低運作速度,因為被占用的寄存器不能再用于其它目的,或—者變量被使用的次數不夠多,不足以抵消裝入和存儲變量所帶來的額外開銷。

那麼,什麼時候應該使用register修飾符呢?回答是,對現有的大多數編譯程式來說,永遠不要使用register修飾符。早期的C編譯程式不會把變量儲存在寄存器中,除非你指令它這樣做,這時register修飾符是C語言的一種很有價值的補充。然而,随着編譯程式設計技術的進步,在決定哪些變量應該被存到寄存器中時,現在的C編譯程式能比程式員作出更好的決定。

實際上,許多C編譯程式會忽略register修飾符,因為盡管它完全合法,但它僅僅是暗示而不是指令。

在極罕見的情況下,程式運作速度很慢,而你也知道這是因為有一個變量被存儲在記憶體中,也許你最後會試圖在該變量前面加上register修飾符,但是,如果這并沒有加快程式的運作速度,你也不要感到奇怪。

請參見:

2.6 什麼時候應該使用volatile修飾符?

volatile修飾符告訴編譯程式不要對該變量所參與的操作進行某些優化。在兩種特殊的情況下需要使用volatile修飾符:第一種情況涉及到記憶體映射硬體(memory-mapped hardware,如圖形擴充卡,這類裝置對計算機來說就好象是記憶體的一部分一樣),第二種情況涉及到共享記憶體(shared memory,即被兩個以上同時運作的程式所使用的記憶體)。

大多數計算機擁有一系列寄存器,其存取速度比計算機主存更快。好的編譯程式能進行一種被稱為“備援裝入和存儲的删去”(redundant load and store removal)的優化,即編譯程式會·在程式中尋找并删去這樣兩類代碼:一類是可以删去的從記憶體裝入資料的指令,因為相應的資料已經被存放在寄存器中;另一種是可以删去的将資料存入記憶體的指令,因為相應的資料在再次被改變之前可以一直保留在寄存器中。

如果一個指針變量指向普通記憶體以外的位置,如指向一個外圍裝置的記憶體映射端口,那麼備援裝入和存儲的優化對它來說可能是有害的。例如,為了調整某個操作的時間,可能會用到下述函數:

time_t time_addition(volatile const struct timer * t, int a),

{

int n

int x

time_t then

x=O;

then= t->value

for (n=O; n<1O00; n++)

{

x=x+a ;

}

return t->value - then;

}

在上述函數中,變量t->value實際上是一個硬體計數器,其值随時間增加。該函數執行1000次把a值加到x上的操作,然後傳回t->value在這1000次加法的執行期間所增加的值。

如果不使用volatile修飾符,一個聰明的編譯程式可能就會認為t->value在該函數執行期間不會改變,因為該函數内沒有明确地改變t->value的語句。這樣,編譯程式就會認為沒有必要再次從記憶體中讀入t->value并将其減去then,因為答案永遠是0。是以,編譯程式可能會對該函數進行“優化”,結果使得該函數的傳回值永遠是0。

如果一個指針變量指向共享記憶體中的資料,那麼備援裝入和存儲的優化對它來說可能也是有害的,共享記憶體通常用來實作兩個程式之間的互相通訊,即讓一個程式把資料存到共享的那塊記憶體中,而讓另一個程式從這塊記憶體中讀資料。如果從共享記憶體裝入資料或把資料存入共享記憶體的代碼被編譯程式優化掉了,程式之間的通訊就會受到影響。

請參見:

2.7 一個變量可以同時被說明為const和volatile嗎?

2.14 什麼時候不應該使用類型強制轉換(typecast)?

 可以。const修飾符的含義是變量的值不能被使用了const修飾符的那段代碼修改,但這并不意味着它不能被這段代碼以外的其它手段修改。例如,在2.6的例子中,通過一個volatile const指針t來存取timer結構。函數time_addition()本身并不修改t->value的值,是以t->value被說明為const。不過,計算機的硬體會修改這個值,是以t->value又被說明為volatile。如果同時用const和volatile來說明一個變量,那麼這兩個修飾符随便哪個在先都行,

請參見:

2.6什麼時候應該使用volatile修飾符?

2.8什麼時候應該使用const修飾符?

2.14什麼時候不應該使用類型強制轉換(typecast)?

 使用const修飾符有幾個原因,第一個原因是這樣能使編譯程式找出程式中不小心改變變量值的錯誤。請看下例:

while ( * str=0) / * programmer meant to write * str! =0 * /

{

/ * some code here * /

strq++;

}

其中的“=”符号是輸入錯誤。如果在說明str時沒有使用const修飾符,那麼相應的程式能通過編譯但不能被正确執行。

第二個原因是效率。如果編譯程式知道某個變量不會被修改,那麼它可能會對生成的代碼進行某些優化。

如果一個函數參數是一個指針,并且你不希望它所指向的資料被該函數或該函數所調用的函數修改,那麼你應該把該參數說明為const指針。如果一個函數參數通過值(而不是通過指針)被傳遞給函數,并且你不希望其值被該函數所調用的函數修改,那麼你應該把該參數說明為const。然而,在實際程式設計中,隻有在編譯程式通過指針存取這些資料的效率比拷貝這些資料更高時,才把這些參數說明為const。

請參見:

2.7 一個變量可以同時被說明為const和volatile嗎?

2.14 什麼時候不應該使用類型強制轉換(typecast)?

2.18用const說明常量有什麼好處?

浮點數是計算機程式設計中的“魔法(black art)”,原因之一是沒有一種理想的方式可以表示一個任意的數字。電子電氣工程協會(IEEE)已經制定出浮點數的表示标準,但你不能保證所使用的每台機器都遵循這一标準。

即使你使用的機器遵循這一标準,還存在更深的問題。從數學意義上講,兩個不同的數字之間有無窮個實數。計算機隻能區分至少有一位(bit)不同的兩個數字。如果要表示那些無窮無盡的各不相同的數字,就要使用無窮數目的位。計算機隻能用較少的位(通常是32位或64位)來表示一個很大的範圍内的數字,是以它隻能近似地表示大多數數字。

由于浮點數是如此難對付,是以比較一個浮點數和某個值是否相等或不等通常是不好的程式設計習慣。但是,判斷一個浮點數是否大于或小于某個值就安全多了。例如,如果你想以較小的步長依次使用一個範圍内的數字,你可能會編寫這樣一個程式:

#include <stdio.h>

const float first = O.O;

const float last = 70.0

const float small= O.007

main ( )

{

float f;

for (f=first; f !=last && f<last+1.O; f +=small)

printf("f is now %gn", f);

}

然而,舍入誤差(rounding error)和變量small的表示誤差可能導緻f永遠不等于last(f可能會從稍小于last的一個數增加到一個稍大于last的數),這樣,循環會跳過last。加入不等式"f<last+1.0"就是為了防止在這種情況發生後程式繼續運作很長時間。如果運作該程式并且被列印出來的f值是71或更大的數值,就說明已經發生了這種情況。

一種較安全的方法是用不等式"f<last"作為條件來終止循環,例如:

float f;

for(f=first; f<last; f+=small)

你甚至可以預先算出循環次數,然後通過這個整數進行循環計數:

float f;

int count=(last-first)/small;

for(f=first;count-->0;f+=small)

請參見:

2.11 對不同類型的變量進行算術運算會有問題嗎?

要判斷某種特定類型可以容納的最大值或最小值,一種簡便的方法是使用ANSI标準頭檔案limits.h中的預定義值。該檔案包含一些很有用的常量,它們定義了各種類型所能容納的值,下表列出了這些常量:

----------------------------------------------------------------

常 量 描 述

----------------------------------------------------------------

CHAR—BIT char的位數(bit)

CHAR—MAX char的十進制整數最大值

CHAR—MIN char的十進制整數最小值

MB—LEN—MAX 多位元組字元的最大位元組(byte)數

INT—MAX int的十進制最大值

INT—MIN int的十進制最小值

LONG—MAX long的十進制最大值

LONG—MIN long的十進制最小值

SCHAR—MAX signedchar的十進制整數最大值

SCHAR—MIN signedchar的十進制整數最小值

SHRT—MIN short的十進制最小值

SHRT—MAX short的十進制最大值

UCHAR—MAX unsignedchar的十進制整數最大值

UINT—MAX unsignedint的十進制最大值

ULONG—MAX unsignedlongint的十進制最大值

USHRT—MAX unsignedshortint的十進制最大值

-----------------------------------------------------------------

對于整數類型,在使用2的補碼運算的機器(你将使用的機器幾乎都屬此類)上,一個有符号類型可以容納的數字範圍為-2位數-1到(+2位數-1-1),一個無符号類型可以容納的數字範圍為0到(+2位數-1)。例如,一個16位有符号整數可以容納的數字範圍為--215(即-32768)到(+215-1)(即+32767)。

請參見:

10.1用什麼方法存儲标志(flag)效率最高?

10.2什麼是“位螢幕(bitmasking)”?

10.6 16位和32位的數是怎樣存儲的?

C有三類固有的資料類型:指針類型、整數類型和浮點類型;

指針類型的運算限制最嚴,隻限于以下兩種運算:

- 兩個指針相減,僅在兩個指針指向同一數組中的元素時有效。運算結果與對應于兩個指針的數組下标相減的結果相同。

+ 指針和整數類型相加。運算結果為一個指針,該指針與原指針之間相距n個元素,n就是與原指針相加的整數。

浮點類型包括float,double和longdouble這三種固有類型。整數類型包括char,unsigned char,short,unsigned short,int,unsigned int,long和unsigned long。對這些類型都可進行以下4種算術運算:

+ 加

- 減

* 乘

/ 除

對整數類型不僅可以進行上述4種運算,還可進行以下幾種運算:

% 取模或求餘

>> 右移

<< 左移

& 按位與

| 按位或

^ 按位異或

! 邏輯非

~ 取反

盡管C允許你使用“混合模式”的表達式(包含不同類型的算術表達式),但是,在進行運算之前,它會把不同的類型轉換成同一類型(前面提到的指針運算除外)。這種自動轉換類型的過程被稱為“運算符更新(operator promotion)”。

請參見:

2.12什麼是運算符更新(operatorpromotion)?

當兩個不同類型的運算分量(operand)進行運算時,它們會被轉換為能容納它們的最小的類型,并且運算結果也是這種類型。下表列出了其中的規則,在應用這些規則時,你應該從表的頂端開始往下尋找,直到找到第一條适用的規則。

-------------------------------------------------------------

運算分量1 運算分量2 轉換結果

-------------------------------------------------------------

long double 其它任何類型 long double

double 任何更小的類型 double

float 任何更小的類 float

unsigned long 任何整數類 unsigned long

long unsigned>LONG_MAX unsigned long

long 任何更小的類型 long

unsigned 任何有符号類型 unsigned

-------------------------------------------------------------

下面的程式中就有幾個運算符更新的例子。變量n被指派為3/4,因為3和4都是整數,是以先進行整數除法運算,結果為整數0。變量f2被指派為3/4.0,因為4.0是一個float類型,是以整數3也被轉換為float類型,結果為float類型0.75。

#include <stdio.h>

main ()

{

float f1 = 3/4;

float f2 = 3/4.0

printf("3/4== %g or %g depending on the type used. n",f1, f2);

}

請參見:

2.11對不同類型的變量進行算術運算會有問題嗎?

2.13什麼時候應該使用類型強制轉換(typecast)?

在兩種情況下需要使用類型強制轉換。第一種情況是改變運算分量的類型,進而使運算能正确地進行。下面的程式與2.12中的例子相似,但有不同之處。變量n被指派為整數i除以整數j的結果,因為是整數相除,是以結果為0。變量f2也被指派為i除以j的結果,但本例通過(float)類型強制轉換把i轉換成一個float類型,是以執行的是浮點數除法運算(見2.11),結果為0.75。

#include <stdio.h>

main ( )

{

int i = 3;

int j = 4

float f1 =i/j;

float f2= (float) i/j;

printf("3/4== %g or %g depending on the type used. n",f1, f2);

}

第二種情況是在指針類型和void * 類型之間進行強制轉換,進而與期望或傳回void指針的函數進行正确的交接。例如,下述語句就把函數malloc()的傳回值強制轉換為一個指向foo結構的指針:

struct foo *p=(struct foo *)malloc(sizeof(struct foo));

請參見:

2.6什麼時候應該使用volatile修飾符?

2.8什麼時候應該使用const修飾符?

2.11對不同類型的變量進行算術運算會有問題嗎?

2.12 什麼是運算符更新(operator promotion)?

2.14 什麼時候不應該使用類型強制轉換(typecast)?

7.5 什麼是void指針?

7.6 什麼時候使用void指針?

7.21 什麼是堆(heap)?

7.27 可以對void指針進行算術運算嗎?

不應該對用const或volatile說明了的對象進行類型強制轉換,否則程式就不能正确運作。

不應該用類型強制轉換把指向一種結構類型或資料類型的指針轉換成指向另一種結構類型或資料類型的指針。在極少數需要進行這種類型強制轉換的情況下,用共用體(union)來存放有關資料能更清楚地表達程式員的意圖。

請參見:

2. 6什麼時候應該使用volatile修飾符?

2. 8什麼時候應該使用const修飾符?

被多個檔案存取的全局變量可以并且應該在一個頭檔案中說明,并且必須在一個源檔案中定義。變量不應該在頭檔案中定義,因為一個頭檔案可能被多個源檔案包含,而這将導緻變量被多次定義。如果變量的初始化隻發生一次,ANSIC标準允許變量有多次外部定義;但是,這樣做沒有任何好處,是以最好避免這樣做,以使程式有更強的可移植性。

注意:變量的說明和定義是兩個不同的概念,在2.16中将講解兩者之間的差別。

僅供一個檔案使用的“全局”變量應該被說明為static,而且不應該出現在頭檔案中。

請參見:

2. 16 說明一個變量和定義一個變量有什麼差別?

2. 17 可以在頭檔案中說明static變量嗎?

說明一個變量意味着向編譯程式描述變量的類型,但并不為變量配置設定存儲空間。定義一個變量意味着在說明變量的同時還要為變量配置設定存儲空間。在定義一個變量的同時還可以對變量進行初始化。下例說明了一個變量和一個結構,定義了兩個變量,其中一個定義帶初始化:

extern int decll; / * this is a declaration * /

struct decl2 {

int member;

} ; / * this just declares the type--no variable mentioned * /

int def1 = 8; / * this is a definition * /

int def2; / * this is a definition * /

換句話說,說明一個變量相當于告訴編譯程式“在程式的某個位置将用到一個變量,這裡給出了它的名稱和類型”,定義一個變量則相當于告訴編譯程式“具有這個名稱和這種類型的變量就在這裡”。

一個變量可以被說明許多次,但隻能被定義一次。是以,不應該在頭檔案中定義變量,因為一個頭檔案可能會被一個程式的許多源檔案所包含。

請參見;

2.17可以在頭檔案中說明static變量嗎?

如果說明了一個static變量,就必須在同一個檔案中定義該變量(因為存儲類型修飾符static和extern是互斥的)。你可以在頭檔案中定義一個static變量,但這會使包含該頭檔案的源檔案都得到該變量的一份私有拷貝,而這通常不是你想得到的結果。

請參見:

2.16 說明一個變量和定義一個變量有什麼差別?

使用關鍵字const有兩個好處;第一,如果編譯程式知道一個變量的值不會改變,編譯程.序就能對程式進行優化;第二,編譯程式會試圖保證該變量的值不會因為程式員的疏忽而被改變。

當然,用#define來定義常量也有同樣的好處。用const而不用#define來定義常量的原因是const變量可以是任何類型(如結構,而用#define定義的常量不能表示結構)。此外,const變量是真正的變量,它有可供使用的位址,并且該位址是唯一的(有些編譯程式在每次使用用#define定義的字元串時都會生成一份新的拷貝,見9.9)。

請參見:

2.7 一個變量可以同時被說明為const和volatile嗎?

2.8 什麼時候應該使用const修飾符?

2.14 什麼時候不應該使用類型強制轉換(typecast)?

9.9 字元串和數組有什麼不同?