一站式學習C編程(升級版)_第1頁
一站式學習C編程(升級版)_第2頁
一站式學習C編程(升級版)_第3頁
一站式學習C編程(升級版)_第4頁
一站式學習C編程(升級版)_第5頁
已閱讀5頁,還剩490頁未讀 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

一站式學習C編程升級版目錄\h上篇C語言入門\h第1章程序的基本概念\h1.1程序和編程語言\h1.2自然語言和形式語言\h1.3程序的調試\h1.4第一個程序\h第2章常量、變量和表達式\h2.1繼續HelloWorld\h2.2常量\h2.3變量\h2.4賦值\h2.5表達式\h2.6字符類型與字符編碼\h第3章簡單函數\h3.1數學函數\h3.2自定義函數\h3.3形參和實參\h3.4全局變量、局部變量和作用域\h第4章分支語句\h4.1if語句\h4.2if/else語句\h4.3布爾代數\h4.4switch語句\h第5章深入理解函數\h5.1return語句\h5.2增量式開發\h5.3遞歸\h第6章循環語句\h6.1while語句\h6.2do/while語句\h6.3for語句\h6.4break和continue語句\h6.5嵌套循環\h6.6goto語句和標號\h第7章結構體\h7.1復合類型與結構體\h7.2數據抽象\h7.3數據類型標志\h7.4嵌套結構體\h第8章數組\h8.1數組的基本概念\h8.2數組應用實例:統計隨機數\h8.3數組應用實例:直方圖\h8.4字符串\h8.5多維數組\h第9章編碼風格\h9.1縮進和空白\h9.2注釋\h9.3標識符命名\h9.4函數\h9.5indent工具\h第10章gdb\h10.1單步執行和跟蹤函數調用\h10.2斷點\h10.3觀察點\h10.4段錯誤\h第11章排序與查找\h11.1算法的概念\h11.2插入排序\h11.3算法的時間復雜度分析\h11.4歸并排序\h11.5線性查找\h11.6折半查找\h第12章棧與隊列\h12.1數據結構的概念\h12.2堆棧\h12.3深度優先搜索\h12.4隊列與廣度優先搜索\h12.5環形隊列\h本階段總結\h下篇C語言本質\h第13章計算機中數的表示\h13.1為什么計算機用二進制計數\h13.2不同進制之間的換算\h13.3整數的加減運算\h13.4浮點數\h第14章數據類型詳解\h14.1整型\h14.2浮點型\h14.3類型轉換\h第15章運算符詳解\h15.1位運算\h15.2其他運算符\h15.3SideEffect與SequencePoint\h15.4運算符總結\h第16章計算機體系結構基礎\h16.1內存與地址\h16.2CPU\h16.3設備\h16.4MMU\h16.5MemoryHierarchy\h第17章x86匯編程序基礎\h17.1最簡單的匯編程序\h17.2x86的寄存器\h17.3第二個匯編程序\h17.4尋址方式\h17.5ELF文件\h第18章匯編與C之間的關系\h18.1函數調用\h18.2main函數、啟動例程和退出狀態\h18.3變量的存儲布局\h18.4結構體和聯合體\h18.5C內聯匯編\h18.6volatile限定符\h第19章鏈接詳解\h19.1多目標文件的鏈接\h19.2定義和聲明\h19.3靜態庫\h19.4共享庫\h19.5虛擬內存管理\h第20章預處理\h20.1預處理的步驟\h20.2宏定義\h20.3條件預處理指示\h20.4其他預處理特性\h第21章Makefile基礎\h21.1基本規則\h21.2隱含規則和模式規則\h21.3變量\h21.4自動處理頭文件的依賴關系\h21.5常用的make命令行選項\h第22章指針\h22.1指針的基本概念\h22.2指針類型的參數和返回值\h22.3指針與數組\h22.4指針與const限定符\h22.5指針與結構體\h22.6指向指針的指針與指針數組\h22.7指向數組的指針與多維數組\h22.8函數類型和函數指針類型\h22.9不完全類型和復雜聲明\h第23章函數接口\h23.1本章的預備知識\h23.2傳入參數與傳出參數\h23.3兩層指針的參數\h23.4返回值是指針的情況\h23.5回調函數\h23.6可變參數\h第24章C標準庫\h24.1字符串操作函數\h24.2標準I/O庫函數\h24.3數值字符串轉換函數\h24.4分配內存的函數\h第25章鏈表、二叉樹和哈希表\h25.1鏈表\h25.2二叉樹\h25.3哈希表\h本階段總結\h附錄A字符編碼上篇C語言入門第1章程序的基本概念第2章常量、變量和表達式第3章簡單函數第4章分支語句第5章深入理解函數第6章循環語句第7章結構體第8章數組第9章編碼風格第10章gdb第11章排序與查找第12章棧與隊列本階段總結第1章程序的基本概念1.1程序和編程語言程序(Program)告訴計算機應如何完成一個計算任務,這里的計算可以是數學運算,比如解方程,也可以是符號運算,比如查找和替換文檔中的某個單詞。從根本上說,計算機是由數字電路組成的運算機器,只能對數字做運算,程序之所以能做符號運算,是因為符號在計算機內部也是用數字表示的。此外,程序還可以處理聲音和圖像,聲音和圖像在計算機內部必然也是用數字表示的,這些數字經過專門的硬件設備轉換成人可以聽到的聲音和看到的圖像。程序由一系列指令(Instruction)組成,指令是指示計算機做某種運算的命令,通常包括以下幾類:輸入(Input)從鍵盤、文件或者其他設備獲取數據。輸出(Output)把數據顯示到屏幕,或者存入一個文件,或者發送到其他設備。基本運算執行最基本的數學運算(加減乘除)和數據存取。測試和分支測試某個條件,然后根據不同的測試結果執行不同的后續指令。循環重復執行一系列操作。對于程序來說,有上面這幾類指令就足夠了。你曾用過的任何一個程序,不管它有多么復雜,都是由這幾類指令組成的。程序是那么的復雜,而編寫程序可以用的指令卻只有這么簡單的幾種,這中間巨大的落差就要由程序員去填補了,所以編寫程序理應是一件相當復雜的工作。編寫程序可以說就是這樣一個過程:把復雜的任務分解成子任務,把子任務再分解成更簡單的任務,層層分解,直到最后簡單得可以用以上指令來完成。編程語言(ProgrammingLanguage)分為低級語言(Low-levelLanguage)和高級語言(High-levelLanguage)。機器語言(MachineLanguage)和匯編語言(AssemblyLanguage)屬于低級語言,直接用計算機指令編寫程序。而C、C++、Java、Python等屬于高級語言,用語句(Statement)編寫程序,語句是計算機指令的抽象表示。舉個例子,同樣一個語句用C語言、匯編語言和機器語言分別表示如表1.1所示。表1.1一個語句的三種表示計算機只能對數字做運算,符號、聲音、圖像在計算機內部都要用數字表示,指令也不例外,表1.1中的機器語言完全由十六進制數字組成。最早的程序員都是直接用機器語言編程,但是很麻煩,需要查大量的表格來確定每個數字表示什么意思,編寫出來的程序很不直觀,而且容易出錯,于是有了匯編語言,把機器語言中一組一組的數字用助記符(Mnemonic)表示,直接用這些助記符寫出匯編程序,然后讓匯編器(Assembler)去查表把助記符替換成數字,也就把匯編語言翻譯成了機器語言。從上面的例子可以看出,匯編語言和機器語言的指令是一一對應的,匯編語言有三條指令,機器語言也有三條指令,匯編器就是做一個簡單的替換工作,例如在第一條指令中,把movl?,%eax這種格式的指令替換成機器碼a1?,?表示一個地址,在匯編指令中是0x804a01c,轉換成機器碼之后是1ca00408(這是指令中十六進制數的小端表示,小端表示將在第16.2節介紹)。從上面的例子還可以看出,C語言的語句和低級語言的指令之間不是簡單的一一對應關系,一條a=b+1;語句要翻譯成三條匯編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比匯編器要復雜得多。用C語言編寫的程序必須經過編譯轉成機器指令才能被計算機執行,編譯需要花一些時間,這是用高級語言編程的一個缺點,然而更多的是優點。首先,用C語言編程更容易,寫出來的代碼更緊湊,可讀性更強,出了錯也更容易改正。其次,C語言是可移植的(Portable)或者稱為平臺無關的(PlatformIndependent)。“平臺”這個詞有很多種解釋,可以指計算機體系結構(Architecture),也可以指操作系統,也可以指開發平臺(編譯器、鏈接器等)。不同的計算機體系結構有不同的指令集(InstructionSet),可以識別的機器指令格式是不同的,直接用某種體系結構的匯編或機器指令寫出來的程序只能在這種體系結構的計算機上運行,然而各種體系結構的計算機都有各自的C編譯器,可以把C程序編譯成各種不同體系結構的機器指令,這意味著用C語言寫的程序只需稍加修改甚至不用修改就可以在各種不同的計算機上編譯運行。各種高級語言都具有C語言的這些優點,所以絕大部分程序是用高級語言編寫的,只有和硬件關系密切的少數程序(例如驅動程序)才會用到低級語言。還要注意一點,即使在相同的體系結構和操作系統下,用不同的C編譯器(或者同一個C編譯器的不同版本)編譯同一個程序得到的結果也有可能不同,C語言有些語法特性在C標準中并沒有明確規定,各編譯器有不同的實現,編譯出來的指令的行為特性也會有所不同,應該盡量避免使用不可移植的語法特性。總結一下編譯執行的過程,首先你用文本編輯器寫一個C程序,然后保存成一個文件,例如program.c(通常C程序的文件名后綴是.c),這稱為源代碼(SourceCode)或源文件,然后運行編譯器對它進行編譯,編譯的過程并不執行程序,而是把源代碼全部翻譯成機器指令,再加上一些描述信息,生成一個新的文件,例如a.out,這稱為可執行文件,可執行文件可以被操作系統加載運行,計算機執行該文件中由編譯器生成的指令,如圖1.1所示。圖1.1編譯執行的過程有些高級語言以解釋(Interpret)的方式執行,解釋執行過程和C語言的編譯執行過程很不一樣。例如編寫一個Shell腳本script.sh,內容如下:定義Shell變量VAR的初始值是1,然后自增1,然后打印VAR的值。用Shell程序/bin/sh解釋執行這個腳本,結果如下:這里的/bin/sh稱為解釋器(Interpreter),它把腳本中的每一行當做一條命令解釋執行,而不需要先生成包含機器指令的可執行文件再執行。如果把腳本中的這三行當做三條命令直接敲到Shell提示符下,也能得到同樣的結果:解釋執行的過程如圖1.2所示。還有很多編程語言采用編譯和解釋相結合的方式執行,這種方式相當流行,Java、Python、Perl等編程語言都采用這種方式。以Python為例,程序員寫的源代碼.py文件首先被編譯成.pyc文件,稱為字節碼(ByteCode),然后字節碼被Python虛擬機解釋執行。字節碼是Python虛擬機的指令而非機器指令,所以它是平臺無關的,如果把字節碼文件從一種平臺拷貝到另一種平臺上,只要另一種平臺也安裝了Python虛擬機,就能運行這個字節碼文件。虛擬機執行過程如圖1.3所示。圖1.2解釋執行的過程圖1.3虛擬機執行過程編程語言仍在發展演化。以上介紹的機器語言稱為第一代語言(1GL,1stGenerationProgrammingLanguage),匯編語言稱為第二代語言(2GL,2ndGenerationProgrammingLanguage),C、C++、Java、Python等可以稱為第三代語言(3GL,3rdGenerationProgrammingLanguage)。目前已經有了4GL(4thGenerationProgrammingLanguage)和5GL(5thGenerationProgrammingLanguage)的概念。3GL的編程語言雖然是用語句編程而不直接用指令編程,但語句也分為輸入、輸出、基本運算、測試分支和循環等幾種,和指令有直接的對應關系。而4GL以后的編程語言更多是描述要做什么(Declarative)而不描述具體每一步怎么做(Imperative),具體步驟完全由編譯器或解釋器決定,例如SQL(SQL,StructuredQueryLanguage,結構化查詢語言)就是這樣的例子。習題1.解釋執行的語言相比編譯執行的語言有什么優缺點?這是我們的第一個思考題。本書的思考題通常要求讀者系統地總結當前小節的知識,結合以前的知識,并經過一定的推理,然后做答。本書強調的是基本概念,讀者應該抓住概念的定義和概念之間的關系來總結,比如本節介紹了很多概念:程序由語句或指令組成,計算機只能執行低級語言中的指令(匯編語言的指令要先轉成機器碼才能執行),高級語言要執行就必須先翻譯成低級語言,翻譯的方法有兩種——編譯和解釋,雖然有這樣的不便,但高級語言有一個好處是平臺無關性。什么是平臺?一種平臺,就是一種體系結構,就是一種指令集,就是一種機器語言,這些都可看作是一一對應的,上文并沒有用“一一對應”這個詞,但讀者應該能推理出這個結論,而高級語言和它們不是一一對應的,因此高級語言是平臺無關的,概念之間像這樣的數量對應關系尤其重要。那么編譯和解釋的過程有哪些不同?主要的不同在于什么時候翻譯和什么時候執行。現在回答這個思考題,根據編譯和解釋的不同原理,你能否在執行效率和平臺無關性等方面做一下比較?希望讀者掌握以概念為中心的閱讀思考習慣,每讀一節就總結一套概念之間的關系圖并畫在書上空白處。如果讀到后面某一節看到一個講過的概念,但是記不清在哪一節講過了,沒關系,書后的索引可以幫你找到它是在哪一節定義的。1.2自然語言和形式語言自然語言(NaturalLanguage)就是人類講的語言,比如漢語、英語和法語。這類語言不是人為設計(雖然有人試圖強加一些規則)而是自然進化的。形式語言(FormalLanguage)是為了特定應用而人為設計的語言。例如數學家用的數字和運算符號、化學家用的分子式等。編程語言也是一種形式語言,是專門設計用來表達計算過程的形式語言。形式語言有嚴格的語法(Syntax)規則,例如,3+3=6是一個語法正確的數學等式,而3=+6$則不是,H2O是一個正確的分子式,而則不是。語法規則是由符號(Token)和結構(Structure)的規則所組成的。Token的概念相當于自然語言中的單詞和標點、數學式中的數和運算符、化學分子式中的元素名和數字,例如3=+6$的問題之一在于$不是一個合法的數也不是一個事先定義好的運算符,而的問題之一在于沒有一種元素的縮寫是Zz。結構是指Token的排列方式,3=+6$還有一個結構上的錯誤,雖然加號和等號都是合法的運算符,但是不能在等號之后緊跟加號,而的另一個問題在于分子式中必須把下標寫在化學元素名稱之后而不是前面。關于Token的規則稱為詞法(Lexical)規則,而關于結構的規則稱為語法(Grammar)規則\h①。當閱讀一個自然語言的句子或者一種形式語言的語句時,你不僅要搞清楚每個詞(Token)是什么意思,而且必須搞清楚整個句子的結構是什么樣的(在自然語言中你只是沒有意識到,但確實這樣做了,尤其是在讀外語時你肯定也意識到了)。這個分析句子結構的過程稱為解析(Parse)。例如,當你聽到“Theothershoefell.”這個句子時,你理解theothershoe是主語而fell是謂語動詞,一旦解析完成,你就搞懂了句子的意思,如果知道shoe是什么東西,fall意味著什么,這句話是在什么上下文(Context)中說的,你還能理解這個句子主要暗示的內容,這些屬于語義(Semantic)的范疇。雖然形式語言和自然語言有很多共同之處,包括Token、結構和語義,但是也有很多不一樣的地方。歧義性(Ambiguity)自然語言充滿歧義,人們通過上下文的線索和自己的常識來解決這個問題。形式語言的設計要求是清晰的、毫無歧義的,這意味著每個語句都必須有確切的含義而不管上下文如何。冗余性(Redundancy)為了消除歧義減少誤解,自然語言引入了相當多的冗余。結果是自然語言經常說得啰里啰唆,而形式語言則更加緊湊,極少有冗余。與字面意思的一致性自然語言充斥著成語和隱喻(Metaphor),我在某種場合下說“Theothershoefell”,可能并不是說誰的鞋掉了。而形式語言中字面(Literal)意思基本上就是真實意思,也會有一些例外,例如第2章要講的C語言轉義序列,但即使有例外也會明確規定哪些字面意思不是真實意思,它們所表示的真實意思又是什么。說自然語言長大的人(實際上沒有人例外),往往有一個適應形式語言的困難過程。某種意義上,形式語言和自然語言之間的不同正像詩歌和說明文的區別,當然,前者之間的區別比后者更明顯。詩歌詞語的發音和意思一樣重要,全詩作為一個整體創造出一種效果或者表達一種感情。歧義和非字面意思不僅是常見的而且是刻意使用的。說明文詞語的字面意思顯得更重要,并且結構能傳達更多的信息。詩歌只能看一個整體,而說明文更適合逐字逐句分析,但仍然充滿歧義。程序計算機程序是毫無歧義的,字面和本意高度一致,能夠完全通過對Token和結構的分析加以理解。現在給出一些關于閱讀程序(包括其他形式語言)的建議。首先請記住形式語言遠比自然語言緊湊,所以要多花點時間來讀。其次,結構很重要,從上到下從左到右讀往往不是一個好辦法,而應該學會在大腦里解析:識別Token,分解結構。最后,請記住細節的影響,諸如拼寫錯誤和標點錯誤這些在自然語言中可以忽略的小毛病會把形式語言搞得面目全非。1.3程序的調試編程是一件復雜的工作,因為是人做的事情,所以難免經常出錯。據說有這樣一個典故:早期的計算機體積都很大,有一次一臺計算機不能正常工作,工程師們找了半天原因最后發現是一只蟲子(Bug)鉆進計算機中造成的。從此以后,程序中的錯誤被叫做Bug,而找到這些Bug并加以糾正的過程就叫做調試(Debug)。有時候調試是一項非常復雜的工作,要求程序員概念明確、邏輯清晰、性格沉穩,還需要一點運氣。調試的技能我們在后續的學習中慢慢培養,但首先我們要清楚程序中的Bug分為哪幾類。編譯時錯誤編譯器只能翻譯語法正確的程序,否則將導致編譯失敗,無法生成可執行文件。對于自然語言來說,一點語法錯誤不是很嚴重的問題,因為我們仍然可以讀懂句子。而編譯器就沒那么寬容了,哪怕只有一個很小的語法錯誤,編譯器也會輸出一條錯誤提示信息然后罷工,你就得不到想要的結果。雖然大部分情況下編譯器給出的錯誤提示信息就是你出錯的代碼行,但也有個別時候編譯器給出的錯誤提示信息幫助不大,甚至會誤導你。在開始學習編程的前幾個星期,你可能會花大量的時間來糾正語法錯誤。等到有了一些經驗之后,還是會犯類似的錯誤,不過會少得多,而且你能更快地發現錯誤原因。等到經驗更豐富之后你就會覺得,語法錯誤是最簡單最低級的錯誤,編譯器的錯誤提示也就那么幾種,即使錯誤提示是有誤導的也能夠立刻找出真正的錯誤原因是什么。相比下面兩種錯誤,語法錯誤解決起來要容易得多。運行時錯誤編譯器檢查不出這類錯誤,仍然可以生成可執行文件,但在運行時會出錯而導致程序崩潰。對于我們接下來的幾章將編寫的簡單程序來說,運行時錯誤很少見,到了后面的章節你會遇到越來越多的運行時錯誤。讀者在以后的學習中要時刻注意區分編譯時和運行時(Run-time)這兩個概念,不僅在調試時需要區分這兩個概念,在學習C語言的很多語法時都需要區分這兩個概念,有些事情在編譯時做,有些事情則在運行時做。邏輯錯誤和語義錯誤第三類錯誤是邏輯錯誤和語義錯誤。如果程序里有邏輯錯誤,編譯和運行都會很順利,看上去也不產生任何錯誤信息,但是程序沒有干它該干的事情,而是干了別的事情。當然不管怎么樣,計算機只會按你寫的程序去做,問題在于你寫的程序不是你真正想要的,這意味著程序的意思(即語義)是錯的。找到邏輯錯誤在哪需要十分清醒的頭腦,要通過觀察程序的輸出回過頭來判斷它到底在做什么。通過本書你將掌握的最重要的技巧之一就是調試。調試的過程可能會讓你感到一些沮喪,但調試也是編程中最需要動腦的、最有挑戰和樂趣的部分。從某種角度看調試就像偵探工作,根據掌握的線索來推斷是什么原因和過程導致了你所看到的結果。調試也像是一門實驗科學,每次想到哪里可能有錯,就修改程序然后再試一次。如果假設是對的,就能得到預期的正確結果,就可以接著調試下一個Bug,一步一步逼近正確的程序;如果假設錯誤,只好另外找思路再做假設。“當你把不可能的全部剔除,剩下的——即使看起來再怎么不可能——就一定是事實。”(即使你沒看過福爾摩斯也該看過柯南吧)。也有一種觀點認為,編程和調試是一回事,編程的過程就是逐步調試直到獲得期望的結果為止。你應該總是從一個能正確運行的小規模程序開始,每做一步小的改動就立刻進行調試,這樣的好處是總有一個正確的程序做參考:如果正確就繼續編程,如果不正確,那么一定是剛才的小改動出了問題。例如,Linux操作系統包含了成千上萬行代碼,但它也不是一開始就規劃好了內存管理、設備管理、文件系統、網絡等大的模塊,一開始它僅僅是LinusTorvalds用來琢磨Intel80386芯片而寫的小程序。據LarryGreenfield說,“Linus的早期工程之一是編寫一個交替打印AAAA和BBBB的程序,這玩意兒后來進化成了Linux。”(引自TheLinuxUser'sGuideBeta1版)在后面的章節中會給出更多關于調試和編程實踐的建議。1.4第一個程序在開始寫程序之前首先要搭建開發環境,安裝編譯器、頭文件、庫文件、開發文檔等。在Linux系統下如何安裝軟件包和搭建開發環境不是本書的重點,這些問題需要讀者自己解決,但我在這里簡單列出需要安裝的軟件包供讀者參考(假定你用的是Debian或Ubuntu發行版):?gcc:TheGNUCcompiler?libc6-dev:GNUCLibrary:DevelopmentLibrariesandHeaderFiles?manpages-dev:ManualpagesaboutusingGNU/Linuxfordevelopment?manpages-posix-dev:ManualpagesaboutusingaPOSIXsystemfordevelopment?binutils:TheGNUassembler,linkerandbinaryutilities?gdb:TheGNUDebugger?make:TheGNUversionofthe"make"utility本書所有代碼都在Ubuntu10.04LTS(32位x86平臺)發行版上編譯測試通過。讀者如果用其他Linux發行版,或者不使用發行版提供的軟件包而是用自己從源代碼編譯出的軟件包,則編譯運行本書的代碼得到的結果會有些不同,但不影響學習。在Windows平臺上使用微軟的開發環境也可以編譯運行本書的大部分代碼。從/express/Downloads/可以下載免費版的VisualC++2010Express,建議選擇英文版下載安裝,因為很多編譯選項術語根本沒有準確的中文翻譯。安裝之后打開開始菜單→所有程序→MicrosoftVisualStudio2010Express→VisualStudioCommandPrompt(2010),進入命令行編譯和運行程序,不要使用IDE,我在前言中已經解釋過理由了。在Windows平臺上編譯運行C程序也可以使用MinGW(GNU開發工具的Windows版本)、Cygwin(在Windows系統中模擬的Linux環境)或者Intel的編譯器,本書不做詳細介紹。通常一本教編程的書中第一個例子都是打印“Hello,World.”,這個傳統源自參考文獻[3],用C語言寫這個程序可以這樣寫:例1.1HelloWorld在Linux平臺上,將這個程序保存成主目錄下的main.c,然后編譯運行:gcc是Linux平臺的C編譯器,編譯后在當前目錄下生成可執行文件a.out\h②,直接在命令行輸入這個可執行文件的路徑就可以執行它。如果不想把文件名叫a.out,可以用gcc的-o參數自己指定文件名:在Windows平臺上,將這個程序保存成C:\目錄下的main.c,打開VisualStudio命令行,用cl命令編譯,然后運行:雖然這只是一個很小的程序,但我們目前暫時還不具備相關的知識來完全理解這個程序,比如程序的第一行,還有程序主體的intmain(void){...return0;}結構,這些部分我們暫時不詳細解釋,讀者現在只需要把它們看成是每個程序按慣例必須要寫的部分(Boilerplate)。但要注意main是一個特殊的名字,C程序總是從main里面的第一條語句開始執行的,在這個程序中是指printf這條語句。第3行的/*...*/結構是一個注釋(Comment),其中可以寫一些描述性的話,解釋這段程序在做什么。注釋只是寫給程序員看的,編譯器會忽略從/*到*/的所有字符,所以寫注釋沒有語法規則,愛怎么寫就怎么寫,并且不管寫多少都不會被編譯進可執行文件中。printf語句的作用是把消息打印到屏幕。注意語句的末尾以;號(Semicolon)結束,下一條語句return0;也是如此。C語言用{}括號(Brace或CurlyBrace)把語法結構分成組,在上面的程序中printf和return語句套在main的{}括號中,表示它們屬于main的定義之中。我們看到這兩句相比main那一行都縮進(Indent)了一些,在代碼中可以用若干個空格(Blank)和Tab字符來縮進,縮進不是必需的,但這樣使我們更容易看出這兩行是屬于main的定義之中的,要寫出漂亮的程序必須有整齊的縮進,第9.1節將介紹推薦的縮進寫法。正如前面所說,編譯器對于語法錯誤是毫不留情的,如果你的程序有一點拼寫錯誤,例如第一行寫成了stdoi.h,在編譯時會得到錯誤提示:這個錯誤提示非常緊湊,初學者往往不容易看明白出了什么錯誤,即使知道這個錯誤提示說的是第1行有錯誤,很多初學者對照著書看好幾遍也看不出自己這一行哪里有錯誤,因為他們對符號和拼寫不敏感(尤其是英文較差的初學者),他們還不知道這些符號是什么意思又如何能記住正確的拼寫?對于初學者來說,最想看到的錯誤提示其實是這樣的:“在main.c程序第1行的第19列,您試圖包含一個叫做stdoi.h的文件,可惜我沒有找到這個文件,但我卻找到了一個叫做stdio.h的文件,我猜這個才是您想要的,對嗎?”可惜沒有任何編譯器會友善到這個程度,大多數時候你所得到的錯誤提示并不能直接指出誰是犯人,而只是一個線索,你需要根據這個線索做一些偵探和推理。有些時候編譯器的提示信息不是error而是warning,例如把上例中的printf("Hello,world.\n");改成printf(1);然后編譯運行:這個警告信息是說類型不匹配,但勉強還能配得上。警告信息不是致命錯誤,編譯仍然可以繼續,如果整個編譯過程只有警告信息而沒有錯誤信息,仍然可以生成可執行文件。但是,警告信息也是不容忽視的。出警告信息說明你的程序寫得不夠規范,可能有Bug,雖然能編譯生成可執行文件,但程序的運行結果往往是不正確的,例如上面的程序運行時出了一個段錯誤,這屬于運行時錯誤。各種警告信息的嚴重程度不同,像上面這種警告幾乎一定表明程序中有Bug,而另外一些警告只表明程序寫得不夠規范,一般還是能正確運行的,有些不重要的警告信息gcc默認是不提示的,但這些警告信息也有可能表明程序中有Bug。一個好的習慣是打開gcc的-Wall選項,讓gcc提示所有的警告信息,不管是嚴重的還是不嚴重的,然后把這些問題從代碼中全部消滅。比如把上例中的printf("Hello,world.\n");改成printf(0);然后編譯運行:編譯既不報錯也不報警告,一切正常,但是運行程序什么也不打印。如果打開-Wall選項編譯就會報警告了:如果printf中的0是你不小心寫上去的(例如錯誤地使用了編輯器的查找替換功能),這個警告就能幫助你發現錯誤。雖然本書的命令行為了突出重點通常省略-Wall選項,但是強烈建議你寫每一個編譯命令時都加上-Wall選項。習題1.盡管編譯器的錯誤提示不夠友好,但仍然是學習過程中一個很有用的工具。你可以像上面那樣,從一個正確的程序開始每次改動一小點,然后編譯看是什么結果,如果出錯了,就盡量記住編譯器給出的錯誤提示并把改動還原。因為錯誤是你改出來的,你已經知道錯誤原因是什么了,所以能很容易地把錯誤原因和錯誤提示信息對應起來記住,這樣下次你在毫無防備的情況下撞到這個錯誤提示時就會很容易想到錯誤原因是什么了。這樣反復練習,有了一定的經驗積累之后面對編譯器的錯誤提示就會從容得多了。第2章常量、變量和表達式2.1繼續HelloWorld在第1.4節中,讀者應該已經嘗試對Helloworld程序做各種改動看編譯運行結果,其中有些改動會導致編譯出錯,有些改動會影響程序的輸出,有些改動則沒有任何影響,下面我們總結一下。首先,注釋可以跨行,也可以穿插在程序之中,看下面的例子。例2.1帶更多注釋的HelloWorld第一個注釋跨了四行,頭尾兩行是注釋的界定符(Delimiter)/*和*/,中間兩行開頭的*號(Asterisk)并沒有特殊含義,只是為了看起來整齊,這不是語法規則而是大家都遵守的C代碼風格(CodingStyle)之一,代碼風格將在第9章詳細介紹。使用注釋需要注意以下兩點。1.注釋不能嵌套(Nest)使用,就是說一個注釋的文字中不能再出現/*和*/了,例如/*text1/*text2*/text3*/是錯誤的,編譯器只把/*text1/*text2*/看成注釋,后面的text3*/無法解析,因而會報錯。2.有的C代碼中有類似//comment的注釋,兩個/斜線(Slash)表示從這里直到該行末尾的所有字符都屬于注釋,這種注釋不能跨行,也不能穿插在一行代碼中間。這是從C++借鑒的語法,在C99中被標準化。提示:C語言標準C語言的發展歷史大致上分為三個階段:OldStyleC、C89和C99。KenThompson和DennisRitchie最初發明C語言時有很多語法和現在最常用的寫法并不一樣,但為了向后兼容性(BackwardCompatibility),這些語法仍然在C89和C99中保留下來了,本書不詳細講OldStyleC,但在必要的地方會加以說明。C89是最早的C語言規范,于1989年提出,1990年首先由ANSI(美國國家標準委員會,AmericanNationalStandardsInstitute)推出,后來被采納為ISO國際標準(ISO/IEC9899:1990),因而有時也稱為C90,最經典的C語言教材參考文獻[3]就是基于這個版本的,C89是目前最廣泛采用的C語言標準,大多數編譯器都完全支持C89。C99標準(ISO/IEC9899:1999)是在1999年推出的,加入了許多新特性,但目前仍沒有得到廣泛支持,在C99推出之后相當長的一段時間里,連gcc也沒有完全實現C99的所有特性。C99標準詳見參考文獻[8]。本書講C的語法以C99為準,但示例代碼通常只使用C89語法,很少使用C99的新特性。C標準的目的是為了精確定義C語言,而不是為了教別人怎么編程,C標準在表達上追求準確和無歧義,卻十分不容易看懂,參考文獻[4]和參考文獻[5]是對C89及其修訂版本的闡釋(可惜作者沒有隨C99更新這兩本書),比C標準更容易看懂,另外,參考文獻[6]也有助于加深對C標準的理解。像"Hello,world.\n"這種由雙引號(DoubleQuote)引起來的一串字符稱為字符串字面值(StringLiteral),或者簡稱字符串。注意,程序的運行結果并沒有雙引號,printf打印出來的只是里面的一串字符Hello,world.,因此雙引號是字符串字面值的界定符,夾在雙引號中間的一串字符才是它的內容。注意,打印出來的結果也沒有\n這兩個字符,這是為什么呢?在第1.2節中提到過,C語言規定了一些轉義序列(EscapeSequence),這里的\n并不表示它的字面意思,也就是說并不表示\和n這兩個字符本身,而是合起來表示一個換行符(LineFeed)。例如我們寫三條打印語句:運行的結果是第一條語句單獨打到第一行,后兩條語句都打到第二行。為了節省篇幅突出重點,以后的例子通常省略#include和intmain(void){...}這些Boilerplate,但讀者在練習時需要加上這些構成一個完整的程序才能編譯通過。C標準規定的轉義字符有以下幾種,如表2.1所示。表2.1C標準規定的轉義字符如果在字符串字面值中要表示單引號和問號,既可以使用轉義序列\'和\?,也可以直接用字符'和?,而要表示\或"則必須使用轉義序列,因為\字符表示轉義而不表示它的字面含義,"表示字符串的界定符而不表示它的字面含義。可見轉義序列有兩個作用:一是把普通字符轉義成特殊字符,例如把字母n轉義成換行符;二是把特殊字符轉義成普通字符,例如\和"是特殊字符,轉義后取它的字面值。C語言規定了幾個控制字符,不能用鍵盤直接輸入,因此采用\加字母的轉義序列表示。\a是響鈴字符,在字符終端下顯示這個字符的效果是PC喇叭發出嘀的一聲,在圖形界面終端下的輸出效果取決于終端的配置。在終端下顯示\b和按下退格鍵的效果相同。\f是分頁符,主要用于控制打印機在打印源代碼時提前分頁,這樣可以避免一個函數跨兩頁打印。\n和\r分別表示LineFeed和CarriageReturn,這兩個詞來自老式的英文打字機,LineFeed是跳到下一行(進紙,喂紙,有個喂的動作所以是feed),CarriageReturn是回到本行開頭(Carriage是卷著紙的軸,隨著打字慢慢左移,打完一行就一下子移回最右邊,如果你看過歐美的老電影應該能想起來這是什么)。用老式打字機打完一行之后需要這么兩個動作,\r\n,所以現在Windows平臺的文本文件用\r\n做換行符,許多應用層網絡協議(如HTTP)也用\r\n做換行符,而Linux和各種UNIX平臺的文本文件只用\n做換行符。在終端下顯示\t和按下Tab鍵的效果相同,用于在終端下定位表格的下一列,\v用于在終端下定位表格的下一行。\v比較少用,\t比較常用,以后將“水平制表符”簡稱為“制表符”或Tab。請讀者用printf語句試試這幾個控制字符的作用。注意"Goodbye,"末尾的空格,字符串字面值中的空格也算一個字符,也會出現在輸出結果中,而程序中別處的空格和Tab多一個少一個往往是無關緊要的,不會對編譯的結果產生任何影響,例如不縮進不會影響程序的結果,main后面多幾個空格也沒影響,但是int和main之間至少要有一個空格分隔開:不僅空格和Tab是無關緊要的,換行也是如此,我甚至可以把整個程序寫成一行,但是include必須單獨占一行:這樣也行,但肯定不是好的代碼風格,去掉縮進已經很影響可讀性了,寫成現在這個樣子可讀性更差。如果編譯器說第2行有錯誤,也很難判斷是第2行的哪個語句出的錯。所以,好的代碼風格要求縮進整齊,每個語句一行,適當留空行。2.2常量常量(Constant)是程序中最基本的元素,有字符(Character)常量、整數(Integer)常量、浮點數(FloatingPoint)常量和枚舉常量。枚舉常量將在第7.3節介紹。下面看一個例子:字符常量要用單引號括起來,例如上面的'}',注意單引號只能括一個字符而不能像雙引號那樣括一串字符,字符常量也可以是一個轉義序列,例如'\n',這時雖然單引號括了兩個字符,但實際上只表示一個字符。和字符串字面值中使用轉義序列有一點區別,如果在字符常量中要表示雙引號"和問號?,既可以使用轉義序列\"和\?,也可以直接用字符"和?,而要表示'和\則必須使用轉義序列\h③。在計算機中整數和小數的內部表示方式不同(將在第13章詳細介紹),因而在C語言中是兩種不同的類型(Type),通常小數在計算機中的表示方式稱為浮點數,詳見第13.4節。上例的34和3.14分別是整數常量和浮點數常量。上例的printf語句輸出結果和Helloworld那個例子不太一樣,字符串"character:%c\ninteger:%d\nfloatingpoint:%f\n"并不是按原樣打印輸出的,而是輸出成這樣:printf中的第一個字符串稱為格式化字符串(FormatString),它規定了后面幾個常量以何種格式插入到這個字符串中,在格式化字符串中%號(PercentSign)后面加上字母c、d、f分別表示字符型、整型和浮點型的轉換說明(ConversionSpecification),轉換說明只在格式化字符串中占個位置,并不出現在最終的打印結果中,這種用法通常叫做占位符(Placeholder)。這也是一種字面意思與真實意思不同的情況,但是轉換說明和轉義序列又有區別,轉義序列是在編譯時處理的,而轉換說明是在運行時調用printf函數處理的:?源文件中的字符串字面值是"character:%c\ninteger:%d\nfloatingpoint:%f\n",\n占兩個字符;?編譯之后保存在可執行文件中的字符串是character:%c換行integer:%d換行floatingpoint:%f換行,\n已經被替換成一個換行符,而%c這兩個字符不變;?在運行時這個字符串被傳給printf,printf再把其中的%c、%d、%f解釋成轉換說明。有時候不同類型的數據很容易弄混,例如"5"、'5'、5,如果你注意了它們的界定符就會很清楚,第一個是字符串字面值,第二個是字符,第三個是整數,看了本章后面幾節你就知道為什么一定要嚴格區分它們之間的差別了。習題1、我們知道,用\斜線表示轉義序列和在printf格式化字符串中用%號表示占位符是兩種不同的機制,前者在編譯時處理,后者在運行時處理。但兩者在語法上具有類似的規律,想想在printf格式化字符串中怎么表示一個%字符?寫個小程序試驗一下。2.3變量變量(Variable)是編程語言最重要的概念之一,在程序中變量是一個名字,而這個名字代表的是計算機存儲器中的一塊空間,可以在里面保存一個值(Value),保存的值是可以隨時變的,比如這次存個字符'a',變量的值就是'a',下次存個字符'b',變量的值就變成'b',正因為變量的值可以隨時變所以才叫變量。常量有不同的類型,變量也有不同的類型,變量的類型決定了它所占的存儲空間的大小。在C語言中用聲明(Declaration)來規定變量的名字和類型,例如下面有四條聲明,規定了四個變量fred、bob、jimmy和tom的類型分別是字符型、整型、單精度浮點型、雙精度浮點型:提示:聲明和定義C語言中的聲明有變量聲明、函數聲明和類型聲明三種。本節只講變量聲明,下一章會講到函數聲明,從第7章開始我們會看到類型聲明。從另一個角度來看,聲明分為“是定義(Definition)的聲明”和“不是定義的聲明”,那么什么樣的聲明同時也是定義呢?簡單地說,分配存儲空間的聲明同時也是定義,不分配存儲空間的聲明不是定義。?如果一個變量聲明要求編譯器為它分配存儲空間,那么這個聲明同時也是變量的定義。本章和接下來幾章的示例代碼中的變量聲明都是要分配存儲空間的,因而都是定義;等學到第19.2節我們會看到有些變量聲明不分配存儲空間,因而不是定義。?如果一個函數聲明帶有函數體,要求編譯器為它生成指令(當然也需要分配存儲空間來保存這些指令),那么這個聲明同時也是函數的定義。在下一章我們會看到帶函數體的聲明和不帶函數體的聲明,不帶函數體的聲明不是函數定義。?類型聲明總是不分配存儲空間的,所以嚴格來說只有類型聲明而沒有類型定義,但通常我們習慣說“定義了某種類型”,所以在本書中“類型定義”和“類型聲明”表示相同的含義,不加區分。聲明也是以;號結尾,這一點和語句類似,但是在語法上聲明和語句是有區別的,語句只能出現在函數體中,而聲明既可以出現在函數體中也可以出現在所有函數之外。浮點型有三種:float是單精度浮點型;double是雙精度浮點型;longdouble是精度更高的浮點型。它們之間的區別和轉換規則將在第14章詳細介紹,在隨后的幾章中我們只使用double類型,上一節介紹的常量3.14是double類型的常量,printf的%f也是double型的轉換說明(注意%f不是float型的轉換說明)。給變量起名不能太隨意,上面四個變量的名字就不夠好,我們猜不出這些變量是用來存什么的,像下面這樣起名就比較有意義:在這個例子中我們還看到兩個相同類型的變量(同樣是int類型的hour和minute)可以一起聲明。給變量起名有一定的限制,C語言規定必須以字母或下劃線_(Underscore)開頭,后面可以跟若干個字母、數字、下劃線,但不能有其他字符。例如這些是合法的變量名:Abc、__abc__、_123。但這些是不合法的變量名:3abc、ab$。其實這條規則不僅適用于變量名,也適用于所有可以由程序員起名的語法元素,例如以后要講的函數名、宏定義、結構體成員名等,在C語言中這些統稱為標識符(Identifier)。另外要注意,表示類型的char、int、float、double等雖然符合上述規則,但也不能用作標識符。在C語言中有些單詞有特殊意義,不允許用作標識符,這些單詞稱為關鍵字(Keyword)或保留字(ReservedWord)。通常用于編程的文本編輯器都會高亮顯示(Highlight)這些關鍵字,所以只要小心一點通常不會誤用作標識符。C99規定的關鍵字有:還有一點要注意,一般來說應避免使用以下劃線開頭的標識符,以下劃線開頭的標識符只要不和C語言關鍵字沖突都是合法的,但是往往被編譯器用作一些功能擴展(比如第18.4節講到gcc的__attribute__語法),C標準庫也定義了很多以下劃線開頭的標識符留作內部使用,所以除非你對編譯器的特性和C標準庫的實現特別清楚,一般應避免使用這種標識符,以免造成命名沖突。請記住:理解一個概念不是把定義背下來就行了,一定要理解它的外延和內涵,也就是什么情況屬于這個概念,什么情況不屬于這個概念,什么情況雖然屬于這個概念但一般推薦的做法(BestPractice)是要盡量避免這種情況,這才算是真正理解了。2.4賦值定義了變量之后,我們要把值存到它們所表示的存儲空間里,可以用賦值(Assignment)語句實現:注意變量一定要先聲明后使用,編譯器必須先看到變量聲明,才知道firstletter、hour和minute是變量名,各自代表一塊存儲空間。另外,變量聲明中的類型表明這個變量代表多大的一塊存儲空間,這樣編譯器才知道如何讀寫這塊存儲空間。還要注意,這里的等號不表示數學里的相等關系,和1+1=2的等號是不同的,這里的等號表示賦值。在數學上不會有i=i+1這種等式成立,而在C語言中這條語句表示把變量i的存儲空間中的值取出來,再加上1,得到的結果再存回i的存儲空間中。再比如,在數學上a=7和7=a是一樣的,而在C語言中后者是不合法的。總結一下:定義一個變量,就是分配一塊存儲空間并給它命名;給一個變量賦值,就是把一個值保存到這塊存儲空間中。變量的定義和賦值也可以一步完成,這稱為變量的初始化(Initialization),例如要達到上面代碼的效果也可以這樣寫:其中等號右邊的值叫做Initializer,例如上面的'a'、11和59。注意,初始化是一種特殊的聲明,而不是一種賦值語句。就目前來看,先定義一個變量再給它賦值和定義這個變量的同時給它初始化所達到的效果是一樣的,C語言的很多語法規則既適用于賦值也適用于初始化,但在以后的學習中你也會了解到它們之間的不同,請在學習過程中注意總結賦值和初始化的相同和不同之處。如果在紙上“跑”一個程序\h④,可以用一個框表示變量的存儲空間,在框的外邊標上變量名,在框里記上它的值,如圖2.1所示。圖2.1在紙上表示變量你可以用不同形狀的框表示不同類型的變量,這樣可以提醒你給變量賦的值必須符合它的類型。如果所賦的值和變量的類型不符會導致編譯器報警告或報錯(這是一種語義錯誤),例如:注意最后一個語句,把"59"賦給minute看起來像是對的,但是類型不對,字符串字面值不能賦給整型變量。既然可以為變量的存儲空間賦值,就應該可以把值取出來用,現在我們取出這些變量的值用printf打印:變量名用在等號左邊表示賦值,而用在printf中表示把它的存儲空間中的值取出來替換在那里。不同類型的變量所占的存儲空間大小是不同的,數據表示方式也不同,變量的最小存儲單位是字節(Byte),在C語言中char型變量占一個字節,其他類型的變量占多少字節在不同平臺上有不同的規定,將在第14章詳細討論。2.5表達式常量和變量都可以參與加減乘除運算,例如1+1、hour-1、hour*60+minute、minute/60等。這里的+-*/稱為運算符(Operator),而參與運算的常量和變量稱為操作數(Operand),上面四個由運算符和操作數所組成的算式稱為表達式(Expression)。和數學上規定的一樣,hour*60+minute這個表達式應該先算乘再算加,也就是說運算符是有優先級(Precedence)的,*和/是同一優先級,+和-是同一優先級,*和/的優先級高于+和-。對于同一優先級的運算從左到右計算,如果不希望按默認的優先級計算則要加()括號(Parenthesis)。例如(3+4)*5/6應先算3+4,再算*5,再算/6。前面講過打印語句和賦值語句,現在我們定義:在任意表達式后面加個;號也是一種語句,稱為表達式語句。例如:這是個合法的語句,但這個語句在程序中起不到任何作用,把hour的值和minute的值取出來相加和相乘,得到的計算結果卻沒有保存,白算了一通。再比如:這個語句就很有意義,把計算結果保存在另一個變量total_minute里。事實上等號也是一種運算符,稱為賦值運算符,賦值語句就是一種表達式語句,等號的優先級比+和*都低,所以先算出等號右邊的結果然后才做賦值操作,整個表達式total_minute=hour*60+minute加個;號構成一個語句。任何表達式都有值和類型兩個基本屬性。hour*60+minute的值是由三個int型的操作數計算出來的,所以這個表達式的類型也是int型。同理,表達式total_minute=hour*60+minute的類型也是int,它的值是多少呢?C語言規定等號運算符的計算結果就是等號左邊被賦予的那個值,所以這個表達式的值和hour*60+minute的值相同,也和total_minute被賦值之后的值相同。等號運算符還有一個和+-*/不同的特性,如果一個表達式中出現多個等號,不是從左到右計算而是從右到左計算,例如:計算順序是先算hour*60+minute得到一個結果,然后算右邊的等號,就是把hour*60+minute的結果賦給變量total_minute,這個結果同時也是整個表達式total_minute=hour*60+minute的值,再算左邊的等號,即把這個值再賦給變量total。如果一個操作數的左右兩側各有一個相同優先級的運算符,這個操作數與左邊的運算符結合還是與右邊的運算符結合取決于運算符的結合性(Associativity),相同優先級的運算符應該具有相同的結合性,+-和*/是左結合的,而等號是右結合的。在上面的表達式中,操作數total_minute的左右兩邊都有等號,應該和右邊的等號結合,相當于total=(total_minute=hour*60+minute),而不是(total=total_minute)=hour*60+minute。現在我們總結一下到目前為止學過的語法規則:表達式→標識符表達式→常量表達式→字符串字面值表達式→(表達式)表達式→表達式+表達式表達式→表達式-表達式表達式→表達式*表達式表達式→表達式/表達式表達式→表達式=表達式語句→表達式;語句→printf(表達式,表達式,表達式,...);變量聲明→類型標識符=Initializer,標識符=Initializer,...;(=Initializer的部分可以不寫)注意,本書所列的語法規則都是簡化過的,是不準確的,目的是為了便于初學者理解,比如上面所列的語法規則并沒有描述運算符的優先級和結合性。完整的C語法規則請查看參考文獻[8]的AnnexA。表達式可以是單個的常量或變量,也可以是根據以上規則組合而成的更復雜的表達式。以前我們用printf打印常量或變量的值,現在可以用printf打印更復雜的表達式的值,例如:編譯器在翻譯這條語句時,首先根據上述語法規則把這個語句解析成如圖2.2所示的語法樹,然后再根據語法樹生成相應的指令。圖2.2語法樹語法樹的每一步分解利用一條語法規則,直到分解成Token為止,所以語法樹的末端全部是Token。語法解析的過程十分復雜,我們不深入討論如何分解,而是反過來從組合的角度來理解語法規則,比如表達式hour*60+minute是這樣組合而成的:1.hour是標識符,根據規則“表達式→標識符”,它也是表達式;2.60是常量,根據規則“表達式→常量”,它也是表達式;3.既然hour和60都是表達式,根據規則“表達式→表達式*表達式”,hour*60可以組合成表達式;4.minute是標識符,根據規則“表達式→標識符”,它也是表達式;5.既然hour*60和minute都是表達式,根據規則“表達式→表達式+表達式”,hour*60+minute可以組合成表達式。根據這些語法規則進一步組合可以寫出更復雜的語句,比如在一條語句中完成計算、賦值和打印功能:理解組合(Composition)規則是理解語法規則的關鍵所在,正因為可以根據語法規則任意組合,我們才可以用簡單的常量、變量、表達式、語句和聲明搭建出任意復雜的程序,以后我們學習新的語法規則時會進一步體會到這一點。從上面的例子可以看出,表達式不宜過度組合,否則會給閱讀和調試帶來困難。根據語法規則組合出來的表達式在語義上并不總是正確的,例如:等號左邊的表達式要求表示一個存儲位置而不是一個值,這是等號運算符和+-*/運算符的又一個顯著的不同。有的表達式既可以表示一個存儲位置也可以表示一個值,而有的表達式只能表示值,不能表示存儲位置,例如minute+1這個表達式就不能表示存儲位置,放在等號左邊是語義錯誤。表達式所表示的存儲位置稱為左值(lvalue),允許放在等號左邊,而之前我們所說的表達式的值也稱為右值(rvalue),只能放在等號右邊。上面的話換一種說法就是:有的表達式既可以做左值也可以做右值,而有的表達式只能做右值。目前我們學過的表達式中只有變量可以做左值,可以做左值的表達式還有幾種,以后會講到。我們看一個有意思的例子,如果定義三個變量inta,b,c;,表達式a=b=c是合法的,先求b=c的值,再把這個值賦給a,而表達式(a=b)=c是不合法的,先求(a=b)的值沒問題,但(a=b)這個表達式不能再做左值了,因此放在=c的等號左邊是錯的。關于整數除法運算有一點特殊之處:執行結果是11and0hours,也就是說59/60得0,這是因為兩個int型操作數相除的表達式仍為int型,只能保存計算結果的整數部分,即使小數部分是0.98也要舍去。向下取整的運算稱為Floor,用數學符號「」表示;向上取整的運算稱為Ceiling,用數學符號「」表示。例如:在C語言中整數除法取的既不是Floor也不是Ceiling,無論操作數是正是負總是把小數部分截掉,在數軸上向零的方向取整(TruncatetowardsZero),或者說當操作數為正的時候相當于Floor,當操作數為負的時候相當于Ceiling。回到先前的例子,要得到更精確的結果可以這樣:在第二個printf中,表達式是minute/60.0,60.0是double型的,/運算符要求左右兩邊的操作數類型一致,而現在并不一致。C語言規定了一套隱式類型轉換規則,在這里編譯器自動把左邊的minute也轉成double型來計算,整個表達式的值也是double型的,在格式化字符串中應該用%f轉換說明與之對應。本來編程語言作為一種形式語言要求有簡單而嚴格的規則,自動類型轉換規則不僅很復雜,而且使C語言的形式看起來也不那么嚴格了,C語言這么設計是為了書寫程序簡便而做的折中,有些事情編譯器可以自動做好,程序員就不必每次都寫一堆繁瑣的轉換代碼。然而C語言的類型轉換規則非常難掌握,本書的前幾章會盡量避免類型轉換,到第14.3節再集中解決這個問題。習題1、假設變量x和n是兩個正整數,我們知道x/n這個表達式的結果要取Floor,例如x是17,n是4,則結果是4。如果希望結果取Ceiling應該怎么寫表達式呢?例如x是17,n是4,則結果是5;x是16,n是4,則結果是4。2.6字符類型與字符編碼字符常量或字符型變量也可以當做整數參與運算,例如:執行結果是b。我們知道,符號在計算機內部也用數字表示,每個字符在計算機內部用一個整數表示,稱為字符編碼(CharacterEncoding),目前最常用的是ASCII碼(AmericanStandardCodeforInformationInterchange),詳見表A.1。表中每一欄的最后一列是字符,前三列分別是用十進制(Dec)、十六進制(Hx)和八進制(Oct)表示的字符編碼,各種進制之間的換算將在第13.2節介紹。從十進制那一列可以看出ASCII碼的取值范圍是0~127。表中的很多字符是不可見字符(Non-printableCharacter)和空白字符(Whitespace)\h⑤,不能像字母a這樣把字符本身填在表中,而是用一個名字來描述該字符,例如CR(CarriageReturn)、LF(NLLineFeed,Newline)、DEL等。作為練習,請讀者查一查表2.1中的字符在ASCII碼表中的什么位置。回到剛才的例子,在ASCII碼中字符a是97,字符b是98。計算'a'+1這個表達式,應該按ASCII碼把'a'當做整數值97,然后加1,得到98,然后printf把98這個整數值當做ASCII碼來解釋,打印出相應的字符b。之前我們說“整型”是指int型,而現在我們知道char型本質上就是整數,只不過取值范圍比int型小,所以以后我們把char型和int型統稱為整數類型(IntegerType)或簡稱整型,后面我們還要學習幾種類型也屬于整型,將在第14.1節詳細介紹。字符'a'~'z'、'A'~'Z'、'0'~'9'的ASCII碼都是連續的,因此表達式'a'+25和'z'的值相等,'0'+9和'9'的值也相等。注意'0'~'9'的ASCII碼是十六進制的30~39,和整數值0~9是不相等的。字符也可以用ASCII碼轉義序列表示,這種轉義序列由\加上1~3個八進制數字組成,或者由\x加上1~2個十六進制數字組成,可以用在字符常量或字符串字面值中。例如'\0'表示NUL字符(NullCharacter),'\11'或'\x9'表示Tab字符,"\11"或"\x9"表示由Tab字符組成的字符串。注意'0'的ASCII碼是48,而'\0'的ASCII碼是0,兩者是不同的。第3章簡單函數3.1數學函數在數學中我們用過sin和ln這樣的函數,例如sin(π/2)=1,ln1=0等,在C語言中也可以使用這些函數(數學函數sin在C標準庫中就是sin函數,而數學函數ln在C標準庫中對應的是log函數)。例3.1在C語言中使用數學函數編譯運行這個程序,結果如下:在數學中寫一個函數有時候可以省略括號,而C語言要求一定要加上括號,例如log(1.0)。在C語言的術語中,1.0是參數,log是函數(Function),log(1.0)是函數調用(FunctionCall)。sin(pi/2)和log(1.0)這兩個函數調用在我們的printf語句中處于什么位置呢?在上一章講過,這應該是寫表達式的位置,因此函數調用也是一種表達式。log(1.0)這個表達式由操作數log和函數調用運算符()括號組成,函數調用運算符是一種后綴運算符(PostfixOperator),()括號及其中的參數是操作數log的后綴。操作數log是一個函數名(FunctionDesignator),它的類型是一種函數類型(FunctionType)。log(1.0)這個表達式的值是取自然對數運算的結果,類型是double型,在C語言中函數調用表達式的值稱為函數的返回值(ReturnValue)。總結一下我們新學的語法規則:表達式→函數名表達式→表達式(參數列表)參數列表→表達式,表達式,...現在我們可以完全理解printf語句了:原來printf也是一個函數,上例中的printf("sin(pi/2)=%f\nln1=%f\n",sin(pi/2),log(1.0))是帶三個參數的函數調用,而函數調用也是一種表達式,因此printf語句也是表達式語句的一種。但是printf感覺不像一個數學函數,為什么呢?因為像log這種函數,我們傳進去一個參數會得到一個返回值,我們調用log函數就是為了得到它的返回值,至于printf,通常我們并不關心它的返回值(事實上它也有返回值,表示實際打印的字符數),我們調用printf不是為了得到它的返回值,而是為了利用它所產生的副作用(SideEffect)——打印。C語言的函數可以有SideEffect,這一點是它和數學函數在概念上的根本區別。SideEffect這個概念也適用于運算符組成的表達式。比如a+b這個表達式也可以看成一個函數調用,把運算符+看作函數,它的兩個參數是a和b,返回值是兩個參數的和,傳入兩個參數,得到一個返回值,并沒有產生任何SideEffect。而賦值運算符是有SideEffect的,如果把a=b這個表達式看成函數調用,返回值就是所賦的值,既是b的值也是a被賦予的值,但除此之外還產生了SideEffect,就是變量a被改變了,改變計算機存儲單元里的數據或者做輸入輸出操作都算SideEffect。回想一下我們的學習過程,一開始我們說賦值是一種語句,后來學了表達式,我們說賦值語句是表達式語句的一種;一開始我們說printf是一種語句,現在學了函數,我們又說printf也是表達式語句的一種。隨著我們一步步的學習,把原來看似不同類型的語句統一成一種語句了。學習的過程總是這樣,初學者一開始接觸的很多概念從嚴格意義上說是錯的,但是很容易理解,隨著一步步學習,在理解原有概念的基礎上不斷糾正,不斷泛化(Generalize)。比如一年級老師說小數不能減大數,其實這個概念是錯的,后來引入了負數就可以減了,后來引入了分數,原來的正數和負數的概念就泛化為整數,上初中學了無理數,原來的整數和分數的概念就泛化為有理數,再上高中學了復數,有理數和無理數的概念就泛化為實數。坦白說,到目前為止本書的很多說法都是不完全正確的,但這是學習理解的必經階段,到后面的章節都會逐步糾正的。程序第一行的#號(PoundSign,NumberSign或HashSign)和include表示包含一個頭文件(HeaderFile),后面尖括號(AngleBracket)中就是文件名(這些頭文件通常位于/usr/include目錄下)。頭文件中聲明了我們程序中使用的庫函數,根據先聲明后使用的原則,要使用printf函數必須包含stdio.h,要使用數學函數必須包含math.h,如果什么庫函數都不使用就不必包含任何頭文件,例如寫一個程序intmain(void){inta;a=2;return0;},不需要包含頭文件就可以編譯通過,當然這個程序什么也做不了。使用math.h中聲明的庫函數還有一點特殊之處,gcc命令行必須加-lm選項,因為數學函數位于libm.so庫文件中(這些庫文件通常位于/lib目錄下),-lm選項告訴編譯器,我們程序中用到的數學函數要到這個庫文件里找。注意庫文件名是libm,但使用-l選項指定庫文件時省略lib,只寫成-lm。本書用到的大部分庫函數(例如printf)位于libc.so庫文件中,使用libc.so中的庫函數在編譯時不需要加-lc選項,當然加了也不算錯,因為這個選項是gcc的默認選項。關于頭文件和庫函數目前理解這么多就可以了,到第19章再詳細解釋。提示:C標準庫和glibcC標準主要由兩部分組成,一部分描述C的語法,另一部分描述C標準庫。C標準庫定義了一組標準頭文件,每個頭文件中包含一些相關的函數、變量、類型聲明和宏定義。要在一個平臺上支持C語言,不僅要實現C編譯器,還要實現C標準庫,這樣的實現才算符合C標準。不符合C標準的實現也是存在的,例如很多單片機的C語言開發工具中只有C編譯器而沒有完整的C標準庫。在Linux平臺上最廣泛使用的C函數庫是glibc,其中包括C標準庫的實現,也包括本書第三部分介紹的所有系統函數。幾乎所有C程序都要調用glibc的庫函數,所以glibc是Linux平臺C程序運行的基礎。glibc提供一組頭文件和一組庫文件,最基本、最常用的C標準庫函數和系統函數在libc.so庫文件中,幾乎所有C程序的運行都依賴于libc.so,有些做數學計算的C程序除了libc.so之外還依賴于libm.so,還有很多C程序依賴于glibc的其他庫文件。以后我說libc時專指libc.so這個庫文件,而說glibc時指的是glibc提供的所有庫文件。glibc并不是Linux平臺唯一的基礎C函數庫,也有人在開發別的C函數庫,比如適用于嵌入式系統的uClibc。3.2自定義函數我們不僅可以調用C標準庫提供的函數,也可以定義自己的函數,事實上我們已經這么做了:我們定義了main函數。例如:main函數的特殊之處在于執行程序時它自動被操作系統調用,操作系統就認準了main這個名字,除了名字特殊之外,main函數和別的函數沒有區別。我們對照著main函數的定義來看語法規則:函數定義→返回值類型函數名(參數列表)函數體函數體→{語句列表}語句列表→語句列表項語句列表項...語句列表項→語句語句列表項→變量聲明、類型聲明或非定義的函數聲明非定義的函數聲明→返回值類型函數名(參數列表);我們稍后再詳細解釋“函數定義”和“非定義的函數聲明”的區別。從第7章開始我們才會看到類型聲明,所以現在暫不討論。給函數命名也要遵循上一章講過的標識符命名規則。由于我們定義的main函數不帶任何參數,參數列表應寫成void。函數體可以由若干條語句和聲明組成,C89要求所有聲明寫在所有語句之前(本書的示例代碼都遵循這一規定),而C99的新特性允許語句和聲明按任意順序排列,只要每個標識符都遵循先聲明后使用的原則就行。main函數的返回值是int型的,return0;這個語句表示返回值是0,main函數的返回值是返回給操作系統看的,因為

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論