C语言从入门到熟悉------第三阶段

数组

什么是数组呢?顾名思义数组就是很多数的组合!那么这些数有没有什么要求呢?是不是不管什么数组合在一起都是数组呢?第一,这些数的类型必须相同!第二,这些数在内存中必须是连续存储的。也就是说,数组是在内存中连续存储的有着相同类型的一组数据的集合。

一维数组的使用

一维数组的定义方式如下:

    类型说明符 数组名[常量表达式];

例如:

    int a[5];

它表示定义了一个整型数组,数组名为a,定义的数组就称为数组a。数组名a除了表示该数组之外,还表示该数组的首地址。

此时数组a中有5个元素,每个元素都是int型变量,而且它们在内存中的地址是连续分配的。也就是说,int型变量占4字节的内存空间,那么5个int型变量就占20字节的内存空间,而且它们的地址是连续分配的。

说明:

(1)元素就是变量的意思,数组中习惯上称为元素。

(2)在定义数组时,需要指定数组中元素的个数。数组中元素的个数又称数组的长度。

(3)在数组中“下标是从0开始的”。

那么为什么下标是从0开始而不是从1开始呢?大家想想,如果从1开始,那么数组的第5个元素就是a[5],而定义数组时是int a[5],两个都是a[5]就容易产生混淆。而下标从0开始就不存在这个问题了!所以定义一个数组a[n],那么这个数组中元素最大的下标是n -1;而元素a[i]表示数组a中第i+1个元素。

一维数组的初始化

1)定义数组时给所有元素赋初值,这叫“完全初始化”。例如:

    int a[5] = {1, 2, 3, 4, 5};

经过上面的初始化之后,a[0]=1;a[1]=2; a[2]=3; a[3]=4; a[4]=5

2)可以只给一部分元素赋值,这叫“不完全初始化”。例如:

    int a[5] = {1, 2};

这表示只给前面两个元素a[0]、a[1]初始化,而后面三个元素都没有被初始化。不完全初始化时,没有被初始化的元素自动为0。

这里需要注意的是,“不完全初始化”和“完全不初始化”不一样。如果“完全不初始化”,即只定义“int a[5]; ”而不初始化,那么各个元素的值就不是0了,所有元素中都是垃圾值-858993460。

你也不能写成“int a[5]={}; ”。如果大括号中什么都不写,那就是极其严重的语法错误。大括号中最少要写一个数。比如“int a[5]={0}; ”,这时就是给数组“清零”,此时数组中每个元素都是零。

3)如果定义数组时就给数组中所有元素赋初值,那么就可以不指定数组的长度,因为此时元素的个数已经确定了。可以写成:

    int a[] = {1, 2, 3, 4, 5};

如果定义数组时不初始化,那么省略数组长度就是语法错误。

一维数组元素的引用

数组必须先定义,然后使用。C语言规定,只能逐个引用数组元素,而不能一次引用整个数组。前面讲过,数组元素的表示形式为:

    数组名[下标]

下标可以是整型常量或整型表达式,比如:

    a[0] = a[5] + a[7] - a[2 * 3]

千万要注意,定义数组时用到的“数组名[常量表达式]”和引用数组元素时用到的“数组名[下标]”是有区别的。定义数组时的常量表达式表示的是数组的长度,而引用数组元素时的下标表示的是元素的编号。所以下面这个程序是错的:

#include<stdio.h>int main(void) {int a[5];a[5] = {1, 2, 3, 4, 5};return 0;
}

如何将数组a赋给数组b

有人会这样写:

    b = a;

这样写明显是错的,因为前面说过,a和b是数组名,而数组名表示的是数组“第一个元素”的“起始地址”。即a和b表示的是地址,是一个常数,不能将一个常数赋给另一个常数。这种错误就类似于将3赋给2,所以这么写是错误的。正确的写法是用for循环,将数组a中的元素一个一个赋给数组b的元素。

如何编程获取数组的长度

我们要用编程的手段求数组的长度,而不是自己一个个去数。

如何只编写一次代码就能实现不管增加还是减少数组元素,程序都能自动获取数组的长度呢?使用关键字sizeof!

前面说过,用sizeof可以获得数据类型或变量在内存中所占的字节数。同样,用sizeof也可以获得整个数组在内存中所占的字节数。因为数组中每个元素的类型都是一样的,在内存中所占的字节数都是相同的,所以总的字节数除以一个元素所占的字节数就是数组的长度。

那么如何用sizeof获得数组总的字节数呢?只要对数组名使用sizeof,求出的就是该数组总的字节数。

下面给出程序:

