程式的基本概念

程式設計師詩人發表於2017-06-02

程式的基本概念
1.1. 程式和程式語言
程式(Program)告訴計算機應如何完成一個計算任務,這裡的計算可以是數學運算,比如解方程,也可以是符號運算,比如查詢和替換文件中的某個單詞。從根本上說,計算機是由數位電路組成的運算機器,只能對數字做運算,程式之所以能做符號運算,是因為符號在計算機內部也是用數字表示的。此外,程式還可以處理聲音和影像,聲音和影像在計算機內部必然也是用數字表示的,這些數字經過專門的硬體裝置轉換成人可以聽到、看到的聲音和影像。
程式由一系列基本操作組成,基本操作有以下幾類:
輸入(Input)
從鍵盤、檔案或者其他裝置獲取資料。

輸出(Output)
把資料顯示到螢幕,或者存入一個檔案,或者傳送到其他裝置。
基本運算
最基本的資料訪問和數學運算(加減乘除)。
測試和分支
測試某個條件,然後根據不同的測試結果執行不同的後續操作。
迴圈
重複執行一系列操作。

你曾用過的任何一個程式,不管它有多麼複雜,都是按這幾類基本操作一步一步執行的。程式是那麼的複雜,而編寫程式可以用的基本操作卻只有這麼簡單的幾種,這中間巨大的落差就要由程式設計師去填補了,所以編寫程式理應是一件相當複雜的工作。 編寫程式可以說是這樣一個過程:把複雜的任務分解成子任務,把子任務再分解成更簡單的任務,層層分解,直到最後簡單得可以用以上幾種基本操作來完成。
程式語言(Programming Language)分為低階語言(Low-level Language)和高階語言(High-level Language)。機器語言(Machine Language)和組合語言(Assembly Language)屬於低階語言,直接用計算機指令(Instruction)編寫程式。而C、C++、Java、Python等屬於高階語言,用語句(Statement)編寫程式,語句是計算機指令的抽象表示。
舉個例子,同樣一個語句用機器語言、組合語言和C語言分別表示如下:
一個語句的三種表示(32位x86平臺)

程式語言
表示形式

機器語言
a1 18 a0 04 0883 c0 01a3 1c a0 04 08

組合語言
mov 0x804a018,%eaxadd $0x1,%eaxmov %eax,0x804a01c

C語言
a = b + 1;

計算機只能對數字做運算,符號、聲音、影像在計算機內部都要用數字表示,指令也不例外,上表中的機器語言完全由十六進位制數字組成。最早的程式設計師都是直接用機器語言程式設計,但是很麻煩,需要查大量的表格來確定每個數字表示什麼意思,編寫出來的程式很不直觀,而且容易出錯,於是有了組合語言,把機器語言中一組一組的數字用助記符(Mnemonic)表示,直接用這些助記符寫出彙編程式,然後讓彙編器(Assembler)去查表把助記符替換成數字,也就把組合語言翻譯成了機器語言。
從上面的例子可以看出,組合語言和機器語言的指令是一一對應的,組合語言有三條指令,機器語言也有三條指令,彙編器就是做一個簡單的替換工作。例如在第一條指令中,把 mov ?,%eax
這種格式的指令替換成機器碼 a1 ?
,?表示一個地址,在彙編指令中是 0x804a018
,轉換成機器碼之後是 18 a0 04 08
(這是指令中十六進位制數的小端表示,小端表示將在 CPU 介紹)。
從上面的例子還可以看出,C語言的語句和低階語言的指令之間不是簡單的一一對應關係,一條 a = b + 1;
語句要翻譯成三條彙編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比彙編器要複雜得多。
編寫、編譯和執行一個C程式的步驟如下:
用文字編輯器寫一個C程式,然後儲存成一個檔案,例如 program.c
(通常C程式的檔名字尾是 .c
),這稱為原始碼(Source Code)或原始檔。
執行編譯器對它進行編譯,編譯的過程並不執行程式,而是把原始碼全部翻譯成機器指令,再加上一些描述資訊,生成一個新的檔案,例如 a.out
,這個檔案稱為可執行檔案(Executable)。
可執行檔案可以被作業系統(Operating System)載入執行,計算機執行該檔案中由編譯器生成的指令。

../_images/intro.compile.png

編譯執行的過程

現在你就可以跳到本章後面的 第一個程式 ,按照書上的步驟自己動手試試。
有些高階語言寫的程式不需要經過編譯這個步驟,而是以解釋的方式執行,解釋執行(Interpret)的程式通常又叫做指令碼(Script),解釋執行的過程和C語言的編譯執行過程很不一樣。例如編寫一個Shell指令碼 script.sh
,內容如下:

#! /bin/sh
VAR=1
VAR=$(($VAR+1))
echo $VAR

這個指令碼的第一行表明它是個Shell指令碼,後面三行的意思分別是:定義變數 VAR
的初始值是1,然後自增1,最後列印 VAR
的值。用Shell程式 /bin/sh
解釋執行這個指令碼,結果如下:
$ /bin/sh script.sh2

這裡的 /bin/sh
稱為直譯器(Interpreter),直譯器本身是一個可執行檔案,而我們寫的指令碼 script.sh
卻不是一個真正的可執行檔案。直譯器 /bin/sh
也是由C程式經過編譯得到的包含機器指令的可執行檔案,它被作業系統載入執行時,它所包含的機器指令指示它做這樣的事情:把我們寫的指令碼 script.sh
當成資料檔案讀取,理解我們所寫的每一行程式的意思,並一行一行地執行相應的操作。

../_images/intro.interpret.png

解釋執行的過程

理解了這些概念之後,我們在編譯型高階語言、解釋型高階語言和低階語言之間做個比較。用高階語言寫的程式不能直接被計算機執行,需要經過編譯之後變成可執行檔案才能執行,或者需要通過一個直譯器來解釋執行。但用高階語言寫程式有很多優點:首先,用高階語言寫程式更簡便,寫出來的程式碼更緊湊,可讀性更強,出了錯也更容易改正;其次,高階語言是可移植的(Portable),或者稱為平臺無關的(Platform Independent)、跨平臺的(Cross-platform)。
平臺(Platform)這個詞可以指計算機體系結構(Architecture),也可以指作業系統(Operating System)。有些程式只能在某個特定平臺上執行,而有些程式拿到各種不同的平臺上都可以執行,後者就稱為平臺無關的程式。下面我們來具體分析:
作業系統相同,計算機體系結構不同會怎麼樣?
不同的計算機體系結構有不同的指令集(Instruction Set),可以識別的機器指令格式是不同的,直接用某種體系結構的彙編或機器指令寫出來的程式只能在這種體系結構的計算機上執行。
不同體系結構的計算機有各自的C編譯器,可以把C程式編譯成相應的機器指令,這意味著用C語言寫的程式要想在各種不同體系結構的計算機上執行,只需用相應的編譯器編譯過即可。比如上面 a = b + 1;
的例子是在32位x86平臺下編譯的得到的結果,如果在ARMv4平臺上編譯則得到完全不同的結果(編譯生成的可執行檔案也完全不同):
一個語句的三種表示(ARMv4平臺)

程式語言

表示形式

機器語言

e59f2018e59f3018e5933000e2833001e5823000

組合語言

