讀古今文學網 > CPrimerPlus(第6版)(中文版) > 第1章 初識C語言 >

第1章 初識C語言

本章介紹以下內容:

C的歷史和特性

編寫程序的步驟

編譯器和鏈接器的一些知識

C標準

歡迎來到C語言的世界。C是一門功能強大的專業化編程語言,深受業餘編程愛好者和專業程序員的喜愛。本章為讀者學習這一強大而流行的語言打好基礎,並介紹幾種開發C程序最可能使用的環境。

我們先來瞭解C語言的起源和一些特性,包括它的優缺點。然後,介紹編程的起源並探討一些編程的基本原則。最後,討論如何在一些常見系統中運行C程序。

1.1 C語言的起源

1972年,貝爾實驗室的丹尼斯·裡奇(Dennis Ritch)和肯·湯普遜(Ken Thompson)在開發UNIX操作系統時設計了C語言。然而,C語言不完全是裡奇突發奇想而來,他是在B語言(湯普遜發明)的基礎上進行設計。至於 B 語言的起源,那是另一個故事。C 語言設計的初衷是將其作為程序員使用的一種編程工具,因此,其主要目標是成為有用的語言。

雖然絕大多數語言都以實用為目標,但是通常也會考慮其他方面。例如,Pascal 的主要目標是為更好地學習編程原理提供紮實的基礎;而BASIC的主要目標是開發出類似英文的語言,讓不熟悉計算機的學生輕鬆學習編程。這些目標固然很重要,但是隨著計算機的迅猛發展,它們已經不是主流語言。然而,最初為程序員設計開發的C語言,現在已成為首選的編程語言之一。

1.2 選擇C語言的理由

在過去40多年裡,C語言已成為最重要、最流行的編程語言之一。它的成長歸功於使用過的人都對它很滿意。過去20多年裡,雖然許多人都從C語言轉而使用其他編程語言(如,C++、Objective C、Java等),但是C語言仍憑借自身實力在眾多語言中脫穎而出。在學習C語言的過程中,會發現它的許多優點(見圖1.1)。下面,我們來看看其中較為突出的幾點。

1.2.1 設計特性

C是一門流行的語言,融合了計算機科學理論和實踐的控制特性。C語言的設計理念讓用戶能輕鬆地完成自頂向下的規劃、結構化編程和模塊化設計。因此,用C語言編寫的程序更易懂、更可靠。

1.2.2 高效性

C是高效的語言。在設計上,它充分利用了當前計算機的優勢,因此 C程序相對更緊湊,而且運行速度很快。實際上,C 語言具有通常是彙編語言才具有的微調控制能力(彙編語言是為特殊的中央處理單元設計的一系列內部指令,使用助記符來表示;不同的 CPU 系列使用不同的彙編語言),可以根據具體情況微調程序以獲得最大運行速度或最有效地使用內存。

圖1.1 C語言的優點

1.2.3 可移植性

C是可移植的語言。這意味著,在一種系統中編寫的 C程序稍作修改或不修改就能在其他系統運行。如需修改,也只需簡單更改主程序頭文件中的少許項即可。大部分語言都希望成為可移植語言,但是,如果經歷過把IBM PC BASIC程序轉換成蘋果BASIC(兩者是近親),或者在UNIX系統中運行IBM大型機的FORTRAN程序的人都知道,移植是最麻煩的事。C語言是可移植方面的佼佼者。從8位微處理器到克雷超級計算機,許多計算機體系結構都可以使用C編譯器(C編譯器是把C代碼轉換成計算機內部指令的程序)。但是要注意,程序中針對特殊硬件設備(如,顯示監視器)或操作系統特殊功能(如,Windows 8或OS X)編寫的部分,通常是不可移植的。

由於C語言與UNIX關係密切,UNIX系統通常會將C編譯器作為軟件包的一部分。安裝Linux時,通常也會安裝C編譯器。供個人計算機使用的C編譯器很多,運行各種版本的Windows和Macintosh(即, Mac)的PC都能找到合適的C編譯器。因此,無論是使用家庭計算機、專業工作站,還是大型機,都能找到針對特定系統的C編譯器。

1.2.4 強大而靈活

C語言功能強大且靈活(計算機領域經常使用這兩個詞)。例如,功能強大且靈活的UNIX操作系統,大部分是用C語言寫的;其他語言(如,FORTRAN、Perl、Python、Pascal、LISP、Logo、BASIC)的許多編譯器和解釋器都是用C語言編寫的。因此,在UNIX機上使用FORTRAN時,最終是由C程序生成最後的可執行程序。C程序可以用於解決物理學和工程學的問題,甚至可用於製作電影的動畫特效。

1.2.5 面向程序員

C 語言是為了滿足程序員的需求而設計的,程序員利用 C 可以訪問硬件、操控內存中的位。C 語言有豐富的運算符,能讓程序員簡潔地表達自己的意圖。C沒有Pascal嚴謹,但是卻比C++的限制多。這樣的靈活性既是優點也是缺點。優點是,許多任務用C來處理都非常簡潔(如,轉換數據的格式);缺點是,你可能會犯一些莫名其妙的錯誤,這些錯誤不可能在其他語言中出現。C 語言在提供更多自由的同時,也讓使用者承擔了更大的責任。

另外,大多數C實現都有一個大型的庫,包含眾多有用的C函數。這些函數用於處理程序員經常需要解決的問題。

1.2.6 缺點

人無完人,金無足赤。C語言也有一些缺點。例如,前面提到的,要享受用C語言自由編程的樂趣,就必須承擔更多的責任。特別是,C語言使用指針,而涉及指針的編程錯誤往往難以察覺。有句話說的好:想擁有自由就必須時刻保持警惕。

C 語言緊湊簡潔,結合了大量的運算符。正因如此,我們也可以編寫出讓人極其費解的代碼。雖然沒必要強迫自己編寫晦澀的代碼,但是有興趣寫寫也無妨。試問,除 C語言外還為哪種語言舉辦過年度混亂代碼大賽[1]?

瑕不掩瑜,C語言的優點比缺點多很多。我們不想在這裡多費筆墨,還是來聊聊C語言的其他話題。

1.3 C語言的應用範圍

早在20世紀80年代,C語言就已經成為小型計算機(UNIX系統)使用的主流語言。從那以後,C語言的應用範圍擴展到微型機(個人計算機)和大型機(龐然大物)。如圖1.2所示,許多軟件公司都用C語言來開發文字處理程序、電子錶格、編譯器和其他產品,因為用 C語言編寫的程序緊湊而高效。更重要的是,C程序很方便修改,而且移植到新型號的計算機中也沒什麼問題。

無論是軟件公司、經驗豐富的C程序員,還是其他用戶,都能從C語言中受益。越來越多的計算機用戶已轉而求助C語言解決一些安全問題。不一定非得是計算機專家也能使用C語言。

20世紀90年代,許多軟件公司開始改用C++來開發大型的編程項目。C++在C語言的基礎上嫁接了面向對像編程工具(面向對像編程是一門哲學,它通過對語言建模來適應問題,而不是對問題建模以適應語言)。C++幾乎是C的超集,這意味著任何C程序差不多就是一個C++程序。學習C語言,也相當於學習了許多C++的知識。

圖1.2 C語言的應用範圍

雖然這些年來C++和JAVA非常流行,但是C語言仍是軟件業中的核心技能。在最想具備的技能中,C語言通常位居前十。特別是,C 語言已成為嵌入式系統編程的流行語言。也就是說,越來越多的汽車、照相機、DVD 播放機和其他現代化設備的微處理器都用 C 語言進行編程。除此之外,C 語言還從長期被FORTRAN獨佔的科學編程領域分得一杯羹。最終,作為開發操作系統的卓越語言,C在Linux開發中扮演著極其重要的角色。因此,在進入21世紀的第2個10年中,C語言仍然保持著強勁的勢頭。

簡而言之,C 語言是最重要的編程語言之一,將來也是如此。如果你想拿下一份編程的工作,被問到是否會C語言時,最好回答「是」。

1.4 計算機能做什麼

在學習如何用C語言編程之前,最好先瞭解一下計算機的工作原理。這些知識有助於你理解用C語言編寫程序和運行C程序時所發生的事情之間有什麼聯繫。

現代的計算機由多種部件構成。中央處理單元(CPU)承擔絕大部分的運算工作。隨機存取內存(RAM)是存儲程序和文件的工作區;而永久內存存儲設備(過去一般指機械硬盤,現在還包括固態硬盤)即使在關閉計算機後,也不會丟失之前儲存的程序和文件。另外,還有各種外圍設備(如,鍵盤、鼠標、觸摸屏、監視器)提供人與計算機之間的交互。CPU負責處理程序,接下來我們重點討論它的工作原理。

CPU 的工作非常簡單,至少從以下簡短的描述中看是這樣。它從內存中獲取並執行一條指令,然後再從內存中獲取並執行下一條指令,諸如此類(一個吉赫茲的CPU一秒鐘能重複這樣的操作大約十億次,因此,CPU 能以驚人的速度從事枯燥的工作)。CPU 有自己的小工作區——由若干個寄存器組成,每個寄存器都可以儲存一個數字。一個寄存器儲存下一條指令的內存地址,CPU 使用該地址來獲取和更新下一條指令。在獲取指令後,CPU在另一個寄存器中儲存該指令,並更新第1個寄存器儲存下一條指令的地址。CPU能理解的指令有限(這些指令的集合叫作指令集)。而且,這些指令相當具體,其中的許多指令都是用於請求計算機把一個數字從一個位置移動到另一個位置。例如,從內存移動到寄存器。

下面介紹兩個有趣的知識。其一,儲存在計算機中的所有內容都是數字。計算機以數字形式儲存數字和字符(如,在文本文檔中使用的字母)。每個字符都有一個數字碼。計算機載入寄存器的指令也以數字形式儲存,指令集中的每條指令都有一個數字碼。其二,計算機程序最終必須以數字指令碼(即,機器語言)來表示。

簡而言之,計算機的工作原理是:如果希望計算機做某些事,就必須為其提供特殊的指令列表(程序),確切地告訴計算機要做的事以及如何做。你必須用計算機能直接明白的語言(機器語言)創建程序。這是一項繁瑣、乏味、費力的任務。計算機要完成諸如兩數相加這樣簡單的事,就得分成類似以下幾個步驟。

1.從內存位置2000上把一個數字拷貝到寄存器1。

2.從內存位置2004上把另一個數字拷貝到寄存器2。

3.把寄存器2中的內容與寄存器1中的內容相加,把結果儲存在寄存器1中。

4.把寄存器1中的內容拷貝到內存位置2008。

而你要做的是,必須用數字碼來表示以上的每個步驟!

如果以這種方式編寫程序很合你的意,那不得不說抱歉,因為用機器語言編程的黃金時代已一去不復返。但是,如果你對有趣的事情比較感興趣,不妨試試高級編程語言。

1.5 高級計算機語言和編譯器

高級編程語言(如,C)以多種方式簡化了編程工作。首先,不必用數字碼表示指令;其次,使用的指令更貼近你如何想這個問題,而不是類似計算機那樣繁瑣的步驟。使用高級編程語言,可以在更抽像的層面表達你的想法,不用考慮CPU在完成任務時具體需要哪些步驟。例如,對於兩數相加,可以這樣寫:

total = mine + yours;

對我們而言,光看這行代碼就知道要計算機做什麼;而看用機器語言寫成的等價指令(多條以數字碼形式表現的指令)則費勁得多。但是,對計算機而言卻恰恰相反。在計算機看來,高級指令就是一堆無法理解的無用數據。編譯器在這裡派上了用場。編譯器是把高級語言程序翻譯成計算機能理解的機器語言指令集的程序。程序員進行高級思維活動,而編譯器則負責處理冗長乏味的細節工作。

編譯器還有一個優勢。一般而言,不同CPU製造商使用的指令系統和編碼格式不同。例如,用Intel Core i7 (英特爾酷睿i7)CPU編寫的機器語言程序對於ARM Cortex-A57 CPU而言什麼都不是。但是,可以找到與特定類型CPU匹配的編譯器。因此,使用合適的編譯器或編譯器集,便可把一種高級語言程序轉換成供各種不同類型 CPU 使用的機器語言程序。一旦解決了一個編程問題,便可讓編譯器集翻譯成不同 CPU 使用的機器語言。

簡而言之,高級語言(如C、Java、Pascal)以更抽像的方式描述行為,不受限於特定CPU或指令集。而且,高級語言簡單易學,用高級語言編程比用機器語言編程容易得多。

1964年,控制數據公司(Control Data Corporation)研製出了CDC 6600計算機。這台龐然大物是世界上首台超級計算機,當時的售價是600萬美元。它是高能核物理研究的首選。然而,現在的普通智能手機在計算能力和內存方面都超過它數百倍,而且能看視頻,放音樂。

1964 年,在工程和科學領域的主流編程語言是 FORTRAN。雖然編程語言不如硬件發展那麼突飛猛進,但是也發生了很大變化。為了應對越來越大型的編程項目,語言先後為結構化編程和面向對像編程提供了更多的支持。隨著時間的推移,不僅新語言層出不窮,而且現有語言也會發生變化。

1.6 語言標準

目前,有許多C實現可用。在理想情況下,編寫C程序時,假設該程序中未使用機器特定的編程技術,那麼它的運行情況在任何實現中都應該相同。要在實踐中做到這一點,不同的實現要遵循同一個標準。

C語言發展之初,並沒有所謂的C標準。1987年,布萊恩·柯林漢(Brian Kernighan)和丹尼斯·裡奇(Dennis Ritchie)合著的The C Programming Language(《C語言程序設計》)第1版是公認的C標準,通常稱之為K&R C或經典C。特別是,該書中的附錄中的「C語言參考手冊」已成為實現C的指導標準。例如,編譯器都聲稱提供完整的K&R實現。雖然這本書中的附錄定義了C語言,但卻沒有定義C庫。與大多數語言不同的是,C語言比其他語言更依賴庫,因此需要一個標準庫。實際上,由於缺乏官方標準,UNIX實現提供的庫已成為了標準庫。

1.6.1 第1個ANSI/ISO C標準

隨著C的不斷發展,越來越廣泛地應用於更多系統中,C社區意識到需要一個更全面、更新穎、更嚴格的標準。鑒於此,美國國家標準協會(ANSI)於 1983 年組建了一個委員會(X3J11),開發了一套新標準,並於1989年正式公佈。該標準(ANSI C)定義了C語言和C標準庫。國際標準化組織於1990年採用了這套C標準(ISO C)。ISO C和ANSI C是完全相同的標準。ANSI/ISO標準的最終版本通常叫作C89(因為ANSI於1989年批准該標準)或C90(因為ISO於1990年批准該標準)。另外,由於ANSI先公佈C標準,因此業界人士通常使用ANSI C。

在該委員會制定的指導原則中,最有趣的可能是:保持 C的精神。委員會在表述這一精神時列出了以下幾點:

信任程序員;

不要妨礙程序員做需要做的事;

保持語言精練簡單;

只提供一種方法執行一項操作;

讓程序運行更快,即使不能保證其可移植性。

在最後一點上,標準委員會的用意是:作為實現,應該針對目標計算機來定義最合適的某特定操作,而不是強加一個抽像、統一的定義。在學習C語言過程中,許多方面都反映了這一哲學思想。

1.6.2 C99標準

1994年,ANSI/ISO聯合委員會(C9X委員會)開始修訂C標準,最終發佈了C99標準。該委員會遵循了最初C90標準的原則,包括保持語言的精練簡單。委員會的用意不是在C語言中添加新特性,而是為了達到新的目標。第1個目標是,支持國際化編程。例如,提供多種方法處理國際字符集。第2個目標是,「調整現有實踐致力於解決明顯的缺陷」。因此,在遇到需要將C移至64位處理器時,委員會根據現實生活中處理問題的經驗來添加標準。第3個目標是,為適應科學和工程項目中的關鍵數值計算,提高C的適應性,讓C比FORTRAN更有競爭力。

這3點(國際化、彌補缺陷和提高計算的實用性)是主要的修訂目標。在其他方面的改變則更為保守,例如,盡量與C90、C++兼容,讓語言在概念上保持簡單。用委員會的話說:「„„委員會很滿意讓C++成為大型、功能強大的語言」。

C99的修訂保留了C語言的精髓,C仍是一門簡潔高效的語言。本書指出了許多C99修改的地方。雖然該標準已發佈了很長時間,但並非所有的編譯器都完全實現C99的所有改動。因此,你可能發現C99的一些改動在自己的系統中不可用,或者只有改變編譯器的設置才可用。

1.6.3 C11標準

維護標準任重道遠。標準委員會在2007年承諾C標準的下一個版本是C1X,2011年終於發佈了C11標準。此次,委員會提出了一些新的指導原則。出於對當前編程安全的擔憂,不那麼強調「信任程序員」目標了。而且,供應商並未像對C90那樣很好地接受和支持C99。這使得C99的一些特性成為C11的可選項。因為委員會認為,不應要求服務小型機市場的供應商支持其目標環境中用不到的特性。另外需要強調的是,修訂標準的原因不是因為原標準不能用,而是需要跟進新的技術。例如,新標準添加了可選項支持當前使用多處理器的計算機。對於C11標準,我們淺嘗輒止,深入分析這部分內容已超出本書討論的範圍。

注意

本書使用術語ANSI C、ISO C或ANSI/ISO C講解C89/90和較新標準共有的特性,用C99或C11介紹新的特性。有時也使用C90(例如,討論一個特性被首次加入C語言時)。

1.7 使用C語言的7個步驟

C是編譯型語言。如果之前使用過編譯型語言(如,Pascal或FORTRAN),就會很熟悉組建C程序的幾個基本步驟。但是,如果以前使用的是解釋型語言(如,BASIC)或面向圖形界面語言(如,Visual Basic),或者甚至沒接觸過任何編程語言,就有必要學習如何編譯。別擔心,這並不複雜。首先,為了讓讀者對編程有大概的瞭解,我們把編寫C程序的過程分解成7個步驟(見圖1.3)。注意,這是理想狀態。在實際的使用過程中,尤其是在較大型的項目中,可能要做一些重複的工作,根據下一個步驟的情況來調整或改進上一個步驟。