#include<stdio.h>
int main(void) {int a[10] = {0};int cnt = sizeof(a) / sizeof(a[0]);printf("cnt = %d\n", cnt);return 0;
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
cnt = 10
--------------------------------------
*/

这样不管数组是增加还是减少元素,sizeof(a) / sizeof(a[0])都能自动求出数组的长度。这种写法在实际编程中是非常专业的,希望大家能够学习。但是需要注意的是,它求出的是数组的总长度,而不是数组中存放的有意义的数据的个数。

宏定义:#define

在C语言中可以用#define定义一个标识符来表示一个常量。其特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。预编译又叫预处理。预编译不是编译,而是编译前的处理。这个操作是在正式编译之前由系统自动完成的,所以叫预编译。用#define定义标识符的一般形式为:

    #define  标识符  常量   //注意,最后没有分号

#define和#include一样,也是以“#”开头的。凡是以“#”开头的均为预处理指令,所以#define也是一条预处理指令。#define又称宏定义,标识符为所定义的宏名,简称宏。标识符的命名规则与前面讲的变量的命名规则是一样的。#define的功能是将标识符定义为其后的常量。一经定义,程序中就可以直接用标识符来表示这个常量。是不是与定义变量类似?但是要区分开!变量名表示的是一个变量,但宏名表示的是一个常量。变量可以给它赋值,但常量当然就不能给它赋值了!

宏所表示的常量可以是数字、字符、字符串、表达式。其中最常用的是数字。

宏定义的优点是方便和易于维护。

程序在预编译的时候是怎么处理宏定义的呢?或者说是怎么处理预处理指令的呢?其实预编译所执行的操作就是简单的“文本”替换。当单击“编译”的时候实际上是执行了两个操作,即先预编译,然后才正式编译。

需要注意的是,预处理指令不是语句,所以后面不能加分号。宏定义#define一般都写在函数外面,与#include写在一起。当然写在函数里面也没有语法错误,但我们通常不那么写。

#define的作用域为自#define那一行起到源程序结束。如果要终止其作用域可以使用#undef命令,格式为:

    #undef  标识符

undef后面的标识符表示你所要终止的宏。

为了将标识符与变量名区别开来,习惯上标识符全部用大写字母表示。宏定义用得最多的地方是在数组中用于指定数组的长度。

二维数组的使用

二维数组与一维数组相似,但是用法上要比一维数组复杂一点,理解起来也比一维数组难一点。在后面的编程中二维数组用得很少,因为二维数组的本质就是一维数组,只不过形式上是二维的。能用二维数组解决的问题用一维数组也能解决。但是在某些情况下,比如矩阵,对于程序员来说使用二维数组会更形象直观,但是对于计算机而言与一维数组是一样的。

二维数组的定义:

二维数组定义的一般形式为:

    类型说明符 数组名[常量表达式][常量表达式];

比如:

    int a[3][4];

表示定义了一个3×4,即3行4列总共有12个元素的数组a。这12个元素的名字依次是:a[0][0]、a[0][1]、a[0][2]、a[0][3]; a[1][0]、a[1][1]、a[1][2]、a[1][3]; a[2][0]、a[2][1]、a[2][2]、a[2][3]。

与一维数组一样,行序号和列序号的下标都是从0开始的。元素a[i][j]表示第i+1行、第j+1列的元素。数组int a[m][n]最大范围处的元素是a[m-1][n-1]。

此外,与一维数组一样,定义数组时用到的“数组名[常量表达式][常量表达式]”和引用数组元素时用到的“数组名[下标][下标]”是有区别的。前者是定义一个数组,以及该数组的维数和各维的大小。而后者仅仅是元素的下标,像坐标一样,对应一个具体的元素。

C语言对二维数组采用这样的定义方式,使得二维数组可被看作一种特殊的一维数组,即它的元素为一维数组。比如“int a[3][4]; ”可以看作有三个元素,每个元素都为一个长度为4的一维数组。而且a[0]、a[2]、a[3]分别是这三个一维数组的数组名。下面来验证一下看看是不是这么回事儿:

#include<stdio.h>
int main(void) {int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};printf("%d\n", sizeof(a[0]));return 0;
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
16
--------------------------------------
*/

可见a[0]确实是第一行一维数组的数组名。

在C语言中,二维数组中元素排列的顺序是按行存放的,即在内存中先顺序存放第一行的元素,再存放第二行的元素,这样依次存放。

二维数组的初始化:

1)分行给二维数组赋初值,比如上面程序的赋值方法:

    int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};

2)也可以将所有数据写在一个花括号内,按数组排列的顺序对各元素赋初值。比如:

    int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

效果与第1种是一样的。但第1种方法更好,一行对一行,界限清楚。第2种方法如果数据多,写成一大片,容易遗漏,也不易检查。

3)也可以只对部分元素赋初值。比如:

    int a[3][4] = {{1, 2}, {5}, {9}};

它的作用是对第一行的前两个元素赋值、第二行和第三行的第一个元素赋值。其余元素自动为0。

4)如果在定义数组时就对全部元素赋初值,即完全初始化,则第一维的长度可以不指定,但第二维的长度不能省。比如:

    int a[3][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

等价于:

    int a[][4] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

系统会根据数据总数和第二维的长度算出第一维的长度。但这种省略的写法几乎不用,因为可读性差。

5)再看一种初始化方式:

int a[3][4] = {0};

二维数组“清零”,里面每一个元素都是零。

二维数组如何输出?

二维数组元素的输出使用两个for循环嵌套输出即可。下面给出程序:

#include<stdio.h>
int main(void) {int a[3][4] = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}};int i;  //行循环变量int j;  //列循环变量for (i=0; i<3; ++i) {for (j=0; j<4; ++j) {printf("%-2d\x20", a[i][j]);}printf("\n");}return 0;
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
1   2   3   4
5   6   7   8
9   10 11 12
--------------------------------------
*/

这里有一个知识点要跟大家讲一下,就是“printf("%-2d\x20", a[i][j]); ”这条语句中的“%-2d”。“-”表示左对齐,如果不写“-”则默认表示右对齐;“2”表示这个元素输出时占两个空格的空间,所以连同后面的\x20则每个元素输出时都占三个空格的空间。(\x20是空格)

是否存在多维数组

是否存在多维数组?不存在!为什么不存在?

第一,因为内存是线性一维的;

第二,n维数组可以当做每个元素是n -1维数组的数组。这么说是不是二维数组也不存在?是的。内存中是不分行也不分列的,元素都按顺序一个一个往后排。只不过为了模拟的方便,我们将它当作是多维的。这也是为什么说二维数组的本质就是一维数组的原因。

函数

前面说过,学习C语言有两个知识点是必须要学的,一个是函数,另一个是指针,这两个知识点是C语言的主体和核心,由此可见其重要性,所以必须要好好学。

C语言的函数有一个特点,就是它有固定的格式和固定的模型。

什么是函数:

第一,函数就是C语言的模块,一块一块的,有较强的独立性,但是可以相互调用。这是C和C++的区别。C++面向对象,对象独立完成功能,无需调用。而C程序可以是一个函数,也可以是在一个函数里面调用n个函数,即大函数调用小函数,小函数又调用“小小”函数。这就是结构化程序设计,所以面向过程的语言又叫结构化语言。

