手机版
你好,游客 登录 注册
背景:
阅读新闻

Linux C编程实战之路

函数要义

[日期:2014-05-05] 来源:Linux社区  作者:vincent [字体: ]

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 中第 9 行就是对函数 max() 的调用,调用函数来实现他的功能,注意,一旦调用函数,程序就会从调用语句那里直接跳转到函数的定义语句的地方执行,直到从该函数返回为止(除非是内联函数)。
 
第三,函数的定义。函数一被调用,就跳转到定义的地方。这是一个函数具体做什么事情的地方,比如example3.c 中的max()函数的定义,里面通过一个条件运算符计算出了a和b的最大值,然后将最大值 x 通过return语句返回给调用者。
 


下面是重要内容,重要程度 五颗星 ★★★★★
 仔细观察函数调用和函数定义的代码:
 函数调用:  k = max(i, j);
 函数定义:
 int max(int a, int b)
 {
        ......
 }
 在函数的调用中,我们将传递给函数max()的两个变量 i 和 j 称为实参(arguments),注意这两个变量是在main函数中定义的。而在函数 max( ) 的定义代码中,我们将接收实参值的两个变量 a 和 b 称为形参(parameters)。
 实参和形参的关系至关重要,也很简单,记住两点:
 1, 它们是相对独立的(意思是它们分别占用不同的内容,你是你我是我)
 2, 形参是由实参来初始化的,你看形参a和b并没有给他们赋值,但是它们实际上初始值分别是 i 和 j,它们拿 i 和 j 的值来进行运算。
 实参一般定义在调用函数中(除非是全局变量),在 example3.c中,i 和 j 都是在 main函数中定义的,意味着只有在main函数中才能对他们进行直接访问,实际上,i 和j 存在于 main函数的栈帧(stack frame)中。当退出main函数之后它们就会消失(被释放)。
 形参定义在被调用函数中,比如example3.c中的 a 和b,它们只能在函数 max() 中被使用,事实上它们存在于函数 max() 的栈帧中,其他函数是看不见它们的,一旦函数 max( ) 退出,它们也会被立即释放。
 函数形参和实参的这些特点,使得我们在使用函数的时候,有以下几点需要注意:
 1,永远不要返回局部变量的地址,因为局部变量在函数返回的时候会被立即释放,亦即该块内存已被系统收回,此时它的地址就不能再被使用了。
 2,按值传递的形参是实参的一个副本,或者说一个拷贝,他们是不同的两个变量,不能指望通过形参来修改实参(除非形参是一个指向实参的指针)。比如example3.c中所示,形参 a 是实参 i 的拷贝,对a的任何操作都不会对 i 有任何影响。
 3,如果一个函数想要修改它的实参,那么必须要传递实参的地址,比如有两个变量 x 和 y,想要用一个函数 swap() 来交换他们的值,由于需要修改它们,因此我们的代码应该这么写:

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

example4.c 中,我们不再把 x 和 y 当做实参直接传递,而是把它们的地址 &x 和 &y 当做实参来传递,这时由于传递的是整型的地址,所以形参就应该用一个专门存放整型地址的变量来接收,所以形参是整形指针 px 和 py。

后面讲完指针之后 会更了解这方面的内容。

只有这样,我们才能通过形参来修改实参。

C语言中,有以下几种特殊的函数:

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双方共同遵循接口原则,那么它们就可以分工协作,哪怕处于不同的时空。

linux
【内容导航】
第1页:Linux下C编程概要 第2页:基本数据类型
第3页:基本IO函数操作 第4页:运算符
第5页:控制流 第6页:函数要义
第7页:数组与指针 第8页:存储类,动态内存
第9页:复合数据类型
相关资讯       Linux c  Linux C编程