圖1.3 編程的7個步驟

1.7.1 第1步:定義程序的目標

在動手寫程序之前,要在腦中有清晰的思路。想要程序去做什麼首先自己要明確自己想做什麼,思考你的程序需要哪些信息,要進行哪些計算和控制,以及程序應該要報告什麼信息。在這一步驟中,不涉及具體的計算機語言,應該用一般術語來描述問題。

1.7.2 第2步:設計程序

對程序應該完成什麼任務有概念性的認識後,就應該考慮如何用程序來完成它。例如,用戶界面應該是怎樣的?如何組織程序?目標用戶是誰?準備花多長時間來完成這個程序?

除此之外,還要決定在程序(還可能是輔助文件)中如何表示數據,以及用什麼方法處理數據。學習C語言之初,遇到的問題都很簡單,沒什麼可選的。但是,隨著要處理的情況越來越複雜,需要決策和考慮的方面也越來越多。通常,選擇一個合適的方式表示信息可以更容易地設計程序和處理數據。

再次強調,應該用一般術語來描述問題,而不是用具體的代碼。但是,你的某些決策可能取決於語言的特性。例如,在數據表示方面,C的程序員就比Pascal的程序員有更多選擇。

1.7.3 第3步:編寫代碼

設計好程序後,就可以編寫代碼來實現它。也就是說,把你設計的程序翻譯成 C語言。這裡是真正需要使用C語言的地方。可以把思路寫在紙上,但是最終還是要把代碼輸入計算機。這個過程的機制取決於編程環境,我們稍後會詳細介紹一些常見的環境。一般而言,使用文本編輯器創建源代碼文件。該文件中內容就是你翻譯的C語言代碼。程序清單1.1是一個C源代碼的示例。

程序清單1.1 C源代碼示例

#include <stdio.h>

int main(void)

{

int dogs;

printf("How many dogs do you have?\n");

scanf("%d", &dogs);

printf("So you have %d dog(s)!\n", dogs);

return 0;

}

在這一步驟中,應該給自己編寫的程序添加文字註釋。最簡單的方式是使用 C的註釋工具在源代碼中加入對代碼的解釋。第2章將詳細介紹如何在代碼中添加註釋。

1.7.4 第4步:編譯

接下來的這一步是編譯源代碼。再次提醒讀者注意,編譯的細節取決於編程的環境,我們稍後馬上介紹一些常見的編程環境。現在,先從概念的角度講解編譯發生了什麼事情。

前面介紹過,編譯器是把源代碼轉換成可執行代碼的程序。可執行代碼是用計算機的機器語言表示的代碼。這種語言由數字碼表示的指令組成。如前所述,不同的計算機使用不同的機器語言方案。C 編譯器負責把C代碼翻譯成特定的機器語言。此外,C編譯器還將源代碼與C庫(庫中包含大量的標準函數供用戶使用,如printf和scanf)的代碼合併成最終的程序(更精確地說,應該是由一個被稱為鏈接器的程序來鏈接庫函數,但是在大多數系統中,編譯器運行鏈接器)。其結果是,生成一個用戶可以運行的可執行文件,其中包含著計算機能理解的代碼。

編譯器還會檢查C語言程序是否有效。如果C編譯器發現錯誤,就不生成可執行文件並報錯。理解特定編譯器報告的錯誤或警告信息是程序員要掌握的另一項技能。

1.7.5 第5步:運行程序

傳統上,可執行文件是可運行的程序。在常見環境(包括Windows命令提示符模式、UNIX終端模式和Linux終端模式)中運行程序要輸入可執行文件的文件名,而其他環境可能要運行命令(如,在VAX中的VMS[2])或一些其他機制。例如,在Windows和Macintosh提供的集成開發環境(IDE)中,用戶可以在IDE中通過選擇菜單中的選項或按下特殊鍵來編輯和執行C程序。最終生成的程序可通過單擊或雙擊文件名或圖標直接在操作系統中運行。

1.7.6 第6步:測試和調試程序

程序能運行是個好跡象,但有時也可能會出現運行錯誤。接下來,應該檢查程序是否按照你所設計的思路運行。你會發現你的程序中有一些錯誤,計算機行話叫作bug。查找並修復程序錯誤的過程叫調試。學習的過程中不可避免會犯錯,學習編程也是如此。因此,當你把所學的知識應用於編程時,最好為自己會犯錯做好心理準備。隨著你越來越老練,你所寫的程序中的錯誤也會越來越不易察覺。

將來犯錯的機會很多。你可能會犯基本的設計錯誤,可能錯誤地實現了一個好想法,可能忽視了輸入檢查導致程序癱瘓,可能會把圓括號放錯地方,可能誤用 C語言或打錯字,等等。把你將來犯錯的地方列出來,這份錯誤列表應該會很長。

看到這裡你可能會有些絕望,但是情況沒那麼糟。現在的編譯器會捕獲許多錯誤,而且自己也可以找到編譯器未發現的錯誤。在學習本書的過程中,我們會給讀者提供一些調試的建議。

1.7.7 第7步:維護和修改代碼

創建完程序後,你發現程序有錯,或者想擴展程序的用途,這時就要修改程序。例如,用戶輸入以Zz開頭的姓名時程序出現錯誤、你想到了一個更好的解決方案、想添加一個更好的新特性,或者要修改程序使其能在不同的計算機系統中運行,等等。如果在編寫程序時清楚地做了註釋並採用了合理的設計方案,這些事情都很簡單。

1.7.8 說明

編程並非像描述那樣是一個線性的過程。有時,要在不同的步驟之間往復。例如,在寫代碼時發現之前的設計不切實際,或者想到了一個更好的解決方案,或者等程序運行後,想改變原來的設計思路。對程序做文字註釋為今後的修改提供了方便。