第二,函数就是一系列C语句的集合,能完成某个特定的功能。需要该功能的时候直接调用该函数即可,不用每次都堆叠代码。需要修改该功能时,也只需要修改和维护这一个函数即可。

C程序的组成和编译单位

一个C程序是由一个或多个程序模块组成的,每个程序模块即为一个源程序文件(即一个.c文件)。对于较大的程序,我们不希望将所有的内容都放在一个.c文件里,而是将它们分别放在若干个.c文件中,再由若干个.c文件组成一个C程序,这样便于分别编写、编译,提高效率。而且一个.c文件可以被多个C程序所共用。

而一个.c文件由一个或多个函数以及其他相关内容(如命令行、数据定义等)组成。一个.c文件是一个编译单位,程序在编译时是以.c文件为单位进行编译的,而不是以函数为单位进行编译的。前面介绍过,编译时是将每个.c文件编译生成.obj文件,然后链接时再将所有的.obj文件链接生成一个.exe可执行文件。

库函数和自定义函数

综上所述,C源程序是由函数组成的。虽然前面各章的程序大都只有一个主函数main(),但是在实际编程中程序往往是由多个函数组成的。函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能。

C语言不仅提供了极为丰富的库函数(几百个),还允许用户定义自己的函数。用户可以将自己的算法编成一个个相对独立的函数模块,然后通过调用来使用这些函数。在实际的C编程中用得最多的就是自己定义的函数。库函数只能提供一些基本的功能,我们所需要的大多数功能还是需要自己写。

函数的调用

在C语言中,所有函数的定义,包括主函数main在内,都是“平行”的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上将调用者称为主调函数,被调用者称为被调函数。函数还可以自己调用自己,称为递归调用。递归对于初学者而言不是很重要,不是必须要掌握的内容。一方面,递归比较难理解,要想理解递归必须要掌握栈;另一方面,即便现在将递归弄清楚了,暂时也用不到。

此外,main函数是主函数,它可以调用其他函数,但不允许被其他函数调用。C程序的执行总是从main函数开始的(也是由main结束的),就算定义的函数放在main的前面,程序仍然从main开始执行。如果执行到函数调用则执行被调用的函数,完成函数调用后再返回到main函数继续往下执行,最后由main函数结束整个程序。一个C源程序必须有,也只能有一个主函数main。

为什么需要函数

第一,将语句集合成函数的好处是方便代码重用。

第二,将语句集合成函数方便代码的维护。

所以函数有利于程序的模块化。这个实际上就是面向过程的思想。面向过程语言最基本的单位不是语句,而是函数。

面向过程的思想有一个特点,就是将复杂的大问题分解成一个个小问题,如果小问题还很复杂,就继续分解成更小的问题。分解到最后,每一个问题都使用函数编写成一个个功能块,功能复杂的函数直接调用功能简单的函数即可。

综上所述,整个程序由很多功能模块组成,彼此的功能相互独立,修改某一部分的功能并不会影响另外一部分的功能。

有参函数

从形式上看,函数分为两类:无参函数和有参函数。所谓无参函数是指,在主调函数调用被调函数时,主调函数不向被调函数传递数据。无参函数一般用来执行特定的功能,可以有返回值,也可以没有返回值,但一般以没有返回值居多。

有参函数是指,在主调函数调用被调函数时,主调函数通过参数向被调函数传递数据。在一般情况下,有参函数在执行被调函数时会得到一个值并返回给主调函数使用。

有参函数定义的一般形式

有参函数定义的一般形式:

    函数类型 函数名(参数类型1 参数名1,参数类型2 参数名2, …,参数类型n参数名n){声明部分语句部分}

参数可以是一个,也可以是多个。下面给大家举一个例子:

#include<stdio.h>
int main(void) {int Max(int x, int y);  //对Max函数的声明,x、y称为形参int a = 1, b = 2;printf("max = %d\n", Max(a, b));  //a、b称为实参return 0;
}/*
定义Max函数
*/
int Max(int x, int y) {int z;  //函数体中的声明部分if (x > y) { //下面是语句部分z = x;} else {z = y;}return (z);  //返回z的值给主调函数使用
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
max = 2
--------------------------------------
*/

这个程序分两个部分,一个是主函数main,另一个是笔者自己定义的函数Max。Max函数在主函数main的下面,它有两个参数,它的功能是求出x和y二者中的最大值。 

形参和实参

在定义Max函数时,函数名Max后面括号中的参数x、y称为形式参数,简称形参。而在主调函数main中调用Max函数时,Max函数名后面括号中的参数a、b称为实际参数,简称实参。实参可以是常量、变量或表达式,但它们必须要有确定的数值。在调用被调函数时将实参的值赋给形参。

在传递数据时,实参与形参是按顺序一一对应的。在C语言中,实参向形参的数据传递是“值传递”、单向传递。只能由实参传给形参,不能由形参传回给实参。

而且在未出现函数调用时,形参并不占用内存中的存储单元。只有在发生函数调用时,函数Max中的形参才会被分配内存单元。调用结束后,形参所占的内存单元随之会被释放。

定义函数时,第一行“int Max(int x, int y)”称为函数首部。函数首部有两个数据类型,一个是“函数类型”,另一个是“参数类型”,这两个类型不要混淆了。函数名左边的类型叫“函数类型”,或“函数的返回值类型”。如果不想要返回值,那么这里就写void。但是如果这里写了void,就不能有返回值,否则就是语法错误。但是需要注意的是,不能有返回值不代表不能有return语句:

    return;

这也是正确的。只要return后面什么都不加就行,因为什么都不加也表示没有返回值。而且这么写也有跳出被调函数的功能。

函数名后面括号中的数据类型是所传递的参数的类型。如果不希望定义的函数接收数据,或者说不想有参数传递进来,那么这里就写void,比如int Max(void)。这就表示拒绝接受数据传递,这样实参的值就传不进来了。主函数main的首部都是这么写的:

    int main(void)

即不允许有值传递进来。但是,如果被调函数的参数类型定义成void,那么主调函数在调用它的时候就不能有实参,否则也是语法错误。

此外,如果函数名后面括号中什么都不写,那么默认的也是void,但这是不规范的写法。

主调函数中对被调函数的声明

下面看主函数main中的第一句:

    int Max(int x, int y);

这句称为“对被调函数的声明”。因为函数声明是一条语句,没有分号就不成语句了。

下面对被调函数的“函数声明”有几点说明。

1)首先被调函数必须是已经存在的函数,要么是库函数,要么是自己定义的函数。如果是库函数,那么必须在程序开头用#include命令将该库函数所在的头文件包含进来。

