跳转到内容

1. 数学函数

在数学中我们用过 sin 和 ln 这样的函数,例如 sin(π/2)=1,ln1=0 等等,在 C 语言中也可以使用这些函数(ln 函数在 C 标准库中叫做 log ):

例 3.1. 在 C 语言中使用数学函数

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;
}

编译运行这个程序,结果如下:

bash
$ 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