C語言基本知識

C語言的字符集

C語言字符集由字母,數字,空格,標點和特殊字符組成。

1.字母

小寫字母az26

大寫字母AZ26

2.數字

0910

3.空白符

空格符、製表符、換行符等統稱爲空白符。空白符只在字符常量和字符串常量中起作用。在其它地方出現時,只起間隔作用,編譯程序對它們忽略不計。因此在程序中使用空白符與否,對程序的編譯不發生影響,但在程序中適當的地方使用空白符將增加程序的清晰性和可讀性。

4.標點和特殊字符

27個特殊字符:+ - * / = : ;? \ ~| ! # % & () [] ^ <> _ , .空格

1.10 書寫程序時應遵循的規則

1.一個說明或一個語句佔一行。

2.{}括起來的部分,通常表示了程序的某一層次結構。{}一般與該結構語句的第一個字母對齊,並單獨佔一行。

3.低一層次的語句或說明可比高一層次的語句或說明縮進若干格後書寫。以便看起來更加清晰,增加程序的可讀性。

1.8 輸入和輸出函數scanfprintf

 

常用的轉義字符及其含義

轉義字符

轉義字符的意義

ASCII代碼

\n

回車換行

10

\t

橫向跳到下一製表位置

9

\b

退格

8

\r

回車

13

\f

走紙換頁

12

\\

反斜線符"\"

92

\'

單引號符

39

\”

雙引號符

34

\a

鳴鈴

7

\ddd

13位八進制數所代表的字符

\xhh

12位十六進制數所代表的字符

 

運算符和表達式

C語言的運算符可分爲以下幾類:

1. 算術運算符:用於各類數值運算。包括加(+)、減(-)、乘(*)、除(/)、求餘(或稱模運算,%)、自增(++)、自減(--)-(取負)共八種。

注意:a:除運算符/要求兩個操作數(被除數和除數),其中除數不可以爲零;且當兩個整數相除時,他們的結果一定爲整數。如7/2結果爲3.

b:取餘運算符%兩邊只能是證書或整形變量。如18%7值爲4。靈活運用%和運算符可以方便地取出一個整數的個位數(對10取餘)或去掉一個整數的個位(整除10

c:自增運算符++和自減運算符都是單目運算符。若運算符放在變量的前面,則稱前置運算,反之。

++i—I    前置

I++i--    後置

前置運算是變量的值先加一或減一,然後參加表達式的運算。後置運算則是變量先參加表達式的運算,然後再增一或減一

2. 關係運算符:用於比較運算。包括大於(>)、小於(<)、等於(==)、大於等於(>=)、小於等於(<=)和不等於(!=)六種。

 

3. 邏輯運算符:用於邏輯運算。包括與(&&)、或(||)、非(!)三種。

4. 位操作運算符:參與運算的量,按二進制位進行運算。包括位與(&)、位或(|)、位非(~)、位異或(^)、左移(<<)、右移(>>)六種。

5. 賦值運算符:用於賦值運算,分爲簡單賦值(=)、複合算術賦值(+=,-=,*=,/=,%=)和複合位運算賦值(&=,|=,^=,>>=,<<=)三類共十一種。

構成複合賦值表達式的一般形式爲:

變量 雙目運算符=表達式

它等效於

變量=變量運算符 表達式

例如:

a+=5等價於a=a+5

x*=y+7等價於x=x*(y+7)

r%=p等價於r=r%p

複合賦值符這種寫法,對初學者可能不習慣,但十分有利於編譯處理,能提高編譯效率併產生質量較高的目標代碼。

6. 條件運算符:這是一個三目運算符,用於條件求值(?:)

7. 逗號運算符:用於把若干表達式組合成一個表達式()

8. 指針運算符:用於取內容(*)和取地址(&)二種運算。

9. 求字節數運算符:用於計算數據類型所佔的字節數(sizeof)

10. 特殊運算符:有括號(),下標[],成員(→,.)等幾種。

 

4.5.1 printf函數(格式輸出函數)

printf函數稱爲格式輸出函數,其關鍵字最末一個字母f即爲格式(format)之意。其功能是按用戶指定的格式,把指定的數據顯示到顯示器屏幕上。

%d表示按十進制整型輸出;

%ld表示按十進制長整型輸出;

%c表示按字符型輸出等。

 

2. 格式字符串 在Turbo C中格式字符串的一般形式爲:

[標誌][輸出最小寬度][.精度][長度]類型

其中方括號[]中的項爲可選項。

各項的意義介紹如下:

1) 類型:類型字符用以表示輸出數據的類型,其格式符和意義如下表所示:

格式字符

意 義

d

以十進制形式輸出帶符號整數(正數不輸出符號)

o

以八進制形式輸出無符號整數(不輸出前綴0)

x,X

以十六進制形式輸出無符號整數(不輸出前綴Ox)

u

以十進制形式輸出無符號整數

f

以小數形式輸出單、雙精度實數

e,E

以指數形式輸出單、雙精度實數

g,G

以%f或%e中較短的輸出寬度輸出單、雙精度實數

c

輸出單個字符

s

輸出字符串

 

 

2) 標誌:標誌字符爲-、+、#、空格四種,其意義下表所示:

標 志

意 義

-

結果左對齊,右邊填空格

+

輸出符號(正號或負號)

空格

輸出值爲正時冠以空格,爲負時冠以負號

#

對c,s,d,u類無影響;對o類,在輸出時加前綴o;對x類,在輸出時加前綴0x;對e,g,f 類當結果有小數時纔給出小數點

3) 輸出最小寬度:用十進制整數來表示輸出的最少位數。若實際位數多於定義的寬度,則按實際位數輸出,若實際位數少於定義的寬度則補以空格或0。

4) 精度:精度格式符以.開頭,後跟十進制整數。本項的意義是:如果輸出數字,則表示小數的位數;如果輸出的是字符,則表示輸出字符的個數;若實際位數大於所定義的精度數,則截去超過的部分。

5.長度:長度格式符爲h,l兩種,h表示按短整型量輸出,l表示按長整型量輸出。

 

4.5.2 scanf函數(格式輸入函數)

1. scanf函數的一般形式 scanf函數是一個標準庫函數,它的函數原型在頭文件stdio.h中,與printf函數相同,C語言也允許在使用scanf函數之前不必包含stdio.h文件。

scanf函數的一般形式爲:

scanf(格式控制字符串,地址表列);

 

2. 格式字符串 格式字符串的一般形式爲:

%[*][輸入數據寬度][長度]類型

其中有方括號[]的項爲任選項。各項的意義如下:

1) 類型:表示輸入數據的類型,其格式符和意義如下表所示。

表示輸入數據的類型,其格式符和意義如下表所示。

格式

字符意義

d

輸入十進制整數

o

輸入八進制整數

x

輸入十六進制整數

u

輸入無符號十進制整數

f或e

輸入實型數(用小數形式或指數形式)

c

輸入單個字符

s

輸入字符串

 

2) *符:用以表示該輸入項,讀入後不賦予相應的變量,即跳過該輸入值。 如:

scanf("%d %*d %d",&a,&b);

當輸入爲:1 2 3時,把1賦予a,2被跳過,3賦予b。

3) 寬度:用十進制整數指定輸入的寬度(即字符數)。

例如:

scanf("%5d",&a);

輸入:12345678

只把12345賦予變量a,其餘部分被截去。

又如:

 

scanf("%4d%4d",&a,&b);

輸入:12345678

將把1234賦予a,而把5678賦予b。

4) 長度:長度格式符爲l和h,l表示輸入長整型數據(如%ld) 和雙精度浮點數(如%lf)。h表示輸入短整型數據。

使用scanf函數還必須注意以下幾點:

1) scanf函數中沒有精度控制,如:scanf("%5.2f",&a);是非法的。不能企圖用此語句輸入小數爲2位的實數。

2) scanf中要求給出變量地址,如給出變量名則會出錯。如scanf("%d",a);是非法的,應改爲scnaf("%d",&a);纔是合法的。

3) 在輸入多個數值數據時,若格式控制串中沒有非格式字符作輸入數據之間的間隔則可用空格,TAB或回車作間隔。C編譯在碰到空格,TAB,回車或非法數據(如對%d輸入12A時,A即爲非法數據)時即認爲該數據結束。

4) 在輸入字符數據時,若格式控制串中無非格式字符,則認爲所有輸入的字符均爲有效字符。

 

4.4.1 putchar 函數(字符輸出函數)

 

putchar 函數是字符輸出函數, 其功能是在顯示器上輸出單個字符。

其一般形式爲:

putchar(字符變量)

 

例如:

putchar('A');(輸出大寫字母A

putchar(x);(輸出字符變量x的值)

putchar(\101);(也是輸出字符A

putchar('\n'); (換行)

對控制字符則執行控制功能,不在屏幕上顯示。

使用本函數前必須要用文件包含命令:

#include<stdio.h>

#include stdio.h

 

4.4.2 getchar函數(鍵盤輸入函數)

getchar函數的功能是從鍵盤上輸入一個字符。

其一般形式爲:

getchar();

通常把輸入的字符賦予一個字符變量,構成賦值語句,如:

char c;

c=getchar();

使用getchar函數還應注意幾個問題:

1) getchar函數只能接受單個字符,輸入數字也按字符處理。輸入多於一個字符時,只接收第一個字符。

2) 使用本函數前必須包含文件stdio.h

3) 在TC屏幕下運行含本函數程序時,將退出TC 屏幕進入用戶屏幕等待用戶輸入。輸入完畢再返回TC屏幕。

4) 程序最後兩行可用下面兩行的任意一行代替:

putchar(getchar());

printf(%c,getchar())

 

4 最簡單的C程序設計順序程序設計

C程序的執行部分是由語句組成的。 程序的功能也是由執行語句實現的。

C語句可分爲以下五類:

1) 表達式語句

2) 函數調用語句

3) 控制語句

4) 複合語句

5) 空語句

 

1. 表達式語句:表達式語句由表達式加上分號組成。

其一般形式爲:

表達式;

執行表達式語句就是計算表達式的值。

例如:

 

x=y+z; 賦值語句;

y+z; 加法運算語句,但計算結果不能保留,無實際意義;

i++; 自增1語句,i值增1。

2. 函數調用語句:由函數名、實際參數加上分號組成。

其一般形式爲:

函數名(實際參數表);

執行函數語句就是調用函數體並把實際參數賦予函數定義中的形式參數,然後執行被調函數體中的語句,求取函數值 (在後面函數中再詳細介紹) 。

例如:

printf("C Program");調用庫函數,輸出字符串。