2)如果被调函数是用户自己定义的函数,而该函数的位置又在调用它的函数即主调函数的后面(在同一个文件中),那么就必须要在主调函数中,在调用位置之前对被调函数进行声明。但是如果被调函数的定义是在主调函数之前,比如上面程序将Max函数和主函数main换个位置,那就可以不用对Max函数进行声明。因为编译系统已经知道了Max函数的相关情况。但是一般我们都是将定义的函数写在主调函数的后面,这样就都要在前面进行声明。但是现在有一个问题:“是不是只要在调用位置之前声明就行?”不是的,我们在前面讲过,C89标准规定所有的声明,包含函数声明、变量定义,都必须写在程序或复合语句的开头。所以如果函数声明是写在主调函数中,那么不仅要在调用位置之前,而且必须要在主调函数的开头。

3)在函数声明时也可以不写形参名,只写形参的类型。因为在编译时,系统只会检查“函数返回值类型”、“函数名”“参数个数”和“参数类型”,并不检查参数名。所以上面这句“函数声明”也可以写成:

    int Max(int, int);

4)并非在main中声明过了在其他函数中调用Max就不用再声明了。只要有函数调用Max,且Max的函数体在这个函数的后面,就要在这个函数的调用位置之前对Max进行声明。但是如果在所有函数之前,包括主函数main之前,即在#include的上面或下面对某个被调函数进行了声明,那么在各主调函数中就都不用再作声明了。在实际编程中一般也都是使用这种写法,即把函数的声明写在程序的开头。这时声明的格式与在主调函数中声明的格式是一模一样的,并不因为在外面就与众不同。但习惯上我们都是写在#include的下面,写在上面不美观。

注意,函数的“定义”和“声明”不是一回事。

定义函数时应指定返回值类型

下面来看所定义的Max函数的程序。首先来看Max函数的首部:

    int Max(int x, int y)

定义函数时应当指定函数的返回值类型。如果定义函数时不指定函数的返回值类型,那么系统会默认函数的返回值类型为int型。所以Max前面的int可以省略,即写成:

    Max(int x, int y)

但是这么写可读性很差,而且这么写可移植性很低,并不是所有的编译器都能编译通过。所以我建议你们在定义函数时对所有函数都要指定函数返回值类型。如果没有返回值就写void。

Max函数首部下面的大括号内是Max函数的函数体,它包括声明部分和语句部分。声明部分主要是对函数中要用到的变量进行定义。

函数的返回值

通常我们希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。函数的返回值是通过函数中的return语句获得的。return语句将被调函数中的一个确定的值带回到主调函数中,供主调函数使用。

在调用函数时,如果需要从被调函数返回一个值供主调函数使用,那么返回值类型必须定义成非void型。此时被调函数中必须包含return语句,而且return后面必须要有返回值,否则就是语法错误。而且如果函数有返回值,那么return语句后面的括号可以不要,比如“return(z);”等价于“return z;”。如果不需要返回值则可以不要return语句。但是对于不需要返回值的函数,最好用void定义函数为“无类型”,否则会有警告。定义成void类型之后,系统就会保证函数不带回任何值。但需要注意的是,此时在函数体中,要么不写return语句,要么return后面不加返回值,即仅写“return;”。

最后需要强调的是,一个函数中可以有多个return语句,但并不是所有的return语句都起作用。

return是如何将值返回给主调函数的

事实上在执行return语句时系统是在内部自动创建了一个临时变量,然后将return要返回的那个值赋给这个临时变量。所以当被调函数运行结束后return后面的返回值真的就被释放掉了,最后是通过这个临时变量将值返回给主调函数的。

函数的命名规则

1)函数的命名规则和变量的命名规则一样,都是字母、数字、下划线的组合,而且不能以数字开头,通常以字母开头。

2) 因为库函数名都是小写,所以为了与库函数区别开,自定义函数的函数名都以大写字母开头。而且如果函数名由几个英文单词组成的话,那么每个英文单词的首字母全部都要大写,必要时可用下划线间隔。

3)函数名不要使用缩写,长一点也不要紧,因为要能通过函数名就知道该函数的主要功能。但是如果项目要求使用缩写,那么必须要对该函数的功能进行注释。

函数的递归调用

函数调用是通过栈实现的。在调用函数时,系统会将被调函数所需的程序空间安排在一个栈中。每当调用一个函数时,就在栈顶为它分配一个存储区。每当从一个函数退出时就释放它的存储区。

栈是先进后出的(或者说是后进先出的),所以当有多个函数嵌套调用时,会按照先调用后返回的原则(或者说是后调用先返回的原则)进行返回。

递归也是一种函数调用,只不过是函数自己调用自己,是一种特殊的函数调用。因为递归也是函数调用,所以递归也是用栈实现的。

自己调用自己必须要满足一个条件,就是必须要知道什么时候结束调用。不然函数就会一直不停地调用,造成“死递归”。死递归就是递归的时候没有出口,不知道什么时候停下来,不停地自己调用自己,直到栈满没有地方放了为止。这时计算机也死机了。

使用递归必须要满足的两个条件:

