C语言之所以被称为模块化语言,原因在于C语言的程序结构是由一个个的“模块”搭建起来的,这些所谓的模块就是函数,因此,函数是构成C程序的最基本的组件,我们的程序的功能可能很复杂,但是我们可以通过函数来分解,然后在组装它们,这种做法在遇到大规模软件工程之前,是非常主流的想法。在目前的软件开发中,也大量使用像C语言这样的模块化语言来描述问题,只不过当今世界,在面临大规模软件工程的开发时,面向对象语言也许是更好的选择。
C语言程序要完成一个复杂的功能,不能一个函数搞定,就像一家公司要做的事情有很多,不可能一个人做完,那就必须雇佣不同技能的人才。C的函数就像是一个个具有不同技能的人才一样,各自完成自己的本分工作,其中,任何一个C程序,都必须包含而且只能包含一个称为 main 的函数,这个函数就像是公司里的boss,他主要的工作是指挥或者说组织各个人才的工作,使之完成更大的目标。这个 main 函数是系统规定的。它的样子如下:
// example1.c int main(void) // 函数头 { ... ... // 函数体 }或者长成这样:
// example2.c int main(int argc, char **argv) // 函数头 { ... ... // 函数体 }
以上两种形式是 main 函数的标准形式。
下面来分析一下一个函数的各个部分,以 example1.c 为例子,其中,int main(void) 称为函数头,而 函数名字就是 main, 写在函数名前面的 int 指的是这个函数最终的返回值的数据类型,简单讲就是这个函数最后会得到一个什么东西。 而跟在函数名 main 后面的圆括号,用来装这个函数执行的时候需要的参数,就像一个人要干一个活儿所需要的材料一样,比如你调用你的一位员工,帮你去采购一批货物,你不可能让他空手去,你可能会先给他一些样本做参考,也许你还会给他一些钱,他需要这些材料才能成功地采购你所需要的东西,这些东西就称之为参数,假如这个函数不需要参数,那么我们就在这个圆括号里面写一个 void 表示。
下面,我们来让这个 Boss 调用一个小兵,这个小兵的工作是,帮我们找出两个整数中的比较大的一个,我承认这件事情很简单,但是我们的 main 身为一个 Boss 不想亲历亲为,于是,我们的第一个自己设计的小兵屁颠屁颠地上场了:
// example3.c int max(int a, int b); // 这是函数max()的声明 int main(void) // 这是main函数,他是老板! { int i = 100, j = 200; int k; k = max(i, j); // 调用 max() 这个小兵,给他两个数:i和j,让它帮忙找出他们的最大值,返回给k } // 以下是函数max()的定义 int max(int a, int b) { int x; // 用来存放最大值 x = a > b ? a : b; return x; // 将最大值返回给调用者 }在以上例子中,我们要注意 3 个地方:
第三,函数的定义。函数一被调用,就跳转到定义的地方。这是一个函数具体做什么事情的地方,比如example3.c 中的max()函数的定义,里面通过一个条件运算符计算出了a和b的最大值,然后将最大值 x 通过return语句返回给调用者。
// example4.c void swap(int *px, int *py) // px 和 py 是整型指针 int main(void) { int x = 100; y = 200; printf("%d, %d\n", x, y); // 打印100和200 swap(&x, &y); // 传递的是变量的地址而不是内容 printf("%d, %d\n", x, y); // 打印200和100 } void swap(int *px, int *py) { int tmp; // 以下代码利用形参指针px和py,来间接地交换 // 实参 x 和 y 的值。 tmp = *px; *px = *py; *py = tmp; }
1,递归函数。所谓的递归函数指的是嵌套地调用自己,比如经典的求阶乘函数,因为我们知道要得到一个数N的阶乘就要知道它的前一个数N-1的阶乘,要知道 N-1 的阶乘就要知道 N-2 的阶乘,而且求某个数的阶乘的算法是一样的,都是 n! = n * (n-1)* (n-2) * …… * 2 *1 ,你发现这是个递归的问题,代码如下:
// example5.c int factor(int n) { if(n == 1) return 1; int m; m = n * factor(n-1); // 递归地调用自己 return m; }使用递归函数有几个要注意的地方,第一,一定要有一个无需递归就能直接返回的条件,比如example5.c中的第5,6行,当n为1时,阶乘为1,不需要递归。如果没有这样的条件,递归函数将会走向“不归路”。 第二,递归函数不适合处理递归层次较深的场合,因为每一次递归调用自己的时候都会产生一个新的函数,产生新的局部变量,嵌套太深效率就会很低,而且还会有栈溢出的风险。最后,什么问题可以用递归函数解决呢? 当你发现一个问题可以分解为更小的问题,而且这个小问题的算法跟原先的一样的时候就可以考虑使用递归函数了。
2,内联函数。
内联函数是这样的函数:
inline void func(int a) { ... ... }
注意到 函数 func 的前面有个修饰符 inline,这个修饰符的含义有点像宏,它会将该函数的定义在所有调用它的地方展开,这样做的目的在于省却函数切换的时间,但是会浪费内存空间,因为普通函数只需要一段代码就够了,而内联函数会在所有调用的地方统统展开,相当于多了很多个副本。
所以使用内联函数有一定的要求:第一,函数足够短小,这里的短小指的是函数本身的执行时间要跟函数的切换开销时间在同等量级,否则,一个函数很复杂,执行起来需要10秒钟,而函数的切换只需要0.001ms,这样的话省略这个切换时间就毫无意义了。 第二,这个函数被频繁地调用,它的执行速度或成程序性能的瓶颈,这时我们需要考虑更快速的执行它,甚至可以为了这个而牺牲一部分内存空间。
内联函数是典型的用空间换取时间的例子。
3,回调函数。
回调函数的概念有点抽象,让我用一个例子来给你说明白:假如你老妈有一天没空,要你帮忙做顿中午饭,本来如果你做得一桌好菜,你老妈“调用”你这个函数也没啥好担心的,但可惜你其实只会炒鸡蛋但不会蒸排骨,于是你老妈预先给你准备了一位专门蒸排骨的保姆(是的,这位保姆除了蒸得一手好排骨啥也不会),告诉了你她的电话号码,你到时候给她电话随传随到。这个保姆,就是一个回调函数。
下面再来用计算机语言阐述一遍:当你调用一个函数 f( ) 的时候,你原本指望这个函数可以帮你做完一顿午饭,但无奈这个函数 f( ) 只会炒鸡蛋,不会蒸排骨,于是你在调用这个函数的时候,必须事先将如何蒸排骨这件事情写好,比如叫做 void zhengpaigumiji( int a, char b, float c),这样你就可以在调用 f( ) 的同时将 zhengpaigumiji 作为参数传递给 f( ) ,让他在做午饭的时候知道怎么蒸排骨,站在你的角度,这个 zhengpaigumiji( ) 是你写的,但是你不直接调用,而是让另一个函数 f( ) “回过头来调用” ,因此将这个函数 zhengpaigumiji( ) 称为回调函数。
再举个实际的例子,比如你要调用函数 signal( ) 来帮你处理某个信号,这个工作的内容内核都帮会你搞定,除了该信号的自定义响应函数之外,换句话讲,内核会搞定除了“蒸排骨”之外的一切事情,那你要让signal( ) 正常工作,就要传递一个所谓的“自定义信号响应函数”给内核,比如 signal( SIGINT, my_func) , 这样调用signal,就等于告诉内核:你来帮我搞一下SIGINT 这个信号,顺便说一下,它的响应函数是 my_func( ) 。
回调函数在实现软件分层设计时十分有用,试想,当A模块要实现的部分功能须由B模块提供时,我们可以事先定义好该功能接口,只要A,B双方共同遵循接口原则,那么它们就可以分工协作,哪怕处于不同的时空。