許多初學者經常忽略第1步和第2步(定義程序目標和設計程序),直接跳到第3步(編寫代碼)。剛開始學習時,編寫的程序非常簡單,完全可以在腦中構思好整個過程。即使寫錯了,也很容易發現。但是,隨著編寫的程序越來越龐大、越來越複雜,動腦不動手可不行,而且程序中隱藏的錯誤也越來越難找。最終,那些跳過前兩個步驟的人往往浪費了更多的時間,因為他們寫出的程序難看、缺乏條理、讓人難以理解。要編寫的程序越大越複雜,事先定義和設計程序環節的工作量就越大。

磨刀不誤砍柴工,應該養成先規劃再動手編寫代碼的好習慣,用紙和筆記錄下程序的目標和設計框架。這樣在編寫代碼的過程中會更加得心應手、條理清晰。

1.8 編程機制

生成程序的具體過程因計算機環境而異。C是可移植性語言,因此可以在許多環境中使用,包括UNIX、Linux、MS-DOS(一些人仍在使用)、Windows和Macintosh OS。有些產品會隨著時間的推移發生演變或被取代,本書無法涵蓋所有環境。

首先,來看看許多C環境(包括上面提到的5種環境)共有的一些方面。雖然不必詳細瞭解計算機內部如何運行C程序,但是,瞭解一下編程機制不僅能豐富編程相關的背景知識,還有助於理解為何要經過一些特殊的步驟才能得到C程序。

用C語言編寫程序時,編寫的內容被儲存在文本文件中,該文件被稱為源代碼文件(source code file)。大部分C系統,包括之前提到的,都要求文件名以.c結尾(如,wordcount.c和budget.c)。在文件名中,點號(.)前面的部分稱為基本名(basename),點號後面的部分稱為擴展名(extension)。因此,budget是基本名,c是擴展名。基本名與擴展名的組合(budget.c)就是文件名。文件名應該滿足特定計算機操作系統的特殊要求。例如,MS-DOS是IBM PC及其兼容機的操作系統,比較老舊,它要求基本名不能超過8個字符。因此,剛才提到的文件名wordcount.c就是無效的DOS文件名。有些UNIX系統限制整個文件名(包括擴展名)不超過14個字符,而有些UNIX系統則允許使用更長的文件名,最多255個字符。Linux、Windows和Macintosh OS都允許使用長文件名。

接下來,我們來看一下具體的應用,假設有一個名為concrete.c的源文件,其中的C源代碼如程序清單1.2所示。

程序清單1.2 c程序

#include <stdio.h>

int main(void)

{

printf("Concrete contains gravel and cement.\n");

return 0;

}

如果看不懂程序清單1.2中的代碼,不用擔心,我們將在第2章學習相關知識。

1.8.1 目標代碼文件、可執行文件和庫

C編程的基本策略是,用程序把源代碼文件轉換為可執行文件(其中包含可直接運行的機器語言代碼)。典型的C實現通過編譯和鏈接兩個步驟來完成這一過程。編譯器把源代碼轉換成中間代碼,鏈接器把中間代碼和其他代碼合併,生成可執行文件。C 使用這種分而治之的方法方便對程序進行模塊化,可以獨立編譯單獨的模塊,稍後再用鏈接器合併已編譯的模塊。通過這種方式,如果只更改某個模塊,不必因此重新編譯其他模塊。另外,鏈接器還將你編寫的程序和預編譯的庫代碼合併。

中間文件有多種形式。我們在這裡描述的是最普遍的一種形式,即把源代碼轉換為機器語言代碼,並把結果放在目標代碼文件(或簡稱目標文件)中(這裡假設源代碼只有一個文件)。雖然目標文件中包含機器語言代碼,但是並不能直接運行該文件。因為目標文件中儲存的是編譯器翻譯的源代碼,這還不是一個完整的程序。

目標代碼文件缺失啟動代碼(startup code)。啟動代碼充當著程序和操作系統之間的接口。例如,可以在MS Windows或Linux系統下運行IBM PC兼容機。這兩種情況所使用的硬件相同,所以目標代碼相同,但是Windows和Linux所需的啟動代碼不同,因為這些系統處理程序的方式不同。

目標代碼還缺少庫函數。幾乎所有的C程序都要使用C標準庫中的函數。例如,concrete.c中就使用了 printf函數。目標代碼文件並不包含該函數的代碼,它只包含了使用 printf函數的指令。printf函數真正的代碼儲存在另一個被稱為庫的文件中。庫文件中有許多函數的目標代碼。

鏈接器的作用是,把你編寫的目標代碼、系統的標準啟動代碼和庫代碼這 3 部分合併成一個文件,即可執行文件。對於庫代碼,鏈接器只會把程序中要用到的庫函數代碼提取出來(見圖1.4)。

圖1.4 編譯器和鏈接器

簡而言之,目標文件和可執行文件都由機器語言指令組成的。然而,目標文件中只包含編譯器為你編寫的代碼翻譯的機器語言代碼,可執行文件中還包含你編寫的程序中使用的庫函數和啟動代碼的機器代碼。

在有些系統中,必須分別運行編譯程序和鏈接程序,而在另一些系統中,編譯器會自動啟動鏈接器,用戶只需給出編譯命令即可。

接下來,瞭解一些具體的系統。

1.8.2 UNIX系統

由於C語言因UNIX系統而生,也因此而流行,所以我們從UNIX系統開始(注意:我們提到的UNIX還包含其他系統,如FreeBSD,它是UNIX的一個分支,但是由於法律原因不使用該名稱)。

1.在UNIX系統上編輯

UNIX C沒有自己的編輯器,但是可以使用通用的UNIX編輯器,如emacs、jove、vi或X Window System文本編輯器。

作為程序員,要負責輸入正確的程序和為儲存該程序的文件起一個合適的文件名。如前所述,文件名應該以.c結尾。注意,UNIX區分大小寫。因此,budget.c、BUDGET.c和Budget.c是3個不同但都有效的C源文件名。但是BUDGET.C是無效文件名,因為該名稱的擴展名使用了大寫C而不是小寫c。

假設我們在vi編譯器中編寫了下面的程序,並將其儲存在inform.c文件中:

#include <stdio.h>

int main(void)

{

printf("A .c is used to end a C program filename.\n");

return 0;

}

以上文本就是源代碼,inform.c是源文件。注意,源文件是整個編譯過程的開始,不是結束。

2.在UNIX系統上編譯

雖然在我們看來,程序完美無缺,但是對計算機而言,這是一堆亂碼。計算機不明白#include 和printf是什麼(也許你現在也不明白,但是學到後面就會明白,而計算機卻不會)。如前所述,我們需要編譯器將我們編寫的代碼(源代碼)翻譯成計算機能看懂的代碼(機器代碼)。最後生成的可執行文件中包含計算機要完成任務所需的所有機器代碼。

以前,UNIX C編譯器要調用語言定義的cc命令。但是,它沒有跟上標準發展的腳步,已經退出了歷史舞台。但是,UNIX系統提供的C編譯器通常來自一些其他源,然後以cc命令作為編譯器的別名。因此,雖然在不同的系統中會調用不同的編譯器,但用戶仍可以繼續使用相同的命令。

編譯inform.c,要輸入以下命令:

cc inform.c

幾秒鐘後,會返回 UNIX 的提示,告訴用戶任務已完成。如果程序編寫錯誤,你可能會看到警告或錯誤消息,但我們先假設編寫的程序完全正確(如果編譯器報告void的錯誤,說明你的系統未更新成ANSI C編譯器,只需刪除void即可)。如果使用ls命令列出文件,會發現有一個a.out文件(見圖1.5)。該文件是包含已翻譯(或已編譯)程序的可執行文件。要運行該文件,只需輸入:

a.out

輸出內容如下:

A .c is used to end a C program filename.

圖1.5 用UNIX準備C程序

如果要儲存可執行文件(a.out),應該把它重命名。否則,該文件會被下一次編譯程序時生成的新a.out文件替換。

如何處理目標代碼?C 編譯器會創建一個與源代碼基本名相同的目標代碼文件,但是其擴展名是.o。在該例中,目標代碼文件是 inform.o。然而,卻找不到這個文件,因為一旦鏈接器生成了完整的可執行程序,就會將其刪除。如果原始程序有多個源代碼文件,則保留目標代碼文件。學到後面多文件程序時,你會明白到這樣做的好處。

1.8.3 GNU編譯器集合和LLVM項目

GNU項目始於1987年,是一個開發大量免費UNIX軟件的集合(GNU的意思是「GNU』s Not UNIX」,即GNU不是UNIX)。GNU編譯器集合(也被稱為GCC,其中包含GCC C編譯器)是該項目的產品之一。GCC在一個指導委員會的帶領下,持續不斷地開發,它的C編譯器緊跟C標準的改動。GCC有各種版本以適應不同的硬件平台和操作系統,包括UNIX、Linux和Windows。用gcc命令便可調用GCC C編譯器。許多使用gcc的系統都用cc作為gcc的別名。

LLVM項目成為cc的另一個替代品。該項目是與編譯器相關的開源軟件集合,始於伊利諾伊大學的2000份研究項目。它的 Clang編譯器處理 C代碼,可以通過 clang調用。有多種版本供不同的平台使用,包括Linux。2012年,Clang成為FreeBSD的默認C編譯器。Clang也對最新的C標準支持得很好。

GNU和LLVM都可以使用-v選項來顯示版本信息,因此各系統都使用cc別名來代替gcc或clang命令。以下組合:

cc -v

顯示你所使用的編譯器及其版本。

gcc和clang命令都可以根據不同的版本選擇運行時選項來調用不同C標準。

gcc -std=c99 inform.c[3]

gcc -std=c1x inform.c

gcc -std=c11 inform.c

第1行調用C99標準,第2行調用GCC接受C11之前的草案標準,第3行調用GCC接受的C11標準版本。Clang編譯器在這一點上用法與GCC相同。

1.8.4 Linux系統

Linux是一個開源、流行、類似於UNIX的操作系統,可在不同平台(包括PC和Mac)上運行。在Linux中準備C程序與在UNIX系統中幾乎一樣,不同的是要使用GNU提供的GCC公共域C編譯器。編譯命令類似於:

gcc inform.c

注意,在安裝Linux時,可選擇是否安裝GCC。如果之前沒有安裝GCC,則必須安裝。通常,安裝過程會將cc作為gcc的別名,因此可以在命令行中使用cc來代替gcc。

欲詳細瞭解GCC和最新發佈的版本,請訪問http://www.gnu.org/software/gcc/index.html。

1.8.5 PC的命令行編譯器

C編譯器不是標準Windows軟件包的一部分,因此需要從別處獲取並安裝C編譯器。可以從互聯網免費下載Cygwin和MinGW,這樣便可在PC上通過命令行使用GCC編譯器。Cygwin在自己的視窗運行,模仿Linux命令行環境,有一行命令提示。MinGW在Windows的命令提示模式中運行。這和GCC的最新版本一樣,支持C99和C11最新的一些功能。Borland的C++編譯器5.5也可以免費下載,支持C90。

源代碼文件應該是文本文件,不是字處理器文件(字處理器文件包含許多額外的信息,如字體和格式等)。因此,要使用文本編輯器(如,Windows Notepad)來編輯源代碼。如果使用字處理器,要以文本模式另存文件。源代碼文件的擴展名應該是.c。一些字處理器會為文本文件自動添加.txt 擴展名。如果出現這種情況,要更改文件名,把txt替換成c。

通常,C編譯器生成的中間目標代碼文件的擴展名是.obj(也可能是其他擴展名)。與UNIX編譯器不同,這些編譯器在完成編譯後通常不會刪除這些中間文件。有些編譯器生成帶.asm擴展名的彙編語言文件,而有些編譯器則使用自己特有的格式。

一些編譯器在編譯後會自動運行鏈接器,另一些要求用戶手動運行鏈接器。在可執行文件中鏈接的結果是,在原始的源代碼基本名後面加上.exe擴展名。例如,編譯和鏈接concrete.c源代碼文件,生成的是concrete.exe文件。可以在命令行輸入基本名來運行該程序:

C>concrete

1.8.6 集成開發環境(Windows)

許多供應商(包括微軟、Embarcadero、Digital Mars)都提供Windows下的集成開發環境,或稱為IDE(目前,大多數IDE都是C和C++結合的編譯器)。可以免費下載的IDE有Microsoft Visual Studio Express和Pelles C。利用集成開發環境可以快速開發C程序。關鍵是,這些IDE都內置了用於編寫C程序的編輯器。這類集成開發環境都提供了各種菜單(如,命名、保存源代碼文件、編譯程序、運行程序等),用戶不用離開IDE就能順利編寫、編譯和運行程序。如果編譯器發現錯誤,會返回編輯器中,標出有錯誤的行號,並簡單描述情況。

初次接觸Windows IDE可能會望而生畏,因為它提供了多種目標(target),即運行程序的多種環境。例如,IDE提供了32位Windows程序、64位Windows程序、動態鏈接庫文件(DLL)等。許多目標都涉及Windows圖形界面。要管理這些(及其他)選擇,通常要先創建一個項目(project),以便稍後在其中添加待使用的源代碼文件名。不同的產品具體步驟不同。一般而言,首先使用【文件】菜單或【項目】菜單創建一個項目。選擇正確的項目形式非常重要。本書中的例子都是一般示例,針對在簡單的命令行環境中運行而設計。Windows IDE提供多種選擇以滿足用戶的不同需求。例如,Microsoft Visual Studio提供【Win32控制台應用程序】選項。對於其他系統,查找一個諸如【DOS EXE】、【Console】或【Character Mode】的可執行選項。選擇這些模式後,將在一個類控制台窗口中運行可執行程序。選擇好正確的項目類型後,使用IDE的菜單打開一個新的源代碼文件。對於大多數產品而言,使用【文件】菜單就能完成。你可能需要其他步驟將源文件添加到項目中。

通常,Windows IDE既可處理C也可處理C++,因此要指定待處理的程序是C還是C++。有些產品用項目類型來區分兩者,有些產品(如,Microsoft Visual C++)用.c文件擴展名來指明使用C而不是C++。當然,大多數C程序也可以作為C++程序運行。欲瞭解C和C++的區別,請參閱參考資料IX。

你可能會遇到一個問題:在程序執行完畢後,執行程序的窗口立即消失。如果不希望出現這種情況,可以讓程序暫停,直到按下Enter鍵,窗口才消失。要實現這種效果,可以在程序的最後(return這行代碼之前)添加下面一行代碼:

getchar;

該行讀取一次鍵的按下,所以程序在用戶按下Enter鍵之前會暫停。有時根據程序的需要,可能還需要一個擊鍵等待。這種情況下,必須用兩次getchar:

getchar;

getchar;

例如,程序在最後提示用戶輸入體重。用戶鍵入體重後,按下Enter鍵以輸入數據。程序將讀取體重,第1個getchar讀取Enter鍵,第2個getchar會導致程序暫停,直至用戶再次按下Enter鍵。如果你現在不知所云,沒關係,在學完C輸出後就會明白。到時,我們會提醒讀者使用這種方法。

雖然許多IDE在使用上大體一致,但是細節上有所不同。就一個產品的系列而言,不同版本也是如此。要經過一段時間的實踐,才會熟悉編譯器的工作方式。必要時,還需閱讀使用手冊或網上教程。

Microsoft Visual Studio和C標準

在Windows軟件開發中,Microsoft Visual Studio及其免費版本Microsoft Visual Studio Express都久負盛名,它們與C標準的關係也很重要。然而,微軟鼓勵程序員從C轉向C++和C#。雖然Visual Studio支持C89/90,但是到目前為止,它只選擇性地支持那些在C++新特性中能找到的C標準(如,long long類型)。而且,自2012版本起,Visual Studio不再把C作為項目類型的選項。儘管如此,本書中的絕大多數程序仍可用Visual Studio來編譯。在新建項目時,選擇C++選項,然後選擇【Win32控制台應用程序】,在應用設置中選擇【空項目】。幾乎所有的C程序都能與C++程序兼容。所以,本書中的絕大多數C程序都可作為C++程序運行。或者,在選擇C++選項後,將默認的源文件擴展名.cpp替換成.c,編譯器便會使用C語言的規則代替C++。

1.8.7 Windows/Linux

許多Linux發行版都可以安裝在Windows系統中,以創建雙系統。一些存儲器會為Linux系統預留空間,以便可以啟動Windows或Linux。可以在Windows系統中運行Linux程序,或在Linux系統中運行Windows程序。不能通過Windows系統訪問Linux文件,但是可以通過Linux系統訪問Windows文檔。

1.8.8 Macintosh中的C

目前,蘋果免費提供Xcode開發系統下載(過去,它有時免費,有時付費)。它允許用戶選擇不同的編程語言,包括C語言。

Xcode 憑借可處理多種編程語言的能力,可用於多平台,開發超大型的項目。但是,首先要學會如何編寫簡單的C程序。在Xcode 4.6中,通過【File】菜單選擇【New Project】,然後選擇【OS X Application Command Line Tool】,接著輸入產品名並選擇C類型。Xcode使用Clang或GCC C編譯器來編譯C代碼,它以前默認使用GCC,但是現在默認使用Clang。可以設置選擇使用哪一個編譯器和哪一套C標準(因為許可方面的事宜,Xcode中Clang的版本比GCC的版本要新)。

UNIX系統內置Mac OS X,終端工具打開的窗口是讓用戶在UNIX命令行環境中運行程序。蘋果在標準軟件包中不提供命令行編譯器,但是,如果下載了 Xcode,還可以下載可選的命令行工具,這樣就可以使用clang和gcc命令在命令行模式中編譯。

1.9 本書的組織結構