递归的思想是:为了解决当前问题F(n),就需要解决问题F(n -1),而F(n -1)的解决依赖于F(n -2)的解决……就这样逐层分解,分解成很多相似的小事件,当最小的事件解决完之后,就能解决高层次的事件。这种“逐层分解,逐层合并”的方式就构成了递归的思想。使用递归最主要的是要找到递归的出口和递归的方式。

综上所述,使用递归必须要满足的两个条件就是:

1)要有递归公式。

2)要有终止条件。

递归和循环的关系

递归和循环存在很多关系。理论上讲,所有的循环都可以转化成递归,但是利用递归可以解决的问题,使用循环不一定能解决。

循环又称迭代。递归算法与迭代算法设计思路的主要区别在于:函数或算法是否具备收敛性!当且仅当一个算法存在预期的收敛效果时,采用递归算法才是可行的。否则就不能使用递归算法。所谓收敛性就是指要有终止条件,不能无休止地递归下去。

递归的优缺点

递归的优点是简化程序设计,结构简洁清晰,容易编程,可读性强,容易理解。

递归的缺点也很明显:速度慢,运行效率低,对存储空间的占用比循环多。严格讲,循环几乎不浪费任何存储空间,而递归浪费的空间实在是太大了,而且速度慢。因为递归是用栈机制实现的,每深入一层都要占用一块栈数据区域。对嵌套层数深的一些算法,递归就会显得力不从心,最后都会以内存崩溃而告终。而且递归也带来了大量的函数调用,这也有许多额外的时间开销。函数调用要发送实参,要为被调函数分配存储空间,还要保存返回的值,又要释放空间并将值返回给主调函数,这些都太浪费空间和时间了!

虽然递归有那么多缺点,但是没有办法,有些问题太复杂,不用递归就解决不了!

下面给大家编写个程序,主要通过这个例子让大家对递归有一个了解。

练习——用递归求n的阶乘:

解析:

n!也可以写成n×(n -1)!,这就是递归公式。

#include<stdio.h>
long Factorial(int n);  //函数声明int main(void) {int n;printf("请输入n的值:");scanf("%d", &n);printf("%d! = %ld\n", n, Factorial(n));return 0;
}long Factorial(int n) { //阶乘的英文为factorialif (n < 0) {return -1;} else if (n==0 || n==1)  /*关系运算符的优先级大于逻辑运算符的优点级,所以不用加括号*/ {return 1;} else {return n * Factorial(n-1);}
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
请输入n的值:10
10! = 3628800
--------------------------------------
*/

n的值不要太大,不然容易溢出,long类型也放不下。

我们看到,用递归编写的程序很简单,但是运行效率很低。我们说过,衡量一个算法的好坏关键是看时间复杂度和空间复杂度。递归的算法简单,但是时间复杂度和空间复杂度都很大!

比如求5! :

1)调用Factorial(),即Factorial(5),为Factorial(5)分配栈空间。但调用的结果是,“要求5!必须先求4! ”,于是将Factorial(5)压入栈中,重新调用Factorial(),即Factorial(4)。

2)为Factorial(4)分配栈空间,但调用的结果是,“要求4!必须先求3! ”,于是又将Factorial(4)压入栈中,再次调用Factorial(),即Factorial(3)。

3)为Factorial(3)分配栈空间,但调用的结果是,“要求3!必须先求2! ”,于是又将Factorial(3)压入栈中,再次调用Factorial(),即Factorial(2)。

4)为Factorial(2)分配栈空间,但调用的结果是,“要求2!必须先求1! ”,于是又将Factorial(2)压入栈中,再次调用Factorial(),即Factorial(1)。

5)为Factorial(1)分配栈空间,调用Factorial(1)的结果是return 1,终于能求出一个值了。实际上这就是求出的最小事件的值。此时栈中的位置关系为:

我们可以看出,当前运行的程序肯定是在栈顶的。Factorial(5)到Factorial(2)都曾为栈顶,因为它们都曾为“当前运行程序”。一个栈中只有一个栈顶,所以也只有一个当前运行程序,其他程序都会被压栈。

6)Factorial(1)求出1后把1返回给Factorial(2),然后释放Factorial(1)的内存空间,Factorial (2)成为栈顶,所谓释放就是出栈。

7)因为求出了Factorial(1),所以Factorial(2)就能求出2!,然后把值返回给Factorial (3),然后释放Factorial(2)的内存空间,Factorial(3)成为栈顶。

8)因为求出了Factorial(2),所以Factorial(3)就能求出3!,然后把值返回给Factorial (4),然后释放Factorial(3)的内存空间,Factorial(4)成为栈顶。

9)因为求出了Factorial(3),所以Factorial(4)也就能求出4!,然后把值返回给Factorial (5),然后释放Factorial(4)的内存空间,Factorial(5)成为栈顶。

10)因为求出了Factorial(4),所以Factorial(5)也就能求出5!,然后把值返回给main(),释放Factorial(5)的内存空间,函数调用结束。

仅仅求5!的过程就极其复杂。递归一次就要压栈一次,就要申请一块栈空间,最后得到最小事件的值后还要逐个出栈,既耗空间又耗时间。所以不到万不得已不建议使用递归。

数组名作为函数参数

数组名作为函数参数的内容如果大家理解起来觉得有点困难的话就先放一放,可以等学完指针之后再来阅读。

我先问大家一个问题:“要确定一个一维数组需要知道哪些信息?”一个是数组的首地址,另一个是数组的长度。这样就可以唯一地确定一个一维数组。因为数组是连续存放的,只要知道数组的首地址和数组的长度就能找到这个数组中所有的元素。所以要想通过实参和形参将一个数组从主调函数传到被调函数,那么只需要传递这两个信息即可。而一维数组的数组名就表示一维数组的首地址。所以只需要传递数组名和数组长度这两个参数就可以将数组从主调函数传入被调函数中。

