從頭開始介紹一門編程語言總是顯得很困難,因為有許多細節還沒有介紹,很難讓讀者在頭腦中形成一幅完整的圖。在本章中,我将向大家展示一個例子程序,并逐行講解它的工作過程,試圖讓大家對C語言的整體有一個大概的印象。這個例子程序同時向你展示了你所熟悉的過程在C語言中是如何實現的。這些信息再加上本章所讨論的其他主題,向你介紹了c語言的基礎知識,這樣你就可以自己編寫有用的C程序了。
我們所要分析的這個程序從标準輸入讀取文本并對其進行修改,然後把它寫到标準輸出。程序1.1首先讀取一串列标号。這些列标号成對出現,表示輸入行的列範圍。這串列标号以一個負值結尾,作為結束标志。剩餘的輸入行被程序讀入并打印,然後輸入行中被選中範圍的字符串被提取出來并打印。注意,每行第1列的列标号為零。例如,如果輸入如下:
則程序的輸出如下:
這個程序的重要之處在于它展示了當你開始編寫C程序時所需要知道的絕大多數基本技巧。
程序1.1重排字符
1.1.1 空白和注釋
現在,讓我們仔細觀察這個程序。首先需要注意的是程序的空白:空行将程序的不同部分分隔開來;制表符( tab )用于縮進語句,更好地顯示程序的結構等等。C是一種自由格式的語言,并沒有規則要求你必須怎樣書寫語句。然而,如果你在編寫程序時能夠遵守一些約定還是非常值得的,它可以使代碼更加容易閱讀和修改,千萬不要小看了這一點。
清晰地顯示程序的結構固然重要,但告訴讀者程序能做些什麼以及怎樣做則更為重要。注釋(comment)就是用于實現這個功能。
這段文字就是注釋。注釋以符号/*開始,以符号*/結束。在C程序中,凡是可以插入空白的地方都可以插入注釋。然而,注釋不能嵌套,也就是說,第1個*/符号和第1個*/符号之間的内容都被看作是注釋,不管裡面還有多少個/*符号。
在有些語言中,注釋有時用于把一段代碼“注釋掉",也就是使這段代碼在程序中不起作用,但并不将其真正從源文件中删除。在C語言中,這可不是個好主意,如果你試圖在一段代碼的首尾分别加上/*和*/符号來“注釋掉”這段代碼,你不一定能如願。如果這段代碼内部原先就有注釋存在,這樣做就會出問題。要從邏輯上删除一段C代碼,更好的辦法是使用#if指令。隻要像下面這樣使用:
在#if和#endif之間的程序段就可以有效地從程序中去除,即使這段代碼之間原先存在注釋也無妨,所以這是一種更為安全的方法。預處理指令的作用遠比你想象的要大,我将在第14章詳細讨論這個問題。
1.1.2 預處理指令
這5行稱為預處理指令(preprocessor directives) ,因為它們是由預處理器(preprocessor)解釋的。預處理器讀入源代碼,根據預處理指令對其進行修改,然後把修改過的源代碼遞交給編譯器。
在我們的例子程序中,預處理器用名叫stdio.h的庫函數頭文件的内容替換第1條#include指令語句,其結果就仿佛是stdio.n的内容被逐字寫到源文件的那個位置。第2、3條指令的功能類似,隻是它們所替換的頭文件分别是stdlib.n和string.h.
stdio.h頭文件使我們可以訪問标準I/O庫(Standard I/O Library)中的函數,這組函數用于執行輸入和輸出。 stdlib.h定義了EXIT_SUCCESS和EXIT_FAILURE符号。我們需要string.h頭文件提供的函數來操縱字符串。
這些聲明被稱為函數原型(function prototype),它們告訴編譯器這些以後将在源文件中定義的函數的特征。這樣,當這些函數被調用時,編譯器就能對它們進行準确性檢查。每個原型以一個類型名開頭,表示函數返回值的類型。跟在返回類型名後面的是函數的名字,再後面是函數期望接受的參數。所以,函數read_column_numbers返回一個整數,接受兩個類型分别是整型數組和整型标量的參數。函數原型中參數的名字并非必需,我這裡給出參數名的目的是提示它們的作用。
rearrange函數接受4個參數。其中第1個和第2個參數都是指針(pointer),指針指定一個存儲于計算機内存中的值的地址,類似于門牌号碼指定某個特定的家庭位于街道的何處。指針賦予C語言強大的威力,我将在第6章詳細講解指針。第2個和第4個參數被聲明為const ,這表示函數将不會修改函數調用者所傳遞的這兩個參數。關鍵字void表示函數并不返回任何值,在其他語言裡,這種無返回值的函數被稱為過程(procedure)。
1.1.3 main函數
這幾行構成了main函數定義的起始部分。每個C程序都必須有一個main函數,因為它是程序執行的起點。關鍵字int表示函數返回一個整型值,關鍵字void表示函數不接受任何參數。main函數的函數體包括左花括号和與之相匹配的右花括号之間的任何内容。
請觀察一下縮進是如何使程序的結構顯得更為清晰的。
這幾行聲明了4個變量:一個整型标量,一個整型數組以及兩個字符數組。所有4個變量都是main函數的局部變量,其他函數不能根據它們的名字訪問它們。當然,它們可以作為參數傳遞給其他函數。
這條語句調用函數read_column_numbers,數組columns和MAX_COLS所代表的常量(20)作為參數傳遞給這個函數。在C語言中,數組參數是以引用(reference)形式進行傳遞的,也就是傳址調用,而标量和常量則是按值(value)傳遞的(分别類似于Pascal和Modula中的var參數和值參數)。在函數中對标量參數的任何修改都會在函數返回時丢失,因此,被調用函數無法修改調用函數以傳值形式傳遞給它的參數。然而,當被調用函數修改數組參數的其中一個元素時,調用函數所傳遞的數組就會被實際地修改。
事實上,關于C函數的參數傳遞規則可以表述如下:
所有傳遞給函數的參數都是按值傳遞的。
但是,當數組名作為參數時就會産生按引用傳遞的效果,如上所示。規則和現實行為之間似乎存在明顯的矛盾之處,第8章會對此作出詳細解釋。
用于描述這段代碼的注釋看上去似乎有些多餘。但是,如今軟件開銷的最大之處并非在于編寫,而是在于維護。在修改一段代碼時所遇到的第1個問題就是要搞清楚代碼的功能。所以,如果你在代碼中插入一些東西,能使其他人(或許就是你自己! )在以後更容易理解它,那就非常值得這樣做。但是,要注意書寫正确的注釋,并且在你修改代碼時要注意注釋的更新。注釋如果不正确那還不如沒有!
這段代碼包含了一個while循環。在C語言中, while循環的功能和它在其他語言中一樣。它首先測試表達式的值,如果是假的(0)就跳過循環體。如果表達式的值是真的(非0) ,就執行循環體内的代碼,然後再重新測試表達式的值。
這個循環代表了這個程序的主要邏輯。簡而言之,它表示:
gets函數從标準輸入讀取一行文本并把它存儲于作為參數傳遞給它的數組中。一行輸入由一串字符組成,以一個換行符(newline)結尾。gets函數丢棄換行符,并在該行的末尾存儲一個NUL字節(一個NUL字節是指字節模式為全0的字節,類似‘\0’這樣的字符常量)。然後, gets函數返回一個非NULL值,表示該行已被成功讀取。當gets函數被調用但事實上不存在輸入行時,它就返回NULL值,表示它到達了輸入的末尾(文件尾)。
在C程序中,處理字符串是常見的任務之一。盡管C語言并不存在"string”數據類型,但在整個語言中存在一項約定:字符串就是一串以NUL字節結尾的字符。NUL是作為字符串終止符,它本身并不被看作是字符串的一部分。字符串常量(string literal)就是源程序中被雙引号括起來的一串字符。例如,字符串常量:
“Hello”
在内存中占據6個字節的空間,按順序分别是H、 e、l、l、o和NUL。
printf函數執行格式化的輸出。C語言的格式化輸出比較簡單,如果你是Modula或Pascal的用戶,你肯定會對此感到愉快。printf函數接受多個參數,其中第一個參數是一個字符串,描述輸出的格式,剩餘的參數就是需要打印的值。格式常常以字符串常量的形式出現。
格式字符串包含格式指定符(格式代碼)以及一些普通字符。這些普通字符将按照原樣逐字打印出來,但每個格式指定符将使後續參數的值按照它所指定的格式打印。表1.1列出了一些常用的格式指定符。如果數組input包含字符串Hi friend!,那麼下面這條語句
的打印結果是:
後面以一個換行符終止。
表1.1 常用printf格式代碼
例子程序接下來的一條語句調用rearrange函數。後面3個參數是傳遞給函數的值,第1個參數則是函數将要創建并返回給main函數的答案。記住,這種參數是唯一可以返回答案的方法,因為它是一個數組。最後一個print函數顯示輸入行重新整理後的結果。
最後,當循環結束時, main函數返回值EXIT_SUCCESS,該值向操作系統提示程序成功執行。右花括号标志着main函數體的結束。
1.1.4 read_column_numbers函數
這幾行構成了read_column_numbers函數的起始部分。注意,這個聲明和早先出現在程序中的該函數原型的參數個數和類型以及函數的返回值完全匹配。如果出現不匹配的情況,編譯嚣就會報錯
在函數聲明的數組參數中,并未指定數組的長度。這種格式是正确的,因為不論調用函數的程序傳遞給它的數組參數的長度是多少,這個函數都将照收不誤。這是一個偉大的特性,它允許單個函數操縱任意長度的一維數組。這個特性不利的一面是函數沒法知道該數組的長度。如果确實需要數組的長度,它的值必須作為一個單獨的參數傳遞給函數。
當本例的read_column_numbers函數被調用時,傳遞給函數的其中一個參數的名字碰巧與上面給出的形參名字相同。但是,其餘幾個參數的名字與對應的形參名字并不相同。和絕大多數語言一樣,C語言中形式參數的名字和實際參數的名字并沒有什麼關系。你可以讓兩者相同,但這并非必須。
這裡聲明了兩個變量,它們是該函數的局部變量。第1個變量在聲明時被初始化為0,但第2個變量并未初始化。更準确地說,它的初始值将是一個不可預料的值,也就是垃圾。在這個函數裡,它沒有初始值并不礙事,因為函數對這個變量所執行的第1個操作就是對它賦值。
這又是一個循環,用于讀取列标号。scanf函數從标準輸入讀取字符并根據格式字符串對它們進行轉換-類似于printf函數的逆操作。 scanf函數接受幾個參數,其中第1個參數是一個格式字符串,用于描述期望的輸入類型。剩餘幾個參數都是變量,用于存儲函教所讀取的輸入數據。scanf函數的返回值是函數成功轉換并存儲于參數中的值的個數。
我們現在可以解釋表達式:
格式碼%d表示需要讀取一個整型值。字符是從标準輸入讀取,前導空白将被跳過。然後這些數字被轉換為一個整數,結果存儲于指定的數組元素中。我們需要在參數前加上一個"&"符号,因為數組下标選擇的是一個單一的數組元素,它是一個标量。
while循環的測試條件由3個部分組成:
這個測試條件确保函數不會讀取過多的值,從而導緻數組溢出。如果scanf函數轉換了一個整數之後,它就會返回1這個值。最後,
這個表達式确保函數所讀取的值是正數。如果兩個測試條件之一的值為假,循環就會終止。
表1.2 常用scanf格式碼
&&是“邏輯與"操作符。要使整個表達式為真,&&操作符兩邊的表達式都必須為真。然而,如果左邊的表達式為假,右邊的表達式便不再進行求值,因為不管它是真是假,整個表達式總是假的。在這個例子中,如果num到達了它的最大值,循環就會終止,而表達式
便不再被求值。
scanf函數每次調用時都從标準輸入讀取一個十進制整數。如果轉換失敗,不管是因為文件已經讀完還是因為下一次輸入的字符無法轉換為整數,函數都會返回0,這樣就會使整個循環終止。如果輸入的字符可以合法地轉換為整數,那麼這個值就會轉換為二進制數存儲于數組元素columns[num]中。然後, scanf 函數返回1。
接下來的一個&&操作符确保在scanf函數成功讀取了一個數之後才對這個數進行是否賦值的測試。語句
使變量num的值增加1,它相當于下面這個表達式
以後我将解釋為什麼C語言提供了兩種不同的方式來增加一個變量的值。
這個測試檢查程序所讀取的整數是否為偶數個,這是程序規定的,因為這些數字要求成對出現。%操作符執行整數的除法,但它給出的結果是除法的餘數而不是商。如果num不是一個偶數,它除以2之後的餘數将不是0。
puts函數是gets函數的輸出版本,它把指定的字符串寫到标準輸出并在末尾添上一個換行符。程序接着調用exit函數,終止程序的運行, EXIT_FAILURE這個值被返回給操作系統,提示出現了錯誤。
當scan函數對輸入值進行轉換時,它隻讀取需要讀取的字符。這樣,該輸入行包含了最後一個值的剩餘部分仍會留在那裡,等待被讀取。它可能隻包含作為終止符的換行符,也可能包含其他字符。不論如何while循環将讀取并丢棄這些剩餘的字符,防止它們被解釋為第1行數據。
下面這個表達式
值得花點時間讨論。首先, getchar函數從标準輸入讀取一個字符并返回它的值。如果輸入中不再存在任何字符,函數就會返回常量EOF(在stdio.h中定義),用于提示文件的結尾。
從getchar函數返回的值被賦給變量ch ,然後把它與EOF進行比較。在賦值表達式兩端加上括号用于确保賦值操作先于比較操作進行。如果ch等于EOF,整個表達式的值就為假,循環将終止。若非如此,再把ch與換行符進行比較,如果兩者相等,循環也将終止。因此,隻有當輸入尚未到達文件尾并且輸入的字符并非換行符時,表達式的值才是真的(循環将繼續執行)。這樣,這個循環就能剔除當前輸入行最後的剩餘字符。
現在讓我們進入有趣的部分。在大多數其他語言中,我們将像下面這個樣子編寫循環:
它将讀取一個字符,接下來如果我們尚未到達文件的末尾或讀取的字符并不是換行符,它将繼續讀取下一個字符。注意這裡兩次出現了下面這條語句
C可以把賦值操作蘊含于while語句内部,這樣就允許程序員消除冗餘語句。
一個經常問到的問題是:為什麼ch被聲明為整型,而我們事實上需要它來讀取字符?答案是EOF是一個整型值,它的位數比字符類型要多,把ch聲明為整型可以防止從輸入讀取的字符意外地被解釋為EOF,但同時,這也意味着接收字符的ch必須足夠大,足以容納EOF ,這就是ch使用整型值的原因。正如第3章所讨論的那樣,字符隻是小整型數而已,所以用一個整型變量容納字符值并不會引起任何問題。
return語句就是函數向調用它的表達式返回一個值。在這個例子裡,變量num的值被返回給調用該函數的程序,後者把這個返回值賦值給主程序的n_columns變量。
1.1.5 rearrange函數
這些語句定義了rearrange函數并聲明了一些局部變量。此處最有趣的一點是:前兩個參數被聲明為指針,但在函數實際調用時,傳給它們的參數卻是數組名。當數組名作為實參時,傳給函數的實際上是一個指向數組起始位置的指針,也就是數組在内存中的地址。正因為實際傳遞的是一個指針而不是一份數組的拷貝,才使數組名作為參數時具備了傳址調用的語義。函數可以按照操縱指針的方式來操縱實參,也可以像使用數組名一樣用下标來引用數組的元素。第8章将對這些技巧進行更詳細的說明。
但是,由于它的傳址調用語義,如果函數修改了形參數組的元素,它實際上将修改實參數組的對應元素。因此,例子程序把columns聲明為const就有兩方面的作用。首先,它聲明該函數的作者的意圖是這個參數不能被修改。其次,它導緻編譯器去驗證是否違背該意圖。因此,這個函數的調用者不必擔心例子程序中作為第4個參數傳遞給函數的數組中的元素會被修改。
這個函數的真正工作是從這裡開始的。我們首先獲得輸入字符串的長度,這樣如果列标号超出了輸入行的範圍,我們就忽略它們。C語言的for語句跟它在其他語言中不太像,它更像是while語句的一種常用風格的簡寫法。for語句包含3個表達式(順便說一下,這3個表達式都是可選的)。第一個表達式是初始部分,它隻在循環開始前執行一次。第二個表達式是測試部分,它在循環每執行一次後都要執行一次。第三個表達式是調整部分,它在每次循環執行完畢後都要執行一次,但它在測試部分之前執行。為了清楚起見,上面這個for循環可以改寫為如下所示的while循環:
這是for循環的循環體,它一開始計算當前列範圍内字符的個數,然後決定是否繼續進行循環。如果輸入行比起始列短,或者輸出行已滿,它便不再執行任務,使用break語句立即退出循環。
接下來的一個測試檢查這個範圍内的所有字符是否都能放入輸出行中,如果不行,它就把nchars調整為數組能夠容納的大小。
最後, strncpy函數把選中的字符從輸入行複制到輸出行中可用的下一個位置。strncpy函數的前兩個參數分别是目标字符串和源字符串的地址。在這個調用中,目标字符串的位置是輸出數組的起始地址向後偏移output_col列的地址,源字符串的位置則是輸入數組起始地址向後偏移columns[col]個位置的地址。第3個參數指定需要複制的字符數,輸出列計數器随後向後移動nchars個位置。
循環結束之後,輸出字符串将以一個NUL字符作為終止符。注意,在循環體中,函數經過精心設計,确保數組仍有空間容納這個終止符。然後,程序執行流便到達了函數的未尾,于是執行一條隐式的return語句。由于不存在顯式的return語句,所以沒有任何值返回給調用這個函數的表達式。在這裡,不存在返回值并不會有問題,因為這個函數被聲明為void (也就是說,不返回任何值) ,并且當它被調用時,并不對它的返回值進行比較操作或把它賦值給其他變量。
1.2補充說明
本章的例子程序描述了許多C語言的基礎知識。但在你親自動手編寫程序之前,你還應該知道一些東西。首先是putchar函數,它與getchar函數相對應,它接受一個整型參數,并在标準輸出中打印該字符(如前所述,字符在本質上也是整型)
同時,在函數庫裡存在許多操縱字符串的函數。這裡我将簡單地介紹幾個最有用的。除非特别說明,這些函數的參數既可以是字符串常量,也可以是字符型數組名,還可以是一個指向字符的指針。
strcpy函數與strncpy函數類似,但它并沒有限制需要複制的字符數量。它接受兩個參數:第2個字符串參數将被複制到第1個字符串參數,第1個字符串原有的字符将被覆蓋。 strcat函數也接受兩個參數,但它把第2個字符串參數添加到第1個字符串參數的末尾。在這兩個函數中,它們的第1個字符串參數不能是字符串常量。而且,确保目标字符串有足夠的空間是程序員的責任,函數并不對其進行檢查。
在字符串内進行搜索的函數是strchr,它接受兩個參數,第1個參數是字符串,第2個參數是一個字符。這個函數在字符串參數内搜索字符參數第1次出現的位置,如果搜索成功就返回指向這個位置的指針,如果搜索失敗就返回一個NULL指針。 strstr函數的功能類似,但它的第2個參數也是一個字符串,它搜索第2個字符串在第1個字符串中第1次出現的位置。
1.3編譯
你編譯和運行C程序的方法取決于你所使用的系統類型。在UNIX系統中,要編譯一個存儲于文件testing.c的程序,要使用以下命令:
cc testing.c
a.out
在PC中,你需要知道你所使用的是哪一種編譯器。如果是Borland C ,在MS-DOS窗口中,可以使用下面的命令:
hcc testing.c
testing
1.4總結
本章的目的是描述足夠的C語言的基礎知識,使你對C語言有一個整體的印象。有了這方面的基礎,在接下來章節的學習中,你會更加容易理解。
本章的例子程序說明了許多要點。注釋以/*開始,以*/結束,用于在程序中添加一些描述性的說明。
#include預處理指令可以使一個函數庫頭文件的内容由編譯器進行處理, #define指令允許你給字面值常量取個符号名。
所有的C程序必須有一個main函數,應是程序執行的起點。函數的标量參教通過傳值的方式進行傳遞而數組名參數則具有傳址調用的語義。字符串是一串由NUL字節結尾的字符,并且有一組庫函數以不同的方式專門用于操縱字符串。printf函數執行格式化輸出, scanf函數用于格式化輸入, getchar和putchar分别執行非格式化字符的輸入和輸出。If和while語句在C語言中的用途跟它們在其他語言中的用途差不太多。
通過觀察例子程序的運行之後,你或許想親自編寫一些程序。你可能覺得C語言所包含的内容應該遠遠不止這些,确實如此,但是,這個例子程序應該足以讓你上手了。
1.5 警告的總結
1.在scanf函數的标量參數前未添加&字符。
2.機械地把printf函數的格式代碼照搬于scan函數。
3.在應該使用&&操作符的地方誤用了&操作符。
4.誤用=操作符而不是==操作符來測試相等性。
1.6編程提示的總結
1. 使用#include指令避免重複聲明
2. 使用#define指令給常量值取名。
3. 在#include文件中放置函數原型。
4. 在使用下标前先檢查它們的值。
5. 在while或i表達式中蘊含賦值操作。
6.如何編寫一個空循環體。
7.始終要進行檢查,确保數組不越界。
1.7問題
1.C是一種自由形式的語言,也就是說并沒有規則規定它的外觀究竟應該怎樣。但本章的例子程序遵循了一定的空白使用規則。你對此有何想法?
2.把聲明(如函數原型的聲明)放在頭文件中,并在需要時用#include指令把它們包含于源文件中,這種做法有什麼好處?
3.使用#deine字面值常量取名有什麼好處?
4.依次打印一個十進制整數、字符串和浮點值,你應該在printf函數中分别使用什麼格式代碼?試編一例,讓這些打印值以空格分隔,并在輸出行的末尾添加一個換行符。
5.編寫一條scanf語句,它需要讀取兩個整數,分别保存于quantity和price變量,然後再讀取一個字符串,保存在一個名叫department的字符數組中。
6.C語言并不執行數組下标的有效性檢查。你覺得為什麼這個明顯的安全手段會從語言中省略?
7.本章描述的rearrange程序包含下面的語句
strncpy( output output_col,
input columns [col], nchars);
strcpy函數隻接受兩個參數,所以它實際上所複制的字符數由第2個參數指定。在本程序中,如果用strcpy函數取代strncpy函數會出現什麼結果?
8. rearrange程序包含下面的語句
while( gets( input ) != NULL ){
你認為這段代碼可能會出現什麼問題?
本文節選自《C和指針》
本書提供與C語言編程相關的全面資源和深入讨論。本書通過對指針的基礎知識和高級特性的探讨,幫助程序員把指針的強大功能融入到自己的程序中去。 全書共18章,覆蓋了數據、語句、操作符和表達式、指針、函數、數組、字符串、結構和聯合等幾乎所有重要的C編程話題。書中給出了很多編程技巧和提示,每章後面有針對性很強的練習,附錄部分則給出了部分練習的解答。
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!