3. 控制語句:控制語句用於控制程序的流程, 以實現程序的各種結構方式。它們由特定的語句定義符組成。C語言有九種控制語句。可分成以下三類:

1) 條件判斷語句:if語句、switch語句;

2) 循環執行語句:do while語句、while語句、for語句;

3) 轉向語句:break語句、goto語句、continue語句、return語句。

4. 複合語句:把多個語句用括號{}括起來組成的一個語句稱複合語句。

在程序中應把複合語句看成是單條語句,而不是多條語句。

例如:

{ x=y+z;

a=b+c;

printf(%d%d,x,a);

}

是一條複合語句。

複合語句內的各條語句都必須以分號;結尾,在括號}外不能加分號。

5. 空語句:只有分號組成的語句稱爲空語句。空語句是什麼也不執行的語句。在程序中空語句可用來作空循環體。

例如

while(getchar()!='\n')

;

本語句的功能是,只要從鍵盤輸入的字符不是回車則重新輸入。

這裏的循環體爲空語句。

 

5.3 if語句

 

5.3.1 if語句的三種形式

1. 第一種形式爲基本形式:if if(表達式) 語句

其語義是:如果表達式的值爲真,則執行其後的語句, 否則不執行該語句。

 

2. 第二種形式爲: if-else if(表達式)

 

語句1

else

語句2

其語義是:如果表達式的值爲真,則執行語句1,否則執行語句2 。

 

3. 第三種形式爲if-else-if形式前二種形式的if語句一般都用於兩個分支的情況。 當有多個分支選擇時,可採用if-else-if語句,其一般形式爲:

if(表達式1)

 

語句1

elseif(表達式2)

語句2

elseif(表達式3)

語句3

elseif(表達式m)

語句m

else

語句n

其語義是:依次判斷表達式的值,當出現某個值爲真時,則執行其對應的語句。然後跳到整個if語句之外繼續執行程序。 如果所有的表達式均爲假,則執行語句n。然後繼續執行後續程序。

 

4. 在使用if語句中還應注意以下問題:

 

1) 在三種形式的if語句中,在if關鍵字之後均爲表達式。 該表達式通常是邏輯表達式或關係表達式, 但也可以是其它表達式,如賦值表達式等,甚至也可以是一個變量。例如:

if(a=5) 語句;

if(b) 語句;

都是允許的。只要表達式的值爲非0,即爲

如在:

if(a=5)

中表達式的值永遠爲非0,所以其後的語句總是要執行的,當然這種情況在程序中不一定會出現,但在語法上是合法的。

又如,有程序段:

if(a=b)

 

printf("%d",a);

else

printf("a=0");

本語句的語義是,把b值賦予a,如爲非0則輸出該值,否則輸出a=0字符串。這種用法在程序中是經常出現的。

2) 在if語句中,條件判斷表達式必須用括號括起來,在語句之後必須加分號。

3) 在if語句的三種形式中,所有的語句應爲單個語句,如果要想在滿足條件時執行一組(多個)語句,則必須把這一組語句用{}括起來組成一個複合語句。但要注意的是在}之後不能再加分號。

例如:

if(a>b)

{a++;

 

b++;}

else

{a=0;

b=10;}

C語言規定,else 總是與它前面最近的if配對,

 

5.4 switch語句C語言還提供了另一種用於多分支選擇的switch語句,其一般形式爲:

switch(表達式){

 

case常量表達式1: 語句1;

case常量表達式2: 語句2;

case常量表達式n: 語句n;

default: 語句n+1;

}

其語義是:計算表達式的值。 並逐個與其後的常量表達式值相比較,當表達式的值與某個常量表達式的值相等時,即執行其後的語句,然後不再進行判斷,繼續執行後面所有case後的語句。如表達式的值與所有case後的常量表達式均不相同時,則執行default後

的語句。

【例4.9

main(){

inta;

printf("inputinteger number: ");

scanf("%d",&a);

switch(a){

case1:printf("Monday\n");

case2:printf("Tuesday\n");

case3:printf("Wednesday\n");

case4:printf("Thursday\n");

case5:printf("Friday\n");

case6:printf("Saturday\n");

case7:printf("Sunday\n");

default:printf("error\n");

}

}

本程序是要求輸入一個數字,輸出一個英文單詞。但是當輸入3之後,卻執行了case3以及以後的所有語句,輸出了Wednesday 及以後的所有單詞。這當然是不希望的。爲什麼會出現這種情況呢?這恰恰反應了switch語句的一個特點。在switch語句中,case常量表達式只相當於一個語句標號, 表達式的值和某標號相等則轉向該標號執行,但不能在執行完該標號的語句後自動跳出整個switch語句,所以出現了繼續執行所有後面case語句的情況。 這是與前面介紹的if語句完全不同的,應特別注意。爲了避免上述情況,C語言還提供了一種break語句,專用於跳出switch語句,break 語句只有關鍵字break,沒有參數。在後面還將詳細介紹。修改例題的程序,在每一case語句之後增加break 語句, 使每一次執行之後均可跳出switch語句,從而避免輸出不應有的結果。

【例4.10

main(){

inta;

printf("inputinteger number: ");

scanf("%d",&a);

switch(a){

case1:printf("Monday\n");break;

case2:printf("Tuesday\n"); break;

case3:printf("Wednesday\n");break;

case4:printf("Thursday\n");break;

case5:printf("Friday\n");break;

case6:printf("Saturday\n");break;

case7:printf("Sunday\n");break;

default:printf("error\n");

}

}

在使用switch語句時還應注意以下幾點:

1) 在case後的各常量表達式的值不能相同,否則會出現錯誤。

2) 在case後,允許有多個語句,可以不用{}括起來。

3) 各case和default子句的先後順序可以變動,而不會影響程序執行結果。

4) default子句可以省略不用。

 

6 循環控制

6.1 概述循環結構是程序中一種很重要的結構。其特點是,在給定條件成立時,反覆執行某程序段,直到條件不成立爲止。給定的條件稱爲循環條件,反覆執行的程序段稱爲循環體。C語言提供了多種循環語句,可以組成各種不同形式的循環結構。

1) 用goto語句和if語句構成循環;

2) 用while語句;

3) 用do-while語句;

4) 用for語句;

 

6.2 goto語句以及用goto語句構成循環goto語句是一種無條件轉移語句,與BASIC中的goto語句相似。goto 語句的使用格式爲:

 

goto 語句標號;

其中標號是一個有效的標識符,這個標識符加上一個:一起出現在函數內某處,執行goto語句後,程序將跳轉到該標號處並執行其後的語句。另外標號必須與goto語句同處於一個函數中,但可以不在一個循環層中。通常goto語句與if條件語句連用, 當滿足某一條件時, 程序跳到標號處運行。

goto語句通常不用,主要因爲它將使程序層次不清,且不易讀,但在多層嵌套退出時, 用goto語句則比較合理。

【例6.1】用goto語句和if語句構成循環,。Σ=1001nn

main()

{

inti,sum=0;

i=1;

loop:if(i<=100)

{sum=sum+i;

i++;

gotoloop;}

printf("%d\n",sum);

}

 

6.3 while語句

while語句的一般形式爲:

while(表達式)語句

 

其中表達式是循環條件,語句爲循環體。

while語句的語義是:計算表達式的值,當值爲真(非0)時, 執行循環體語句。

【例6.2】用while語句求。 Σ=1001nn

用傳統流程圖和N-S結構流程圖表示算法

 

6.4 do-while語句do-while語句的一般形式爲:

 

do

語句

while(表達式)

這個循環與while循環的不同在於:它先執行循環中的語句,然後再判斷表達式是否爲真, 如果爲真則繼續循環;如果爲假, 則終止循環。因此, do-while循環至少要執行一次循環語句。

 

6.5 for語句在C語言中,for語句使用最爲靈活,它完全可以取代 while 語句。它的一般形式爲:

 

for(表達式1;表達式2;表達式3) 語句

它的執行過程如下:

1) 先求解表達式1

2) 求解表達式2,若其值爲真(非0),則執行for語句中指定的內嵌語句,然後執行下面第3)步;若其值爲假(0),則結束循環,轉到第5)步。

3) 求解表達式3

4) 轉回上面第2)步繼續執行。

5) 循環結束,執行for語句下面的一個語句。

for語句最簡單的應用形式也是最容易理解的形式如下:

for(循環變量賦初值;循環條件;循環變量增量)語句

循環變量賦初值總是一個賦值語句, 它用來給循環控制變量賦初值; 循環條件是一個關係表達式,它決定什麼時候退出循環;循環變量增量,定義循環控制變量每循環一次後按什麼方式變化。

對於for循環中語句的一般形式,就是如下的while循環形式:

表達式1

while(表達式2

{語句

表達式3

}

注意:

1) for循環中的表達式1(循環變量賦初值)表達式2(循環條件)表達式3(

 

環變量增量)都是選擇項,即可以缺省,但不能缺省。

2) 省略了表達式1(循環變量賦初值),表示不對循環控制變量賦初值。

3) 省略了表達式2(循環條件),則不做其它處理時便成爲死循環。 例如:

for(i=1;;i++)sum=sum+i;

相當於:

i=1;

while(1)

{sum=sum+i;

i++;}

4) 省略了表達式3(循環變量增量),則不對循環控制變量進行操作,這時可在語句體中加入修改循環控制變量的語句。

例如:

for(i=1;i<=100;)

{sum=sum+i;

i++;}

5) 省略了表達式1(循環變量賦初值)表達式3(循環變量增量)

例如:

for(;i<=100;)

{sum=sum+i;

i++;}

相當於:

while(i<=100)

{sum=sum+i;

i++;}

6) 3個表達式都可以省略。

例如:

for(;;)語句

相當於:

while(1)語句

7) 表達式1可以是設置循環變量的初值的賦值表達式,也可以是其他表達式。

例如:

for(sum=0;i<=100;i++)sum=sum+i;

8) 表達式1和表達式3可以是一個簡單表達式也可以是逗號表達式。

for(sum=0,i=1;i<=100;i++)sum=sum+i;

 

或:

for(i=0,j=100;i<=100;i++,j--)k=i+j;

9) 表達式2一般是關係表達式或邏輯表達式,但也可是數值表達式或字符表達式,只要其值非零,就執行循環體。

例如:

for(i=0;(c=getchar())!=\n;i+=c);

又如:

for(;(c=getchar())!=\n;)

printf(%c,c);

 

6.7 幾種循環的比較

1) 四種循環都可以用來處理同一個問題,一般可以互相代替。但一般不提倡用goto型循環。

2) whiledo-while循環,循環體中應包括使循環趨於結束的語句。for語句功能最強。

