




已阅读5页,还剩43页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
八、多维数组一、 高维数组有时,数组的维数并不止一维,例如一个记录消费中心在第一季度里各个月的收入数据就可以用二维数组来表示。定义二维数组的方法是在一维数组定义的后面再加上一个用方括号括起来的维数说明。例如:float array38;实际上,这个数组可以看成3个连续的一维数组,每个一维数组具有8个元素。该数组在内存中的存储格式为最左边的维数相同的元素连续存储,也即按行存储的。首先存储第一行8个元素,其次是第二行,最后是第三行。main()int array33=1,2,3,4,5,6,7,8,9;int i,j;for(i=0;i3;i+)for(j=0;j3;j+) printf(%3d);printf(n);它的输出结果为:1 2 34 5 67 8 9可以看出,二维数组元素是按行存储的。我们也可以对数组进行赋值,而不是初始化。main()int array33;int i,j;for(j=0;j3;j+)for(i=0;i3;i+) scanf(%d,&arrayij);for(i=0;i3;i+)for(j=0;j3;j+) printf(%3d);printf(n);当输入1 2 3 4 5 6 7 8 9输出为:1 4 72 5 83 6 9数组可以是二维、三维甚至是更高维数的,虽然C语言对维数的处理没有上限,但是处理高维数组是很头疼的事。一般尽量避免处理四维和四维以上的数组。下面看一个三维数组的例子:main() int array234;int i,j,k;for(i=0;i2;i+)for(j=0;j3;j+)for(k=0;k4;k+) arrayijk=i*12+j*4+k;这个三维数组可以看成2个二维数组,每个二维数组又可以看成3个一维数组。可以在头脑里想象成两个平行平面,每个平面内有3*4个点。所以共有24个元素。二、字符串数组上面讲的都是存放数值的,有一类数组,用来处理字符串的,我们叫字符串数组。其实字符串数组也是二维数组,只是它的特殊性,才单独拿出来说的。main()char s1010;int i;for(i=0;i10;i+) scanf(%s,si);先看它的输入特性,前面在说输入语句的时候说过,遇到字符串输入,可以不加&,现在只要记住这个特性就可以,以后说指针的时候再讲为什么。但是这儿为什么用si,可能很多人不太明白。我们定义的是二维数组,而输入的时候,却使用一维数组的形式。这是因为字符串在内存里地址可以用它的名字表示,就好象这种形式:main()char s10;scanf(%s,s);定义的是一维数组,输入语句用变量形式表示一样。通过前面的%s形式可以看出,si是一个数组,所以s就是二维数组了。这里要注意一点,scanf()函数在输入字符串时候不能支持空格,看下面的例子:main()char s310;int i;for(i=0;i10;i+)scanf(%s,si);for(i=0;i3;i+)printf(%sn,si);我们输入:11112222 33334444我们是想把1111赋值给s0,2222 3333赋值给s1,4444赋值给s2。可实际上编译器是这样做的,把1111赋值给s0,把2222赋值给1,把3333赋值给s2。实际输出:111122223333在输入字符串的时候,如果使用scanf(),就把空格当作下一个输入了。那么我们怎么解决这个问题呢?毕竟很多情况下,一行字符串肯定有空格出现的。我们使用新的函数gets()。这个函数是专门接受字符串输入的,它跳过了空格的影响。把上面的输入语言修改为gets(si)即可。我们定义了char s310,超过10个字符肯定不行,如果少于10个字符,电脑怎么处理呢?电脑是在每个字符串的后面自动补上0,作为字符串的结束标志。我们经常在填写一些可选择的内容时经常发现,待选的字符串都是按字母排列好的,我们怎么用C语言实现这个功能?在C语言里,字符串的排序是按照字符的ASCII码来的,如果第一个字符一样,则比较第二个,依次类推。main()char s16=addfgh,s25=asdlg;int i;for(i=0;s1i!=0&s2i!=0;i+)if(s1iS2I)printf(s1s2i)printf(s1s2n);exit(1);else ;if(s1i=0 & s2i!=0) printf(s1s2n);else printf(s1=s2n);上面的例子就是比较两个字符串大小的,先比较第一个,如果相同,接着比较第二个,如果不相同,则分出大小。一直往后比较,直到其中某一个到0,你也可以先用strlen()函数找出最小的长度。exit()函数的作用是退出程序,具体它的用法可以看看相关资料。其实C语言把我们经常需要的字符串处理函数都做好了,我们只需要调用它即可。如strcmp()用来比较、strcpy()用来拷贝等等。看看它们的用法:#include string.hmain()char s110,s210,s210;int k;gets(s1);gets(s2);k=strcmp(s1,s2); /*比较s1和s2大小*/if(k=0) printf(s1=s2n);else if(k0) printf(s1s2n);else printf(s1S2N);strcpy(s3,s1); /*把s1拷贝到s3*/printf(%sn,s3); 可以看出,比较大小时,如果ks2;如果k=0,则s1=s2。实际上这是一个函数,具体什么是函数,以及为什么写成那种形式,我们下节再说。这些函数都包含在string.h头文件中,所以在程序的开头,都要写上#include string.h。字符串处理有很多函数,你们可以看看相关的书,也可以看看Turbo C的帮助。九、函数的定义和调用本节介绍C程序的基本单元-函数。函数中包含了程序的可执行代码。每个C程序的入口和出口都位于函数main()之中。main()函数可以调用其他函数,这些函数执行完毕后程序的控制又返回到main()函数中,main()函数不能被别的函数所调用。通常我们把这些被调用的函数称为下层(lower-level)函数。函数调用发生时,立即执行被调用的函数,而调用者则进入等待状态,直到被调用函数执行完毕。函数可以有参数和返回值。程序员一般把函数当作“黑箱”处理,并不关心它内部的实现细节。当然程序员也可以自己开发函数库。说明一点,函数这一节很重要,可以说一个程序的优劣集中体现在函数上。如果函数使用的恰当,可以让程序看起来有条理,容易看懂。如果函数使用的乱七八糟,或者是没有使用函数,程序就会显得很乱,不仅让别人无法查看,就连自己也容易晕头转向。可以这样说,如果超过100行的程序中没有使用函数,那么这个程序一定很罗嗦(有些绝对,但也是事实)。一、函数的定义一个函数包括函数头和语句体两部分。函数头由下列三不分组成:函数返回值类型函数名参数表一个完整的函数应该是这样的:函数返回值类型 函数名(参数表)语句体;函数返回值类型可以是前面说到的某个数据类型、或者是某个数据类型的指针、指向结构的指针、指向数组的指针。指针概念到以后再介绍。函数名在程序中必须是唯一的,它也遵循标识符命名规则。参数表可以没有也可以有多个,在函数调用的时候,实际参数将被拷贝到这些变量中。语句体包括局部变量的声明和可执行代码。我们在前面其实已经接触过函数了,如abs(),sqrt(),我们并不知道它的内部是什么,我们只要会使用它即可。这一节主要讲解无参数无返回值的函数调用。二、函数的声明和调用为了调用一个函数,必须事先声明该函数的返回值类型和参数类型,这和使用变量的道理是一样的(有一种可以例外,就是函数的定义在调用之前,下面再讲述)。看一个简单的例子:void a(); /*函数声明*/main()a(); /*函数调用*/void a() /*函数定义*/int num;scanf(%d,&num);printf(%dn,num);在main()的前面声明了一个函数,函数类型是void型,函数名为a,无参数。然后在main()函数里面调用这个函数,该函数的作用很简单,就是输入一个整数然后再显示它。在调用函数之前声明了该函数其实它和下面这个程序的功能是一样的:main()int num;scanf(%d,&num);printf(%dn,num);可以看出,实际上就是把a()函数里面的所有内容直接搬到main()函数里面(注意,这句话不是绝对的。)我们前面已经说了,当定义在调用之前时,可以不声明函数。所以上面的程序和下面这个也是等价的:void a()int num;scanf(%d,&num);printf(%dn,num);main()a();因为定义在调用之前,所以可以不声明函数,这是因为编译器在编译的时候,已经发现a是一个函数名,是无返回值类型无参数的函数了。那么很多人也许就会想,那我们何必还要声明这一步呢?我们只要把所有的函数的定义都放在前面不就可以了吗?这种想法是不可取的,一个好的程序员总是在程序的开头声明所有用到的函数和变量,这是为了以后好检查。前面说了,在调用之前,必须先声明函数,所以下面的做法也是正确的(但在这里我个人并不提倡)。main()void a();a();void a()int num;scanf(%d,&num);printf(%dn,num);一般来说,比较好的程序书写顺序是,先声明函数,然后写主函数,然后再写那些自定义的函数。既然main()函数可以调用别的函数,那么我们自己定义的函数能不能再调用其他函数呢?答案是可以的。看下面的例子:void a();void b();main()a();void a()b();void b()int num;scanf(%d,&num);printf(%dn,num);main()函数先调用a()函数,而a()函数又调用b()函数。在C语言里,对调用函数的层数没有严格的限制,我们可以往下调用100层、1000层,但是在这里我们并不提倡调用的层数太多(除非是递归),因为层数太多,对以后的检查有一些干扰,函数调过来调过去,容易让自己都晕头转向。某些人可能就不明白了,看上面的例子,好象使用函数后,程序变的更长了,更不让人理解。当然,我举的这个例子的确没有必要用函数来实现,但是对于某些实际问题,如果不使用函数,会让程序变的很乱,这涉及到参数问题,我们下一节再说。10.函数参数的传递和值返回前面我们说的都是无参数无返回值的函数,实际程序中,我们经常使用到带参数有返回值的函数。一、函数参数传递1.形式参数和实际参数函数的调用值把一些表达式作为参数传递给函数。函数定义中的参数是形式参数,函数的调用者提供给函数的参数叫实际参数。在函数调用之前,实际参数的值将被拷贝到这些形式参数中。2.参数传递先看一个例子:void a(int); /*注意函数声明的形式*/main()int num;scanf(%d,&num);a(num); /*注意调用形式*/void a(int num_back) /*注意定义形式*/printf(%dn,num_back);在主函数中,先定义一个变量,然后输入一个值,在a()这个函数中输出。当程序运行a(num);这一步时,把num的值赋值给num_back,在运行程序过程中,把实际参数的值传给形式参数,这就是函数参数的传递。形参和实参可能不只一个,如果多于一个时,函数声明、调用、定义的形式都要一一对应,不仅个数要对应,参数的数据类型也要对应。void a(int,float);main()int num1;float num2;scanf(%d,&num1);scanf(%f,&num2);a(num1,num2);void a(int num1_back,float num2_back)printf(%d,%fn,num1_back,num2_back);上面的例子中,函数有两个参数,一个是整型,一个是浮点型,那么在声明、调用、定义的时候,不仅个数要一样,类型也要对应。如果不对应,有可能使的编译错误,即使没错误,也有可能让数据传递过程中出现错误。再看一个例子:void a(int);main()int num;scanf(%d,&num);a(num);void a(int num)printf(%dn,num);看上面的例子,形式参数和实际参数的标识符都是num,程序把实际参数num的值传递给形式参数num。有些人可能就不明白了,既然两个都是num,为什么还要传递呢?干脆这样不就行了吗:void a();main()int num;scanf(%d,&num);a();void a()printf(%dn,num);其实不然,这就要涉及到标识符作用域的问题。作用域的意思就是说,哪些变量在哪些范围内有效。一个标识符在一个语句块中声明,那么这个标识符仅在当前和更低的语句块中可见,在函数外部的其实地方不可见,其他地方同名的标识符不受影响,后面我们会系统讲解作用域的问题。在这儿你就要知道两个同名的变量在不同的函数中是互不干扰的。前面将的都是变量与变量之间的值传递,其实函数也可以传递数组之间的值。看下面的例子:void a(int );main()int array5,i;for(i=0;i5;i+) scanf(%d,&arrayi);a(array);void a(int array)int i;for(i=0;i5;i+) printf(%dt,arrayi);printf(n);这就是数组之间的值传递。注意他们的声明和定义形式,和变量参数传递有什么区别?有了后面的就表明传递的是一个数组。其中在定义的时候,也可以写成void a(int array5);想想,如果我们写成了int array4会有什么情况发生?目前我们只学了数组和变量,以后还会知道指针、结构,到那是,函数也可以传递它们之间的值。二、函数值的返回其实我们也可以把函数当作一个变量来看,既然是变量,那一定也可以有类型。还举最前面的例子,现在要求在main()函数里输入一个整数作为正方形的边长,在子函数里求正方形的面积,然后再在主函数里输出这个面积。我们前面的程序都是在子函数里输出的,现在要求在主函数里输出,这就需要把算好的值返回回来。先看例子:int a(int); /*声明函数*/main()int num,area;scanf(%d,&num);area=a(num); /*调用时的形式*/printf(%d,area);int a(int num)int area_back;area_back=num*num;return area_back; /*返回一个值*/和前面的程序有几点不同:(1).声明函数类型时,不是void,而是int。这是由于最后要求的面积是整型的,所以声明函数的返回值类型是整型。(2).return语句 它的意思就是返回一个值。在C语言中,return一定是在函数的最后一行。(3).调用函数的时候,由于函数有一个返回值,所以必须要用变量接受这个返回值(不是绝对的),如果我们不用一个变量接受这个值,函数还照样返回,但是返回的这个值没有使用。上面的例子运行过程是这样的,先把实参的值传递给形参,然后在子函数里计算面积得到area_back,然后返回这个面积到主函数,也就是把area_back赋值给area,最后输出。前面说了,返回值有时不一定非要用一个变量来接受,我们可以把上面的程序简化为:int a(int);main()int num;scanf(%d,&num);printf(%d,a(num); /*函数调用放在这儿*/int a(int num)int area_back;area_back=num*num;return area_back;这样函数返回的值就可以直接放到输出缓冲区直接输出了。还可以再简化为:int a(int);main()int num;scanf(%d,&num);printf(%d,a(num);int a(int num)return num*num; /*直接在这儿返回*/对于函数而言,一个函数只能返回一个值,如果想返回一组数值,就要使用数组或者结构或者指针。其实对于这些,还是返回一个值,只是这个值是一个地址而已。但是对于数组的返回有和变量不同,因为数组和地址是联系在一起的。看一个例子:void a(int );main()int array5=1,2,3,4,5,i;a(array);for(i=0;i5;i+) printf(%d,arrayi);void a(int array)int i;for(i=0;i5;i+) arrayi+;看看这个程序,好象函数没有返回值,但是函数的功能的确实现了,在主函数当中输出的值的确都各加了1上来。这就是因为数组和变量不同的缘故,在后面讲指针的时候再详细说明。下面看一个实际例子,加深对函数的理解:用函数实现,判断一个整数是不是素数?在主函数里输入输出,子函数里判断。#include math.hint judge(int);main()int num,result;scanf(%d,&num);result=judge(num);if(result=1) printf(yesn);else printf(non);judge(int num)int i,flag=1;for(i=2;i=sqrt(num);i+)if(num%i=0)flag=0;break;return flag;可以看出,函数的功能就是为了让程序看起来有条理,一个函数实现一个特定的功能。如果我们还和以前那样,把所有代码都放在main()函数,好象程序就显的臃肿了。而且函数有一个显著的好处就是很方便的使用。这里面的judge()函数判断一个数是不是素数,如果我们以后还有判断某个数是不是素数,就可以直接使用这个函数了。我们这样,把下面的代码:judge(int num)int i,flag=1;for(i=2;i=sqrt(num);i+)if(num%i=0)flag=0;break;return flag;保存为judge.h文件,放到include目录里面。以后就可以直接使用这个函数了,就好象直接使用abs(),sqrt()这些函数一样方便。#include math.h /*必须要有它*/#include judge.hmain()int num,result;scanf(%d,&num);result=judge(num);if(result=1) printf(yesn);else printf(non);看上面的例子,我们在程序中直接使用了函数judge(),这就是我们自己编写的第一个所谓的库函数。但是程序的第一行要包含math.h文件,这是因为在judge.h里面使用了sqrt()函数,所以为了方便,我们可以把math.h放到judge.h里面,也就是在judge.h文件的第一行加上include math.h,这样,我们的主程序中就不需要包含它了,但是这样做也有副作用,具体有什么副作用,我们以后接触到时再介绍。我们实际用到的一些程序,也许代码有很长,上千行,甚至上万行,这些代码不可能放在一个*.c文件中,所以我们经常把一些功能做成*.h,*c的文件形式,然后在主程序中包含这些文件,这样就把一个大程序分割成几个小块,不仅浏览方便,对以后的修改也有很多好处。我们在平时就应该有这样的好习惯,把一些经常使用的功能做成库函数的形式保存下来,也许刚开始你会觉得很烦琐,可到了后来,也许几年过去了,你会发现,一个好几千行上万行的程序,有一大半的功能你都有,直接调用就可,这会大大缩短你的程序开发周期的。就好象这里的判断素数一样,如果以后还需要判断一个数是不是素数,就没必要再写那些代码了,直接调用judge()函数就可。11、变量的作用域和存储类型一、作用域和生存期C程序的标识符作用域有三种:局部、全局、文件。标识符的作用域决定了程序中的哪些语句可以使用它,换句话说,就是标识符在程序其他部分的可见性。通常,标识符的作用域都是通过它在程序中的位置隐式说明的。1.局部作用域前面各个例子中的变量都是局部作用域,他们都是声明在函数内部,无法被其他函数的代码所访问。函数的形式参数的作用域也是局部的,它们的作用范围仅限于函数内部所用的语句块。void add(int);main()int num=5;add(num);printf(%dn,num); /*输出5*/void add(int num)num+;printf(%dn,num); /*输出6*/上面例子里的两个num变量都是局部变量,只在本身函数里可见。前面我们说了,在两个函数出现同名的变量不会互相干扰,就是这个道理。所以上面的两个输出,在主函数里仍然是5,在add()函数里输出是6。2.全局作用域对于具有全局作用域的变量,我们可以在程序的任何位置访问它们。当一个变量是在所有函数的外部声明,也就是在程序的开头声明,那么这个变量就是全局变量。void add(int);int num;main()int n=5;add(n);printf(%dn,num); /*输出6*/void add(num) /*形式参数没有指定类型*/num+;printf(%dn,num); /*输出6*/上面的main()和add()里面,并没有声明num,但是在最后输出的时候却要求输出num,这是由于在程序的开始声明了num是全局变量,也就是在所有函数里都可以使用这个变量。这时候一个函数里改变了变量的值,其他函数里的值也会出现影响。上面的例子输出都是6,因为在add()函数里改变了num的值,由于num是全局变量,就好象它们两个函数共用一个变量,所以在main()函数里的num也随之改变了。3.文件作用域在很多C语言书上,都没有说明文件作用域,或者只是略微的提到,其实文件作用域在较大程序中很有作用(在多文件系统中)。文件作用域是指外部标识符仅在声明它的同一个转换单元内的函数汇总可见。所谓转换单元是指定义这些变量和函数的源代码文件(包括任何通过#include指令包含的源代码文件)。static存储类型修饰符指定了变量具有文件作用域。static int num;static void add(int);main()scanf(%d,&num);add(num)printf(%dn,num);void add(num)num+;上面的程序中变量num和函数add()在声明是采用了static存储类型修饰符,这使得它们具有文件作用域,仅爱定义它们的文件内可见。由于我们提到的大多数程序都只有一个编译文件组成,所以这种写法没有实际意义。但是实际工程上的文件有很多,它们不是由一个人写成的,由很多人共同完成,这些文件都是各自编译的,这难免使得某些人使用了一样的全局变量名,那么为了以后程序中各自的变量和函数不互相干扰,就可以使用static修饰符,这样在连接到同一个程序的其他代码文件而言就是不可见的。二、变量存储类型前面我们说了,声明变量时用如下类似的形式:int num;float total;它们都没有存储类型修饰符,我们在声明时也可以通过存储类型修饰符来告诉编译器将要处理什么类型的变量。存储类型有以下四种:自动(auto)、静态(static)、外部(extern)、寄存器(regiser)。1.自动存储类型自动存储类型修饰符指定了一个局部变量为自动的,这意味着,每次执行到定义该变量的语句块时,都将会为该变量在内存中产生一个新的拷贝,并对其进行初始化。实际上,如果不特别指明,局部变量的存储类型就默认为自动的,因此,加不加auto都可以。main()auto int num=5;printf(%dn,num);在这个例子中,不论变量num的声明是否包含关键字auto,代码的执行效果都是一样的。函数的形式参数存储类型默认也是自动的。2.静态存储变量前面已经使用了static关键字,但是对于局部变量,静态存储类型的意义是不一样的,这时,它是和自动存储类型相对而言的。静态局部变量的作用域仍然近局限于声明它的语句块中,但是在语句块执行期间,变量将始终保持它的值。而且,初始化值只在语句块第一次执行是起作用。在随后的运行过程中,变量将保持语句块上一次执行时的值。看下面两个对应的程序:/*1.C*/ /*2.C*/int add(); int add();main() main() int result; int result;result=add() result=add();printf(%d ,result); printf(%d ,result);result=add(); result=add();printf(%d ,result); printf(%d ,result);result=add(); result=add();printf(%d,result); printf(%d,result); int add() int add() int num=50; static int num=50;num+; num+;return num; return num; 上面两个源文件,只有函数add()里的变量声明有所不同,一个是自动存储类型,一个是静态存储类型。对于1.C文件,输出结果为51 51 51;这很好理解,每次初始值都是50,然后加1上来。对于2.C文件,输出结果为51 52 53;这是由于变量是静态的,只在第一次初始化了50,以后都是使用上次的结果值。当第一次调用add()时,初始化为50,然后加1,输出为51;当第二次调用时,就不初始化了,这时num的值为上次的51,然后加1,输出52;当第三次调用时,num为52,加1就是53了。比较就会发现它们的不同之处了。静态变量在下一节要说的递归函数中经常使用到。当第一次不指明静态变量的初始值时,默认为0。下面举一个例子,把我们说到的静态变量理解一下。求1+2+100的值void add();int result;main()int i;result=0;for(i=0;i100;i+) add();printf(%dn,result);void add()static int num=0;num+;result+=num;add()函数被调用了100次,num的值从1一直变到100,这样就可以求出它们的和了。如果写成int num=0;那就是求1+1+1这100个1的值了。实际上类似的这类问题我们可以通过递归函数来解决,什么是递归,我们下一节介绍。3.外部存储类型外部存储类型声明了程序将要用到的、但尚未定义的外部变量。通常,外部存储类型都是用于声明在另一个转换单元中定义的变量。下面举一个例子,这个例子包括两个文件。/*1.C*/void a();main()extern int num;a();printf(%dn,num);/*2.C*/int num;void a()num=5;这两个程序是分别编译的,然后连接成一个执行文件。具体如何操作,可以查看一些手册,这儿我简单说了一下。把上面两个文件都编译好后,再制作一个.prj文件,里面的内容是:1.c2.c只有这两行,这可在编辑状态下写成,存盘,取名为1.prj。然后选择project选项,选择project name,填入1.prj文件名,按F9后,即可生成1.exe文件。main()函数中变量num是在另一个文件中定义的。因此,当编译器编译1.c时,无法确定该变量的地址。这时,外部存储类型声明告诉编译器,把所有对num的引用当作暂且无法确定的引用,等到所有便宜好的目标代码连接成一个可执行程序模块时,再来处理对变量num的引用。外部变量的声明既可以在引用它的函数的内部,也可以在外部。如果变量声明在函数外部,那么同一转换单元内的所有函数都可以使用这个外部变量。反之,如果在函数内部,那么只有这一个函数可以使用该变量。前面说了文件作用域的问题,如果在声明全局变量时,加上static修饰符,那么该变量只在当前文件内可见,而extern又可以引用其它文件里的变量。所以在一个大型程序中,每个程序员只是完成其中的一小块,为了让自己的变量不让其他程序员使用,保持一定的独立性,经常在全局变量前加static。我们可以这样来说明一下:还是上面的两个文件,现在再增加一个文件3.c,内容为:static int num;void a()num=6;把1.prj文件后面加上3.c 这样,我们生成的1.exe文件,执行时输出是5,而不是6。因为3.c文件的num变量增加了文件作用域,在其他文件中是无法使用它的。4.寄存器存储类型被声明为寄存器存储类型的变量,除了程序无法得到其地址外,其余都和自动变量一样。至于什么是变量地址,以后说指针时会详细介绍。main()register int num;num=100;printf(%d,num);使用寄存器存储类型的目的是让程序员指定某个局部变量存放在计算机的某个硬件寄存器里而不是内存中,以提高程序的运行速度。不过,这只是反映了程序员的主观意愿,编译器可以忽略寄存器存储类型修饰符。寄存器变量的地址是无法取得的,因为绝大多数计算机的硬件寄存器都不占用内存地址。而且,即使编译器忽略寄存器类型修饰符把变量放在可设定地址的内存中,我们也无法取地址的限制仍然存在。要想有效的利用寄存器存储类型,必须象汇编语言程序员那样了解处理器的内部构造,知道可用于存放变量的寄存器的数量和种类,以及他们是如何工作的。但是,不同计算机在这些细节上未必是一样的,因此对于一个可移植的程序来说,寄存器存储类型的作用不大。特别是现在很多编译器都能提供很好的优化效果,远比程序员来选择有效的多。不过,寄存器存储类型还是可以为优化器提供重要的参考。12、函数递归一、栈在说函数递归的时候,顺便说一下栈的概念。栈是一个后进先出的压入(push)和弹出(pop)式数据结构。在程序运行时,系统每次向栈中压入一个对象,然后栈指针向下移动一个位置。当系统从栈中弹出一个对象时,最近进栈的对象将被弹出。然后栈指针向上移动一个位置。程序员经常利用栈这种数据结构来处理那些最适合用后进先出逻辑来描述的编程问题。这里讨论的程序中的栈在每个程序中都是存在的,它不需要程序员编写代码去维护,而是由运行是系统自动处理。所谓的系统自动维护,实际上就是编译器所产生的程序代码。尽管在源代码中看不到它们,但程序员应该对此有所了解。再来看看程序中的栈是如何工作的。当一个函数(调用者)调用另一个函数(被调用者)时,运行时系统将把调用者的所有实参和返回地址压入到栈中,栈指针将移到合适的位置来容纳这些数据。最后进栈的是调用者的返回地址。当被调用者开始执行时,系统把被调用者的自变量压入到栈中,并把栈指针再向下移,以保证有足够的空间存储被调用者声明的所有自变量。当调用者把实参压入栈后,被调用者就在栈中以自变量的形式建立了形参。被调用者内部的其他自变量也是存放在栈中的。由于这些进栈操作,栈指针已经移动所有这些局部变量之下。但是被调用者记录了它刚开始执行时的初始栈指针,以他为参考,用正或负的偏移值来访问栈中的变量。当被调用者准备返回时,系统弹出栈中所有的自变量,这时栈指针移动了被调用者刚开始执行时的位置。接着被调用者返回,系统从栈中弹出返回地址,调用者就可以继续执行了。当调用者继续执行时,系统还将从栈中弹出调用者的实参,于是栈指针回到了调用发生前的位置。可能刚开始学的人看不太懂上面的讲解,栈涉及到指针问题,具体可以看看一些数据结构的书。要想学好编程语言,数据结构是一定要学的。二、递归递归,是函数实现的一个很重要的环节,很多程序中都或多或少的使用了递归函数。递归的意思就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。递归之所以能实现,是因为函数的每个执行过程都在栈中有自己的形参和局部变量的拷贝,这些拷贝和函数的其他执行过程毫不相干。这种机制是当代大多数程序设计语言实现子程序结构的基础,是使得递归成为可能。假定某个调用函数调用了一个被调用函数,再假定被调用函数又反过来调用了调用函数。这第二个调用就被称为调用函数的递归,因为它发生在调用函数的当前执行过程运行完毕之前。而且,因为这个原先的调用函数、现在的被调用函数在栈中较低的位置有它独立的一组参数和自变量,原先的参数和变量将不受影响,所以递归能正常工作。程序遍历执行这些函数的过程就被称为递归下降。程序员需保证递归函数不会随意改变静态变量和全局变量的值,以避免在递归下降过程中的上层函数出错。程序员还必须确保有一个终止条件来结束递归下降过程,并且返回到顶层。例如这样的程序就是递归:void a(int);main()int num=5;a(num);void a(int num)if(num=0) return;printf(%d,num);a(-num);在函数a()里面又调用了自己,也就是自己调用本身,这样就是递归。那么有些人可能要想,这不是死循环吗?所以在递归函数中,一定要有return语句,没有return语句的递归函数是死循环。我们分析上面的例子,先调用a(5),然后输出5,再在函数中调用本身a(4),接着回到函数起点,输出4,一直到调用a(0),这时发现已经满足if条件,不在调用而是返回了,所以这个递归一共进行了5次。如果没有这个return,肯定是死循环的。虽然递归不难理解,但是很多在在使用递归函数的时候,问题多多。这里面一般有两个原因:一是如何往下递归,也就是不知道怎么取一个变量递归下去;二是不知道怎么终止递归,经常弄个死循环出来。下面看几个例子:1.求1+2+100的和先分析一下。第一递归变量的问题,从题目上看应该取1,2,100这些变量的值作为递归的条件;第二就是如何终止的问题,从题目上看应该是当数为100的时候就不能往下加了。那么我们试着写一下程序。int add(int);main()int num=1,sn;sn=add(num);printf(%dn,sn);getch();int add(int num)static int sn;sn+=num;if(num=100) return sn;add(+num);分析一下程序:前调用add(1),然后在子函数中把这个1加到sn上面。接着调用add(2),再把sn加2上来。这样一直到100,到了100的时候,先加上来,然后发现满足了if条件,这时返回sn的值,也就是1+2+100的值了。这里有一个问题一定要注意,就是static int sn; 有些人就不明白,为什么要使用static类型修饰符,为什么不使用int sn=0;?如果使用int sn=0;这样的语句,在每次调用函数add()的时候,sn的值都是赋值为0,也就是第一步虽然加了1上来,可是第二次调用的时候,sn又回到了0。我们前面说了,static能保证本次初始化的值是上次执行后的值,这样也就保证了前面想加的结果不会丢失。如果你修改为int sn=0,最后结果一定是最后的100这个值而不是5050。2.求数列s(n)=s(n-1)+s(n-2)的第n项。其中s(1)=s(2)=1。可以看出,终止条件一定是s(1)=s(2)=1。递归下降的参数一定是n。int a(int);main()int n,s;scanf(%d,&n);s=a(n);printf(%dn,s);getch();int a(int n)if(n3) return 1;return a(n-1)+a(n-2);这个题目主要说明的是,在函数中,不一定只有一个return语句,可以有很多,但是每次对归的时候只有一个起作用。题目不难理解,这儿不分析了。说了这些递归,其实它和函数的调用没有大的区别,主要就是一个终止条件要选好。递归函数很多时候都能用循环来处理。main()int n=20,array20;int i;for(i=0;iN;I+)if(i2) arrayi=1;else arrayi=arrayi-1+arrayi-2;printf(%dn,array19);getch();上面的程序就是实现一模一样的功能的。但是它有一个缺陷,就是n的值不是通过键盘输入来得到。如果想通过键盘来得到n,可以这样:main()int n,i;int s1=1,s2=1,tempscanf(%d,&n);for(i=3;i=n;i+)temp=s2;s2+=s1;s1=temp;printf(%dn,s2);getch();但是在某些场合,使用递归比使用循环要简单的多。而且有些题目,一看就知道应该使用递归而不是循环来处理。13、预处理过程预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。在C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令:指令 用途# 空指令,无任何效果#include 包含一个源代码文件#define 定义宏#undef 取消已定义的宏#if 如果给定条件为真,则编译下面代码#ifdef 如果宏已经定义,则编译下面代码#ifndef 如果宏没有定义,则编译下面代码#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码#endif 结束一个#if#else条件编译块#error 停止编译并显示错误信息一、文件包含#include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时,通过给定编译时的
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025公务员体育面试题及答案
- 2025公务员面试题真题及答案
- 小学专业考试题目及答案
- 会计专业笔试试题及答案
- 国企专业面试题目及答案
- 影楼年度工作总结
- 手术室护理核心收获总结
- 设备外委人员工作总结
- 新手外贸工作总结
- 公司送货司机个人年终总结
- 2025年大一上学期java期末考试题及答案
- 团校考试试题及答案浙江
- 2025-2026学年湘美版(2024)小学美术二年级上册(全册)教学设计(附目录P208)
- 法国方言政策的沿袭与变革
- 2024年秦皇岛市市直机关遴选考试真题
- 2025年贵州省中考化学真题卷含答案解析
- 高压供电设备基础知识培训课件
- 2025年中医确有专长考试题及答案
- (2025年标准)教师定岗协议书
- GB/T 45980-2025飞机供电特性数字式测试要求
- 2025年度粉末涂料生产与销售合同范本
评论
0/150
提交评论