Chinaunix首页 | 论坛 | 博客
  • 博客访问: 91121
  • 博文数量: 7
  • 博客积分: 1436
  • 博客等级: 上尉
  • 技术积分: 184
  • 用 户 组: 普通用户
  • 注册时间: 2007-12-19 10:42
文章存档

2010年(1)

2009年(6)

我的朋友

分类:

2009-08-05 22:26:38

    这篇文章要说的C语言程序设计很精华的部分,可以看做是高级编译的部分。

首先介绍一下C语言最小的结构单元--函数
关于函数,大家都知道,C语言之所以能够以模块化、结构化闻名,相信函数是一个不可略功的功臣,不同的功能定义在不同的函数模块中,不仅实现了功能上的逻辑隔离及文件隔离,最大化的提高了程序的简洁性、高阅读性及代码重用,也使程序的结构带来了显而易见的精简和明了。
同时,在这里先说函数的目的,主要是为了下面说到的文件分割和make部分的知识做一下基础。

在C语言中大致将函数分为如下2大类:
1、库函数
   此类函数是由C语言系统提供,用户无须自己定义也不用在程序中声明其所属类型,换句话说,只需要在使用时拿来用即可,但在C语言中,我们要礼貌一点,即请事先打个招呼,在程序的开始部分,引用一下相应的头文件即可在程序中信手拈来。比如我们使用了头文件stdio.h,就可以直接使用printf(),scanf()等函数。
2、用户定义函数
   上面说的是不需要定义,只管使用的函数,虽好用,但也仅是标准的东西,要针对个性化的程序设计及对应的算法,我们就必须设计符合自己要求的函数了。对于这类函数,我们不仅要定义函数,还在在程序中对它进行函数原型的说明,然后才能使用它。只是有一点,要在这里说一下,函数这个结构,是不能像其他结构一样嵌套定义的,在C语言系统中,所有的函数定义都是平级,只是负责不同的功能块而已。
   形如:
    int fun(int x)
    {
      function body;
    }

下面我们来说说函数的执行概况。
其实,在函数的执行时,非常像我们所了解的中断的概念,当用户调用函数时,程序会自动进入到被调用的函数来执行语句,当执行完毕后,又会回到原先的执行位置继续执行下一条语句。
   也可以理解为,函数就是一个模块,即它是一个整体,可以当成是一条语句一样看待,用户的一个操作调用这个函数时,并不关心其内部是什么,只知道会实现某种功能,即是透明的一个动作,完成后,接下去执行下面的语句。
  无论你怎么理解,都难以摆脱其结构化和模块化的影子,因为函数就是为此而生。
  函数在定义时不能嵌套,但在使用时可以多个函数之间互相调用,嵌套调用,还可以自已调用自身,即递归调用,从这里就可以看出C,作为linux操作系统的主体语言,它的强大,它的魅力及自己动手的乐趣。
  函数的调用,中间有参数在传递着,作着传输条件和结果的中介。一般来讲,调用者被我们称为主调函数,那么我们调用的那一个就被称为被调函数了。函数的参数分为,乳名称:实参和形参,身份证上的名字就是实际参数与形式参数。
  函数在调用时,主调参数把实际参数传递给被调函数的形式参数,然后进入被调函数进行相应的数据处理,再将最终需要的结果传递给主调函数,这一过程又称为函数的返回值。当然,一些情况下,函数也可以无返回值,如果void类型的就是一个空类型,它不需要返回任何信息,但多数情况下,我们会希望函数有一个标志性的返回值,用来指明运行结果或是方便我们调试程序。
  下面我以一个例子来说明,也是一个经典的C语言程序题,求自方幂数。
  我们以3次方幂数为例,它有一个更好听的名字叫水仙花数,即各位上的数字的立方和等于这个数本身,如
        153 = 1的三次方 + 5的三次方 + 3的三次方