3) whiledo-while循環時,循環變量初始化的操作應在whiledo-while語句之前完成,而for語句可以在表達式1中實現循環變量的初始化。

 

 

7.2 二維數組的定義和引用

7.2.1 二維數組的定義前面介紹的數組只有一個下標,稱爲一維數組,其數組元素也稱爲單下標變量。在實際問題中有很多量是二維的或多維的,因此C語言允許構造多維數組。多維數組元素有多個下標,以標識它在數組中的位置,所以也稱爲多下標變量。本小節只介紹二維數組,多維數組可由二維數組類推而得到。

二維數組定義的一般形式是:

類型說明符 數組名[常量表達式1][常量表達式2]

其中常量表達式1表示第一維下標的長度,常量表達式2 表示第二維下標的長度。

例如:

int a[3][4];

說明了一個三行四列的數組,數組名爲a,其下標變量的類型爲整型。該數組的下標變量共有3×4個,即:

a[0][0],a[0][1],a[0][2],a[0][3]

a[1][0],a[1][1],a[1][2],a[1][3]

a[2][0],a[2][1],a[2][2],a[2][3]

二維數組在概念上是二維的,即是說其下標在兩個方向上變化,下標變量在數組中的位置也處於一個平面之中,而不是象一維數組只是一個向量。但是,實際的硬件存儲器卻是連續編址的,也就是說存儲器單元是按一維線性排列的。如何在一維存儲器中存放二維數組,可有兩種方式:一種是按行排列, 即放完一行之後順次放入第二行。另一種是按列排列, 即放完一列之後再順次放入第二列。在C語言中,二維數組是按行排列的。

即:

先存放a[0]行,再存放a[1]行,最後存放a[2]行。每行中有四個元素也是依次存放。由於數組a說明爲int類型,該類型佔兩個字節的內存空間,所以每個元素均佔有兩個字節)。

 

8.2 函數定義的一般形式

1. 無參函數的定義形式

類型標識符 函數名()

{聲明部分

語句

}

其中類型標識符和函數名稱爲函數頭。類型標識符指明瞭本函數的類型,函數的類型實際上是函數返回值的類型。 該類型標識符與前面介紹的各種說明符相同。函數名是由用戶定義的標識符,函數名後有一個空括號,其中無參數,但括號不可少。

{}中的內容稱爲函數體。在函數體中聲明部分,是對函數體內部所用到的變量的類型說明。

在很多情況下都不要求無參函數有返回值,此時函數類型符可以寫爲void

我們可以改寫一個函數定義:

void Hello()

{

printf("Hello,world \n");

}

這裏,只把main改爲Hello作爲函數名,其餘不變。Hello函數是一個無參函數,當被其它函數調用時,輸出Hello world字符串。

2. 有參函數定義的一般形式

類型標識符 函數名(形式參數表列)

{聲明部分

語句

}

有參函數比無參函數多了一個內容,即形式參數表列。在形參表中給出的參數稱爲形式參數,它們可以是各種類型的變量,各參數之間用逗號間隔。在進行函數調用時,主調函數將賦予這些形式參數實際的值。形參既然是變量,必須在形參表中給出形參的類型說明。

例如,定義一個函數,用於求兩個數中的大數,可寫爲:

intmax(int a, int b)

{

if(a>b) return a;

elsereturn b;

}

第一行說明max函數是一個整型函數,其返回的函數值是一個整數。形參爲a,b,均爲整型量。a,b的具體值是由主調函數在調用時傳送過來的。在{}中的函數體內,除形參外沒有使用其它變量,因此只有語句而沒有聲明部分。在max函數體中的return語句是把a(或b)的值作爲函數的值返回給主調函數。有返回值函數中至少應有一個return語句。

在C程序中,一個函數的定義可以放在任意位置,既可放在主函數main之前,也可放在main之後。

例如:

可把max 函數置在main之後,也可以把它放在main之前。修改後的程序如下所示。

【例8.1

intmax(int a,int b)

{

if(a>b)returna;

elsereturn b;

}

main()

{

intmax(int a,int b);

intx,y,z;

printf("inputtwo numbers:\n");

scanf("%d%d",&x,&y);

z=max(x,y);

printf("maxmum=%d",z);

}

現在我們可以從函數定義、函數說明及函數調用的角度來分析整個程序,從中進一步瞭解函數的各種特點。

程序的第1行至第5行爲max函數定義。進入主函數後,因爲準備調用max函數,故先對max函數進行說明(程序第8行)。函數定義和函數說明並不是一回事,在後面還要專門討論。 可以看出函數說明與函數定義中的函數頭部分相同,但是末尾要加分號。程序第12 行爲調用max函數,並把x, y中的值傳送給max的形參a, b。max函數執行的結果(a或b

 

8.3.1 形式參數和實際參數

前面已經介紹過,函數的參數分爲形參和實參兩種。在本小節中,進一步介紹形參、實參的特點和兩者的關係。形參出現在函數定義中,在整個函數體內都可以使用,離開該函數則不能使用。實參出現在主調函數中,進入被調函數後,實參變量也不能使用。形參和實參的功能是作數據傳送。發生函數調用時,主調函數把實參的值傳送給被調函數的形參從而實現主調函數向被調函數的數據傳送。

函數的形參和實參具有以下特點:

1. 形參變量只有在被調用時才分配內存單元,在調用結束時,即刻釋放所分配的內存單元。因此,形參只有在函數內部有效。函數調用結束返回主調函數後則不能再使用該形參變量。

2. 實參可以是常量、變量、表達式、函數等,無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值,以便把這些值傳送給形參。因此應預先用賦值,輸入等辦法使實參獲得確定值。

3. 實參和形參在數量上,類型上,順序上應嚴格一致,否則會發生類型不匹配的錯誤。

4. 函數調用中發生的數據傳送是單向的。即只能把實參的值傳送給形參,而不能把形參的值反向地傳送給實參。因此在函數調用過程中,形參的值發生改變,而實參中的值不會變化。

 

【例8.2】可以說明這個問題。

main()

{

intn;

printf("inputnumber\n");

scanf("%d",&n);

s(n);

printf("n=%d\n",n);

}

ints(int n)

{

inti;

for(i=n-1;i>=1;i--)

n=n+i;

printf("n=%d\n",n);

}

本程序中定義了一個函數s,該函數的功能是求Σni的值。在主函數中輸入n值,並作爲實參,在調用時傳送給s 函數的形參量n( 注意,本例的形參變量和實參變量的標識符都爲n,但這是兩個不同的量,各自的作用域不同)。在主函數中用printf 語句輸出一次n值,這個n值是實參n的值。在函數s中也用printf 語句輸出了一次n值,這個n值是形參最後取得的n值0。從運行情況看,輸入n值爲100。即實參n的值爲100。把此值傳給函數s時,形參n的初值也爲100,在執行函數過程中,形參n的值變爲5050。返回主函數之後,輸出實參n的值仍爲100。可見實參的值不隨形參的變化而變化。

8.4.1 函數調用的一般形式

C語言中,函數調用的一般形式爲:

函數名(實際參數表)

對無參函數調用時則無實際參數表。實際參數表中的參數可以是常數,變量或其它構造類型數據及表達式。各實參之間用逗號分隔。

 

8.5 函數的嵌套調用

 

C語言中不允許作嵌套的函數定義。因此各函數之間是平行的,不存在上一級函數和下一級函數的問題。但是C語言允許在一個函數的定義中出現對另一個函數的調用。這樣就出現了函數的嵌套調用。即在被調函數中又調用其它函數。這與其它語言的子程序嵌套的情形是類似的。

 

8.6 函數的遞歸調用一個函數在它的函數體內調用它自身稱爲遞歸調用。這種函數稱爲遞歸函數。C語言允許函數的遞歸調用。在遞歸調用中,主調函數又是被調函數。執行遞歸函數將反覆調用其自身,每調用一次就進入新的一層。

例如有函數f如下:

 

int f(int x)

{

int y;

z=f(y);

return z;

}

這個函數是一個遞歸函數。但是運行該函數將無休止地調用其自身,這當然是不正確的。爲了防止遞歸調用無終止地進行,必須在函數內有終止遞歸調用的手段。常用的辦法是加條件判斷,滿足某種條件後就不再作遞歸調用,然後逐層返回。下面舉例說明遞歸調用的執行過程。

【例8.5】用遞歸法計算n!

用遞歸法計算n!可用下述公式表示:

n!=1(n=0,1)

n×(n-1)!(n>1)

按公式可編程如下:

longff(int n)

{

longf;

if(n<0)printf("n<0,input error");

elseif(n==0||n==1) f=1;

elsef=ff(n-1)*n;

return(f);

}

main()

{

intn;

longy;

printf("\ninputa inteager number:\n");

scanf("%d",&n);

y=ff(n);

printf("%d!=%ld",n,y);

}

程序中給出的函數ff是一個遞歸函數。主函數調用ff後即進入函數ff執行,如果n<0,n==0n=1時都將結束函數的執行,否則就遞歸調用ff函數自身。由於每次遞歸調用的實參爲n-1,即把n-1的值賦予形參n,最後當n-1的值爲1時再作遞歸調用,形參n的值也爲1,將使遞歸終止。然後可逐層退回。

 

8.9.2 auto變量函數中的局部變量,如不專門聲明爲static存儲類別,都是動態地分配存儲空間的,數據存儲在動態存儲區中。函數中的形參和在函數中定義的變量(包括在複合語句中定義的變量),都屬此類,在調用該函數時系統會給它們分配存儲空間,在函數調用結束時就自動釋放這些存儲空間。這類局部變量稱爲自動變量。自動變量用關鍵字auto作存儲類別的聲明。

例如:

int f(int a) /*定義f函數,a爲參數*/

 

{auto int b,c=3; /*定義bc自動變量*/

……

}

a是形參,bc是自動變量,對c賦初值3。執行完f函數後,自動釋放abc所佔的存儲單元。

關鍵字auto可以省略,auto不寫則隱含定爲自動存儲類別,屬於動態存儲方式。

8.9.3 static聲明局部變量

有時希望函數中的局部變量的值在函數調用結束後不消失而保留原值,這時就應該指定局部變量爲靜態局部變量,用關鍵字static進行聲明。

 

【例8.15】考察靜態局部變量的值。

f(inta)

{autob=0;

staticc=3;

b=b+1;

c=c+1;

return(a+b+c);

}

main()

{inta=2,i;

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

printf("%d",f(a));

}

對靜態局部變量的說明:

1) 靜態局部變量屬於靜態存儲類別,在靜態存儲區內分配存儲單元。在程序整個運行期間都不釋放。而自動變量(即動態局部變量)屬於動態存儲類別,佔動態存儲空間,函數調用結束後即釋放。

2) 靜態局部變量在編譯時賦初值,即只賦初值一次;而對自動變量賦初值是在函數調用時進行,每調用一次函數重新給一次初值,相當於執行一次賦值語句。

3) 如果在定義局部變量時不賦初值的話,則對靜態局部變量來說,編譯時自動賦初值0(對數值型變量)或空字符(對字符變量)。而對自動變量來說,如果不賦初值則它的值是一個不確定的值。

 

【例8.16】打印1到5的階乘值。

intfac(int n)

{staticint f=1;

f=f*n;

return(f);

}

main()

{inti;

for(i=1;i<=5;i++)

printf("%d!=%d\n",i,fac(i));

}

8.9.4 register變量

爲了提高效率,C語言允許將局部變量得值放在CPU中的寄存器中,這種變量叫寄存器變量,用關鍵字register作聲明。

 

【例8.17】使用寄存器變量。

intfac(int n)

{registerint i,f=1;

for(i=1;i<=n;i++)

f=f*i

return(f);

}

main()

{int i;

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

printf("%d!=%d\n",i,fac(i));

}

說明:

1) 只有局部自動變量和形式參數可以作爲寄存器變量;

2) 一個計算機系統中的寄存器數目有限,不能定義任意多個寄存器變量;

3) 局部靜態變量不能定義爲寄存器變量。

8.9.5 extern聲明外部變量

外部變量(即全局變量)是在函數的外部定義的,它的作用域爲從變量定義處開始,到本程序文件的末尾。如果外部變量不在文件的開頭定義,其有效的作用範圍只限於定義處到文件終了。如果在定義點之前的函數想引用該外部變量,則應該在引用之前用關鍵字extern對該變量作外部變量聲明。表示該變量是一個已經定義的外部變量。有了此聲明,就可以從聲明處起,合法地使用該外部變量。

 

【例8.18】用extern聲明外部變量,擴展程序文件中的作用域。

intmax(int x,int y)

{intz;

z=x>y?x:y;

return(z);

}

main()

{extern A,B;

printf("%d\n",max(A,B));

}

intA=13,B=-8;

說明:在本程序文件的最後1行定義了外部變量AB,但由於外部變量定義的位置在函數main之後,因此本來在main函數中不能引用外部變量AB。現在我們在main函數中用externAB進行外部變量聲明,就可以從聲明處起,合法地使用該外部變量AB

 

9.2 宏定義

在C語言源程序中允許用一個標識符來表示一個字符串,稱爲。被定義爲的標識符稱爲宏名。在編譯預處理時,對程序中所有出現的宏名,都用宏定義中的字符串去代換,這稱爲宏代換宏展開

宏定義是由源程序中的宏定義命令完成的。宏代換是由預處理程序自動完成的。

在C語言中,分爲有參數和無參數兩種。下面分別討論這兩種的定義和調用。

9.2.1 無參宏定義

無參宏的宏名後不帶參數。

其定義的一般形式爲:

#define 標識符 字符串

其中的#表示這是一條預處理命令。凡是以#開頭的均爲預處理命令。define爲宏定義命令。標識符爲所定義的宏名。字符串可以是常數、表達式、格式串等。

在前面介紹過的符號常量的定義就是一種無參宏定義。此外,常對程序中反覆使用的表達式進行宏定義。

例如:

#define M (y*y+3*y)

它的作用是指定標識符M來代替表達式(y*y+3*y)。在編寫源程序時,所有的(y*y+3*y)都可由M代替,而對源程序作編譯時,將先由預處理程序進行宏代換,即用(y*y+3*y)表達式去置換所有的宏名M,然後再進行編譯。

 

【例9.1

#defineM (y*y+3*y)

main(){


int s,y;

printf("input a number: ");

scanf("%d",&y);

s=3*M+4*M+5*M;

printf("s=%d\n",s);

}


上例程序中首先進行宏定義,定義M來替代表達式(y*y+3*y),在s=3*M+4*M+5*M中作了宏調用。在預處理時經宏展開後該語句變爲:

s=3*(y*y+3*y)+4*(y*y+3*y)+5*(y*y+3*y);

但要注意的是,在宏定義中表達式(y*y+3*y)兩邊的括號不能少。否則會發生錯誤。如當作以下定義後:

#difineM y*y+3*y

在宏展開時將得到下述語句:

s=3*y*y+3*y+4*y*y+3*y+5*y*y+3*y;

這相當於:

3y2+3y+4y2+3y+5y2+3y;

顯然與原題意要求不符。計算結果當然是錯誤的。因此在作宏定義時必須十分注意。應保證在宏代換之後不發生錯誤。

對於宏定義還要說明以下幾點:

1)宏定義是用宏名來表示一個字符串,在宏展開時又以該字符串取代宏名,這只是一種簡單的代換,字符串中可以含任何字符,可以是常數,也可以是表達式,預處理程序對它不作任何檢查。如有錯誤,只能在編譯已被宏展開後的源程序時發現。

2)宏定義不是說明或語句,在行末不必加分號,如加上分號則連分號也一起置換。

3)宏定義必須寫在函數之外,其作用域爲宏定義命令起到源程序結束。如要終止其作用域可使用# undef命令。

例如:

#definePI 3.14159

main()

{

……

}

#undef PI

f1()

{

……

}

 

表示PI只在main函數中有效,在f1中無效。

4) 宏名在源程序中若用引號括起來,則預處理程序不對其作宏代換。

 

【例9.2】

#defineOK 100

main()

{

printf("OK");

printf("\n");

}

上例中定義宏名OK表示100,但在printf語句中OK被引號括起來,因此不作宏代換。程序的運行結果爲:OK這表示把OK當字符串處理。

5) 宏定義允許嵌套,在宏定義的字符串中可以使用已經定義的宏名。在宏展開時由預處理程序層層代換。

例如:

#define PI 3.1415926

#define S PI*y*y /* PI是已定義的宏名*/

 

對語句:

printf("%f",S);

在宏代換後變爲:

printf("%f",3.1415926*y*y);

6) 習慣上宏名用大寫字母表示,以便於與變量區別。但也允許用小寫字母。

7) 可用宏定義表示數據類型,使書寫方便。

例如:

#define STU struct stu

在程序中可用STU作變量說明:

STU body[5],*p;

 

#defineINTEGER int

在程序中即可用INTEGER作整型變量說明:

INTEGER a,b;

應注意用宏定義表示數據類型和用typedef定義數據說明符的區別。

宏定義只是簡單的字符串代換,是在預處理完成的,而typedef是在編譯時處理的,它不是作簡單的代換,而是對類型說明符重新命名。被命名的標識符具有類型定義說明的功能。

請看下面的例子:

#definePIN1 int *

typedef(int *) PIN2;

從形式上看這兩者相似, 但在實際使用中卻不相同。

下面用PIN1,PIN2說明變量時就可以看出它們的區別:

PIN1a,b;在宏代換後變成:

int *a,b;

表示a是指向整型的指針變量,而b是整型變量。

然而:

PIN2 a,b;

表示a,b都是指向整型的指針變量。因爲PIN2是一個類型說明符。由這個例子可見,宏定義雖然也可表示數據類型, 但畢竟是作字符代換。在使用時要分外小心,以避出錯。

8) 輸出格式作宏定義,可以減少書寫麻煩。

【例9.3】中就採用了這種方法。

#defineP printf

#defineD "%d\n"

#defineF "%f\n"

main(){

inta=5, c=8, e=11;

floatb=3.8, d=9.7, f=21.08;

P(DF,a,b);

P(DF,c,d);

P(DF,e,f);

}

9.2.2 帶參宏定義

 

C語言允許宏帶有參數。在宏定義中的參數稱爲形式參數,在宏調用中的參數稱爲實際參數。

對帶參數的宏,在調用中,不僅要宏展開,而且要用實參去代換形參。

帶參宏定義的一般形式爲:

#define宏名(形參表) 字符串

在字符串中含有各個形參。

帶參宏調用的一般形式爲:

宏名(實參表);

例如:

#defineM(y) y*y+3*y /*宏定義*/

……

k=M(5);/*宏調用*/

……

在宏調用時,用實參5去代替形參y,經預處理宏展開後的語句爲:

k=5*5+3*5

【例9.4

#defineMAX(a,b) (a>b)?a:b

main(){

intx,y,max;

printf("inputtwo numbers: ");

scanf("%d%d",&x,&y);

max=MAX(x,y);

printf("max=%d\n",max);

}

上例程序的第一行進行帶參宏定義,用宏名MAX表示條件表達式(a>b)?a:b,形參a,b均出現在條件表達式中。程序第七行max=MAX(x,y)爲宏調用,實參x,y,將代換形參a,b。

宏展開後該語句爲:

max=(x>y)?x:y;

用於計算x,y中的大數。

對於帶參的宏定義有以下問題需要說明:

1. 帶參宏定義中,宏名和形參表之間不能有空格出現。

例如把:

#defineMAX(a,b) (a>b)?a:b

寫爲:

#define MAX (a,b) (a>b)?a:b

將被認爲是無參宏定義,宏名MAX代表字符串 (a,b) (a>b)?a:b。宏展開時,宏調用語句:

max=MAX(x,y);

將變爲:

max=(a,b)(a>b)?a:b(x,y);

這顯然是錯誤的。

2. 在帶參宏定義中,形式參數不分配內存單元,因此不必作類型定義。而宏調用中的實參有具體的值。要用它們去代換形參,因此必須作類型說明。這是與函數中的情況不同的。在函數中,形參和實參是兩個不同的量,各有自己的作用域,調用時要把實參值賦予形參,進行值傳遞。而在帶參宏中,只是符號代換,不存在值傳遞的問題。

3. 在宏定義中的形參是標識符,而宏調用中的實參可以是表達式。

【例9.5

#defineSQ(y) (y)*(y)

main(){

inta,sq;

printf("inputa number: ");

scanf("%d",&a);

sq=SQ(a+1);

printf("sq=%d\n",sq);

}

上例中第一行爲宏定義,形參爲y。程序第七行宏調用中實參爲a+1,是一個表達式,在宏展開時,用a+1代換y,再用(y)*(y) 代換SQ,得到如下語句:

sq=(a+1)*(a+1);

這與函數的調用是不同的,函數調用時要把實參表達式的值求出來再賦予形參。而宏代換中對實參表達式不作計算直接地照原樣代換。

