|
|
Acute
論壇第一大毒王
論壇第一小神童
. 積分: 3281
. 精華: 8
. 文章: 11574
. 收花: 14037 支
. 送花: 3260 支
. 比例: 0.23
. 在線: 323 小時
. 瀏覽: 2250 頁
. 註冊: 8216 天
. 失蹤: 5568 天
|
|
|
|
|
|
|
#5 : 2004-6-13 03:04 PM
只看本作者
|
送花
(12)
送出中...
|
|
|
A-2-6 初值設定
賦予變數初值對絕大部分的程式而言, 是一種經常且必要的動作
。然而, 在K&R C 的定義中, 變數的初值設定有相當大的限制, 甚至
在很多情形下, 變數是無法賦予初值的。在ANSI C的定義中, 這些問
題有了戲劇性的變化, ANSI C允許程式對所有的變數賦予初值, 不過
有些較特殊的資料型態, 仍舊有限定初值的設定方式。
對K&R C 的定義而言, 賦予變數初值的規則可簡單歸納為:
1) 除了等位(union) 外, 所有的外部變數均可設定初值。
2) 除了等位外, 所有的靜態變數均可設定初值; 不論該靜態變
數是屬於外部變數或自動變數。
3) 對自動變數而言, 僅有基本型態的變數, 如:char、int、
float..., 等少數變數可設定初值外, 其餘的延伸或組合型
態, 如:陣列、struct、union..., 等自動變數均不可設定
初值。
對ANSI C的定義而言, 則全面允許程式賦予變數初值。但是對於
等位賦予初值的方式則有限制。ANSI C規定, 賦予等位之變數初值時
, 僅能以等位中第一個元素的型態賦予初值。
底下是一個簡單的範例, 每個宣告式的後面都會有一個註解, 標
示該動作於K&R C 及ANSI C中, 是否屬於合法的宣告式。為了方便起
見, 筆者先定義幾個結構及等位。
/***************************/
/** define struct & union **/
/***************************/
typedef struct tagPOINT {
int x;
int y;
} POINT ;
typedef struct tagREG1 {
char al;
char ah;
} REG1;
typedef union tagREG {
int ax;
REG1 h;
} REG;
/******************************************/
/*** init-declarator in K&R C or ANSI C ***/
/******************************************/
/****** global variable ******/
/* K&R C ANSI C */
char gc='a'; /* pass pass */
char gs="Emperor"; /* pass pass */
int gi=123; /* pass pass */
int gia[]={10,20,30}; /* pass pass */
long gl=123L; /* pass pass */
POINT gp={10,20}; /* pass pass */
REG gu1=100; /* ERROR pass */
REG gu2={1,2}; /* ERROR ERROR */
/****** static variable ******/
/* K&R C ANSI C */
static char gsc='a'; /* pass pass */
static char gss="Emperor"; /* pass pass */
static int gsi=123; /* pass pass */
static int gsia[]={10,20,30}; /* pass pass */
static long gsl=123L; /* pass pass */
static POINT gsp={10,20}; /* pass pass */
static REG gsu1=100; /* ERROR pass */
static REG gsu2={1,2}; /* ERROR ERROR */
func ()
{
/****** local variable ******/
/* K&R C ANSI C */
char lc='a'; /* pass pass */
char ls="Emperor"; /* ERROR pass */
int li=123; /* pass pass */
int lia[]={10,20,30}; /* ERROR pass */
long ll=123L; /* pass pass */
POINT lp={10,20}; /* ERROR pass */
REG lu1=100; /* ERROR pass */
REG lu2={1,2}; /* ERROR ERROR */
/****** static variable ******/
/* K&R C ANSI C */
static char lsc='a'; /* pass pass */
static char lss="Emperor"; /* pass pass */
static int lsi=123; /* pass pass */
static int lsia[]={10,20,30}; /* pass pass */
static long lsl=123L; /* pass pass */
static POINT lsp={10,20}; /* pass pass */
static REG lsu1=100; /* ERROR pass */
static REG lsu2={1,2}; /* ERROR ERROR */
/* ...... */
}
A-2-7 前置處理器
前置處理器 (Preprocessor) 是C 語言相當重要的一項能力, 幾
乎每個程式都會用到的#include與#define 都是屬於前置處理器的處
理範圍。ANSI C在前置處理器上的變動並不多, 但是對K&R 的著作而
言, 前後兩版卻有相當大的變動, 原因是K&R 第一版將前置處理器的
許多內容都放在附錄A中。也造成許多人搞不清楚K&R C 所定義的前
置處理器與ANSI C所定義的有多大之差距。在本節中, 筆者將詳細介
紹此一內容, 而不論是否為ANSI C新增或修正過的內容。
A-2-7-1 引入檔案
引入檔案(File Include)的動作, 在K&R C 中, #include的用法
相當簡單, 程式僅需將欲引入的檔案用一組雙引號("")括起來即可。
請看下面的程式片段:
/* 範例九 */
#include "stdio.h"
#include "extio.h"
上面的例子對一個經驗豐富的程式設計人員而言, 不會產生任何
困擾, 因為"stdio.h" 是屬於標準 [註三] 的引入檔, 而"extio.h"
不是。但是對一個並不很了解C 語言的程式設計人員而言, 他很難分
辨兩個檔案的歸屬問題。
針對上述的問題, K&R 在制定C 語言時已經考慮到了, 他們用角
括號(<>)來表示標準的引入檔, 而原來的雙引號用法, 則保留給使用
者自定的引入檔使用。但是不知道為什麼, K&R 在他們的著作之第一
版時, 並未明確的說明具有角括號的用法, 僅在該書的附錄A中簡略
介紹一番, 直到第二版時, 才在介紹#include時將角括號及雙引號的
差別予以詳細介紹。事實上, 有不少程式設計人員在拜讀完K&R 的第
一版後, 並不曉得雙引號與角括號對#include敘述有何差異性。上述
的範例九採用符合C 標準的寫法為:
/* 範例十 */
#include <stdio.h>
#include "extio.h"
如此, 範例十的可讀性變得比範例九高, 而且任何人都可輕易看
出這兩個檔案的來源, 不會感到疑惑。
[註三] 標準的引入檔, 意指:此檔是標準C 語言所定義的引入檔
, 諸如:stdio.h、stdlib.h、limits.h 等。但是, 大部
分的C 編譯器都將它擴展為:舉凡C 編譯器提供的引入檔
, 均屬『標準的』引入檔。例如:dos.h 等。
對一個符合C 標準的C 編譯器而言, 遇到使用角括號的#include
敘述, 編譯器只會在存放標準引入檔的目錄中找尋, 找不到則發出錯
誤訊息。遇到使用雙引號的#include敘述, 編譯器則會先在目前的工
作目錄中找尋, 找不到時再到存放標準引入檔的目錄中找尋, 再找不
到才會發出錯誤訊息。採用這麼複雜的法則, 大概是為了使許多偷懶
的人可從頭到尾都用雙引號, 但是, 這實在是一個很差勁的壞習慣,
開發程式時應遵照標準, 除了程式容易了解外, 編譯器編譯程式的速
度也會比較快。
有一種很容易犯的錯誤, 常會發生在一個剛開始使用C 語言的使
用者身上, 請思考下面這個範例, 答案可在上面的文句中找到。
/* 範例十一 */
#include <stdio.h>
#include <extio.h>
A-2-7-2 巨集替換
『巨集替換』是C 語言一項很重要的特色, 雖然它有一些無法彌
補的缺陷、有許多容易令人犯錯的缺點、還有一點功能上的限制, 使
得使用時必須非常小心, 否則相當不容易找到因巨集而引發的併發症
。但是, 幾乎每個C 程式都必須使用它, 小到定義一個常數, 大到定
義一群『簡易的、明確的』程式碼, 巨集讓C 程式的可讀性增色不少
、也實質的提昇程式維護的簡易性。
K&R C 對巨集替換的定義相當單純, 大部分懂C 語言的人都曉得
如何運用; 而ANSI C則額外的擴充了幾個使用方法, 使得巨集的能力
更加無遠弗屆, 讓許多從前做不到的動作變成可能。可惜的是, 對於
巨集容易引發錯誤的現象依舊無法消除; 或許, 除非採用類似C++ 的
inline function 作法, 讓編譯器在編譯程式時, 將巨集當作實際的
函式, 根據語言規則解釋、編譯, 否則就既有的替代模式而言, 根本
無法解決因直接替換而引發的併發症。
ANSI C在巨集定義中增加了# 及##兩個運算。這兩個運算主要都
是針對字串常數或文字常數而設計。由於ANSI C對於巨集的說明相當
繁瑣, 而且前半部與舊規則是相同的, 因此筆者僅擷取其後半段的說
明, 解釋這兩個運算的用途。
首先, 若在巨集替換過程中, 巨集參數的前方有一個#, 則會用
雙引號(") 括住該參數的內容, 若該參數的內容中具有\ 或" , 則自
動加入一個\ 字元。
其次, 若在巨集替換過程中, 包含了一個##運算, 則巨集替換後
, 會刪除每一個##以及兩邊的任何空白符號, 因此, 就把兩個相鄰的
語法單元連接起來, 構成一個新的語法單元。若因而造成不正確的語
法單元, 或是結果會因處理##運算的次序而異, 則結果將是沒有定義
的。另外, 替換用的巨集之開始或結尾處都不得有##。
在這兩個巨集指令中, 都會一再反覆進行替換工作, 以進一步找
出被定義過的辨識名稱。但是, 如果在某一次展開過程中, 辨識名稱
被替換過了, 而在反覆檢查過程中又出現了相同的識別名稱, 則不再
替換該識別名稱。
即使巨集展開後, 其結果是由#開始, 它也不會被當成事前置處
理程式的指引敘述看待。
/* 範例十二 */
#define Cat1(str1,str2) #str1 ## #str2
#define Cat2(str1,str2) str1 ## str2
#define Cat3(str1,str2) Cat2(str1,str2)
#define dPrint(arg) printf(#arg " = %d\n",arg)
#define sPrint(arg) printf(#arg " = " arg ## "\n")
main ()
{
int abc=1000;
/* 巨集參數的前方有一個#, 則會用
雙引號(") 括住該參數的內容 */
sPrint (Cat1(c:\\,test.dat));
sPrint (Cat1("c:\\","test.dat")); /* error */
/* ##運算, 會刪除每一個##以及兩邊的任何空
白符號, 把兩個相鄰的語法單元連接起來 */
sPrint (Cat2("c:\\ "," test.dat"));
dPrint (100/5);
/* 如果在某次展開過程中, 辨識名稱被替換過了,
而在反覆檢查......, 則不再替換該識別名稱,
因此, 不可直接使用Cat2 */
dPrint (Cat3(Cat3(a,b),c));
}
執行結果
Cat1(c:\,test.dat) = c:\test.dat
Cat1("c:\\","test.dat") = "c:\\""test.dat"
Cat2("c:\\","test.dat") = c:\test.dat
100/5 = 20
Cat3(Cat3(a,b),c) = 1000
[如果你喜歡本文章,就按本文章之鮮花~送花給作者吧,你的支持就是別人的動力來源]
|
|