本書採用多種方式編排內容,其中最直接的方法是介紹A主題的所有內容、介紹B主題的所有內容,等等。這對參考類書籍來說尤為重要,讀者可以在同一處找到與主題相關的所有內容。但是,這通常不是學習的最佳順序。例如,如果在開始學習英語時,先學完所有的名詞,那你的表達能力一定很有限。雖然可以指著物品說出名稱,但是,如果稍微學習一些名詞、動詞、形容詞等,再學習一些造句規則,那麼你的表達能力一定會大幅提高。

為了讓讀者更好地吸收知識,本書採用螺旋式方法,先在前幾個章節中介紹一些主題,在後面章節再詳細討論相關內容。例如,對學習C語言而言,理解函數至關重要。因此,我們在前幾個章節中安排一些與函數相關的內容,等讀者學到第 9 章時,已對函數有所瞭解,學習使用函數會更加容易。與此類似,前幾章還概述了一些字符串和循環的內容。這樣,讀者在完全弄懂這些內容之前,就可以在自己的程序中使用這些有用的工具。

1.10 本書的約定

在學習C語言之前,先介紹一下本書的格式。

1.10.1 字體

本書用類似在屏幕上或打印輸出時的字體(一種等寬字體),表示文本程序和計算機輸入、輸出。前面已經出現了多次,如果讀者沒有注意到,字體如下所示:

#include <stdio.h>

int main(void)

{

printf("Concrete contains gravel and cement.\n");

return 0;

}

在涉及與代碼相關的術語時,也使用相同的等寬字體,如stdio.h。本書用等寬斜體表示佔位符,可以用具體的項替換這些佔位符。例如,下面是一個聲明的模型:

type_name variable_name;

這裡,可用int替換type_name,用zebra_count替換variable_name。

1.10.2 程序輸出

本書用相同的字體表示計算機的輸出,粗體表示用戶輸入。例如,下面是第14章中一個程序的輸出:

Please enter the book title.

Press [enter] at the start of a line to stop.

My Life as a Budgie

Now enter the author.

Mack Zackles

如上所示,以標準計算機字體顯示的行表示程序的輸出,粗體行表示用戶的輸入。

可以通過多種方式與計算機交互。在這裡,我們假設讀者使用鍵盤鍵入內容,在屏幕上閱讀計算機的響應。

1.特殊的擊鍵

通常,通過按下標有 Enter、c/r、Return 或一些其他文字的鍵來發送指令。本書將這些按鍵統一稱為Enter鍵。一般情況下,我們默認你在每行輸入的末尾都會按下Enter鍵。儘管如此,為了標示一些特定的位置,本書使用[enter]顯式標出Enter鍵。方括號表示按下一次Enter鍵,而不是輸入enter。

除此之外,書中還會提到控制字符(如,Ctrl+D)。這種寫法的意思是,在按下Ctrl鍵(也可能是Control鍵)的同時按下D鍵。

2.本書使用的系統

C 語言的某些方面(如,儲存數字的空間大小)因系統而異。本書在示例中提到「我們的系統」時,通常是指在iMac上運行OS X 10.8.4,使用Xcode 4.6.2開發系統的Clang 3.2編譯器。本書的大部分程序都能使用Windows7系統的Microsoft Visual Studio Express 2012和Pelles C 7.0,以及Ubuntu13.04 Linux系統的GCC 4.7.3進行編譯。

3.讀者的系統

你需要一個C編譯器或訪問一個C編譯器。C程序可以在多種計算機系統中運行,因此你的選擇面很廣。確保你使用的C編譯器與當前使用的計算機系統匹配。本書中,除了某些示例要求編譯器支持C99或C11標準,其餘大部分示例都可在C90編譯器中運行。如果你使用的編譯器是早於ANSI/ISO的老式編譯器,在編譯時肯定要經常調整,很不方便。與其如此,不如換個新的編譯器。

大部分編譯器供應商都為學生和教學人員提供特惠版本,詳情請查看供應商的網站。

1.10.3 特殊元素

本書包含一些強調特定知識點的特殊元素,提示、注意、警告,將以如下形式出現在本書中:

邊欄

邊欄提供更深入的討論或額外的背景,有助於解釋當前的主題。

提示

提示一般都短小精悍,幫助讀者理解一些特殊的編程情況。

警告

用於警告讀者注意一些潛在的陷阱。

注意

提供一些評論,提醒讀者不要誤入歧途。

1.11 本章小結

C是強大而簡潔的編程語言。它之所以流行,在於自身提供大量的實用編程工具,能很好地控制硬件。而且,與大多數其他程序相比,C程序更容易從一個系統移植到另一個系統。

C是編譯型語言。C編譯器和鏈接器是把C語言源代碼轉換成可執行代碼的程序。

用C語言編程可能費力、困難,讓你感到沮喪,但是它也可以激發你的興趣,讓你興奮、滿意。我們希望你在愉快的學習過程中愛上C。

1.12 複習題

複習題的參考答案在附錄A中。

1.對編程而言,可移植性意味著什麼?

2.解釋源代碼文件、目標代碼文件和可執行文件有什麼區別?

3.編程的7個主要步驟是什麼?

4.編譯器的任務是什麼?

5.鏈接器的任務是什麼?

1.13 編程練習

我們尚未要求你編寫C代碼,該練習側重於編程過程的早期步驟。

1.你剛被MacroMuscle有限公司聘用。該公司準備進入歐洲市場,需要一個把英吋單位轉換為厘米單位(1 英吋=2.54 厘米)的程序。該程序要提示用戶輸入英吋值。你的任務是定義程序目標和設計程序(編程過程的第1步和第2步)。

[1].國際C語言混亂代碼大賽(IOCCC,The International Obfuscated C Code Contest)。這是一項國際編程賽事,從1984年開始,每年舉辦一次(1997、1999、2002、2003和2006年除外),目的是寫出最有創意且最讓人難以理解的C語言代碼。——譯者注

[2].VAX(Virtual Address eXtension)是一種可支持機器語言和虛擬地址的32位小型計算機。VMS(Virtual Memory System)是舊名,現在叫OpenVMS,是一種用於服務器的操作系統,可在VAX、Alpha或Itanium處理器系列平台上運行。——譯者注

[3].GCC最基本的用法是:gcc [options] [filenames],其中options是所需的參數,filenames是文件名。——譯者注