4. 在宏定義中,字符串內的形參通常要用括號括起來以避免出錯。在上例中的宏定義中(y)*(y)表達式的y都用括號括起來,因此結果是正確的。如果去掉括號,把程序改爲以下形式:

【例9.6

#defineSQ(y) y*y

main(){

inta,sq;

printf("input a number: ");

scanf("%d",&a);

sq=SQ(a+1);

printf("sq=%d\n",sq);

}

運行結果爲:

inputa number:3

sq=7

同樣輸入3,但結果卻是不一樣的。問題在哪裏呢? 這是由於代換隻作符號代換而不作其它處理而造成的。宏代換後將得到以下語句:

sq=a+1*a+1;

由於a爲3故sq的值爲7。這顯然與題意相違,因此參數兩邊的括號是不能少的。即使在參數兩邊加括號還是不夠的,請看下面程序:

【例9.7

#defineSQ(y) (y)*(y)

main(){

inta,sq;

printf("inputa number: ");

scanf("%d",&a);

sq=160/SQ(a+1);

printf("sq=%d\n",sq);

}

本程序與前例相比,只把宏調用語句改爲:

sq=160/SQ(a+1);

運行本程序如輸入值仍爲3時,希望結果爲10。但實際運行的結果如下:

input a number:3

sq=160

爲什麼會得這樣的結果呢?分析宏調用語句,在宏代換之後變爲:

sq=160/(a+1)*(a+1);

a爲3時,由於/*運算符優先級和結合性相同,則先作160/(3+1)得40,再作40*(3+1)最後得160。爲了得到正確答案應在宏定義中的整個字符串外加括號,程序修改如下:

【例9.8

#defineSQ(y) ((y)*(y))

main(){

inta,sq;

printf("inputa number: ");

scanf("%d",&a);

sq=160/SQ(a+1);

printf("sq=%d\n",sq);

}

以上討論說明,對於宏定義不僅應在參數兩側加括號,也應在整個字符串外加括號。

5. 帶參的宏和帶參函數很相似,但有本質上的不同,除上面已談到的各點外,把同一表達式用函數處理與用宏處理兩者的結果有可能是不同的。

 

【例9.9

main(){

inti=1;

while(i<=5)

printf("%d\n",SQ(i++));

}

SQ(inty)

{

return((y)*(y));

}

【例9.10

#defineSQ(y) ((y)*(y))

main(){

inti=1;

while(i<=5)

printf("%d\n",SQ(i++));

}

在例9.9中函數名爲SQ,形參爲Y,函數體表達式爲((y)*(y))。在例9.10中宏名爲SQ,形參也爲y,字符串表達式爲(y)*(y))。 例9.9的函數調用爲SQ(i++),例9.10的宏調用爲SQ(i++),實參也是相同的。從輸出結果來看,卻大不相同。

分析如下:在例9.9中,函數調用是把實參i值傳給形參y後自增1。 然後輸出函數值。因而要循環5次。輸出1~5的平方值。而在例9.10中宏調用時,只作代換。SQ(i++)被代換爲((i++)*(i++))。在第一次循環時,由於i等於1,其計算過程爲:表達式中前一個i初值爲1,然後i自增1變爲2,因此表達式中第2個i初值爲2,兩相乘的結果也爲2,然後i值再自增1,得3。在第二次循環時,i值已有初值爲3,因此表達式中前一個i爲3,後一個i爲4,乘積爲12,然後i再自增1變爲5。進入第三次循環,由於i 值已爲5,所以這將是最後一次循環。計算表達式的值爲5*6等於30。i值再自增1變爲6,不再滿足循環條件,停止循環。

從以上分析可以看出函數調用和宏調用二者在形式上相似,在本質上是完全不同的。

6. 宏定義也可用來定義多個語句,在宏調用時,把這些語句又代換到源程序內。看下面的例子。

 

【例9.11

#defineSSSV(s1,s2,s3,v) s1=l*w;s2=l*h;s3=w*h;v=w*l*h;

main(){

int l=3,w=4,h=5,sa,sb,sc,vv;

SSSV(sa,sb,sc,vv);

printf("sa=%d\nsb=%d\nsc=%d\nvv=%d\n",sa,sb,sc,vv);

}

程序第一行爲宏定義,用宏名SSSV表示4個賦值語句,4 個形參分別爲4個賦值符左部的變量。在宏調用時,把4個語句展開並用實參代替形參。使計算結果送入實參之中。

9.3 文件包含

文件包含是C預處理程序的另一個重要功能。

文件包含命令行的一般形式爲:

#include"文件名"

 

在前面我們已多次用此命令包含過庫函數的頭文件。例如:

#include"stdio.h"

#include"math.h"

文件包含命令的功能是把指定的文件插入該命令行位置取代該命令行,從而把指定的文件和當前的源程序文件連成一個源文件。

在程序設計中,文件包含是很有用的。一個大的程序可以分爲多個模塊,由多個程序員分別編程。有些公用的符號常量或宏定義等可單獨組成一個文件,在其它文件的開頭用包含命令包含該文件即可使用。這樣,可避免在每個文件開頭都去書寫那些公用量,從而節省時間,並減少出錯。

對文件包含命令還要說明以下幾點:

1. 包含命令中的文件名可以用雙引號括起來,也可以用尖括號括起來。例如以下寫法都是允許的:

#include"stdio.h"

#include<math.h>

但是這兩種形式是有區別的:使用尖括號表示在包含文件目錄中去查找(包含目錄是由用戶在設置環境時設置的),而不在源文件目錄去查找;

使用雙引號則表示首先在當前的源文件目錄中查找,若未找到纔到包含目錄中去查找。用戶編程時可根據自己文件所在的目錄來選擇某一種命令形式。

2. 一個include命令只能指定一個被包含文件,若有多個文件要包含,則需用多個include命令。

3. 文件包含允許嵌套,即在一個被包含的文件中又可以包含另一個文件。

 

9.4 條件編譯

預處理程序提供了條件編譯的功能。可以按不同的條件去編譯不同的程序部分,因而產生不同的目標代碼文件。這對於程序的移植和調試是很有用的。

條件編譯有三種形式,下面分別介紹:

1. 第一種形式:

#ifdef 標識符

程序段1

#else

程序段2

#endif

它的功能是,如果標識符已被 #define命令定義過則對程序段1進行編譯;否則對程序段2進行編譯。如果沒有程序段2(它爲空),本格式中的#else可以沒有,即可以寫爲:

#ifdef 標識符

程序段

#endif

 

2. 第二種形式:

#ifndef 標識符

程序段1

#else

程序段2

#endif

與第一種形式的區別是將ifdef改爲ifndef。它的功能是,如果標識符未被#define命令定義過則對程序段1進行編譯,否則對程序段2進行編譯。這與第一種形式的功能正相反。

3. 第三種形式:

 

#if 常量表達式

程序段1

#else

程序段2

#endif

它的功能是,如常量表達式的值爲真(非0),則對程序段1 進行編譯,否則對程序段2進行編譯。因此可以使程序在不同條件下,完成不同的功能。

 

11 結構體與共用體

11.1 定義一個結構的一般形式

在實際問題中,一組數據往往具有不同的數據類型。例如,在學生登記表中,姓名應爲字符型;學號可爲整型或字符型;年齡應爲整型;性別應爲字符型;成績可爲整型或實型。顯然不能用一個數組來存放這一組數據。因爲數組中各元素的類型和長度都必須一致,以便於編譯系統處理。爲了解決這個問題,C語言中給出了另一種構造數據類型——“結構(structure或叫結構體。 它相當於其它高級語言中的記錄。結構是一種構造類型,它是由若干成員組成的。每一個成員可以是一個基本數據類型或者又是一個構造類型。結構既是一種構造而成的數據類型,那麼在說明和使用之前必須先定義它,也就是構造它。如同在說明和調用函數之前要先定義函數一樣。

定義一個結構的一般形式爲:

struct 結構名

{成員表列};

成員表列由若干個成員組成,每個成員都是該結構的一個組成部分。對每個成員也必須作類型說明,其形式爲:

類型說明符 成員名;

成員名的命名應符合標識符的書寫規定。例如:

structstu

{

intnum;

charname[20];

charsex;

floatscore;

};

在這個結構定義中,結構名爲stu,該結構由4個成員組成。第一個成員爲num,整型變量;第二個成員爲name,字符數組;第三個成員爲sex,字符變量;第四個成員爲score,實型變量。應注意在括號後的分號是不可少的。結構定義之後,即可進行變量說明。凡說明爲結構stu的變量都由上述4個成員組成。由此可見, 結構是一種複雜的數據類型,是數目固定,類型不同的若干有序變量的集合。

11.2 結構類型變量的說明

說明結構變量有以下三種方法。以上面定義的stu爲例來加以說明。

1. 先定義結構,再說明結構變量。

如:

struct stu

 

{

int num;

charname[20];

charsex;

floatscore;

};

structstu boy1,boy2;

說明了兩個變量boy1和boy2爲stu結構類型。也可以用宏定義使一個符號常量來表示一個結構類型。

例如:

#define STU struct stu

STU

{

intnum;

charname[20];

charsex;

floatscore;

};

STU boy1,boy2;

2. 在定義結構類型的同時說明結構變量。

 

例如:

struct stu

{

intnum;

charname[20];

charsex;

floatscore;

}boy1,boy2;

這種形式的說明的一般形式爲:

struct 結構名

{

成員表列

}變量名錶列;

3. 直接說明結構變量。

 

例如:

struct

{

intnum;

charname[20];

charsex;

floatscore;

}boy1,boy2;

這種形式的說明的一般形式爲:

struct

{

成員表列

}變量名錶列;

 

11.3 結構變量成員的表示方法在程序中使用結構變量時,往往不把它作爲一個整體來使用。在ANSIC中除了允許具有相同類型的結構變量相互賦值以外,一般對結構變量的使用,包括賦值、輸入、輸出、運算等都是通過結構變量的成員來實現的。

表示結構變量成員的一般形式是:

結構變量名.成員名

例如:

 

boy1.num 即第一個人的學號

boy2.sex 即第二個人的性別

如果成員本身又是一個結構則必須逐級找到最低級的成員才能使用。

例如:

boy1.birthday.month

即第一個人出生的月份成員可以在程序中單獨使用,與普通變量完全相同。

11.4 結構變量的賦值

結構變量的賦值就是給各成員賦值。可用輸入語句或賦值語句來完成。

 

【例11.1】給結構變量賦值並輸出其值。

main()

{

struct stu

{

int num;

char *name;

char sex;

float score;

} boy1,boy2;

boy1.num=102;

boy1.name="Zhangping";

printf("inputsex and score\n");

scanf("%c%f",&boy1.sex,&boy1.score);

boy2=boy1;

printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

}

本程序中用賦值語句給num和name兩個成員賦值,name是一個字符串指針變量。用scanf函數動態地輸入sex和score成員值,然後把boy1的所有成員的值整體賦予boy2。最後分別輸出boy2的各個成員值。本例表示了結構變量的賦值、輸入和輸出的方法。

11.5 結構變量的初始化

和其他類型變量一樣,對結構變量可以在定義時進行初始化賦值。

 

【例11.2】對結構變量初始化。

main()

{

struct stu /*定義結構*/

