Linux C 编程学习第二天2
数学函数
在C语言中可以使用像是 sin 和 ln 这样的函数,需要引入一个头文件,#include <math.h>
在C语言中,使用数学函数一定要加上括号,例如: log(1.0) 在这个式子里,1.0是参数,log是函数,可以直接通过 printf 输出,还记的前面说的吗,直接使用printf输出一定要注意数据的类型,否则会造成数据丢失
printf("ln 1 = %f", log(1.0));
对于上式 log(1.0) 是函数调用,函数调用也是一种表达式,C语言中函数调用表达式的值称为函数的返回值(Return Value)
实际上 printf() 也是一个函数,但是我们不必在意它的返回值,我们调用printf函数是为了打印,这称之为副作用(Side Effect) C语言的函数可以有副作用,这是和数学函数不一样的地方
改变计算机存储单元里的数据或者做输入输出操作都算 Side Effect
引入一下原作的一段话,我觉得说的很有意思:
泛化 (Generalize) 是一个很有味道的词
在Ubuntu中执行gcc命令有一点需要注意,当使用了 math.h 库的时候,gcc 命令必须加上 -lm
因为数学函数位于libm.so库文件中。
虽然 printf 函数是位于 libc.so 库中的,但使用 libc.so 库函数在编译的时候不需要加 -lc 选项,加了也不算错,因为这个选项是gcc的默认选项,剩下的问题在后面会详细学到。
自定义函数
我们将要开始自定义函数,在一开始学习C语言的时候,我们其实就定义了一个函数,main 函数,我们在main函数中输出了 hello world
函数的命名也要遵循前面的标识符命名规则
由于大学里各种教材教的各种写法,我们先明白几个问题
为什么有的书上写的是 mian() 有的是 int main() 还有的是 int main(void) 有的在main函数中加入了return 0 ,有的没有return?
由于我们定义的main 函数不带任何参数,所以参数列表应该写成 void ,main 的返回值是 int 型的,所以 return 0 表示返回值为0,为什么要返回 0 呢,因为 main 函数是被操作系统调用的,通常程序执行成功就返回 0 ,在执行过程中出错就返回一个非零值,我们可以试试在最后将return 0 ; 改成 return 2 ; 重新编译看看会出现什么现象。
上面的问题并没有结束,main() 这种写法,不写返回值也不写参数列表,这是Old Style C的风格,因为Old Style C 规定不写返回值就表示返回 int ,不写参数就表示参数类型和个数没有明确指出,这种宽松的规定看似方便,但是会倒是编译器无法检查出程序的bug ,虽然 C 标准为了兼容问题依旧保留了这样的作法,但是作为程序员不应该使用这样的写法。
在系统调用 main 函数的时候是传递参数的,实际上int main() 这并不是最标准的写法,最最最标准的写法应该是 int main(int argc, char *argv[]) C语言也允许 int main(void) 这种写法,除了这两种写法外,定义main函数的其他写法都是错误,或不可移植的。
有时候我们定义函数并不是为了该函数返回某个值,我们经常会为了Side Effect 来定义函数,可以来举个例子:
void re_zhu(void)
{
printf("这篇日记是re_zhu写的");
}
当我们调用re_zhu() 这个函数的时候,它就会输出 这篇日记是re_zhu写的 这样的语句
同一个函数可以多次调用,可以相互调用,也可以镶嵌调用,你可与用函数1去调用函数2,而函数2又是调用函数3,至于函数1调用函数2,函数2调用函数3,函数3去调用函数1 这样的死循环写法,我想在计算机还没崩溃前,赶快 Ctrl + c 吧。
就像是shell一样,你可以把很多命令(函数或者操作)整合到一个函数里,这样可以使代码更加简洁,比如我想打印三遍 这篇日记是re_zhu写的 完全没必要调用三次 re_zhu();
可以写一个这样的函数:
void rere_zhu(int i)
{
while( i - - )
re_zhu();
}
这样你想打印3次,就可以直接使用 rere_zhu(3); 就可以了,这样的操作可以使代码更加整洁和好理解。(PS:原谅我的函数命名这么没有品味)
书中讲到读代码的顺序和读文章不一样,有时不是从上往下读并不一定最优解,比如我上面举的例子,按照我的编程习惯,我会将 re_zhu() 函数和 rere_zhu() 函数写在 main() 函数前面,那么按照从上往下读当然使可以的,但是如果我写了很多的自定义函数在 main() 前面,我相信你是没有耐心一个一个往下看的,或者说即使你看完了那些自定义函数,到了main()函数中,就已经记不清那些函数了,这个时候就应该换个方式,直接看 main() 函数,main() 函数调用了上面的某个函数,你在去看那个函数,如果该函数还调用了其他函数,继续跟进,直到你明白了 main() 中的这段代码是什么意思。
读代码的过程就是在模拟计算机执行的过程,尤其是在使用单片机的时候,这种思想更重要,如果你玩过单片机的话,应该能深刻体会这点。
好的,我们现在看一下函数声明,定义,原型,这几个概念
如果你见过 void rere_zhu(int i); 这种写法叫做函数声明,不是函数定义,只有带函数体的声明才叫定义,函数也是和变量一样的,编译器只有见到函数定义才会生成指令,函数声明为编译器提供了有用的信息,编译器在翻译代码的过程中,只有见到函数原型之后才知道函数的名字,参数和返回值,然后碰到函数调用才直到如何生成对应指令,所以函数原型必须出现在调用前,但是还有一种方法,在主函数前我使用 void rere_zhu(int i); void re_zhu(void); 提前声明了这两个函数,而把他们的函数体放在main()函数后,这样也是可行的。
但是!!我们明白这点并不是要按照这样的风格去写代码,了解这些是为了读懂、维护使用这样风格代码,没错,这个风格就是Old Style C ,我们不应该按照这样的方式去写代码,如果把按照上面风格的代码前面的声明去掉,编译器会警告,但是编译依旧能通过,这称之为函数的隐式声明 Implicit Declaration 隐式声明的函数返回值类型都是int,对于上述函数并没有返回值,所以是void,好了,main() 在运行时到了这个函数,编译器认为返回是 int ,但是我们函数的原型是返回 void ,这就是警告的原因,但是我们的函数并没有用到返回值,用的是副作用,所以执行结果是对的,如果我们的函数用到了返回值,那么将会出现错误,所以我们要极力避免使用这样的写法。
形参和实参
上面我们看见了,函数中是可以有参数的,需要注意的是,定义变量可以把同类型的变量列在一起,但是在定义参数的时候,不可以这样,必须逐个列出
例如: re_zhu(int i, int j, float k) 如果写成 re_zhu(int i, j, float k) 就是错误的,切记
使用 man 3 printf 可以查看 printf 函数的原型
这个位置之后用实际的图来补充
printf 也是函数,但是它的参数可以很多,也可以很少,这些参数是不确定的,这称为可变参数,之后会讨论,按理说每个函数都应该明确规定返回值和参数类型和个数,但是实际上,像printf()这样的规定也是一种明确的规定,调用函数时也要严格遵守这些规定。
有时我们把函数叫做接口,调用函数就是使用这个接口,使用接口的前提是必须和接口保持一致。
前面有个关于 man 3 printf 这样的命令
我们来具体看看这个: