C語言編程的十六個問題

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

2006-4-13閱讀4277 評論3

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

參考文獻
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; /*legal*/
str++; /*legal*/
*str='a'; /* illegal */
str[1]='b'; /*illegal*/
前兩條語句是合法的,因爲它們沒有修改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 字符串和數組有什麼不同?
 

發佈了1 篇原創文章 · 獲贊 9 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章