{

int num;

char *name;

char sex;

float score;

}boy2,boy1={102,"Zhang ping",'M',78.5};

boy2=boy1;

printf("Number=%d\nName=%s\n",boy2.num,boy2.name);

printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

}

本例中,boy2,boy1均被定義爲外部結構變量,並對boy1作了初始化賦值。在main函數中,把boy1的值整體賦予boy2,然後用兩個printf語句輸出boy2各成員的值。

11.6 結構數組的定義

數組的元素也可以是結構類型的。因此可以構成結構型數組。結構數組的每一個元素都是具有相同結構類型的下標結構變量。在實際應用中,經常用結構數組來表示具有相同數據結構的一個羣體。如一個班的學生檔案,一個車間職工的工資表等。

方法和結構變量相似,只需說明它爲數組類型即可。

例如:

struct stu

 

{

int num;

char *name;

char sex;

float score;

}boy[5];

定義了一個結構數組boy,共有5個元素,boy[0]~boy[4]。每個數組元素都具有struct stu的結構形式。對結構數組可以作初始化賦值。

例如:

struct stu

{

int num;

char *name;

char sex;

float score;

}boy[5]={

{101,"Liping","M",45},

{102,"Zhangping","M",62.5},

{103,"Hefang","F",92.5},

{104,"Chengling","F",87},

{105,"Wangming","M",58};

}

當對全部元素作初始化賦值時,也可不給出數組長度。

 

11.7 結構指針變量的說明和使用

11.7.1 指向結構變量的指針一個指針變量當用來指向一個結構變量時,稱之爲結構指針變量。結構指針變量中的值是所指向的結構變量的首地址。通過結構指針即可訪問該結構變量,這與數組指針和函數指針的情況是相同的。

結構指針變量說明的一般形式爲:

struct 結構名 *結構指針變量名

例如,在前面的例題中定義了stu這個結構,如要說明一個指向stu的指針變量pstu,可寫爲:

struct stu *pstu;

當然也可在定義stu結構時同時說明pstu。與前面討論的各類指針變量相同,結構指針變量也必須要先賦值後才能使用。

賦值是把結構變量的首地址賦予該指針變量,不能把結構名賦予該指針變量。如果boy是被說明爲stu類型的結構變量,則:

pstu=&boy

是正確的,而:

pstu=&stu

是錯誤的。

結構名和結構變量是兩個不同的概念,不能混淆。結構名只能表示一個結構形式,編譯系統並不對它分配內存空間。只有當某變量被說明爲這種類型的結構時,纔對該變量分配存儲空間。因此上面&stu這種寫法是錯誤的,不可能去取一個結構名的首地址。有了結構指針變量,就能更方便地訪問結構變量的各個成員。

其訪問的一般形式爲:

(*結構指針變量).成員名

或爲:

 

結構指針變量->成員名

例如:

(*pstu).num

或者:

pstu->num

應該注意(*pstu)兩側的括號不可少,因爲成員符.的優先級高於*。如去掉括號寫作*pstu.num則等效於*(pstu.num),這樣,意義就完全不對了。

下面通過例子來說明結構指針變量的具體說明和使用方法。

【例11.5

structstu

{

intnum;

char*name;

charsex;

floatscore;

}boy1={102,"Zhang ping",'M',78.5},*pstu;

main()

{

pstu=&boy1;

printf("Number=%d\nName=%s\n",boy1.num,boy1.name);

printf("Sex=%c\nScore=%f\n\n",boy1.sex,boy1.score);

printf("Number=%d\nName=%s\n",(*pstu).num,(*pstu).name);

printf("Sex=%c\nScore=%f\n\n",(*pstu).sex,(*pstu).score);

printf("Number=%d\nName=%s\n",pstu->num,pstu->name);

printf("Sex=%c\nScore=%f\n\n",pstu->sex,pstu->score);

}

本例程序定義了一個結構stu,定義了stu類型結構變量boy1並作了初始化賦值,還定義了一個指向stu類型結構的指針變量pstu。在main函數中,pstu被賦予boy1的地址,因此pstu指向boy1。然後在printf語句內用三種形式輸出boy1的各個成員值。從運行結果可以看出:

結構變量.成員名

(*結構指針變量).成員名

結構指針變量->成員名

這三種用於表示結構成員的形式是完全等效的。

11.7.2 指向結構數組的指針

指針變量可以指向一個結構數組,這時結構指針變量的值是整個結構數組的首地址。結構指針變量也可指向結構數組的一個元素,這時結構指針變量的值是該結構數組元素的首地址。

設ps爲指向結構數組的指針變量,則ps也指向該結構數組的0號元素,ps+1指向1號元素,ps+i則指向i號元素。這與普通數組的情況是一致的。

 

【例11.6】用指針變量輸出結構數組。

struct stu

{

int num;

char*name;

charsex;

floatscore;

}boy[5]={

{101,"Zhouping",'M',45},

{102,"Zhangping",'M',62.5},

{103,"Lioufang",'F',92.5},

{104,"Chengling",'F',87},

{105,"Wangming",'M',58},

};

main()

{

structstu *ps;

printf("No\tName\t\t\tSex\tScore\t\n");

for(ps=boy;ps<boy+5;ps++)

printf("%d\t%s\t\t%c\t%f\t\n",ps->num,ps->name,ps->sex,ps->score);

}

在程序中,定義了stu結構類型的外部數組boy並作了初始化賦值。在main函數內定義ps爲指向stu類型的指針。在循環語句for的表達式1中,ps被賦予boy的首地址,然後循環5次,輸出boy數組中各成員值。

應該注意的是,一個結構指針變量雖然可以用來訪問結構變量或結構數組元素的成員,但是,不能使它指向一個成員。也就是說不允許取一個成員的地址來賦予它。因此,下面的賦值是錯誤的。

ps=&boy[1].sex;

而只能是:

ps=boy;(賦予數組首地址)

或者是:

ps=&boy[0];(賦予0號元素首地址)

11.7.3 結構指針變量作函數參數

在ANSI C標準中允許用結構變量作函數參數進行整體傳送。但是這種傳送要將全部成員逐個傳送,特別是成員爲數組時將會使傳送的時間和空間開銷很大,嚴重地降低了程序的效率。因此最好的辦法就是使用指針,即用指針變量作函數參數進行傳送。這時由實參傳向形參的只是地址,從而減少了時間和空間的開銷。

 

【例11.7】計算一組學生的平均成績和不及格人數。用結構指針變量作函數參數編程。

struct stu

{

int num;

char *name;

char sex;

float score;}boy[5]={

{101,"Liping",'M',45},

{102,"Zhangping",'M',62.5},

{103,"Hefang",'F',92.5},

{104,"Chengling",'F',87},

{105,"Wangming",'M',58},

};

main()

{

structstu *ps;

voidave(struct stu *ps);

ps=boy;

ave(ps);

}

voidave(struct stu *ps)

{

intc=0,i;

floatave,s=0;

for(i=0;i<5;i++,ps++)

{

s+=ps->score;

if(ps->score<60)c+=1;

}

printf("s=%f\n",s);

ave=s/5;

printf("average=%f\ncount=%d\n",ave,c);

}

本程序中定義了函數ave,其形參爲結構指針變量ps。boy被定義爲外部結構數組,因此在整個源程序中有效。在main函數中定義說明了結構指針變量ps,並把boy的首地址賦予它,使ps指向boy數組。然後以ps作實參調用函數ave。在函數ave中完成計算平均成績和統計不及格人數的工作並輸出結果。

由於本程序全部採用指針變量作運算和處理,故速度更快,程序效率更高。

11.8 動態存儲分配

在數組一章中,曾介紹過數組的長度是預先定義好的,在整個程序中固定不變。C語言中不允許動態數組類型。

例如:

int n;

 

scanf("%d",&n);

int a[n];

用變量表示長度,想對數組的大小作動態說明,這是錯誤的。但是在實際的編程中,往往會發生這種情況,即所需的內存空間取決於實際輸入的數據,而無法預先確定。對於這種

問題,用數組的辦法很難解決。爲了解決上述問題,C語言提供了一些內存管理函數,這些內存管理函數可以按需要動態地分配內存空間,也可把不再使用的空間回收待用,爲有效地利用內存資源提供了手段。

常用的內存管理函數有以下三個:

1. 分配內存空間函數malloc

調用形式:

(類型說明符*)malloc(size)

功能:在內存的動態存儲區中分配一塊長度爲"size"字節的連續區域。函數的返回值爲該區域的首地址。

類型說明符表示把該區域用於何種數據類型。

(類型說明符*)表示把返回值強制轉換爲該類型指針。

size是一個無符號數。

例如:

pc=(char *)malloc(100);

表示分配100個字節的內存空間,並強制轉換爲字符數組類型,函數的返回值爲指向該字符數組的指針,把該指針賦予指針變量pc。

2. 分配內存空間函數 calloc

calloc 也用於分配內存空間。

調用形式:

(類型說明符*)calloc(n,size)

功能:在內存動態存儲區中分配n塊長度爲size字節的連續區域。函數的返回值爲該區域的首地址。

(類型說明符*)用於強制類型轉換。

calloc函數與malloc 函數的區別僅在於一次可以分配n塊區域。

例如:

ps=(struet stu*)calloc(2,sizeof(struct stu));

其中的sizeof(struct stu)是求stu的結構長度。因此該語句的意思是:按stu的長度分配2塊連續區域,強制轉換爲stu類型,並把其首地址賦予指針變量ps。

2. 釋放內存空間函數free

調用形式:

free(void*ptr);

功能:釋放ptr所指向的一塊內存空間,ptr是一個任意類型的指針變量,它指向被釋放區域的首地址。被釋放區應是由malloc或calloc函數所分配的區域。

 

【例11.8】分配一塊區域,輸入一個學生數據。

main()

{

structstu

{

intnum;

char*name;

charsex;

floatscore;

} *ps;

ps=(struct stu*)malloc(sizeof(struct stu));


ps->num=102;

ps->name="Zhang ping";

ps->sex='M';

ps->score=62.5;

printf("Number=%d\nName=%s\n",ps->num,ps->name);

printf("Sex=%c\nScore=%f\n",ps->sex,ps->score);

free(ps);

}