[bruce@server1 programs]$ vim water_flower_number.c
//This program to get all the Water_Flower_Number between 100 and 1000.
#include
int fun(int x);   //求水仙花数函数的声明
int main()
{
        int n;
        for(n=100;n<1000;n++)
        {
                int number;
                number=fun(n);
                if(number == 0)
                        continue;             //如果不是,则不输出此数,程序继续运行
                else
                        printf("%d\n",number);  //如果是,则输出这个数
        }
}
//求水仙花函数的主体实现说明
int fun(int x)
{
        int a,b,c;
        a=x/100;        //求百位上数字
        b=(x-a*100)/10; //求十位上数字
        c=x-a*100-b*10; //求个位上数字
        if(100*a+10*b+c == a*a*a+b*b*b+c*c*c)
        return x;       //是水仙花数,则将此数作为返回值传递给主调函数
        else
        return 0;       //不是水仙花数,返回0作为主程序的输出判断
}

  以上是程序的源代码,如果你不熟悉C语言,上面的中文注释也许可以帮助你理解,就是//后面的内容,它是定义单行注释的工具,如果是多行的话,就以"/*内容*/"来限定即可。
然后使用gcc工具编译即可:
[bruce@server1 programs]$ gcc -g water_flower_number.c -o water_flower_number
  我在编译的时候,习惯加上-g参数,方便进行调试,是否要加,可根据程序开发的场景来判断,如果仅是个人喜好,则可有可无了。我们在这里就不再调试,直接查看运行结果:
[bruce@server1 programs]$ ./water_flower_number
153
370
371
407
[bruce@server1 programs]$
  从上面,我们可以看出输出来的4个水仙花数了。在此要说明的是,这个仅是一个示例,在多数的场合下,我们要传递的不会像示例中那样的值参数,而是传递指针参数,即不是传值,而传址,不明白的兄弟们,可要系统学习一下C语言去了。

文件分割
下面我们说一下C语言中的文件分割
这个功能的提供,极大的说明了,C语言程序设计中所力推的代码重用原则,这也是所有其也高级语言所提倡的东西,我们将不同功能的模块(在这里是指函数为例)单独分立开来,需要的时候,就把它与主调函数的主程序代码一起编译输出到一个执行文件中,达到了代码重用,让程序设计者将重心放在更加细节的东西上,不要在既有的基础上浪费过多精力。
我们以上面做的水仙花数例子来说明这个部分。
首先,我们将fun函数单独写在一个文件fun1.c中
[bruce@server1 programs]$ vim fun1.c
//求水仙花函数的主体实现说明
int fun(int x)
{
        int a,b,c;
        a=x/100;        //求百位上数字
        b=(x-a*100)/10; //求十位上数字
        c=x-a*100-b*10; //求个位上数字
        if(100*a+10*b+c == a*a*a+b*b*b+c*c*c)
        return x;       //是水仙花数,则将此数作为返回值传递给主调函数
        else
        return 0;       //不是水仙花数,返回0作为主程序的输出判断
}

接下来,我们把主调函数(即包含main的那个部分)写在fun2.c中
[bruce@server1 programs]$ vim fun2.c
//This program to get all the Water_Flower_Number between 100 and 1000.
#include
int main()
{
        int n;
        for(n=100;n<1000;n++)
        {
                int number;
                number=fun(n);
                if(number == 0)
                        continue;             //如果不是,则不输出此数,程序继续运行
                else
                        printf("%d\n",number);  //如果是,则输出这个数
        }
}
此时,在编译时,就要将2个文件合并就行了
[bruce@server1 programs]$ gcc fun1.c fun2.c -o water_flower_number
接下的执行和先前例子中一样:
[bruce@server1 programs]$ ./water_flower_number
153
370
371
407
[bruce@server1 programs]$
   由此我们看出来,这样子就实现了很方便的分割文件,但同时大家可以想像的到,如果一个复杂一点的程序,包含了好多个功能函数的写义,那么如果照这样分割的话,编译时,输入的命令估计都要让人惨死了...
当文件非常多时,在windows下IDE的做法,也许是将这些函数写进一个头文件中,像stdio.h那样,在使用的时候,调用即可。此文不做介绍,方法同分割文件中的函数差不多,只需把函数的声明放入头文件即可,函数的具体实现,可以将其加入另外一个文件中,编译时,也是一大堆的文件。

make工程管理器以及makefile的编写
像上面这种情况,在windows下面,例如使用microsoft的visual c++这个IDE时,它会需要你创建一个工程,然后所有相关的文件都包含在这个里面,编译调试时,IDE会为你准备所有的工程文件的编译,然后放入结果中,或是直接执行,可以打包成可执行程序等,显得非常方便,不用像linux下面这样,那么多的文件分割下来后,仍然是杂乱无章...
   其实,linux下同样有这样一个工程管理器,它就是make,它带给我们强大功能时,也同样带给了我们非常好的灵活性,还是免费的,这些都是GNU/linux的初衷,GPL的初衷,而我们要做的,就是学会编写makefile文件。
