在數學中我們用過sin和ln這樣的函數,例如sin(π/2)=1,ln1=0等等,在C語言中也可以使用這些函數(ln函數在C標準庫中叫做log
):
例 3.1. 在C語言中使用數學函數
#include <math.h> #include <stdio.h> int main(void) { double pi = 3.1416; printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0)); return 0; }
編譯運行這個程序,結果如下:
$ gcc main.c -lm $ ./a.out sin(pi/2)=1.000000 ln1=0.000000
在數學中寫一個函數有時候可以省略括號,而C語言要求一定要加上括號,例如log(1.0)
。在C語言的術語中,1.0
是參數(Argument),log
是函數(Function),log(1.0)
是函數調用(Function Call)。sin(pi/2)
和log(1.0)
這兩個函數調用在我們的printf
語句中處于什麼位置呢?在上一章講過,這應該是寫表達式的位置。因此函數調用也是一種表達式,這個表達式由函數調用運算符(()括號)和兩個操作數組成,操作數log
是一個函數名(Function Designator),它的類型是一種函數類型(Function Type),操作數1.0
是double
型的。log(1.0)
這個表達式的值就是對數運算的結果,也是double
型的,在C語言中函數調用表達式的值稱為函數的返回值(Return Value)。總結一下我們新學的語法規則:
表達式 → 函數名
表達式 → 表達式(參數列表)
參數列表 → 表達式, 表達式, ...
現在我們可以完全理解printf
語句了:原來printf
也是一個函數,上例中的printf("sin(pi/2)=%f\nln1=%f\n", sin(pi/2), log(1.0))
是帶三個參數的函數調用,而函數調用也是一種表達式,因此printf
語句也是表達式語句的一種。但是printf
感覺不像一個數學函數,為什麼呢?因為像log
這種函數,我們傳進去一個參數會得到一個返回值,我們調用log
函數就是為了得到它的返回值,至于printf
,我們並不關心它的返回值(事實上它也有返回值,表示實際打印的字元數),我們調用printf
不是為了得到它的返回值,而是為了利用它所產生的副作用(Side Effect)--打印。C語言的函數可以有Side Effect,這一點是它和數學函數在概念上的根本區別。
Side Effect這個概念也適用於運算符組成的表達式。比如a + b
這個表達式也可以看成一個函數調用,把運算符+
看作函數,它的兩個參數是a
和b
,返回值是兩個參數的和,傳入兩個參數,得到一個返回值,並沒有產生任何Side Effect。而賦值運算符是有Side Effect的,如果把a = b
這個表達式看成函數調用,返回值就是所賦的值,既是b
的值也是a
的值,但除此之外還產生了Side Effect,就是變數a
被改變了,改變計算機存儲單元裡的數據或者做輸入輸出操作都算Side Effect。
回想一下我們的學習過程,一開始我們說賦值是一種語句,後來學了表達式,我們說賦值語句是表達式語句的一種;一開始我們說printf
是一種語句,現在學了函數,我們又說printf
也是表達式語句的一種。隨着我們一步步的學習,把原來看似不同類型的語句統一成一種語句了。學習的過程總是這樣,初學者一開始接觸的很多概念從嚴格意義上說是錯的,但是很容易理解,隨着一步步學習,在理解原有概念的基礎上不斷糾正,不斷泛化(Generalize)。比如一年級老師說小數不能減大數,其實這個概念是錯的,後來引入了負數就可以減了,後來引入了分數,原來的正數和負數的概念就泛化為整數,上初中學了無理數,原來的整數和分數的概念就泛化為有理數,再上高中學了複數,有理數和無理數的概念就泛化為實數。坦白說,到目前為止本書的很多說法都是不完全正確的,但這是學習理解的必經階段,到後面的章節都會逐步糾正的。
程序第一行的#號(Pound Sign,Number Sign或Hash Sign)和include
表示包含一個標頭檔(Header File),後面尖括號(Angel Bracket)中就是檔案名(這些標頭檔通常位於/usr/include
目錄下)。標頭檔中聲明了我們程序中使用的庫函數,根據先聲明後使用的原則,要使用printf
函數必須包含stdio.h
,要使用數學函數必須包含math.h
,如果什麼庫函數都不使用就不必包含任何標頭檔,例如寫一個程序int main(void){int a;a=2;return 0;}
,不需要包含標頭檔就可以編譯通過,當然這個程序什麼也做不了。
使用math.h
中聲明的庫函數還有一點特殊之處,gcc
命令行必須加-lm
選項,因為數學函數位于libm.so
庫檔案中(這些庫檔案通常位於/lib
目錄下),-lm
選項告訴編譯器,我們程序中用到的數學函數要到這個庫檔案裡找。本書用到的大部分庫函數(例如printf
)位於libc.so
庫檔案中,使用libc.so
中的庫函數在編譯時不需要加-lc
選項,當然加了也不算錯,因為這個選項是gcc
的預設選項。關於標頭檔和庫函數目前理解這麼多就可以了,到第 20 章 連結詳解再詳細解釋。
C標準主要由兩部分組成,一部分描述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程序依賴于libm.so
,以後我們還會看到多綫程的C程序依賴于libpthread.so
。以後我說libc
時專指libc.so
這個庫檔案,而說glibc
時指的是glibc
提供的所有庫檔案。
glibc
並不是Linux平台唯一的基礎C函式館,也有人在開發別的C函式館,比如適用於嵌入式系統的uClibc
。