本例中,定義了結構stu,定義了stu類型指針變量ps。然後分配一塊stu大內存區,並把首地址賦予ps,使ps指向該區域。再以ps爲指向結構的指針變量對各成員賦值,並用printf輸出各成員值。最後用free函數釋放ps指向的內存空間。整個程序包含了申請內存空間、使用內存空間、釋放內存空間三個步驟,實現存儲空間的動態分配。

11.9 鏈表的概念

在例7.8中採用了動態分配的辦法爲一個結構分配內存空間。每一次分配一塊空間可用來存放一個學生的數據,我們可稱之爲一個結點。有多少個學生就應該申請分配多少塊內存空間,也就是說要建立多少個結點。當然用結構數組也可以完成上述工作,但如果預先不能準確把握學生人數,也就無法確定數組大小。而且當學生留級、退學之後也不能把該元素佔用的空間從數組中釋放出來。

用動態存儲的方法可以很好地解決這些問題。有一個學生就分配一個結點,無須預先確定學生的準確人數,某學生退學,可刪去該結點,並釋放該結點佔用的存儲空間。從而節約了寶貴的內存資源。另一方面,用數組的方法必須佔用一塊連續的內存區域。而使用動態分配時,每個結點之間可以是不連續的(結點內是連續的)。結點之間的聯繫可以用指針實現。 即在結點結構中定義一個成員項用來存放下一結點的首地址,這個用於存放地址的成員,常把它稱爲指針域。

可在第一個結點的指針域內存入第二個結點的首地址,在第二個結點的指針域內又存放第三個結點的首地址,如此串連下去直到最後一個結點。最後一個結點因無後續結點連接,其指針域可賦爲0。這樣一種連接方式,在數據結構中稱爲“鏈表”。

 

11.10 枚舉類型在實際問題中,有些變量的取值被限定在一個有限的範圍內。例如,一個星期內只有七天,一年只有十二個月,一個班每週有六門課程等等。如果把這些量說明爲整型,字符型或其它類型顯然是不妥當的。爲此,C語言提供了一種稱爲枚舉的類型。在枚舉類型的定義中列舉出所有可能的取值,被說明爲該枚舉類型的變量取值不能超過定義的範圍。應該說明的是,枚舉類型是一種基本數據類型,而不是一種構造類型,因爲它不能再分解爲任何基本類型。

11.10.1 枚舉類型的定義和枚舉變量的說明

1. 枚舉的定義枚舉類型定義的一般形式爲:

enum 枚舉名{ 枚舉值表 };

在枚舉值表中應羅列出所有可用值。這些值也稱爲枚舉元素。

例如:

該枚舉名爲weekday,枚舉值共有7個,即一週中的七天。凡被說明爲weekday類型變量的取值只能是七天中的某一天。

2. 枚舉變量的說明

如同結構和聯合一樣,枚舉變量也可用不同的方式說明,即先定義後說明,同時定義說明或直接說明。

設有變量a,b,c被說明爲上述的weekday,可採用下述任一種方式:

enum weekday{ sun,mou,tue,wed,thu,fri,sat };

enum weekday a,b,c;

或者爲:

enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c;

或者爲:

enum { sun,mou,tue,wed,thu,fri,sat }a,b,c;

11.10.2 枚舉類型變量的賦值和使用

枚舉類型在使用中有以下規定:

1. 枚舉值是常量,不是變量。不能在程序中用賦值語句再對它賦值。

例如對枚舉weekday的元素再作以下賦值:

sun=5;

mon=2;

sun=mon;

都是錯誤的。

2. 枚舉元素本身由系統定義了一個表示序號的數值,從0開始順序定義爲0,1,2。如在weekday中,sun值爲0,mon值爲1,,sat值爲6

【例11.10

main(){

 

enum weekday

{ sun,mon,tue,wed,thu,fri,sat } a,b,c;

a=sun;

b=mon;

c=tue;

printf("%d,%d,%d",a,b,c);

}

 

說明:

只能把枚舉值賦予枚舉變量,不能把元素的數值直接賦予枚舉變量。如:

a=sum;

b=mon;

是正確的。而:

a=0;

b=1;

是錯誤的。如一定要把數值賦予枚舉變量,則必須用強制類型轉換。

如:

a=(enum weekday)2;

其意義是將順序號爲2的枚舉元素賦予枚舉變量a,相當於:

a=tue;

還應該說明的是枚舉元素不是字符常量也不是字符串常量,使用時不要加單、雙引號。

【例11.11

main(){

enum body

{ a,b,c,d } month[31],j;

int i;

j=a;

for(i=1;i<=30;i++){

month[i]=j;

j++;

if (j>d) j=a;

}

for(i=1;i<=30;i++){

switch(month[i])

{

case a:printf(" %2d%c\t",i,'a'); break;

case b:printf(" %2d%c\t",i,'b'); break;

case c:printf(" %2d%c\t",i,'c'); break;

case d:printf(" %2d %c\t",i,'d');break;

default:break;

}

}

printf("\n");

 

11.11 類型定義符typedefC語言不僅提供了豐富的數據類型,而且還允許由用戶自己定義類型說明符,也就是說允許由用戶爲數據類型取別名。類型定義符typedef即可用來完成此功能。例如,有整型量a,b,其說明如下:

int a,b;

 

其中int是整型變量的類型說明符。int的完整寫法爲integer,爲了增加程序的可讀性,可把整型說明符用typedef定義爲:

typedef int INTEGER

這以後就可用INTEGER來代替int作整型變量的類型說明了。

例如:

INTEGER a,b;

它等效於:

int a,b;

用typedef定義數組、指針、結構等類型將帶來很大的方便,不僅使程序書寫簡單而且使意義更爲明確,因而增強了可讀性。

例如:

typedef char NAME[20]; 表示NAME是字符數組類型,數組長度爲20。然後可用NAME 說明變量,如:

NAME a1,a2,s1,s2;

完全等效於:

char a1[20],a2[20],s1[20],s2[20]

又如:

typedef struct stu

{ char name[20];

int age;

char sex;

} STU;

定義STU表示stu的結構類型,然後可用STU來說明結構變量:

STU body1,body2;

typedef定義的一般形式爲:

typedef 原類型名 新類型名

其中原類型名中含有定義部分,新類型名一般用大寫表示,以便於區別。

有時也可用宏定義來代替typedef的功能,但是宏定義是由預處理完成的,而typedef則是在編譯時完成的,後者更爲靈活方便。

 

13 文件

13.1 C文件概述

所謂文件是指一組相關數據的有序集合。這個數據集有一個名稱,叫做文件名。實際上在前面的各章中我們已經多次使用了文件,例如源程序文件、目標文件、可執行文件、庫文件 (頭文件)等。

文件通常是駐留在外部介質(如磁盤等)上的,在使用時才調入內存中來。從不同的角度可對文件作不同的分類。從用戶的角度看,文件可分爲普通文件和設備文件兩種。

普通文件是指駐留在磁盤或其它外部介質上的一個有序數據集,可以是源文件、目標文件、可執行程序;也可以是一組待輸入處理的原始數據,或者是一組輸出的結果。對於源文件、目標文件、可執行程序可以稱作程序文件,對輸入輸出數據可稱作數據文件。

設備文件是指與主機相聯的各種外部設備,如顯示器、打印機、鍵盤等。在操作系統中,把外部設備也看作是一個文件來進行管理,把它們的輸入、輸出等同於對磁盤文件的讀和寫。

通常把顯示器定義爲標準輸出文件,一般情況下在屏幕上顯示有關信息就是向標準輸出文件輸出。如前面經常使用的printf,putchar函數就是這類輸出。

鍵盤通常被指定標準的輸入文件,從鍵盤上輸入就意味着從標準輸入文件上輸入數據。scanf,getchar函數就屬於這類輸入。

從文件編碼的方式來看,文件可分爲ASCII碼文件和二進制碼文件兩種。ASCII文件也稱爲文本文件,這種文件在磁盤中存放時每個字符對應一個字節,用於存放對應的ASCII碼。

例如,數5678的存儲形式爲:

ASCII碼:00110101 00110110 00110111 00111000

↓ ↓ ↓ ↓

十進制碼: 5 6 7 8

共佔用4個字節。

ASCII碼文件可在屏幕上按字符顯示,例如源程序文件就是ASCII文件,用DOS命令TYPE可顯示文件的內容。由於是按字符顯示,因此能讀懂文件內容。

二進制文件是按二進制的編碼方式來存放文件的。

例如, 數5678的存儲形式爲:

00010110 00101110

只佔二個字節。二進制文件雖然也可在屏幕上顯示,但其內容無法讀懂。C系統在處理這些文件時,並不區分類型,都看成是字符流,按字節進行處理。

輸入輸出字符流的開始和結束只由程序控制而不受物理符號(如回車符)的控制。 因此也把這種文件稱作流式文件

本章討論流式文件的打開、關閉、讀、寫、 定位等各種操作。

13.2 文件指針

在C語言中用一個指針變量指向一個文件,這個指針稱爲文件指針。通過文件指針就可對它所指的文件進行各種操作。

 

定義說明文件指針的一般形式爲:

FILE *指針變量標識符;

 

其中FILE應爲大寫,它實際上是由系統定義的一個結構,該結構中含有文件名、文件狀態和文件當前位置等信息。在編寫源程序時不必關心FILE結構的細節。

例如:

FILE *fp

表示fp是指向FILE結構的指針變量,通過fp即可找存放某個文件信息的結構變量,然後按結構變量提供的信息找到該文件,實施對文件的操作。習慣上也籠統地把fp稱爲指向一個文件的指針。

13.3 文件的打開與關閉

 

文件在進行讀寫操作之前要先打開,使用完畢要關閉。所謂打開文件,實際上是建立文件的各種有關信息,並使文件指針指向該文件,以便進行其它操作。關閉文件則斷開指針與文件之間的聯繫,也就禁止再對該文件進行操作。

在C語言中,文件操作都是由庫函數來完成的。在本章內將介紹主要的文件操作函數。

13.3.1 文件的打開(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+

讀寫打開一個二進制文件,允許讀,或在文件末追加數據

例如:

fclose(fp);

正常完成關閉文件操作時,fclose函數返回值爲0。如返回非零值則表示有錯誤發生。

13.4 文件的讀寫

對文件的讀和寫是最常用的文件操作。在C語言中提供了多種文件讀寫的函數:

·字符讀寫函數 :fgetc和fputc

 

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

·數據塊讀寫函數:freed和fwrite

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

下面分別予以介紹。使用以上函數都要求包含頭文件stdio.h

13.4.1 字符讀寫函數fgetcfputc

字符讀寫函數是以字符(字節)爲單位的讀寫函數。 每次可從文件讀出或向文件寫入一個字符。

1. 讀字符函數fgetc

fgetc函數的功能是從指定的文件中讀一個字符,函數調用的形式爲:

字符變量=fgetc(文件指針);

 

例如:

ch=fgetc(fp);

其意義是從打開的文件fp中讀取一個字符並送入ch中。

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

1) 在fgetc函數調用中,讀取的文件必須是以讀或讀寫方式打開的。

2) 讀取字符的結果也可以不向字符變量賦值,