makefile,就是在当前目录下,使用make工具时,指明其要做什么,如何做的文件,有明确的格式和非常容易理解的结构.
结构如下:
        目标文件:依赖文件1 [依赖文件2] [依赖文件3][...]
         产生目标文件的命令
    如下面这个实例:
        helloword:helloworld.o  fun.o
               gcc -o helloworld helloworld.o fun.o
        helloworld.o:helloworld.c my.h
               gcc helloworld.c -c
        fun.o:fun.c my.h
               gcc fun.c -c
不知道大家有没有看明白,这面的结构?
   从上到下看,是一个从全局到局部的过程,就像大家所知道的堆栈的意义,想像最后一个结果,就必须将前面的结果全部都出栈才行,makefile中的写法就是这样,如上面,最终结果是helloworld这个文件,列出它的依赖性文件,然后再分别列出其依赖文件的依赖文件,直到列到最终的源文件为止。
下面以一个完整的例子来书写makefile
要求用C语言编制一个程序,运行程序后,输入两个整数,自动求出它们的加运算和减运算,并将结果输出。
按照上面讲过的文件分解,我将这个程序设计如下:
[bruce@server1 demo]$ ls
add.c  demo.c  my.h  sub.c
其中每个程序的文件分别为:
demo.c主程序内容
[bruce@server1 demo]$ vim demo.c
#include "my.h"
int main()
{
        int x,y,z;
        scanf("%d %d",&x,&y);
        z=add(x,y);
        printf("add=%d\n",z);
        z=sub(x,y);
        printf("sub=%d\n",z);
        return 0;
}


my.h头文件的内容(本例中仅为了方便演示makefile的写法)
[bruce@server1 demo]$ vim my.h
#include
~

add.c文件的内容,加法运算函数
[bruce@server1 demo]$ vim add.c
int add(int x, int y)
{
        return x+y;
}
~

sub.c文件的内容,减法运算函数
[bruce@server1 demo]$ vim sub.c
int sub(int x, int y)
{
        return x-y;
}
以上就是需要的几个源文件了,在此有一个地方提一下,就是主程序demo.c中的#include语句和my.h中的#include语句,一个使用的是<>括起了头文件,而另个一个则是使用了""括起了头文件,这主要是由于<>引用的是标准的系统头文件,而""则是用户定义的头文件,以示区别。
下面言归正传,按照上面提及的makefile写法,我将makefile写入当前目录下,内容如下:
#comments --- this makefile for compilering program "demo".
demo:demo.o sub.o add.o
        gcc demo.o sub.o add.o -o demo
demo.o:demo.c my.h
        gcc demo.c -c
sub.o:sub.c
        gcc sub.c -c
add.o:add.c
        gcc add.c -c
~                                                                              
~                                                                              

以#开头的部分就是makefile的注释了,在makefile中,只能够使用单行注释。
OK,下面我们执行一下make看看是什么效果

[bruce@server1 demo]$ make
gcc add.c -c
gcc demo.o sub.o add.o -o demo
[bruce@server1 demo]$ ls
add.c  add.o  demo  demo.c  demo.o  makefile  my.h  sub.c  sub.o

从上面的回显信息,可以看出来已经成功编译了,make工具替我们使用gcc来完成了编译。
执行看看结果:
[bruce@server1 demo]$ ./demo
12 34   -----用户输入
add=46  -----和结果
sub=-22 -----差结果
[bruce@server1 demo]$

到这里为止,我们只是简单介绍了makefile的写法,并加上了一个小的示例,但make的好处显然还没有全部体现出来,就是它可以在应用程序源文件更改后,依据时间标记来进行选则性编译,对更改过的文件来进行编译,这样大大的减少了我们等待的时间,虽然有时一个使用makefile写了的软件,我们安装时,光make就要10多分钟,但是如果换成手工gcc的话,后果可想而知,想必精神病院的床都不够了...
这个测试就大家自己完成吧,尽量选则大一些的文件更改,当然要想看到结果,你得写对文件...

阅读(3200) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~