ldr r2, [pc, #24]ldr r3, [pc, #24]ldr r3, [r3]add r3, r3, #1str r3, [r2]

C語言

a = b + 1;

同樣道理,不同體系結構的計算機有各自的Shell直譯器,一個Shell指令碼要想在不同體系結構的計算機上執行,只需執行相應的Shell直譯器來解釋執行即可。

體系結構相同,作業系統不同會怎麼樣?
同樣是32位x86平臺,把一個Windows下的可執行檔案(通常副檔名是 .exe
)拷到Linux下能執行嗎?答案是不能。雖然這個Windows下的可執行檔案包含的是32位x86指令,但其檔案格式和Linux下的可執行檔案有很大差別,換句話說,能被Windows作業系統載入執行的程式不能被Linux作業系統載入執行,因為這兩種作業系統載入執行程式的機制很不一樣。
那麼,把一個Windows下的C程式的原始碼拷到Linux下,還能用C編譯器編譯執行嗎?答案是不一定。如果這個C程式只用到了C標準庫,是可以跨平臺的;如果這個C程式用到了Windows作業系統提供而Linux作業系統沒有提供的庫和介面,那麼在Linux下是編譯不了的。
把一個在Windows下的Cygwin環境中能正常執行的Shell指令碼拷到Linux下,還能正常執行嗎?答案是能執行,但結果不一定正確。如果這個指令碼訪問了Windows下特有的資源(比如 C:a.txt
這樣的檔案路徑是Windows特有的,Linux的路徑格式完全不同),則不能在Linux下正確執行。

用解釋型語言寫的程式執行起來一定比編譯型語言慢,因為用解釋型語言寫的程式每次執行時直譯器都要把原始碼分析一遍,理解程式設計師寫這些程式碼是想要做什麼,再去執行相應的操作,而對於編譯型語言來說,這個步驟只需要做一次,就是編譯器把原始碼分析一遍生成可執行檔案,而之後可執行檔案在每次執行時就不需要再分析原始碼了。用解釋型語言寫的程式也有它的優點:換個平臺就可以直接執行,而不需要先編譯一遍,此外,解釋型語言寫的程式除錯起來比編譯型語言方便得多。
既然解釋型語言和編譯型語言各有各的優點,有一些高階語言就把兩者的優點結合起來,採用編譯和解釋相結合的方式執行。Java、Python、Perl等程式語言都採用這種方式。以Python為例,程式設計師寫的原始碼檔案(副檔名為 .py
)在首次執行時被編譯成位元組碼(Byte Code)檔案(副檔名為 .pyc
),以後每次執行該程式時Python直譯器直接解釋執行位元組碼檔案,而不再編譯原始碼。位元組碼檔案中也包含指令,但並非機器指令,而是Python語言定義的一種虛擬機器(Virtual Machine)的指令。Python語言在各種平臺上都實現這種虛擬機器,因此位元組碼檔案從一種平臺拷到另一種平臺上仍然能被該平臺的Python直譯器解釋執行。

../_images/intro.bytecode.png

虛擬機器執行的過程

程式語言仍在發展演化。以上介紹的機器語言稱為第一代程式語言(1GL,1st Generation Programming Language),組合語言稱為第二代程式語言(2GL),C、C++、Java、Python等可以稱為第三代程式語言(3GL)。目前已經有了4GL和5GL的概念。3GL的程式語言雖然是用語句程式設計而不直接用指令程式設計,但語句也分為輸入、輸出、基本運算、測試分支和迴圈等幾種,和指令有直接的對應關係。而4GL以後的程式語言更多是描述要做什麼(Declarative)而不描述具體一步一步怎麼做(Imperative),具體一步一步怎麼做完全由編譯器或直譯器決定,例如SQL語言(SQL,Structured Query Language,結構化查詢語言)就是這樣的例子。

1.2. 自然語言和形式語言
自然語言(Natural Language)就是人類講的語言,比如漢語、英語和法語。這類語言不是人為設計(雖然有人試圖強加一些規則)而是自然進化的。形式語言(Formal Language)是為了特定應用而人為設計的語言。例如數學家用的數字和運算子號、化學家用的分子式等。程式語言也是一種形式語言,是專門設計用來表達計算過程的形式語言。
形式語言有嚴格的語法(Syntax)規則,例如,3+3=6是一個語法正確的數學等式,而3=+6$則不是,H2
O是一個正確的分子式,而 2
Zz則不是。語法規則是由符號(Token)和結構的規則所組成的。Token的概念相當於自然語言中的單詞和標點、數學式中的數和運算子、化學分子式中的元素名和數字,例如3=+6$的問題之一在於$不是一個合法的數也不是一個事先定義好的運算子,而 2
Zz的問題之一在於沒有一種元素的縮寫是Zz。結構是指Token的排列方式,3=+6$還有一個結構上的錯誤,雖然加號和等號都是合法的運算子,但是不能在等號之後緊跟加號,而 2
Zz的另一個問題在於分子式中必須把下標寫在化學元素名稱之後而不是前面。關於Token的規則稱為詞法(Lexical)規則,而關於結構的規則稱為語法(Grammar)規則 [1]

[1]
很不幸,Syntax和Grammar通常都翻譯成“語法”,這讓初學者非常混亂,Syntax的含義其實包含了Lexical和Grammar的規則,還包含一部分語義的規則(例如在C程式中變數應先宣告後使用)。即使在英文的文獻中Syntax和Grammar也經常混用,在有些文獻中Syntax的含義不包括Lexical規則,只要注意上下文就不會誤解。另外,本書在翻譯容易引起混淆的時候通常直接用英文名稱,例如Token沒有十分好的翻譯,直接用英文名稱。

當閱讀一個自然語言的句子或者一種形式語言的語句時,你不僅要搞清楚每個詞(Token)是什麼意思,而且必須搞清楚整個句子的結構是什麼樣的(在自然語言中你只是沒有意識到,但確實這樣做了,尤其是在讀外語時你肯定也意識到了)。這個分析句子結構的過程稱為解析(Parse)。例如,當你聽到“The other shoe fell.”這個句子時,你理解the other shoe是主語而fell是謂語動詞,一旦解析完成,你就搞懂了句子的意思,如果知道shoe是什麼東西,fall意味著什麼,這句話是在什麼上下文(Context)中說的,你還能理解這個句子主要暗示的內容--這屬於語義(Semantic)的範疇。
雖然形式語言和自然語言有很多共同之處,包括Token、結構和語義,但是也有很多不一樣的地方。
歧義性(Ambiguity)
自然語言充滿歧義,人們通過上下文的線索和自己的常識來解決這個問題。形式語言的設計要求是清晰的、毫無歧義的,這意味著每個語句都必須有確切的含義而不管上下文如何。
冗餘性(Redundancy)
為了消除歧義減少誤解,自然語言引入了相當多的冗餘。結果是自然語言經常說得囉裡囉唆,而形式語言則更加緊湊,極少有冗餘。
與字面意思的一致性
自然語言充斥著成語和隱喻(Metaphor),我在某種場合下說“The other shoe fell”,可能並不是說誰的鞋掉了。而形式語言中字面(Literal)意思基本上就是真實意思,也會有一些例外,例如下一章要講的C語言轉義序列,但即使有例外也會明確規定哪些字面意思不是真實意思,它們所表示的真實意思又是什麼。

說自然語言長大的人(實際上沒有人例外),往往有一個適應形式語言的困難過程。某種意義上,形式語言和自然語言之間的不同正像詩歌和說明文的區別:
詩歌
詞語的發音和意思一樣重要,全詩作為一個整體創造出一種效果或者表達一種感情。歧義和非字面意思不僅是常見的而且是刻意使用的。
說明文
詞語的字面意思顯得更重要,並且結構能傳達更多的資訊。詩歌只能看一個整體,而說明文更適合逐字逐句分析,但仍然充滿歧義。
程式
計算機程式是毫無歧義的,字面和本意高度一致,能夠完全通過對Token和結構的分析加以理解。

這裡給出一些關於閱讀程式(包括其他形式語言)的建議:首先請記住形式語言遠比自然語言緊湊,所以要多花點時間來讀;其次,結構很重要,從上到下從左到右讀往往不是一個好辦法,而應該學會在大腦裡解析--識別Token,分解結構;最後,請記住細節的影響,諸如拼寫錯誤和標點錯誤這些在自然語言中可以忽略的小毛病會把形式語言搞得面目全非。

1.3. 程式的除錯
只要是人做的事情就難免會出錯,何況程式設計還是一件這麼複雜的工作。據說有這樣一個典故:早期的計算機體積都很大,有一次一臺計算機不能正常工作,工程師們找了半天原因最後發現是一隻蟲子(Bug)鑽進計算機中造成的。從此以後,程式中的錯誤被叫做Bug,而找到這些Bug並加以糾正的過程就叫做除錯(Debug)。有時候除錯是一件非常複雜的工作,要求程式設計師概念明確、邏輯清晰、性格沉穩,還需要一點運氣。除錯的技能我們在後續的學習中慢慢培養,但首先我們要區分清楚程式中的Bug分為哪幾類。
編譯時錯誤
編譯器只能翻譯語法正確的程式,否則將導致編譯失敗,無法生成可執行檔案。對於自然語言來說,一點語法錯誤不是很嚴重的問題,因為我們仍然可以讀懂句子。而編譯器就沒那麼寬容了,只要有哪怕一個很小的語法錯誤,編譯器就會輸出一條錯誤提示資訊然後罷工,你就得不到你想要的結果。雖然大部分情況下編譯器給出的錯誤提示資訊能夠指出錯誤程式碼的位置,但也有個別時候編譯器給出的錯誤提示資訊幫助不大,甚至會誤導你。在開始學習程式設計的前幾個星期,你可能會花大量的時間來糾正語法錯誤。等到有了一些經驗之後,還是會犯這樣的錯誤,不過會少得多,而且你能更快地發現錯誤原因。等到經驗更豐富之後你就會覺得,語法錯誤是最簡單最低階的錯誤,編譯器的錯誤提示也就那麼幾種,即使錯誤提示是有誤導的也能夠立刻找出真正的錯誤原因是什麼。相比下面兩種錯誤,語法錯誤解決起來要容易得多。

執行時錯誤
編譯器檢查不出這類錯誤,仍然可以生成可執行檔案,但在執行時會出錯而導致程式崩潰。對於我們接下來的幾章將編寫的簡單程式來說,執行時錯誤很少見,到了後面的章節你會遇到越來越多的執行時錯誤。讀者在以後的學習中要時刻 注意區分編譯時和執行時(Run-time)這兩個概念 ,不僅在除錯時需要區分這兩個概念,在學習C語言的很多語法和規則時都需要區分這兩個概念,有些事情在編譯時做,有些事情則在執行時做。
邏輯錯誤和語義錯誤
第三類錯誤是邏輯錯誤和語義錯誤。如果程式裡有邏輯錯誤,編譯和執行都會很順利,看上去也不產生任何錯誤資訊,但是程式沒有幹它該乾的事情,而是幹了別的事情。當然不管怎麼樣,計算機只會按你寫的程式去做,問題在於你寫的程式不是你真正想要的,這意味著程式的意思(即語義)是錯的。找到邏輯錯誤在哪需要十分清醒的頭腦,要通過觀察程式的輸出回過頭來判斷它到底在做什麼。

通過本書你將掌握的最重要的技巧之一就是除錯。除錯的過程可能會讓你感到一些沮喪,但除錯也是程式設計中最需要動腦的、最有挑戰和樂趣的部分。從某種角度看除錯就像偵探工作,根據掌握的線索來推斷是什麼原因和過程導致了你所看到的結果。除錯也像是一門實驗科學,每次想到哪裡可能有錯,就修改程式然後再試一次。如果假設是對的,就能得到預期的正確結果,就可以接著除錯下一個Bug,一步一步逼近正確的程式;如果假設錯誤,只好另外再找思路再做假設。“當你把不可能的全部剔除,剩下的——即使看起來再怎麼不可能——就一定是事實。”(即使你沒看過福爾摩斯也該看過柯南吧)
也有一種觀點認為,程式設計和除錯是一回事,程式設計的過程就是逐步除錯直到獲得期望的結果為止。你應該總是從一個能正確執行的小規模程式開始,每做一步小的改動就立刻進行除錯,這樣的好處是總有一個正確的程式做參考:如果正確就繼續程式設計,如果不正確,那麼一定是剛才的小改動出了問題。例如,Linux作業系統包含了成千上萬行程式碼,但它也不是一開始就規劃好了記憶體管理、裝置管理、檔案系統、網路等等大的模組,一開始它僅僅是Linus Torvalds用來琢磨Intel 80386晶片而寫的小程式。據Larry Greenfield說,“Linus的早期工程之一是編寫一個交替列印AAAA和BBBB的程式,這玩意兒後來進化成了Linux。”(引自The Linux User’s Guide Beta1版)在後面的章節中會給出更多關於除錯和程式設計實踐的建議。

1.4. 第一個程式
在開始寫程式之前首先要搭建開發環境,安裝編譯器、標頭檔案、庫檔案、開發文件等。在Linux系統下如何安裝軟體包和搭建開發環境不是本書的重點,這些問題需要讀者自己解決,但我在這裡簡單列出需要安裝的軟體包供參考(假定你用的是Debian或Ubuntu發行版):
gcc: The GNU C compiler
libc6-dev: GNU C Library: Development Libraries and Header Files
manpages-dev: Manual pages about using GNU/Linux for development
manpages-posix-dev: Manual pages about using a POSIX system for development
binutils: The GNU assembler, linker and binary utilities
gdb: The GNU Debugger
make: The GNU version of the “make” utility

本書所有程式碼都在Ubuntu 12.04 LTS(32位x86平臺)發行版上編譯測試通過。讀者如果用其他Linux發行版,或者不使用發行版提供的軟體包而是用自己從原始碼編譯出的軟體包,則編譯執行本書的程式碼得到的結果會有些不同,但不影響學習。
通常一本教程式設計的書中第一個例子都是列印Hello world,這個傳統源自 [K&R]_ ,用C語言寫這個程式可以這樣寫:
123456789

include <stdio.h>/* main: generate some simple output */int main(void){ printf(“Hello, world.
“); return 0;}

將這個程式儲存成主目錄下的 main.c
,然後編譯執行:
$ gcc main.c$ ./a.outHello, world.

gcc 是Linux平臺的C編譯器,編譯後在當前目錄下生成可執行檔案 a.out
[2] ,直接在命令列輸入這個可執行檔案的路徑就可以執行它。如果不想把檔名叫 a.out
,可以用 gcc-o 引數自己指定檔名:
$ gcc main.c -o main$ ./mainHello, world.

[2]
“a.out”是“Assembler Output”的縮寫,實際上一個C程式要先被編譯器翻譯成彙編程式,再被彙編器翻譯成機器指令,最後還要經過連結器的處理才能成為可執行檔案,詳見 main函式、啟動例程和退出狀態

雖然這只是一個很小的程式,但我們目前暫時還不具備相關的知識來完全理解這個程式,比如程式的第一行,還有程式主體的 int main(void){…return 0;}
結構,這些部分我們暫時不詳細解釋,讀者現在只需要把它們看成是每個程式按慣例必須要寫的部分(Boilerplate)。但要注意 main
是一個特殊的名字,C程式總是從 main
裡面的第一條語句開始執行的,在這個程式中是指 printf
這條語句。
第3行的 /* … /
結構是一個註釋(Comment),其中可以寫一些描述性的話,解釋這段程式在做什麼。註釋只是寫給程式設計師看的,編譯器會忽略從 /

到 */
的所有字元,所以寫註釋沒有語法規則,愛怎麼寫就怎麼寫,並且不管寫多少都不會被編譯進可執行檔案中。
printf
語句的作用是把訊息列印到螢幕。注意語句的末尾以;號(Semicolon)結束,下一條語句 return 0;
也是如此。
C語言用{}括號(Brace或Curly Brace)把語法結構分成組,在上面的程式中 printf
和 return
語句套在 main
的{}括號中,表示它們屬於 main
的定義之中。我們看到這兩句相比 main
那一行都縮排(Indent)了一些,在程式碼中可以用若干個空格(Blank)和Tab字元來縮排,縮排不是必須的,但這樣使我們更容易看出這兩行是屬於 main
的定義之中的,要寫出漂亮的程式必須有整齊的縮排, 縮排和空白 將介紹推薦的縮排寫法。
正如前面所說,編譯器對於語法錯誤是毫不留情的,如果你的程式有一點拼寫錯誤,例如第一行寫成了 stdoi.h
,在編譯時會得到錯誤提示:
$ gcc main.cmain.c:1:19: fatal error: stdoi.h: No such file or directorycompilation terminated.

這個錯誤提示非常緊湊,初學者不容易看明白出了什麼錯誤,即使知道這個錯誤提示說的是第1行有錯誤,很多初學者對照著書看好幾遍也看不出自己這一行哪裡有錯誤,因為他們對符號和拼寫不敏感(尤其是英文較差的初學者),他們還不知道這些符號是什麼意思又如何能記住正確的拼寫?對於初學者來說,最想看到的錯誤提示其實是這樣的:“在main.c程式第1行的第19列,您試圖包含一個叫做stdoi.h的檔案,可惜我沒有找到這個檔案,但我卻找到了一個叫stdio.h的檔案,我猜這個才是您想要的,對嗎?”可惜沒有任何編譯器會友善到這個程度,大多數時候你所得到的錯誤提示並不能直接指出誰是犯人,而只是一個線索,你需要根據這個線索做一些偵探和推理。
有些時候編譯器的提示資訊不是 error
而是 warning
,例如把上例中的 printf(“Hello, world.
“);
改成 printf(1);
然後編譯執行:
$ gcc main.cmain.c: In function ‘main’:main.c:7:9: warning: passing argument 1 of ‘printf’ makes pointer from integer without a cast [enabled by default]/usr/include/stdio.h:363:12: note: expected ‘const char * restrict’ but argument is of type ‘int’main.c:7:9: warning: format not a string literal and no format arguments [-Wformat-security]$ ./a.outSegmentation fault (core dumped)

這個警告資訊是說型別不匹配,但勉強還能配得上。警告資訊不是致命錯誤,編譯仍然可以繼續,如果整個編譯過程只有警告資訊而沒有錯誤資訊,仍然可以生成可執行檔案。但是,警告資訊也是不容忽視的。出警告資訊說明你的程式寫得不夠規範,可能有Bug,雖然能編譯生成可執行檔案,但程式的執行結果往往是不正確的,例如上面的程式執行時出了一個段錯誤(Segmentation fault),段錯誤是程式崩潰(Crash)的一種表現,這屬於執行時錯誤。
各種警告資訊的嚴重程度不同,像上面這種警告幾乎一定表明程式中有Bug,而另外一些警告只表明程式寫得不夠規範,一般還是能正確執行的。有些不重要的警告資訊 gcc 預設是不提示的,但這些警告資訊也有可能表明程式中有Bug, 一個好的習慣是開啟gcc的-Wall選項,讓gcc提示所有的警告資訊--不管是嚴重的還是不嚴重的--然後把這些問題從程式碼中全部消滅 。比如把上例中的 printf(“Hello, world.
“);
改成 printf(0);
然後編譯執行:
$ gcc main.c$ ./a.out

編譯既不報錯也不報警告,一切正常,但是執行程式什麼也不列印。如果開啟 -Wall 選項編譯就會報警告了:
$ gcc -Wall main.cmain.c: In function ‘main’:main.c:7:9: warning: null argument where non-null required (argument 1) [-Wnonnull]

如果 printf
後面的 0
是你不小心寫上去的(例如錯誤地使用了編輯器的查詢替換功能),這個警告就能幫助你發現錯誤。雖然本書的命令列為了突出重點通常省略 -Wall 選項,但是強烈建議你寫每一個編譯命令時都加上 -Wall 選項。


相關文章