当数组名作为函数的实参时,形参列表中也应定义相应的数组(或用指针变量),且定义数组的类型必须与实参数组的类型一致,如果不一致就会出错。但形参中定义的数组无须指定数组的长度,而是再定义一个参数用于传递数组的长度。所以在传递实参的时候,数组名和数组长度也只能用两个参数分开传递,而不能写在一起。因为即使写在一起,系统在编译时也只是检查数组名,并不会检查数组长度。所以数组长度要额外定义一个变量进行传递。

综上所述,当将数组从一个函数传到另一个函数中时,并不是将数组中所有的元素一个一个传过来(那样效率就太低了)。而是将能够唯一确定一个数组的信息传过来,即数组名(数组首地址)和数组长度。此时主调函数和被调函数操作的就是同一个数组。下面来写一个程序:

#include<stdio.h>
int AddArray(int array[], int n);  //函数声明
int main(void) {int a[] = {1, 2, 3, 4, 5, 6, 7, 8};int size = sizeof(a) / sizeof(a[0]);  /*数组所占内存总大小除以该数组中一个元素所占内
存的大小,从而得到数组元素的个数*/printf("sum = %d\n", AddArray(a, size));return 0;
}int AddArray(int array[], int n) { //形参数组中不需要写长度int i, sum = 0;for (i=0; i<n; ++i) {sum += array[i];}return sum;
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
sum = 36
--------------------------------------
*/

下面再问大家一个问题:“前面讲过,当对数组名使用sizeof时可以求出整个数组在内存中所占的字节数。那么上面这个程序中,对被调函数AddArray中的数组array使用sizeof得到的值会是多少?”这时有人会说:“实参数组a占32字节,实参a传给形参array,所以array也占32字节。”但实际上,array只占4字节。

那么这是为什么呢?因为数组名做函数参数时,只是将实参数组的“首地址”传给了形参数组。此时被调函数AddArray中的数组array本质上是一个指针变量,里面存放的是主调函数中数组a的地址。指针变量也是一个变量类型。不同于前面所讲的其他变量类型,指针变量里面存放的不是一般的数据,而是地址。在C语言中,指针变量所占的字节数都是4。所以对array使用sizeof求出的就是4(但有些显示求出的可能是8,这跟操作系统有关)。

变量的作用域和存储方式

变量按作用域可分为“局部变量”和“全局变量”。按存储方式又可分为“自动变量(auto)”、“静态变量(static)”、“寄存器变量(register)”和“外部变量(extern)”。

注意,是“自动变量”不是“动态变量”。“动态变量”和“动态存储”比较复杂,我们稍后会详细介绍。

什么叫“寄存器”?我们知道,内存条是用来存储数据的,硬盘也是存储数据的,而在CPU内部也有一些用来存储数据的区域,即寄存器。寄存器是CPU的组成部分,是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。它同内存一样,只不过它能存储的数据要少得多。

以上这些类型的变量中,要着重掌握“局部变量”和“全局变量”, “自动变量(auto)”、“静态变量(static)”、“寄存器变量(register)”和“外部变量(extern)”了解即可,在后面的编程中可用可不用,就算使用也很简单。

局部变量:

局部变量是定义在函数内部的变量,全局变量是定义在函数外部的变量。局部变量只在本函数内有效,即只有在本函数内才能使用,在本函数外不能使用。不同函数中可以定义同名的变量,但它们所表示的是不同的对象,互不干扰。

在一个函数内部,可以在复合语句中定义变量,这些变量只在本复合语句中有效,离开本复合语句就无效,且内存单元随即被释放。所谓复合语句就是用大括号“{}”括起来的多个语句。

局部变量的作用范围准确地说不是以函数来限定的,而是以大括号“{}”来限定的。在一个大括号中定义的局部变量就只能在这个大括号中使用,但因为每个函数体都是用大括号括起来的,而且我们一般也不会在函数体中的其他大括号中定义变量,所以习惯上就说“局部变量是定义在‘函数'内部的变量,只在本‘函数'内有效”。

全局变量:

定义在函数内部的变量叫作局部变量,定义在函数外部的变量叫作全局变量。局部变量只能在该函数内才能使用;而全局变量可以被整个C程序中所有的函数所共用。它的作用范围是从定义的位置开始一直到整个C程序结束。所以根据定义位置的不同,全局变量的作用范围不同。

在一个函数中既可以使用本函数中的局部变量,也可以使用有效的全局变量。但这时要注意一个问题,即“全局变量和局部变量命名冲突的问题”。前面说过,不同函数中可以定义同名的变量,但它们代表的是不同的对象,所以互不干扰,原因是局部变量的作用范围不同。但是全局变量和局部变量的作用范围可能有重叠,这时局部变量和全局变量的名字就不能定义成相同的了,否则就会互相干扰。如果在同一个作用范围内局部变量和全局变量重名,则局部变量起作用,全局变量不起作用。这样局部变量就会屏蔽全局变量。

如果局部变量未初始化,那么系统会自动将“-858993460”放进去。但如果全局变量未初始化,那么系统会自动将其初始化为0。局部变量是在栈中分配的,而全局变量是在静态存储区中分配的。只要是在静态存储区中分配的,如果未初始化则系统都会自动将其初始化为0。

设置全局变量的作用是增加函数间数据联系的渠道。因为全局变量可以被多个函数所共用,这样就可以不通过实参和形参向被调函数传递数据。所以利用全局变量可以减少函数中实参和形参的个数,从而减少内存空间传递数据时的时间消耗。但是除非是迫不得已,否则不建议使用全局变量。

为什么不建议使用全局变量?

1)全局变量在程序的整个执行过程中都占用存储单元,而局部变量仅在需要时才开辟存储单元。

2)全局变量降低了函数的通用性,因为函数在执行时要依赖那些全局变量。

