七、数组
在C语言中,数组属于构造数据类型。
数组根据元素的类型不同,数组又可以分为 数值数组
、字符数组
、指针数组
、结构数组
等各种类别。
本章介绍 数值数组
和 字符数组
。
1. 一维数组的定义和引用
定义: 类型说明符 数组名 [常量表达式];
例如:int a[10];
float b[10],c[20];
char ch[20];
下标从 0 开始;
不能在 []
中使用变量来表示元素的个数,但是可以是符号常数或常量表达式。
引用:数组名[下标]
其中下标只能为整型常量或整数表达式。
如为小数时,C编译将自动取整。。
初始化
给数组赋值的方法除了用赋值语句对数组元素逐个赋值外,还可以采用初始化赋值和动态赋值的方法。
初始化赋值形式举例: int a[10]={0,1,2,3,4,5,6,7,8,9};
C语言对数组的初始化赋值有以下几点规定:
1)可以只给部分元素赋初值。当
{}
中的值的个数少于元素个数时,只给前面部分元素赋值。
例如:int a[10]={0,1,2,3};
表示只给前面 4个元素赋值。而后面的值自动赋 0 值。2)只能给元素逐个赋值,不能给数组整体赋值。例如,给10个元素全部赋1值,只能写为:
int a[5]={1,1,1,1,1};
而不能写为int a[5]=1;
- 如给全部元素赋值,则在数组说明中,可以不给出数组元素的个数。例如:
int a[5]={1,2,3,4,5};
可以写成int a[]={1,2,3,4,5};
- 如给全部元素赋值,则在数组说明中,可以不给出数组元素的个数。例如:
2. 一维数组程序举例:
main() {
int i,max,a[10];
printf("input 10 numbers:\n");
for (i=0;i<10;i++)
scanf("%d",&a[i]);
max=a[0];
for (i=1;i<10;i++)
if(a[i]>max) max=a[i];
printf("maxmum=%d\n",max);
}
例2:排序
main() {
int i,j,p,q,s,a[10];
printf("input 10 numbers:\n");
for (i=0;i<10;i++)
scanf("%d",&a[i]);
for (i=0;i<10;i++) {
p=i;q=a[i];
for(j=i+1;j<10;j++)
if(q<a[j]) {p=j;q=a[j];}
if(i!=p) {
s=a[i];
a[i]=a[p];
a[p]=s;
}
printf("%d",a[i]);
}
}
3. 二维数组的定义和引用
定义:类型说明符 数组名[常量表达式1][常量表达式2]
表达式1
为第一维下标的长度,
表达式2
是第二维下标的长度
例如:int a[3][4];
说明了一个三行四列的数组,数组名为 a,其下标变量的类型为整型。
该数组的下标变量共有 3*4 个。
二维数组在概念上是二维的,但在实际的硬件存储器中却是连续编址的,也就是说存储器单元是按一维线性排列的。
如何在一维存储器中存放二维数组,可有两种方式:
一种是按行排列,即放完一行之后顺次放入第二行。
另一种是按列排列,即放完一列后再顺次放入第二列。
在C语言中,二维数组是按行排列的。即:先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行。
二维数组的初始化
可以按行分段赋值,也可以按行连续赋值。
例如:
int a[5][3]={{80,75,92},{61,65,71},{59,63,80},...};
int a[5][3]={80,75,92,61,65,59,....};
对于二维数组初始化赋值的说明:
1)可以只对部分元素赋初值,未赋初值的元素自动取0值。
例如:
int a[3][3]={{1},{2},{3}};
是对每一行的第一列元素赋初值,未赋值的元素为0.2)如对全部元素赋初值,则第一维的长度可以不给出:例如:
int a[3][3]={1,2,3,4,5,6,7,8,9};
可以写为
int a[][3]={1,2,3,4,5,6,7,8,9};
- 数组是一种构造类型的数据。
二维数组可以看作是一维数组的嵌套而构成的,设一维数组的每个元素都又是一个数组,就组成了二维数组。
当然,前提是各元素类型必须相同。
根据这样的分析,一个二维数组也可以分解成多个一维数组。
C语言允许这种分解。如二维数组a[3][4]
可以分解为 3 个一维数组,其数组名分别为:a[0]
a[1]
a[2]
,对这 3 个一维数组不需要另作说明就可以使用。
必须强调的是a[0]
a[1]
a[2]
不能当作下标变量使用,他们是数组名,而不是一个单纯的下标变量。
- 数组是一种构造类型的数据。
4. 字符数组:
char c[10];
由于字符型和整型通用,也可以定义为 int c[10]
,但这时每个数组元素占2个字节的内存单元。
字符数组初始化的时候未赋初值的元素也是0值。
同样也可以在赋初值时省去数组长度。
5. 字符串和字符串结束标志:
在C语言中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。
前面介绍字符串常量时,已说明字符串总是以 \0
做为串的结束符。
因此当把一个字符串存入一个数组时,也把结束符'\0'存入数组,并以此做为字符串是否结束的标志。
有了 \0
标志后,就不必再用字符数组的长度来判断字符串的长度了。
C语言允许用字符串的方式对数组作初始化赋值。例如:
char c[]=('C',' ','p',... };
可写为: char c[]={"C program"};
或去掉 {}
,写为 char c[]="C program";
\0
是C编译系统自动加上的。
由于采用了 \0
标志,所以在用字符串赋初值时一般无需指定数组的长度,而由系统自行处理。
6. 字符数组的输入输出:
在采用字符串方式后,字符数组的输入输出将变得简单方便。
除了上述用字符串赋初值的办法外,还可以用 printf函数
和 scanf函数
一次性输出输入一个字符数组中的字符串,而不必使用循环语句逐个地输入输入每个字符。例如:
main() {
char c[]="BASIC\nBASE";
printf("%s\n",c);
}
注意格式字符串 %s
表示输出的是一个字符串,而在输出表列中给出数组名即可,不能写为 printf("%s\n",c[]);
又如:
main() {
char st[15];
printf("input string:\n");
scanf("%s",st);
printf("%s\n",st);
}
本例中字符数组长度为15,因此输入的字符串长度必须小于15,以留出一个字节用于存放结束标志'\0'。
应该说明的是,对一个字符数组,如果不作初始化赋值,则必须说明数组长度。
还应该特别注意的是,当用 scanf函数
输入字符串时,字符串中不能含有 空格
,否则将以 空格
做为串的结束符。
例如,当输入的字符串含有空格时,运行情况为:
input string:
this is a book
输出为:
this
从输出结果可以看出空格以后的字符都未能输出。
为了避免这种情况,可多设几个字符数组分段存放含有空格的串。
程序改写如下:
main() {
char st1[6],st2[6],st3[6],st4[6];
printf("input string:\n");
scanf("%s%s%s%s",st1,st2,st3,st4);
printf("%s %s %s %s\n",st1,st2,st3,st4);
}
前面介绍过,scanf
的各输入项必须以地址方式出现,如&a &b
等。
但是现在却是以数组名方式出现的,这是为什么呢?
这是由于C语言中规定,数组名就代表了该数组的首地址。
整个数组是以首地址开头的一块连续的内存单元。如有字符数组 char c[10]
,在内存可表示如下: c[0] c[1] ... c[9]
设数组c的首地址为2000,也就是说 c[0]
单元地址为2000.则数组名c就代表这个首地址。
因此在c前面不能再加地址运算符 &
。
在执行 printf("%s",c)
时,按 数组名c
找到首地址,然后逐个输出数组中的各个字符直到遇到字符串终止地址 \0
为止。
7. 字符串处理函数
使用输入输出的字符串函数时需要包含头文件 stdio.h
,使用其他字符串函数应包含头文件 string.h
。
(1)字符串输出函数puts (字符数组名)
如 char c[]="BASIC\ndBASE"; puts(c);
从上面的例子可以看出puts函数中可以使用转义字符,因此输出结果为两行。
puts函数
完全可以由 printf函数
取代。
当需要一定格式输出时,通常使用 printf函数
。
(2)字符串输入函数gets (字符数组名)
例如:
#include "stdio.h"
main() {
char st[15];
printf("input string:\n");
gets(st);
puts(st);
}
可以看出当输入的字符串中含有空格时,输出仍为全部字符串。
说明 gets函数
并不以空格做为字符串输入结束的标志,而只以回车做为输入结束。
这是与 scanf函数
不同的。
(3)字符串连接函数strcat (字符数组名1,字符数组名2);
功能是把 字符数组2 中的字符串连接到 字符数组1 中字符串的后面,并删去字符串1后的串标志 \0
。
本函数返回值是 字符数组1 的首地址。
例子:
#include "string.h"
main() {
static char st1[30]="My name is ";
int st2[10];
printf("input your name:\n");
gets(st2);
strcat(st1,st2);
puts(st1);
}
上面的例子是初始化赋值的字符数组与动态赋值的字符串连接起来,要注意的是,字符数组1应定义足够的长度,否则不能全部装入被连接的字符串。
(4)字符串拷贝函数strcpy (字符数组名1,字符数组名2);
功能:把 字符数组2 中的字符串拷贝到 字符数组1 中。
串结束标志 \0
也一同拷贝。
例子:
#include "string.h"
main() {
char st1[15],st2[]="C Language";
strcpy(st1,st2);
puts(st1);
printf("\n");
}
注意 字符数组1 应有足够的长度,否则不能全部装入所拷贝的字符串。
(5)字符串比较函数 strcmp(字符数组名1,字符数组名2);
功能:按照ASCII码顺序比较两个数组中的字符串,并由函数返回值返回比较结果。
字符串1=字符串2,返回0
字符串1>字符串2,返回值>0
字符串1<字符串2,返回值<0
(6)测字符串长度函数strlen(字符数组名)
功能:返回字符串的实际长度,不包含 \0
的长度
例子:
#include <string.h>
main() {
int k;
static char st[]="C language";
k=strlen(st);
printf("The length of the string is %d\n",k);
}
8. 程序举例:
- 1)把一个整数按大小顺序插入已排好序的数组中。
- 2)在二维数组a中选出各行最大的元素组成一个一维数组b。
- 3)输入5个国家的名称按字母顺序排列输出。
第八章 函数
1. 函数分类
- 从函数的角度看,函数可分为
库函数
和用户自定义函数
两种。
- 从函数的角度看,函数可分为
- C语言的函数兼有其他语言中的 函数和过程 两种功能,从这个角度看,又可把函数分为
有返回值函数
和无返回值函数
。
- C语言的函数兼有其他语言中的 函数和过程 两种功能,从这个角度看,又可把函数分为
- 从 主调函数和被调函数 之间数据传送的角度看又可分为
无参函数
和有参函数
两种。
- 从 主调函数和被调函数 之间数据传送的角度看又可分为
2. 库函数
C语言提供了极为丰富的库函数,这些库函数又可从功能角度分为以下几类:
(1)字符类型分类函数
用于对字符按ASCII码分类:字母、数字、控制字符、分隔符,大小写字母等。
(2)转换函数
用于字符或字符串的转换;
在字符量和各类数字量(整型、实型等)之间进行转换;
在大、小写之间进行转换。
(3)目录路径函数
用于文件目录和路径操作。
(4)诊断函数
用于内部错误检测。
(5)图形函数
用于屏幕管理和各种图形功能。
(6)输入输出函数
用于完成输入输出功能。
(7)接口函数
用于与DOS、BIOS和硬件的接口。
(8)字符串函数
用于字符串的操作和处理
(9)内存管理函数
用于内存管理
(10)数学函数
用于数学函数计算
(11)日期和时间函数
用于日期、时间转换操作。
(12)进程控制函数
用于进程管理和控制。
(13)其他函数
用于其他各种功能
大部分函数需要自己去查阅相关手册。
函数可以递归调用,但不允许嵌套声明,即函数体内不允许再声明函数。
另外,main是主函数,他可以调用别的函数,但别的函数不能调用他。
因此,C程序总是从main函数开始。
3. 函数定义的一般形式:
函数的定义形式:
类型标识符 函数名(形式参数列表) {
声明部分
语句
使用return返回值
}
例如:
int max(int a,int b) {
if (a>b) return a;
else return b;
}
在主函数中调用上面声明的函数:
main() {
int max(int a,int b);
int x,y,z;
printf("input two numbers:\n");
scanf("%d%d",&x,&y);
z=max(x,y);
}
在main函数中对要调用的函数进行函数说明,函数说明和函数定义是两回事
4. 函数的返回值
函数的返回值通过 return 表达式;
或 return (表达式);
返回,
返回类型以函数类型为准,不一致时系统自动进行类型转换。
如果函数值为 整型,在函数定义时可以省去类型说明
不返回函数值的函数,可以明确定义为“空类型”-void,一旦定义函数为空类型后,就不能在主调函数中使用被调函数的函数值了。
为了使程序有良好的可读写并减少出错,凡不要求返回值的函数都应定义为空类型。
5. 函数调用的方式:
在C语言中,可以用以下几种方式调用函数:
- 1)函数表达式: 例如
z=max(x,y);
- 2)函数语句: 例如:
printf("%d",a); scanf("%d",&b);
- 3)函数实参:例如
printf("%d",max(x,y));
这里要注意的一个问题是求值顺序的问题:
所谓求值顺序是指对实参表中各量是自左至右使用呢?还是自右至左使用?
对此,各系统的规定不一定相同。
介绍printf函数已提到过,这里从函数调用的角度再强调一下:
main() {
int i=8;
printf("%d %d %d %d",++i,--i;i++;i--);
}
如按照 从右至左 的顺序求值是 8 7 7 8,从左至右的顺序求值是 9 8 8 9,
TurboC是自右至左,如果不清楚,上机一试便可知晓。
6. 被调用函数的声明和函数原型
在主调函数中调用某函数之前应对该被调用函数进行说明(声明),这与使用变量之前要先进行变量说明是一样的。
在主调函数中对被调函数做说明的目的是使编译系统知道被调函数返回值的类型,以便在主调函数中按此种类型对返回值做相应的处理。
其一般形式为:
类型说明符 被调函数名(类型 形参,类型 形参...);
或
类型说明符 被调函数名(类型,类型...);
括号内给出了形参的类型和形参名,或只给出形参类型。
这便于编译系统进行检错,以防止可能出现的错误。
C语言又规定在以下几种情况可以省去主调函数中对被调函数的函数说明:
1)如果被调函数的返回值是整型或字符型时,可以不对被调函数做说明,而直接调用。
这时系统将自动对被调函数返回值按整型处理。2)当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再做说明而直接调用。
3)如在所有函数定义之前,在函数外预先说明了各个函数的类型,则在以后的各主调函数中,可不再对被调函数做说明。
4)对库函数的调用不需要再做说明,但必须把头文件
#include
一下。
7. 函数的递归调用
如 计算 n!
,著名的 Hanoi塔问题 等都是使用递归调用的很好例子:
Hanoi塔:
一块板上有3根针,A,B,C。
A针上套有64个大小不等的圆盘,大的在下,小的在上。
要把这64个圆盘从A针移到C针上,每次只能移动一个圆盘,移动可以借助B针进行。
但在任何时候,任何针上的圆盘都必须保持大盘在下,小盘在上。
求移动的步骤。
本题算法分析如下:
设A上有n个盘子
如果n=1,则将圆盘从A移动到C
如果n=2,则
- 1)将A上的n-1(等于1)个圆盘移动到B上。
- 2)再将A上的一个圆盘移动到C上
- 3)最后将B上的n-1(等于1)个圆盘移动到C上。
如果n=3,则:
- A)将A上的n-1(等于2,令其为n)个圆盘移动到B(借助于C),步骤如下:
- 1)将A上的n-1(等于1)个圆盘移动到C上。
- 2)将A上的一个圆盘移动到B
- 3)将C上的n-1(等于1)个圆盘移动到B。
- B)将A上的一个圆盘移动到C
- C)将B上的n-1(等于2,令其为n)个圆盘移动到C(借助A),步骤如下:
- 1)将B上的n-1(等于1)个圆盘移到A
- 2)将B上的一个盘子移动C
- 3)将A上的n-1(等于1)个圆盘移动到C。
到此,完成了3个圆盘的移动过程。
从上面的分析可以看出,当n>=2时,移动的过程可以分解为3个步骤:
第一步 把A上的n-1个圆盘移到B上
第二步 把A上的一个圆盘移动C上
第三步 把B上的n-1个圆盘移到C上;其中第一步和第三步是类同的。
当n=3时,第一步和第三步又分解为类同的三步,即把n-1个圆盘从一个针移到另一个针上,这里的n=n-1.显然这是一个递归过程,据此算法可编程如下:
move(int n,int x,int y,int z) {
if (n==1)
printf("%c-->%c\n",x,z);
else {
move(n-1,x,z,y);
printf("%c-->%c\n",x,z);
move(n-1,y,x,z);
}
}
main() {
int h;
printf("\ninput number:\n");
scanf("%d",&h);
printf("the step to moving %2d diskes:\n",h);
move(h,'a','b','c');
}
8. 数组做为函数参数
数组用作函数参数有两种形式:
一种是把数组元素(下标变量)做为实参使用;
另一种是把数组名做为函数的形参和实参使用。
- 1)数组元素做为函数实参:与普通变量并无区别。
- 2)数组名做为函数参数:与普通变量有所区别:
- a)用数组名做函数参数时,要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。
- b)数组名做函数参数时所进行的传送只是地址的传送,实际上形参数组和实参数组为同一数组,共同拥有一段内存空间。
- c)这样当形参数组发生变化时,实参数组也会随之变化。当然这种情况不能理解为发生了“双向”的值传递。
使用数组名做为函数参数时应注意:
- a)形参数组和实参数组的类型必须一致,否则将引起错误。
- b)形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。
当形参数组的长度与实参数组的长度不一致时,虽不至于出现语法错误,但程序执行结果往往与实际不符合,这是应该予以注意的。
解决办法是在函数声明时不指明数组长度,或再加一个参数n,让调用者指定数组长度。
9. 局部变量和全局变量
-
全局变量:在函数外部定义的变量。他不属于哪一个函数,他属于一个源程序文件。
其作用域为整个源程序。在函数中使用全局一般应作全局变量说明。
只有在函数内经过说明的全局变量才能使用。
全局变量的说明符为extern。
但在一个函数之前定义的全局变量,在函数内可不再加以说明。如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量被“屏蔽”,即他不起作用。
10. 变量的存储类别
静态存储方式和动态存储方式
静态存储方式:是指在程序运行期间分配固定的存储空间的方式
动态存储方式:是在程序运行期间根据需要进行动态的分配存储空间的方式。
用户存储空间可以分为3个部分:
- 1)程序区
- 2)静态存储区
- 3)动态存储区
全局变量全部存放在 静态存储区
,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。
在程序执行过程中他们占据固定的存储单元,而不动态地进行分配和释放;
动态存储区存放以下数据:
- 1)函数形式参数
- 2)自动变量(未加static声明的局部变量)
- 3)函数调用时的现场保护和返回地址
对以上这些数据,在函数开始调用时分配动态存储空间,函数结束时释放这些空间。
在c语言中,每个变量和函数有两个属性:数据类型和数据的存储类别。
11. auto变量
函数中的局部变量,如不专门声明为 static
存储类别,都是动态地分配存储空间的,数据存储在动态存储区内。
函数中的形参和在函数中定义的变量都属此类,在调用该函数时系统会给他们分配存储空间,在函数调用结束时就自动释放这些存储空间。
这类局部变量被称为自动变量。
自动变量使用 关键字auto
作存储类别的声明。
例如:
int f(int a) {
auto int b,c=3;//定义b,c为自动变量
...
}
a是形参,b,c是自动变量,对c赋初值3.
执行完f函数后,自动释放a,b,c所占的存储单元。
关键字auto
可以省略,auto不写则隐含定为“自动存储类别”,属于动态存储方式。
12. 用static声明局部变量:
有时希望函数中的局部变量的值在函数调用结束后不消失而保留原值,这时就应该指定局部变量为“静态局部变量”,用 关键字static
进行声明。
f(int a) {
auto b=0;
static c=3;
b=b+1;
c=c+1;
return (a+b+c);
}
main() {
int a=2,i;
for (i=0;i<3;i++)
printf("%d",f(a));
}
对静态局部变量的说明:
- 1)静态局部变量属于静态存储类别,在静态存储区内分配存储单元。
在程序整个运行期间都不释放。 - 2)静态局部变量在编译时赋初值,即只赋初值一次;
- 3)如果在定义静态局部变量时不赋初值的话,编译时就自动赋初值0或空字符。
而对自动变量来说,不赋初值的话,那么他的值是不确定的。
13. register变量:
为了提高效率,C语言允许将局部变量的值放在CPU中的寄存器中,这种变量叫“寄存器变量”,使用 关键字register
作声明。
例如:
int fac(int n) {
register int i,f=1;
for(i=1;i<=n;i++)
f*=i;
return f;
}
main() {
int i;
for (i=0;i<=5;i++)
printf("%d!=%d\n",i,fac(i));
}
说明:
- 1)只有局部自动变量和形式参数可以做为寄存器变量
- 2)一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量。
- 3)局部静态变量不能定义为寄存器变量。
14. 用extern声明外部变量。
外部变量(即全局变量)是在函数的外部定义的,他的作用域是从变量定义处开始,到本程序文件的末尾。
如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件终了。
如果在定义点之前想引用该外部变量,则应该在引用之前用关键字extern对该变量作“外部变量声明”。
表示该变量是一个已经定义的外部变量。
有了此声明,就可以从“声明”处起,合法地使用该外部变量。