例如:

fgetc(fp);

但是讀出的字符不能保存。

3) 在文件內部有一個位置指針。用來指向文件的當前讀寫字節。在文件打開時,該指針總是指向文件的第一個字節。使用fgetc函數後,該位置指針將向後移動一個字節。 因此可連續多次使用fgetc函數,讀取多個字符。應注意文件指針和文件內部的位置指針不是一回事。文件指針是指向整個文件的,須在程序中定義說明,只要不重新賦值,文件指針的值是不變的。文件內部的位置指針用以指示文件內部的當前讀寫位置,每讀寫一次,該指針均向後移動,它不需在程序中定義說明,而是由系統自動設置的。

 

【例13.1】讀入文件c1.doc,在屏幕上輸出。

#include<stdio.h>

main()

{

FILE *fp;

char ch;

if((fp=fopen("d:\\jrzh\\example\\c1.txt","rt"))==NULL)

{

printf("\nCannot open file strike any keyexit!");

getch();

exit(1);

}

ch=fgetc(fp);

while(ch!=EOF)

{

putchar(ch);

ch=fgetc(fp);

}

fclose(fp);

}

本例程序的功能是從文件中逐個讀取字符,在屏幕上顯示。程序定義了文件指針fp,以讀文本文件方式打開文件d:\\jrzh\\example\\ex1_1.c,並使fp指向該文件。如打開文件出錯,給出提示並退出程序。程序第12行先讀出一個字符,然後進入循環,只要讀出的字符不是文件結束標誌(每個文件末有一結束標誌EOF)就把該字符顯示在屏幕上,再讀入下一字符。每讀一次,文件內部的位置指針向後移動一個字符,文件結束時,該指針指向EOF。執行本程序將顯示整個文件。

2. 寫字符函數fputc

fputc函數的功能是把一個字符寫入指定的文件中,函數調用的形式爲:

fputc(字符量,文件指針)

 

其中,待寫入的字符量可以是字符常量或變量,例如:

fputc('a',fp);

其意義是把字符a寫入fp所指向的文件中。

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

1) 被寫入的文件可以用寫、讀寫、追加方式打開,用寫或讀寫方式打開一個已存在的文件時將清除原有的文件內容,寫入字符從文件首開始。如需保留原有文件內容,希望寫入的字符以文件末開始存放,必須以追加方式打開文件。被寫入的文件若不存在,則創建該文件。

2) 每寫入一個字符,文件內部位置指針向後移動一個字節。

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

 

【例13.2】從鍵盤輸入一行字符,寫入一個文件,再把該文件內容讀出顯示在屏幕上。

#include<stdio.h>

main()

{

FILE*fp;

charch;

if((fp=fopen("d:\\jrzh\\example\\string","wt+"))==NULL)

{

printf("Cannot open file strike any keyexit!");


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行用於讀出文件中的一行內容。

 

13.4.2 字符串讀寫函數fgetsfputs

1. 讀字符串函數fgets 函數的功能是從指定的文件中讀一個字符串到字符數組中,函數調用的形式爲:

fgets(字符數組名,n,文件指針);

其中的n是一個正整數。表示從文件中讀出的字符串不超過 n-1個字符。在讀入的最後一個字符後加上串結束標誌'\0'。

 

例如:

fgets(str,n,fp);

的意義是從fp所指的文件中讀出n-1個字符送入字符數組str中。

【例13.4】從string文件中讀入一個含10個字符的字符串。

#include<stdio.h>

main()

{

FILE *fp;

char str[11];

if((fp=fopen("d:\\jrzh\\example\\string","rt"))==NULL)

{

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

getch();

exit(1);


}

fgets(str,11,fp);

printf("\n%s\n",str);

fclose(fp);

}


本例定義了一個字符數組str共11個字節,在以讀文本文件方式打開文件string後,從中讀出10個字符送入str數組,在數組最後一個單元內將加上'\0',然後在屏幕上顯示輸出str數組。輸出的十個字符正是例13.1程序的前十個字符。

對fgets函數有兩點說明:

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

2)fgets函數也有返回值,其返回值是字符數組的首地址。

 

2.寫字符串函數fputs

fputs函數的功能是向指定的文件寫入一個字符串,其調用形式爲:

fputs(字符串,文件指針);

其中字符串可以是字符串常量,也可以是字符數組名,或指針變量,例如:

fputs(“abcd“,fp);

 

其意義是把字符串“abcd”寫入fp所指的文件之中。

【例13.5】在例13.2中建立的文件string中追加一個字符串。

#include<stdio.h>

main()

{

FILE *fp;

char ch,st[20];

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

{

printf("Cannotopen file strike any key exit!");

getch();

exit(1);

}

printf("inputa 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函數把文件內部位置指針移到文件首。再進入循環逐個顯示當前文件中的全部內容。

13.4.3 數據塊讀寫函數freadfwtrite

 

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中。

【例13.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("d:\\jrzh\\example\\stu_list","wb+"))==NULL)

{


printf("Cannotopen file strike any key exit!");

getch();

exit(1);

}

printf("\ninputdata\n");

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(structstu),2,fp);

rewind(fp);

fread(qq,sizeof(structstu),2,fp);

printf("\n\nname\tnumberage addr\n");

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

printf("%s\t%5d%7d%s\n",qq->name,qq->num,qq->age,qq->addr);

fclose(fp);

}


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

13.4.4 格式化讀寫函數fscanffprintf

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所示。

【例13.7】用fscanf和fprintf函數成例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;

charch;

inti;

pp=boya;

qq=boyb;

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

{

printf("Cannotopen file strike any key exit!");

getch();

exit(1);

}

printf("\ninputdata\n");

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 %s\n",pp->name,pp->num,pp->age,pp->

addr);

rewind(fp);

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

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

printf("\n\nname\tnumberage addr\n");

qq=boyb;

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

printf("%s\t%5d%7d %s\n",qq->name,qq->num, qq->age,

qq->addr);

fclose(fp);

}

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

13.5 文件的隨機讀寫

前面介紹的對文件的讀寫方式都是順序讀寫,即讀寫文件只能從頭開始,順序讀寫各個數據。 但在實際問題中常要求只讀寫文件中某一指定的部分。爲了解決這個問題可移動文件內部的位置指針到需要讀寫的位置,再進行讀寫,這種讀寫稱爲隨機讀寫。

實現隨機讀寫的關鍵是要按要求移動位置指針,這稱爲文件的定位。

 

13.5.1 文件定位

移動文件內部位置指針的函數主要有兩個,即 rewind 函數和fseek函數。

rewind函數前面已多次使用過,其調用形式爲:

rewind(文件指針);

它的功能是把文件內部的位置指針移到文件首。

下面主要介紹fseek函數。

fseek函數用來移動文件內部位置指針,其調用形式爲:

fseek(文件指針,位移量,起始點);

其中:

文件指針指向被移動的文件。

位移量表示移動的字節數,要求位移量是long型數據,以便在文件長度大於64KB 時不會出錯。當用常量表示位移量時,要求加後綴L

起始點表示從何處開始計算位移量,規定的起始點有三種:文件首,當前位置和文件尾。

其表示方法如下表。 起始點

表示符號

數字表示

文件首

SEEK_SET

0

當前位置

SEEK_CUR

1

文件末尾

SEEK_END

2

例如:

fseek(fp,100L,0);

其意義是把位置指針移到離文件首100個字節處。

還要說明的是fseek函數一般用於二進制文件。在文本文件中由於要進行轉換,故往往計算的位置會出現錯誤。

13.5.2 文件的隨機讀寫

在移動位置指針之後,即可用前面介紹的任一種讀寫函數進行讀寫。由於一般是讀寫一個數據據塊,因此常用fread和fwrite函數。

下面用例題來說明文件的隨機讀寫。

 

【例13.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 strikeany key exit!");

getch();

exit(1);

}

rewind(fp);

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

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

printf("\n\nname\tnumber ageaddr\n");

printf("%s\t%5d %7d%s\n",qq->name,qq->num,qq->age,

qq->addr);

}


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

13.6 文件檢測函數

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

13.6.1 文件結束檢測函數feof函數

 

調用格式:

feof(文件指針);

功能:判斷文件是否處於文件結束位置,如文件結束,則返回值爲1,否則爲0。

13.6.2 讀寫文件出錯檢測函數

 

ferror函數調用格式:

ferror(文件指針);

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

13.6.3 文件出錯標誌和文件結束標誌置0函數

 

clearerr函數調用格式:

clearerr(文件指針);

功能:本函數用於清除出錯標誌和文件結束標誌,使它們爲0值。

13.7 C庫文件

C系統提供了豐富的系統文件,稱爲庫文件,C的庫文件分爲兩類,一類是擴展名爲".h"的文件,稱爲頭文件,在前面的包含命令中我們已多次使用過。在".h"文件中包含了常量定義、類型定義、宏定義、函數原型以及各種編譯選擇設置等信息。另一類是函數庫,包括了各種函數的目標代碼,供用戶在程序中調用。 通常在程序中調用一個庫函數時,要在調用之前包含該函數原型所在的".h" 文件。

下面給出Turbo C的全部".h"文件。

 

Turbo C頭文件

ALLOC.H 說明內存管理函數(分配、釋放等)。

ASSERT.H 定義 assert調試宏。

 BIOS.H 說明調用IBMPC ROMBIOS子程序的各個函數。

 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 說明一些串操作和內存操作函數。

 SYS\STAT.H 定義在打開和創建文件時用到的一些符號常量。

 SYS\TYPES.H 說明ftime函數和timeb結構。

 SYS\TIME.H 定義時間的類型time[ZZ(Z] [ZZ)]t

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

 VALUE.H 定義一些重要常量,包括依賴於機器硬件的和爲與UnixSystem V相兼容而說明的一些常量,包括浮點和雙精度值的範圍。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章