在程序设计中,划分模块时要求模块的“内聚性”强、与其他模块的“关联性”弱。一般要求把C程序中的函数做成一个封闭体,除了可以通过“实参-形参”的渠道与外界发生联系外,没有其他渠道。这样的程序可移植性好,可读性强,而使用全局变量会使程序各函数之间的关系变得极其复杂。

3)过多的全局变量会降低程序的清晰性。

4)如果在同一个程序中,全局变量与局部变量同名,则在局部变量的作用范围内,全局变量就会被“屏蔽”,即它不起作用。

自动变量(auto)

前面定义的局部变量其实都是auto型,只不过auto可以省略,因为省略就默认是auto型。

静态变量(static)

static可以用于修饰局部变量,也可以用于修饰全局变量。

用statci修饰过的局部变量称为静态局部变量。局部变量如果不用static进行修饰,那么默认都是auto型的,即存储在栈区。而定义成static之后就存储在静态存储区了。前面说过,存储在静态存储区中的变量如果未初始化,系统会自动将其初始化为0。

静态存储区主要用于存放静态数据和全局数据。存储在静态存储区中的数据存在于程序运行的整个过程中。所以静态局部变量不同于普通局部变量,静态局部变量是存在于程序运行的整个过程中的。

全局变量默认都是静态的,都是存放在静态存储区中的,所以它们的生存周期固定,都是存在于程序运行的整个过程中。所以一个变量生命周期的长短本质上是看它存储在什么地方。存储在栈区中的变量,在函数调用结束后内存空间就会被释放;而存储在静态存储区中的变量会一直存在于程序的整个运行过程中。这就是局部变量和全局变量生命周期不同的原因。

虽然全局变量本身就是存储在静态存储区的,但它仍然可以用static进行修饰。而且修饰和不修饰是有区别的。用static修饰全局变量时,会限定全局变量的作用范围,使它的作用域仅限于本文件中。这个是使用static修饰全局变量的主要目的。那么不用static修饰,全局变量不也是只能在本文件中使用吗?这么说不完全对,因为虽然全局变量的作用范围不会自己主动作用到其他文件中,但不代表其他文件不会使用它。如果不用static进行修饰,那么其他文件只需要用extern对该全局变量进行一下声明,就可以将该全局变量的作用范围扩展到该文件中。但是当该全局变量在定义时用static进行修饰后,那么其他文件不论通过什么方式都不能访问该全局变量。

而且如果一个项目的多个.c文件中存在同名的全局变量,那么在编译的时候就会报错,报错的内容是“同一个变量被多次定义”。但是如果在这些全局变量前面都加上static,那么编译的时候就不会报错。因为用static修饰后,这些全局变量就只属于各自的.c文件了,它们是相互独立的,所以编译的时候就不会发生冲突而产生“多次定义”的错误。所以使用static定义全局变量是非常有用的,因为当一个项目中有很多文件的时候,重名不可避免。这时候只要在所有的全局变量前面都加上static就能很好地解决这个问题。定义全局变量时用static进行修饰可以大大提高代码的质量。

寄存器变量(register)

无论是存储在静态存储区中还是存储在栈区中,变量都是存储在内存中的。当程序用到哪个变量的时候,CPU的控制器就会发出指令将该变量的值从内存读到CPU里面。然后CPU再对它进行处理,处理完了再把结果写回内存。

但是除了内存可以存储数据之外,CPU内部也有一些用来存储数据的区域,这就是寄存器。寄存器是CPU的组成部分,是CPU内部用来存放数据的小型存储区域,用来暂时存放参与运算的数据和运算结果。与内存相比,寄存器所能存储的数据要小得多,但是它的存取速度要比内存快很多。

那为什么寄存器的存取速度比内存快呢?最主要的原因是因为它们的硬件设计不同。

计算机中硬件运行速度由快到慢的顺序是:寄存器 > 缓存 > 内存 > 固态硬盘 > 机械硬盘

为了提高代码执行的效率,可以考虑将经常使用的变量存储到寄存器中。比如循环变量和循环体内每次循环都要使用的局部变量。这种变量叫作寄存器变量,用关键字register声明。如:

    register   int   a;

但是需要注意的是,register关键字只是请求编译器尽可能地将变量存储在CPU内部的寄存器中,但并不一定。因为我们说过,寄存器所能存储的数据是很少的,所以如果寄存器已经满了,那么即使你用register进行声明,数据仍然会被存储到内存中。而且需要注意的是,并不是所有的变量都能定义成寄存器变量,只有局部变量才可以。或者说寄存器变量也是一个局部变量,在函数调用结束后就会被释放。

register看起来很有用,但是要跟大家说的是,这个修饰符现在已经不用了。现在的编译器都比较智能化,它会自动分析,即使变量定义的是auto型(默认的),但如果它发现这个变量经常使用,那么它也会把这个变量放到寄存器中。

这时有些人就提出一个疑问:“既然寄存器速度那么快,那么为什么不把计算机的内存和硬盘都改成寄存器?”这个问题问得非常好!我们前面说过,寄存器之所以比内存速度快,最主要的原因是因为它们的硬件设计不同。从硬件设计的角度来看,寄存器的制作成本要比内存高很多!而且寄存器数量的增加对CPU的性能也提出了极高的要求,而这往往是很难实现的。

外部变量(extern)

extern变量是针对全局变量而言的。通过前面了解到,全局变量都是存放在静态存储区中的,所以它们的生存期是固定的,即存在于程序的整个运行过程中。但是对于全局变量来说,还有一个问题尚待解决,就是它的作用域究竟从什么位置起,到什么位置结束。作用域是包括整个文件范围,还是只包括文件中的一部分?是在一个文件中有效,还是在程序的所有文件中都有效?

一般来说,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾结束。在此作用域内,全局变量可以被程序中各个函数所引用。但是有时程序设计人员希望能扩展全局变量的作用域,如以下几种情况:

(1)在一个文件内扩展全局变量的作用域

