1. 數學函數

在數學中我們用過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.0double型的。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這個表達式也可以看成一個函數調用,把運算符+看作函數,它的兩個參數是ab,返回值是兩個參數的和,傳入兩個參數,得到一個返回值,並沒有產生任何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標準庫和glibc

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