如果全局变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该全局变量。如果出于某种考虑,在定义点之前的函数需要引用该全局变量,那么就在引用之前用关键字extern对该变量作“外部变量声明”,表示将该全局变量的作用域扩展到此位置,比如:

    extern   int   a;

有了此声明就可以从“声明”处起,合法地使用该全局变量了。

(2)将外部变量的作用域扩展到其他文件

一个C程序可以由一个或多个.c文件组成。如果程序只由一个.c文件组成,那么使用全局变量的方法前面已经介绍了。如果程序由多个.c文件组成,那么如何在一个文件中引用另一个文件中定义的全局变量呢?假如在1.c中定义了全局变量“int a=10; ”,如果2.c和3.c也想使用这个变量a,而我们不能在2.c和3.c中重新定义这个a,否则在编译链接时就会出现“重复定义”的错误。正确的做法是在2.c和3.c中分别用extern对a作“外部变量声明”,即在2.c和3.c中使用a之前作如下声明:

    extern   int   a;

但是现在问大家一个问题:“以2.c为例,如果在2.c中对a进行多次声明,即写多个“extern int a; ”,那么程序会不会有错?”答案是不会,C语言中是允许多次声明的,但有效的只有一个。

与全局变量一样,同一个项目的不同.c文件中也不能定义重名的函数。如果2.c和3.c想要使用1.c中定义的函数,那么也只需要在2.c和3.c中分别对该函数进行声明就可以了。即直接把1.c中对函数的声明拷贝过来就行了。而与全局变量不同的是,它的前面不用加extern。因为对于函数而言,默认就是extern,所以声明时前面的extern可以省略。此外在实际编程中,我们一般不会直接把1.c中某个函数的声明拷贝到2.c和3.c中,而是把该声明写在一个.h头文件中,然后分别在2.c和3.c中用# include包含该头文件即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/536832.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java-集合工具类Collections

我们在使用它的时候记得导包 常见API 我们就简单看看第一第二个方法&#xff0c;代码如下&#xff0c;其余的知道用就行

LDA主题模型学习笔记

&#xff08;1&#xff09;LDA的基本介绍&#xff08;wiki&#xff09; LDA是一种典型的词袋模型&#xff0c;即它认为一篇文档是由一组词构成的一个集合&#xff0c;词与词之间没有顺序以及先后的关系。一篇文档可以包含多个主题&#xff0c;文档中每一个词都由其中的一个主题…

STM32第八节:位带操作——GPIO输出和输入

前言 我们讲了GPIO的输出&#xff0c;虽然我们使用的是固件库编程&#xff0c;但是最底层的操作是什么呢&#xff1f;对&#xff0c;我们学习过51单片机的同学肯定学习过 sbit 修改某一位的高低电平&#xff0c;从而实现对于硬件的控制。那么我们现在在STM32中有没有相似的操作…

前端面试 ===> 【Vue2】

Vue2 相关面试题总结 1. 谈谈对Vue的理解 Vue是一种用于构建用户页面的渐进式JavaScript框架&#xff0c;也是一个创建SPA单页面应用的Web应用框架&#xff0c;Vue的核心是 数据驱动试图&#xff0c;通过组件内特定的方法实现视图和模型的交互&#xff1b;特性&#xff1a;&a…

pkav之当php懈垢windows通用上传缺陷

环境&#xff1a; Windowsnginxphp 一、php源码 <?php //U-Mail demo ... if(isset($_POST[submit])){$filename $_POST[filename];$filename preg_replace("/[^\w]/i", "", $filename);$upfile $_FILES[file][name];$upfile str_replace(;,&qu…

清华把大模型用于城市规划,回龙观和大红门地区成研究对象

引言&#xff1a;参与式城市规划的新篇章 随着城市化的不断推进&#xff0c;传统的城市规划方法面临着越来越多的挑战。这些方法往往需要大量的时间和人力&#xff0c;且严重依赖于经验丰富的城市规划师。为了应对这些挑战&#xff0c;参与式城市规划应运而生&#xff0c;它强…

【文献阅读】A Fourier-based Framework for Domain Generalization(基于傅立叶的领域泛化框架)

原文地址&#xff1a;https://arxiv.org/abs/2105.11120 摘要 现代深度神经网络在测试数据和训练数据的不同分布下进行评估时&#xff0c;存在性能下降的问题。领域泛化旨在通过从多个源领域学习可转移的知识&#xff0c;从而泛化到未知的目标领域&#xff0c;从而解决这一问…

面试复盘记录(数据开发)

一、apple外包1.矩阵顺时针旋转遍历2.两表取差集 二、 一、apple外包 没问理论&#xff0c;就两个算法题。 1.矩阵顺时针旋转遍历 Given an m x n matrix, return all elements of the matrix in spiral order.Example 1:Input: matrix [[1,2,3],[4,5,6],[7,8,9]] Output: …

【LeetCode热题100】141. 环形链表(链表)

一.题目要求 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置…

基于FPGA的图像锐化算法(USM)设计

免费获取源码请关注微信号《FPGA学习笔记册》&#xff01; 1.图像锐化算法说明 图像锐化算法在实际的图像处理应用很广泛&#xff0c;例如&#xff1a;医学成像、工业检测和军事领域等&#xff1b;它的作用就是将模糊的图像变的更加清晰。常用的图像锐化算法有拉普拉斯算子、s…

基于SpringCache实现数据缓存

SpringCache SpringCache是一个框架实现了基本注解的缓存功能,只需要简单的添加一个EnableCaching 注解就能实现缓存功能 SpringCache框架只是提供了一层抽象,底层可以切换CacheManager接口的不同实现类即使用不同的缓存技术,默认的实现是ConcurrentMapCacheManagerConcurren…

SpringBoot(Lombok + Spring Initailizr + yaml)

1.Lombok 1.基本介绍 2.应用实例 1.pom.xml 引入Lombok&#xff0c;使用版本仲裁 <!--导入springboot父工程--><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version&g…