printf("%dt",b[i]);
printf(“n”);
}
【运行结果】
5 13 24 35 46 78 85 88 76 31
24 46 78 88 76
【例7.18】将字符串a的字符按顺序存放在b串中,再把a中的字符按逆序连接到b串
的后面。
【解题思路】
利用指针移动逐个取出第一个数组中的每个元素存入,再利用指针从a串中的最后一个
字符取值放入串b中。
·107·
【程序代码】
#include
#include
void main()
{ char a[10],b[10],*p,*q;
printf(“请输入a串的内容:n”);
gets(a);
for(p=a,q=b;*p!= '0';p++,q++)
*q=*p;
for(p--;p>=a;p--,q++)
*q=*p;
*q=’0’;
puts(b);
}
【运行结果】
输入:asdf
输出:asdffdsa
7.5 本章小结
指针是C语言的重要特征之一,从本质上看,指针即变量在内存中的存储地址,它的使
用可以使编程人员直接对内存地址进行操作提高程序的运行速度,还可以使用动态分配内存
空间。
通过本章的学习,读者应该掌握以下内容:
1.指针的概念。每个变量都有地址(即指针),指针变量用于是存放其他变量的地址。
2.指针变量的使用。指针变量与普通变量一样,要先定义后使用。其定义的类型取决
于其所指向变量的类型。定义后指针变量要被赋值,其值为其所指向变量的地址,即指针变
量=&变量;或指针变量1=指针变量2(指针变量2应为已有指向的指针变量)。
3.指针变量的引用。变量可以通过变量名直接访问,也可以利用指针间接访问,“*指
针变量”表示指针变量所指向的变量。
4.指针与一维数组。当指针变量p指向一维数组时,可以通过p值的增减访问数组的
各个元素。
5.指针与二维数组。二维数组的指针可以分为列指针和行指针。列指针为数组元素地
址,可以直接定位到数组中的某一个元素。行指针为各行首地址,只能在行间移动。二维数
组名是行指针常量。
6.指针能够提高程序效率,可以实现动态存储分配。
指针具有使用灵活的特点,使得熟练的程序人员可以利用它编写出颇有特色的、质量优
良的程序,实现许多用其它高级语言难以实现的功能,但也十分容易出错。由于指针运用的
错误甚至会引起整个程序的破坏。如未给指针变量p赋值就往*p赋值,就可能破坏了有用单
·108·
元的内容。因此,使用指针要十分谨慎,多积累经验。
7.6 实训
实训1
【实训内容】指针和指针变量。
【实训目的】掌握指针和指针变量的使用方法。
【实训题目】运行下面的程序,写出结果。并分析指针变量、普通变量之间的关系。
#include
void main()
{ int i,j,*pi,*pj;
pi=&i;
pj=&j;
i=5;j=7;
printf("n %d %d %d %d",i,j,*pi,*pj);
printf("n %d %d %d %d",&i,&j,pi,pj);
}
实训2
【实训内容】一维数组和指针。
【实训目的】掌握利用指针对数组元素进行操作的方法。
【实训题目】运行下面的程序,写出结果。并分析数组元素与指针变量之间的关系。
#include
void main()
{ int a[]={1,2,3},*p,i;
p=a;
for(i=0;i<3;i++)
printf("n %d %d %d %d",a[i],p[i],*(p+i),*(a+i));
}
实训3
【实训内容】二维数组和指针。
【实训目的】掌握利用指针对二维数组元素进行操作的方法。
【实训题目】运行下面的程序,写出结果。并分析数组元素与指针变量之间的关系。
#include
·109·
void main()
{ int i,x[3][3]={9,8,7,6,5,4,3,2,1},*p=&x[1][1];
for(i=0;i<4;i+=2)
printf("%d",p[i]);
}
实训4
【实训内容】字符串和指针。
【实训目的】掌握利用指针对字符串进行操作的方法。
【实训题目】运行下面的程序,写出结果。并分析本程序段所完成的功能。
#include
void main()
{ char *s="ab5ca2cd34ef",*p;
int i,j,a[]={0,0,0,0};
for(p=s;*p!='0';p++)
{j=*p-'a';
if(j>=0&&j<=3) a[j]++;
}
for(i=0;i<4;i++)
printf("%dt",a[i]);
}
习题
以下习题要求用指针方法完成。
7.1 写出下面程序运行结果。
⑴ #include
void main()
{ int x=3,y=5,*p1,*p2,t;
p1=&x;
p2=&y;
t=*p1;
*p1=*p2;
*p2=t;
printf("%d,%dn",x,y);
}
⑵ #include
·110·
void main()
{ int x,y,*p1,*p2,*p;
p1=&x;
p2=&y;
p=p1;
p1=p2;
p2=p;
printf("%d,%dn",x,y);
}
⑶ #include
void main()
{ int x=3,y=5,*p1,*p2,t;
p1=&x;
p2=&y;
t=x;
x=y;
y=t;
printf("%d,%dn",*p1,*p2);
}
⑷ #include
void main()
{ int x=3,y=5,*p1,*p2;
p1=&x;
p2=&y;
x=*p2+10;
y=*p1+10;
printf("%d,%dn",x,y);
}
7.2 写出下列程序的输出结果。
(1)#include
void main()
{int a[]={1,2,3,4,5,6,7,8,9,0},*p;
p=a;
printf(“%dn”,*p+9);
}
(2) #include
void main()
{char a[10]={‘1’,’2’,’3’,’4’,’5’,’6’,’7’,’8’,’9’,’0’},*p;
int i=8;
p=a+i;
·111·
printf(“%sn”,p-3);
}
(3)#include
void main()
{int a[]={1,2,3,4,5,6,7,8,9,0},*p=a;
p++;
printf(“%dn”,*(p+3));
}
(4) #include
void main()
{char *str=”12123434”;
int i,x1=0,x2=0,x3=0,x4=0;
for(i=0;str[i]!=’0’;i++)
switch(str[i])
{case ‘1’:x4++;
case ‘2’:x3++;
case ‘3’:x2++;
default:x1++;
}
printf(“%d,%d,%d,%dn”,x1,x2,x3,x4);
}
(5) #include
void main()
{int a[][3]={{1,2,3},{4,5},{6}};
int i,*p=a[0],(*q)[3]=a;
for(i=0;i<3;i++)
printf(“ %d”,*++p);
printf(“n”);
for(i=0;i<3;i++)
printf(“ %d”,*(*(q+i)+1));
printf(“n”);
}
7.3 输入三个整数,按由小至大顺序输出。
7.4 输入一个字符串,用指针方式逐一显示字符,并求其长度。
7.5 输入到字符数组a中一串字符,按逆序复制到字符数组b中。
7.6 输入一串字符,将其中小写字母换成大写字母。
7.7 从键盘输入一个字符串,按字符顺序从小到大进行排列,并删除重复的字符。
·112·
第8章 函数
使用函数是实现结构化程序设计思想的重要方法。结构化程序设计思想的重点之一就是
模块化,即把一个复杂的较大的程序划分成若干个模块,每个模块完成一个特定的功能,各
个模块通常由不同的人来编写和调试,模块之间相互独立,靠参数的传递实现模块之间进行
联系,从而把一个复杂的问题“分而治之”,这种方法便于组织人力共同完成比较复杂的任务。
本章通过介绍函数的定义、声明与调用、函数之间数据的传递,以及变量在程序中的作
用域,阐述结构化程序设计中的模块化思想。
8.1 一个函数的例子
【例8.1】 用调用函数的方式计算两个整数之和
【解题思路】
程序由多个函数组成,在主函数中用scanf()函数输入两个整数、用s()函数计算两个
整数相加的结果、用printf()函数输出结果。
【程序代码】
#include
int s(int x,int y)
{ int z; /* 变量说明 */
z=x+y;
return z; /* 返回 */
}
void main()
{
int a,b,sum; /* 变量定义 */
printf("请输入两个整数:");
scanf("%d,%d",&a,&b); /* 输入原始数据 */
sum=s(a,b); /* 调用s函数把a和b的值,传递到s函数
的x和y中,并将函数的计算结果赋给sum */
printf("两个数的和=%dn",sum);
}
【运行结果】
请输入两个整数:4,5
两个数的和=9
说明:
1. C程序是由若干个函数构成的,C语言中的所有函数都是一个独立的程序模块。本程
·113·
序中使用了main、scanf、 s和 printf函数。
2. 一个C程序总是从main函数开始执行,调用其它函数后,流程仍将返回到main函
数,最后在main函数中结束程序的运行。
3. scanf和 printf函数是系统提供的库函数,用户只需在程序中根据要求引用,不需
自己编写代码,但由于这些库函数根据功能的不同分别集中在不同的头文件中,所以在程序
使用时它们时必须包含相应的头文件,如上例中的 #include 。
4. s函数是用户自己定义的函数,一旦定义好,就可以像调用其他库函数一样使用。
8.2 函数的定义和调用
8.2.1 函数的定义
函数定义就是确定一个函数完成一定的操作功能,函数定义的一般形式如下:
函数类型说明 函数名(形式参数列表)
形式参数说明
{ 说明部分
语句
}
其中:
1. 函数类型说明指出函数中return语句返回的值的类型,它可以是C中任意合法的
数据类型,如int,float,char等。如果不加函数类型说明符,C语言默认返回值的类型是
整型。函数也可以没有返回值,这时函数类型应说明为void类型。
2. 函数名是用户给函数起的名称,它是一个标识符,是函数定义中不可缺少的部分,
函数名后的一对圆括号是函数的象征,即使没有参数也不能省略。
3. 形式参数列表是写在圆括号中的一组变量名,形式参数之间用逗号分隔。形式参数
称为形式的(或虚参数,简称虚参),是因为形参没有固定的值,形参的值只有函数被调用时
由调用函数的实参提供。C语言中的函数允许没有形式参数,当没有形式参数时,园括号不
能省略,括号内也可以加入void。
4. 形式参数说明是对形式参数表列中的每一个形式参数所作的类型说明,ANSI C新标
准提倡在函数名后的一对圆括号中给出参数表时,同时对每个参数进行参数说明,例如:
int max(int x,int y)
{ if(x>y)
return x;
else
return y;
}
指出函数max的形参x是int型,形参y也是int型。当采用这种方式进行说明时,应
分别说明每个形参的类型。
·114·
5. 用{}括起来的部分称为函数体,由说明部分和语句组成。在函数体中可以定义各种
变量,在函数中定义的变量只有在该函数内使用。函数体中的语句规定了函数执行的操作,
体现了函数的功能,在函数体内通常包含return语句。函数体中可以既无变量定义,也无语
句,但一对花括号是不可省略的。例如:
void null(void)
{}
这是一个空函数,不产生任何操作,但它是一个合法的函数。
8.2.2 函数的调用与函数的返回
在C语言中,除main()函数外,其他函数的执行都是通过被调用实现的,而函数定义仅
仅是定义函数的性质和执行过程,仅具有说明性质。
函数只有在被调用时才能执行,按函数在程序中的作用有三种调用方式:
1. 函数语句。把函数调用作为一个语句,这时不要求函数带回值,只要求完成一定的
操作。例如printf函数的使用:
printf("%d",a);
2. 函数表达式。函数出现在一个表达式中,函数的返回值作为表达式的一部分参与运
算。例如:
x=2*max(a,b);
3. 作函数的参数。函数调用作为另一个函数的一个实参。例如:
printf("%d",max(a,b));
把函数max(a,b)作为printf()函数的一个实参,这种方式实质上也是函数表达式调用
的一种。
在调用函数之前,必须声明被调用函数的原型,包括函数的类型、参数类型、参数个数
及顺序。编译程序按函数声明原型连接调用函数和被调用函数,保证了函数调用顺利完成。
函数声明与函数定义不同,函数定义要给出函数的具体操作代码。函数声明的形式可参照函
数定义中的函数头,一般形式为:
函数类型说明符 函数名(类型说明符 形参,类型说明符 形参,…);
实际上,函数声明就是函数定义中第一行的内容加上一个分号,称为函数原型。在这种
说明方式中,形参的名字是不重要的,重要的是参数的类型。在函数声明中,可以只写形参
的类型名,而不写形参名,但顺序不能写错。
C语言规定,如果被调用函数的定义出现在调用函数之前,也就是函数定义写在前面,
调用函数写在后面,可以不在调用函数前对被调用函数进行声明:
对于C系统提供的标准库函数,函数原型的声明已分类放在扩展名为h的文件中(称为
头文件),如平方根函数sqrt()函数原型的声明在math.h文件中。在调用平方根的程序文件
前面使用文件包含预处理命令#inclue 。
函数返回到调用它的函数有两种方法:
1. 函数执行结束,即遇到最后面的“}”后。
2. 遇到return <表达式>;语句。
return 语句有两个功能:一是宣告函数的一次执行结束,返回到调用它的函数中,一
·115·
个函数中可以有一个以上的return语句,执行到哪一个return语句,哪个语句起作用;二
是把函数的结果带回至调用它的位置。
函数的返回值类型应该与函数定义时函数的类型一致。如果对函数类型的说明与return
语句中表达式的类型不一致,则以函数类型为准。系统自动进行类型转换,将表达式的类型
转换为函数类型。
8.3 参数传递
在C语言中,函数的形参允许为简单变量、数组变量和指针变量。下面分别讨论各种情
况下实参与形参之间结合问题。
8.3.1 变量作为函数形参
当变量作为形参时,对应的实参允许是同类型的常数、变量、数组元素和表达式,而且
应该有确定的值,在参数传递时是将常数、变量、数组元素和表达式的值传递给对应的形参。
当使用多个形参时,实参的个数必须相同,类型也必须一一对应。实参与形参之间按顺序结
合,与参数的名字无关。其结合的规则是单向值传送。
【例8.2】 求长方形的面积函数
【解题思路】
求长方形的面积,首先需要确定长方形的长和宽,所以在形参中有两个变量分别用于接
收从主函数传来的实际的长方形的长和宽的值,计算出面积值后,用return将结果带回调用
的位置上。
【程序代码】
int sss(int a,int b); /* 函数说明 */
#include
void main()
{ int a=3,b=4; /* 长方形的长和宽 */
int s;
s=sss(a,b); /* 函数调用 */
printf(“s=%dn”,s);
}
int sss(int a,int b)
{ int c;
c=a*b;
return c; /* 函数返回 */
}
【运行结果】
s=12
·116·
在主函数调用sss()函数之前,系统不为sss()函数中的参数x、y和z分配存储单元。
只有函数被调用时,才为该函数的形参和变量分配单元,并以调用函数实参提供的值作为对
应形参的初值。应该注意的是,sss()函数中的变量或形参虽然与主函数中的变量名字相同,
但它们都有自己的存储单元,sss()函数中变量a、b的变化不影响main()函数中变量a和b
的值,这是模块化程序设计所要求的,保证了各个函数的独立性。当sss()函数结束时,系
统自动释放该函数中定义的变量单元。
在参数的传递过程中,遵循单向值传送规则,使函数只有一个入口——实参传值给形参,
一个出口——函数返回值。因而函数受外界的影响最小,从而保证了函数的独立性,便于模
块化的程序设计。值得注意的是,实参可以为形参传送数据,是单向的,即在函数调用时实
参的值可以影响形参,但形参值改变后不能影响实参。
【例8.3】 形参值变化不影响实参的实例
【程序代码】
#include
void swap(int a, int b); /* 函数说明 */
void main()
{ int a=3,b=5;
printf("a=%d,b=%dn",a,b);
swap(a,b); /* 函数调用 */
printf("a=%d,b=%dn",a,b);
}
void swap(int a,int b) /* 函数定义 */
{ int t;
t=a;a=b;b=t;
printf("a=%d,b=%dn",a,b);
}
【运行结果】
a=3,b=5
a=5,b=3
a=3,b=5
程序的运行结果表明:在执行swap()函数时,在无返回值的swap()函数中a和b两个
变量值实现了交换,但返回到main()中后,main()中a和b的值并没有改变。说明实参a、b
与形参a、b虽然变量名相同,但它们分别占用不同的内存空间,形参值改变后不能影响实参。
如果需要从被调用函数传回多个数据,或实现形参的变化影响到实参,可以使用指针型
参数来完成。
8.3.2 指针变量作为函数形参
指针变量做形参时,对应的实参必须为它提供确定的地址类型的表达式,以便形参指针
指向实参提供的地址。通过函数中的形参指针间接访问实参地址中的数据,如果向该地址单
元赋给新的值,函数返回后可以使用这个数据。使用指针参数最重要的作用是,除了用return
·117·
返回一个值之外,还可以通过指针参数返回多个数据的目的。
【例8.4】 通过形参改变调用函数中实参变量的值。
【解题思路】
如果以变量作形参,形参值的改变后不影响实参值,根本原因在于实参与形参占用不同
的存储空间,如果要实现形参值的变化改变实参的值,需要二者指向同一地址单元,所以必
须以指针作为形参,在调用这个函数时,实参提供的值则应该是调用函数中变量的地址值。
【程序代码】
#include
void rets(int *px,int *py)
{ int t;
t=*px;
*px=*py;
*py=t;
}
void main()
{ int a=3,b=4;
printf("a=%d,b=%dn",a,b);
rets(&a,&b);
printf("a=%d,b=%dn",a,b);
}
【运行结果】
a=3,b=4
a=4,b=3
本程序中的函数rets的形参是两个整型指针变量px和py,主程序在调用它时,将变量
a的地址传送给了px,变量b的地址传送给了py,这样就使得函数rets()的两个参数px和
py分别指向了调用函数中的变量a和b,而函数rets()中的语句,将px和py所指的地址中
的内容进行交换。对main()函数来说,rets()函数改变了变量a和b的值。
此外,利用指针作形参还可以起到返回多个数据结果的目的。
【例8.5】 求长方形的面积和周长函数
【解题思路】
本题的要求是运行一个函数,得到两个结果,而return 语句只能返回一个值,并且一
个函数中只能有一个return 语句被执行,所以用return 的方法不能实现题目的要求。解决
的办法是在调用函数中将表示面积和周长的变量的地址值传递给被调用函数,而被调用函数
中的形参设置两个指针变量来接收地址值,在函数体内改变这两个地址所指向的值,以达到
同时返回两个值的目的。
【程序代码】
void sss(int a,int b,int *x,int *y); /* 函数说明 */
#include
void main()
{ int a=3,b=4,s,l;
·118·
sss(a,b,&s,&l); /* 函数调用 */
printf("s=%d l=%dn",s,l);
}
void sss(int a,int b,int *x,int *y)
{ *x=a*b;
*y=2*(a+b);
}
【运行结果】
s=12 l=14
从函数执行的效果来看,确实由函数返回了长方形的面积和周长值,好象是从被调用函
数可以向调用函数传送数据。实际上是,形参x和y计算结果不能传回主函数中,而是x使
用了实参s的地址,x发生改变,s值也跟着发生变化,y使用了实参l的地址,y发生改变,l
值也跟着发生变化。
指针变量作形参,仍然附和单向值传送规则,这时传递的值是地址值
8.3.3 数组作为函数形参
一维数组的数组名即该数组的首元素地址,数组名作为函数形参时,对应的实参要提供
若干个连续的存储单元的首地址,作为形参数组的首地址。一般情况下,实参为数组名或某
个数组元素的地址,下面通过例子说明。
【例8.6】 用数组名作参数,求数组元素的平均值。
【解题思路】
本题可使被调用函数形参为数组名b,接收主函数实参a数组的首地址,这样a数组与b
数组共用同一存储空间,在被调用函数中,对b数组的存取操作就相当于对a数组的操作,
求出平均值以后,用return 语句返回即可以实现题目的要求。
【程序代码】
#include
void main()
{ float ave,a[5]={65,75,85,90,95};
float aver(float a[]); /* 函数原型的声明 */
ave=aver(a);
printf("ave=%fn",ave);
}
float aver(float b[5])
{ int i;
float sum=0,ave;
for(i=0;i<5;i++)
sum+=b[i];
ave=sum/5;
return ave;
·119·
}
【运行结果】
ave=82.000000
说明:
1.当用数组名作函数的参数时,实参数组和形参数组要在调用函数和被调用函数中分别
定义。上例中a是实参数组名,b是形参数组名,它们已分别在其函数中定义。即使名字相
同,也必须分别单独定义。
2.实参数组与形参数组的名字可以不同,但类型应一致,否则将出错。
3. 实参数组与形参数组的大小可以不一致,甚至维数也可以不同。C编译程序对形参数
组不作下标越界检查,只是将实参数组的首地址传送给形参数组,使之共用同一段存储单元。
如果要求得到实参数组的所有元素值,形参数组不应大于实参数组。
形参数组也可以不指定大小,在定义数组时,在数组名后跟一个空的方括号,为了在被
调用函数中处理数组元素的需要,可以另设一个参数,以传递数组元素的个数。如例8.6所
示。
【例8.6】 用数组名和数组元素的个数作函数的参数,求数组所有元素的平均值。
【程序代码】
float aver(float b[],int n) /* 函数定义,n为形参数组引用元素的个数*/
{ int i;
float ave,sum=0.0;
for(i=0;i sum+=b[i];
ave=sum/n;
return ave;
}
#include
void main() /* 主函数 */
{ float a1[3]={87.5,90,100};
float a2[5]={98.5,97,91.5,60,55};
printf("The average of a1 is %fn",aver(a1,3));
/* 上面语名也可写为:printf("The average of a1 is %fn",aver(&a1[0],3)); */
printf("The average of a2 is %fn",aver(a2,5));
}
【运行结果】
The average of a1 is 92.500000
The average of a2 is 80.400000
程序中两次调用aver函数时,实参数组的大小是不同的,在调用时用一个实参将数组
的元素个数传送给形参n,这样在函数aver中,对实参数组的所有元素都可访问到,又保证
下标不越界。增加参数n的意义非常重要,可以增强aver函数的独立性和通用性,在实际应
用当中普遍使用。
4.数组名做函数的形参时,对应的实参数组名表示数组的首地址,传给形参一个地址值,
·120·
并作为形参数组的首地址。这样形参数组和实参就共用了同一段存储单元,形参数组元素按
顺序对应使用实参数组元素的地址。因此,在被调函数中对形参数组元素的操作,实际是对
实参数组元素的操作。这种参数传递的方式称为“地址传送”,起到了双向传送数据的目的。
5.由于实参向形参传递的是一段连续空间的首地址值,所以可以利用传递不同首地址值
的方法,灵活对数组进行操作。
【例8.7】 将数组部分元素的值清零。
【解题思路】
将数组部分元素的值清零,要给出需要处理的这一段数组元素的首地址和元素的个数,
首地址用数组名作形参,元素数用普通变量名做形参。
【程序代码】
void clear(int x[],int n)
{ int i;
for(i=0;i x[i]=0;
}
#include
void main()
{ int a[15]={1,3,5,7,9,11,13,15,2,4,6,8,10,12,14},i,j,n;
scanf("%d,%d",&j,&n);
clear(&a[j],n);
for(i=0;i<15;i++)
printf("%4d",a[i]);
}
【运行结果】
10,5
1 3 5 7 9 11 13 15 2 4 0 0 0 0 0
将前5个元素和最后5个元素的值清零,实参分别为某一个元素的地址。当实参为&a[10]
时,实参数组元素a[10]的地址作为形参数组x的首地址。即x[0]使用a[10]的地址,x[1]
使用a[11]元素的地址,最后一个元素x[5]与a[14}地址相同。
8.4 函数的嵌套调用
8.4.1 函数的嵌套调用
C语言中的函数定义是独立的,不允许函数的嵌套定义,但允许嵌套调用,即一个函数
可以调用别的函数,也可以被其他函数调用。函数的嵌套调用为自顶向下,逐步求精,模块
化的结构化程序设计技术提供了最基本的技术支持。
【例8.8】用函数嵌套的方法求:1!+2!+3!+4!+5!
·121·
【解题思路】
主函数只提供数据5和输出结果;sum()函数分别提供1至x,并完成相加,返回相加后
的结果;fac()函数计算x的阶乘,并返回。调用过程是主函数调用sum(),sum()函数调用5
次fac()函数。
【程序代码】
int sum(int x); /* 函数说明 */
int fac(int x); /* 函数说明 */
#include
void main()
{ int s,i=5;
s=sum(i); /* 主函数调用sum函数 */
printf("s=%dn",s);
}
int sum(int x)
{ int z=0,i;
for(i=1;i<=x;i++)
z=z+fac(i); /* sum函数调用fac函数 */
return z; /* sum函数返回到主函数 */
}
int fac(int x)
{ int z=1,i;
for(i=1;i<=x;i++)
z=z*i;
return z; /* fac函数返回至sum函数 */
}
【运行结果】
s=153
本例中函数sum()和fac()是分别定义的,互不从属。但在主程序中调用了函数sum,在
函数sum中又调用了函数fac。这是一个二重嵌套调用,C语言的函数嵌套调用层数在语法上
没有限制。
8.5 变量的作用域
8.5.1 局部变量
C语言中变量有两种属性,即变量的类型和它的存储类型。变量的类型(如char,int,
float等)确定了变量占用内存空间大小和数据的存储格式。变量的存储类型决定变量单元
分配到内存区域的类型,并决定了变量的生存期(何时分配单元,何时释放单元),以及变量
·122·
的作用域(变量的作用范围)。
在C语言中,局部变量包括下面三种:
1. 在函数体体定义的变量。
2. 函数中的形式参数。
3. 在复合语句(分程序)中定义的变量。
局部变量的作用域为所在函数,复合语句中定义变量的作用域仅为复合语句之内。但从
变量的生存期来讲,又可以分为自动变量和静态变量两类。
8.5.1.1 局部自动变量,
定义自动变量的关键字为auto,如:
auto int a,b;
定义了两个整型自动变量a和b。关键字auto可以缺省,因此前面所定义过的变量都默
认为自动变量。上面定义可以简化为:
int a,b;
在函数内部定义的自动变量是在该函数被调用时由系统自动分配存储单元,当函数结束
时系统将变量单元自动释放,因为单元的分配和释放都是由系统自动完成的,所以称为自动
变量。在函数(或复合语句)内定义的变量只能在函数(或复合语句)内使用,作用域是局
部的,因此称为局部变量。在前面程序例子中,在函数中所定义的变量都是局部自动变量,
经常简称局部变量。
函数内定义的变量不能由其它函数使用,这为模块化程序设计带来很大方便,不仅便于
多人共同开发程序软件,也极大地降低了程序调试困难。
函数可以嵌套调用,即被调用函数仍可以调用其它函数(包括自己本身),这时如何理
解各个函数中的局部变量引用不出问题呢?在计算机内存管理技术中,有一种堆栈技术可以
解决这一问题,结合下面程序例子说明。
【例8.9】 计算(a+b)(a-b)的值
【解题思路】
本题设计下面几个函数:fun1函数用来求两个数的和,fun2函数求两个数的差,fun3
函数求两个数的积。
【程序代码】
int fun1(int,int);
int fun2(int,int);
int fun3(int,int);
#include
void main()
{ int a,b,x;
a=5;b=3;
x=fun3(a,b);
printf("x=%dn",x);
}
·123·
int fun1(int x,int y)
{ return x+y;
}
int fun2(int x,int y)
{ return x-y;
}
int fun3(int x,int y)
{ int z;
z=fun1(x,y)*fun2(x,y);
return z;
}
【运行结果】
x=16
说明:
1. 程序从主函数开始,先为三个整型变量a,b,x分配存储单元,并赋值(a=5,b=3)。
在调用函数前将当前现场(变量当前值,将来函数返回地址,内部寄存器信息等)第一次压
入堆栈内存,然后执行fun3()函数,同时提供两个整型的实参数据。
2. fun3函数被调用时,先为两个整型实参x、y和整型变量z分配存储单元,并按单向
值传送的规则接收两个实参数据,使得x=5,y=3。
3. 在fun3函数中,下一步调用fun1函数,在调用之前将当前现场数据第二次压入堆
栈内存,调用fun1函数时x和y已变成有确定值的实参。
4. fun1函数被调用时,先为整型实参x和y分配存储单元,并将实参的值赋给形参,
x=5,y=3。
5. 在fun1函数中遇到return返回命令时,先计算x+y的值,作为返回值保留,然后
释放形参x和y的存储单元,最后弹出第二次压入堆栈的数据,并返回fun3函数。
6. 返回fun3函数时,由于已将原来第二次压入堆栈的信息弹出,弹出就是恢复原来现
场数据,保证继续原来后面操作,在表达式z=fun1(x,y)*fun2(x,y)计算中,继续完成fun2
函数的调用。由于fun1和fun2函数的调用过程是类似的,所以fun2函数的调用过程不在叙
述,现假定两次调用成功并得到z的值(z=16)。
7. 在fun3函数中,当遇到return语句时,自动释放两个形参(x和y)和一个普通变
量z存储单元,然后弹出第一次压入堆栈的数据信息,返回主函数,并将z=16返回到主函数。
8. 回到主函数后,原来压入堆栈的数据得到了恢复,并将fun3函数返回值16赋给x,
并输出结果x=16。
9. 主函数结束之前自动释放主函数中定义的变量,程序结束运行。
堆栈又称为先进后出存储器,有关内存中堆栈的知识请参阅数据结构方面的资料。
在主函数main中定义的变量也是局部变量,只在主函数中有效。主函数也不能使用其
它函数中定义的变量。不同函数中可以使用相同名字的变量,它们代表不同的对象,互不干
扰。
在复合语句中定义的变量也只在定义它们的复合语句中有效。如下例中变量c只在复合
语句内有效,而a和b在整个main函数内有效。
·124·
【例8.10】复合语句内变量作用域
【程序代码】
#include
void main()
{ int a=3,b=5;
if(a
{ int c;
c=a;
a=b;
b=c;
}
printf("%d,%dn",a,b);
}
8.5.1.2 局部静态变量
如果希望函数中的局部变量在调用后不释放,并在下一次调用时继续使用该变量已有
值,可以将变量定义成局部静态变量,定义静态变量的方法是在变量类型定义前加上关键字
static。
【例8.11】 用静态变量编程计算2到5的阶乘值
【解题思路】
如果采用动态变量,函数每次结束后,变量内容被释放不能保存已计算过的值,所以定
义一个函数fac()时,函数体中定义一个静态变量f,保存前一个数的阶乘值,下次调用时,
以前一个值为基础做累乘。
【程序代码】
#include
int fac(int n)
{ static int f=1;
f=f*n;
return f;
}
void main()
{ int i;
for(i=2;i<=5;i++)
printf("%d!=%dn",i,fac(i));
}
【运行结果】
2!=2
3!=6
4!=24
·125·
5!=120
在第一次调用函数fac(2)时,f的值为1,传送给n为2,调用结束时f的值2。由于f
是静态变量,函数调用结束后并不释放,仍保留f=2。第二次调用时它的值是2(上次调用结
束时的值),调用结束后f的值为6。该值又被下一次调用时使用....依此类推,直到整个程
序执行结束。如果不是从2开始依次求3,4,5...的阶乘,则结果显然是错误的。
关于静态局部变量的说明:
1. 静态局部变量分配的存储空间,在程序运行期间不被释放。
2. 如果定义静态局部变量没有初始化值,系统在编译时自动将其初始化。数值型的初
始化值为零,字符型初始化值为空格符。局部动态变量没有自动初始化功能。
3. 虽然静态局部变量在函数结束时不被释放,但仍不能被其它函数访问。
如果不是必要,应尽量少用局部静态变量,一方面是浪费内存空间,另一方面多次调用
函数时,静态局部变量的当前值与上一次调用有关,程序不易调试。
8.5.2 全局变量
全局变量是在函数外部定义的,可以被程序中的各个函数引用,在整个程序运行期间都
有效。
C程序的编译单位是源程序文件,全局变量的作用域为整个源文件,它的有效范围为从
定义位置开始,直到该源文件结束。
【例8.12】 全局变量作用域例子
【解题思路】
本题用于测试变量a和x在程序中的作用范围
【程序代码】
#include
void fun1();
int a=9;
void main()
{ int b=8,c;
c=a+b;
printf("a+b=%dn",c);
fun1();
}
int x=5;
void fun1()
{ printf("x+a=%dn",x+a);
}
【运行结果】
a+b=17
x+a=14
说明:程序中定义了三个全局变量。变量a是在文件开头定义的,可以被文件中所有函
·126·
数引用。变量x是在文件中间定义的,只能由后面fun1函数引用,主函数中不能使用全局变
量x。变量y是在文件最后定义的,在前面两个函数中都不能使用全局变量y。
在函数中,即可以使用本函数中定义的局部变量,也可以使用在它前面定义的全局变量。
如果函数中的变量与全局变量同名,则使用局部变量,如:
【例8.13】局部变量与全局变量同名例子
【程序代码】
#include
void fun1();
int a=9;
void main()
{ int b=8,c;
c=a+b;
printf("a+b=%dn",c);
fun1();
}
int x=5;
void fun1()
{ int a=10;
printf("x+a=%dn",x+a);
}
【运行结果】
a+b=17
x+a=15
为了保证函数的独立,避免出现二义性,在程序中不应过多地使用全局变量。
8.5.3 变量存储类型与模块化程序设计
C程序运行过程中,用户使用的存储空间分为程序区、静态存储区和动态存储区三部分,
数据分别存在静态存储区和动态存储区中。全局变量、静态变量存放在静态存储区中,他们
使用的存储空间在程序运行期间是固定不变的,只有程序运行结束时才释放存储空间。动态
存储区中存放局部自动变量,包括函数内定义的自动变量、函数的形式参数、复合语句中定
义的变量,以及函数调用时现场数据和返回地址等。动态存储技术的实现,一方面解决了函
数之间的数据隔离问题,另一方面一个函数使用的内存空间及时释放,提高了内存的使用效
率。
变量存储类型分为静态存储和动态存储,具体包括四种:自动的(auto)、静态的
(static)、寄存器的(register)和外部的(extern)。其中自动变量、静态变量和外部变
量都已介绍。
为了提高程序的执行效率,C语言允许将局部变量的值存放在通用寄存器中,称此种情
况为寄存器变量。用关键字register定义寄存器变量,下面定义了两个寄存器变量a,b。
register int a,b;
·127·
由于计算机中寄存器的数目是有限的,程序运行时经常不能实现寄存器存放a和b的数
据,所以对于一般程序都不需要使用寄存器变量。
从模块化程序设计的要求来看,提倡模块(函数)的独立性,但完全的独立是不现实的。
只有无参数函数并无返回值时,才可以做到完全独立。调用函数与被调用函数之间需要有数
据联系,这是实际问题所要求的。一般情况下调用函数为被调用函数提供待加工的初始数据,
函数结束时将处理结果返回到调用函数。如果加工结果只有一个数据,利用返回值就可以解
决了,如果要返回多个数据,可以使用指针参数的方法实现。数组名做参数也是函数间数据
联系最方便快捷的手段。
全局变量的使用可以提高函数之间数据传送的效率,可以替代复杂的指针参数用法。但
同时也破坏了函数模块的独立性,对于大型软件系统的设计是不提倡的,函数的编写过程中
要考虑更多的影响因素,调试程序时也更加复杂。在特殊需要时,并且对于程序编写和调试
的难度没有大的影响情况下,可以使用全局变量。总之,如果有其它办法,最好不使用全局
变量。
关于静态变量,在全局变量选择中尽可能使用全局静态变量,在自动变量和静态变量选
择中,最好不使用静态变量。如果有其它办法,就不要使用静态变量。在早期的C编译系统
中,非静态变量不能在定义时初始化,因此为了变量初始化的简便,常常将变量定义为静态
的。现在非静态变量也可以在定义时给出初始化值。
8.6 本章小结
通过本章的学习,读者应理解掌握以下几个内容:
1.函数的概念,函数可以把相对独立的具有某个功能的程序段,是程序中的一个独立实
体。它可以在一个程序中多次重复使用。
2.C语言函数有两类,一是由C语言系统提供、无须用户定义,也不必在程序中作类型
说明;只需在程序前包含有该函数定义的头文件的库函数;二是用户在程序中根据需要而编
写的用户自定义函数。
3.在函数的定义中,确定函数的返回值类型、函数的名称、函数执行所必需的参数的类
型及名称,同时在函数体内完成函数功能的实现操作,通过用return语句返回。
4.函数在被调用时,调用它的函数将为该函数提供相应的实际参数,参数的传递遵循单
向值传递原则。
5.如果被调用函数的定义程序代码出现在调用函数之后,在调用前应对被调用函数作原
型说明。
6.return 语句的使用,return在结束函数的同时,向调用函数返回一个表达式的值。
7.变量的作用域,变量的作用域决定变量的可访问性,其中局部变量不能在函数外使用,
而全局变量则可以在整个程序中使用。
·128·
8.5 实训
实训1
【实训内容】函数的定义和调用。
【实训目的】掌握函数的定义与调用的方法。
【实训题目】下列程序的功能是计算一个整数阶乘,请上机验证,并在/* */中写出本行
的功能。
#include
int fac(int x); /* */
void main()
{ int a=5,ff;
ff=fac(a) ; /* */
printf("%d!=%dn",a,ff);
}
int fac(int n) /* */
{ int i, t=1;
for(i=1;i<=n;i++)
t=t*i;
return t; /* */
}
实训2
【实训内容】函数之间数据的传递。
【实训目的】掌握传值和传地址值的方法。
【实训题目】分析下列程序,试写出运行结果,并上机验证。
(1)
#include
void myprt(int n)
{ int i ;
printf("n") ;
for(i=1;i<=n;i++)
printf("*");
}
void main()
{ int i ;
·129·
for(i=1;i<=3;i++) myprt(i+1) ;
}
(2)
#include
void mywapl(int *x ,int *y)
{ int t ;
t=*x ; *x=*y ; *y=t ;
}
void main()
{ int x=3 ,y=7 ;
printf("n x=%d y=%d",x , y);
mywapl(&x,&y) ;
printf("n x=%d y=%d",x , y);
}
(3)
#include
void fun(int b[] ,int n) ;
void main()
{ int a[10]={0},i ;
fun(a,10);
for(i=0;i<10;i++)
printf(" %d",a[i]);
}
void fun(int b[], int n )
{ int j;
for(j=0 ;j b[j]=j+10 ;
}
实训3
【实训内容】编写函数
【实训目的】掌握函数的编写方法
【实训题目】按题目要求和主函数对它的调用方式,编写下列函数
(1)编写一个函数fun(),判断一个整数是否被另一个整数整除,若能则函数返回1,
否则返回0,主函数调用该函数输出50以内能同时被3和5整除的数(主函数和fun函数原
型说明已经给出)。
·130·
#include
int fun(int x ,int y) ;
void main()
{ int i;
for(i=1;i<=50;i++)
if(fun(i,3)&&fun(i,5))
printf(" %3d",i);
}
int fun(int x, int y )
{
}
(2)编写一个函数fun(),调用此函数将一维数组中的值逆序存放。(主函数和fun函
数原型说明已经给出)。
#include
void fun(int a[] ,int n) ;
void main()
{ int a[9]={0,1,2,3,4,5,6,7,8},i;
for(i=0;i<9;i++)
printf(" %3d",a[i]);
printf(" n");
fun(a,9);
for(i=0;i<9;i++)
printf(" %3d",a[i]);
}
void fun(int b[], int n )
{
}
实训4
【实训内容】变量的作用域
【实训目的】掌握全局变量和局部变量、动态变量、静态变量的概念和使用方法。
【实训题目】分析下列程序,试写出运行结果,并上机验证。
(1)
#include
void fun1();
int a=9;
·131·
void main()
{ int b=8;
printf("a+b=%dn",a+b);
fun1();
}
void fun1()
{ int a=1,b=2;
printf("a+b=%dn",a+b);
}
(2)
#include
int fun(int n)
{ int f=1; /* 本行改为 static int f=1; 结果怎样?*/
f=f+n;
return f;
}
void main()
{ int i=1;
printf("%d n",fun(i));
printf("%d n",fun(i));
}
习题
8.1 编写一个求两个整数和的函数,在主函数中输入两个整数,调用该函数计算并输
出该二数之和。
8.2 编写一个函数求两个整数的和与差,在主函数中调用该函数求两个整数的和与差。
8.3 设x=3,y=6,编写计算阶乘的函数,在主函数中调用阶乘函数计算x!+y!的值。
8.4 编写从指定字符串中删除给定字符的函数,然后调用它从字符串“abcdccf”中删
除字符'c'。
8.5 编写从整型数组中检索给定数值位置的函数,然后调用此函数检索数组序列
10,12,34,45,56,67,78,89,90中某个整数的位置,如果此整数不在数组中输出字符信息。
8.6 编写函数求两个整数的最大公约数,在主程序中输入两个整数,调用这个函数求
它们的最大公约数。
8.7 编写函数判断一个整数是否为素数(质数),在主函数中调用该函数判断一个整数
是否为素数。
8.8 写一个函数,使给定的一个3行3列二维数组转置,即行列互换。
8.9 写一个函数统计字符串中字母、数字、空格和其它字符的个数。在主程序中输入字
·132·
符串并输出结果。
8.10 编写函数,对一个整型数组按由大到小的顺序排序,在主函数中调用该函数实现
数组排序,排序方法不限。
8.11 写一个函数,求一个字符串的长度。在主函数中输入字符串,并输出其长度。
·133·
第9章 编译预处理
编译预处理是C语言区别其他高级语言的一个重要特点,C语言的编译预处理功能,具
有宏定义、文件包含和条件编译等几个特殊的命令。编译系统在进行词法分析、语法分析、
代码生成以及代码优化等工作以前,首先对这些特殊命令控制进行预处理,然后再将其结果
与源程序一块进行编译。本章主要介绍编译预处理中的宏定义和文件包含。
为了区别于一般语句,在预处理命令开头必须加“#”符号作为标志,如:
#include
#define PAI 3.14
等。预处理命令不是C程序语句,因此命令后面不能使用分号。
9.1 宏定义
9.1.1 字符串宏
一般来说,常量都具有一定的意义,但在程序中通常使用的常量,却很难看出它的意义,
以致程序的可读性降低,为此C语言提供了一个用符号来表示一个常量的方法,即字符串宏
来解决此类问题。
字符串宏的定义形式为:
#define 标识符 字符串
【例9.1】计算圆面积和周长
【解题思路】
计算圆的面积一定会用到圆周率π,它的精度会根据题目的要求而改变,如果它在程序
中多次出现,修改程序时也会分别对每一个常量进行修改。用字符串宏的方法可以很好地解
决这一问题,每次只改动一个位置即可。
【程序代码】
#include
#define PAI 3.14
void main()
{ float r=3,s,l;
s=PAI*r*r;
l=2*PAI*r;
printf("l=%fn",l);
printf("s=%fn",s);
}
·134·
【运行结果】
r=18.840000
s=28.260000
如果想提高计算精度,可直接修改为:#define PAI 3.14159
说明:
1. 宏标识符一般使用大写字母表示,以便与程序中的变量相区别,这不是语法所要求
的,而是人们的一种习惯。
2. 执行预处理命令时只作简单的替换,即使将前面例子中的数字字符3.14中的数字1
写成小写字母l,只有到编译时才会发现错误,替换时不进行任何语法检查。
3. 宏定义一般放在源程序文件的开始部分,宏标识符只在该文件内有效。
4. 程序中出现在用双引号里面的字符串如果与宏名相同,则不进行替换。
9.2.2 带参数宏
宏定义中,可以使用参数,带参数的宏的定义形式为:
#define 标识符(参数表) 字符串
其中字符串中应包含参数表中的所指定的参数,如果参数有两个以上,之间用,号分隔。
【例9.2】使用带参数的宏计算长方形的面积
【解题思路】
计算长方形面积,需要长和宽两个参数,所以定义带两个参数的宏,参数之间用逗号分
开,后面的字符串中表示用这两个参数计算出面积的公式。
【程序代码】
#include
#define are(h,w) (h)*(w)
void main()
{ int a=3,b=5,c;
c=are(a+1,b);
printf("c=%dn",c);
}
【运行结果】
c=20
应该注意,h和w参数没有类型问题,使用括号也是必要的,如果将宏定义为下面形式:
#define myfun(x,y) x*y
则表达式myfun(a+1,b)的值等于8,而不是20,因为替换的结果为a+1*b。
9.2.3 函数与宏比较
在某些情况下,宏定义与函数调用可以起到同样的作用。宏定义与函数调用的主要区别
如下:
1.对于函数调用,实参与形参的结合时,先计算实参表达式的值,并传送给形参。宏调
·135·
用时,仅进行简单的字符替换,并且是在程序编译之前完成。
2.宏参数没有类型问题。
3.宏调用是在编译之前进行的,不需要占用内存,节省内存和运行时间。函数调用需要
临时保存现场数据和函数返回地址等数据,效率低于宏调用。
4.函数可以实现任何复杂的操作过程,而宏只能完成简单的操作。
9.2 文件包含
文件包含是C预处理程序的另一个重要功能,文件包含命令可以将另一个文件的全部内
容包含到当前文件中,命令形式为:
#include <文件名>
或:#include "文件名"
如果文件名两边使用尖括号,系统将在系统为include命令设置的目录下查找包含的文
件。若使用双引号,则先在当前工作目录下查找文件,找不到该文件时,再到系统设置的目
录下查找。
在C语言编译系统中有许多以.h为扩展名的文件,它们被称为头文件,在使用C语言编
译系统提供的库函数进行程序设计时,通常需要在源程序中包含进来相对应的头文件,比如:
使用输入输出库函数时,应使用标准输入输出头文件:
#include
使用数学函数编写程序时,应使用数学函数头文件:
#include
文件包含还常常可应用于大规模程序设计过程中,将多个模块公用的符号常量或宏定义
等单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用,以避免在每个文
件开头都去书写那些公用量,从而节省时间,并减少出错。但如果包含文件中有全局变量的
定义,有可能与当前文件定义的全局变量出现冲突,应该引起注意。
9.3 本章小结
通过本章的学习,读者应理解掌握以下几个内容:
1. 预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的。
程序员在程序中用预处理命令来调用这些功能。
2. 宏定义是用一个标识符来表示一个字符串,这个字符串可以是常量、变量或表达式。
在宏调用中将用该字符串代换宏名。
3. 宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
4. 为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数
两边也应加括号。
5. 文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编
译,结果将生成一个目标文件。
·136·
6. 使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
9.4 实训
实训1
【实训内容】宏的定义。
【实训目的】掌握不带参数与带参数的宏的定义与使用的方法。
【实训题目】分析下列程序,试写出运行结果,并上机验证。
(1)
#include
#define M 1000
void main( )
{ int s ;
s=M+100 ;
printf("n两数和= %d",s) ;
s=M-100 ;
printf("n两数差=%d" , s) ;
}
(2)
#include
#define M(x) x*x /* 本行改为 #define M(x) (x)*(x) 结果怎样?*/
void main( )
{ int s ;
s=M(2+3);
printf("s=%dn",s) ;
}
习题
9.1 写出下面程序的运行结果。
#define ABC(X,Y) X*Y
#include
void main()
{ int a=3,b=4,c=5,x;
x=ABC(a+b,c);
printf("x=%dn",x);
·137·
}
9.2 定义一个带两个参数的宏,交换两个参数的值,并写出调用宏的主函数。
9.3 编写一个判断字符是否为英文字母的宏ABC,调用宏的主函数如下:
#include
void main()
{ char ch;
ch=gecchar();
if(ABC(ch))
printf("%c 是英文字母n",ch);
else
printf("%c 不是英文字母n",ch);
}
138·
·
第10章 结构体与共用体
数组是同类型数据元素的集合,用于解决大量同类型数据处理问题,但在实际应用中通
常要处理的对象只用一种简单的数据类型是不能能描述完整的,可能要处理多种类型结合在
一起的复杂的数据结构,例如对学生基本情况的描述,包含学号、姓名、考试成绩、平均成
绩等等,这些构成学生属性的数据不属于同一类型。如果用简单变量来分别代表各个属性,
是难以反映出它们的内在联系,而且使程序冗长难读,用数组又无法容纳不同类型的元素。C
语言提供了一种称为结构体的构造数据类型,用于解决上述问题,与之相近的另一种数据类
型为共用体,此外枚举类型也是一种构造类型,本章针对结构体数据类型的定义、结构体类
型变量的引用、结构体数据和指针作逐一讨论。
10.1 一个结构体的例子
【例10.1】一个学生的基本情况包括学号、姓名、两科的成绩和平均成绩,用结构体类
型变量输入学生的学号、姓名和两科的成绩,输出此学生的基本信息及平均成绩。
【解题思路】
学生的信息由五个数据项组成,它们的数据类型不尽相同,但同属于一个学生,需把这
些数据项用结构体的方式有机结合起来形成一个整体,才能将学生做为一个操作对象来进行
描述。
【程序代码】
#include
#include
struct student /* 定义学生结构体数据类型 */
{ int num; /* 学号用整型数表示 */
char name[10]; /* 姓名用字符型数组表示 */
int score[2]; /* 两科成绩用整型数组表示 */
float aver; /* 平均分用实型数表示 */
};
void main()
{ struct student stu; /* 定义变量stu为学生结构体类型 */
=10001; /* 为变量的学号赋值 */
strcpy(,"
Jones"); /* 为变量的姓名赋值 */
[0]=78; /* 为变量的两科成绩赋值 */
[1]=75;
=([0]+ [1])/2.0; /* 求变量的平均分 */
printf("%dn",);
·139·
printf("%sn",);
printf("%d,%dn", [0], [1]);
printf("%fn", );
}
【运行结果】
10001
Jones
78,75
76.500000
说明:
结构体类型是用户自己定义的数据类型,是构造数据类型,结构体类型变量也同样遵循
先定义后使用的原则。
10.2 结构体的定义与引用
10.2.1 结构体类型和结构体变量的定义
1.结构体类型定义
结构体是一个用同一名字引用的变量集合体,它提供了将相关信息组合在一起的手段。
结构体是用户自定义的数据类型,结构体定义也就是定义结构体名字和组成结构体的成员属
性,是建立一个可用于定义结构体类型变量的模型。
定义一个结构体类型的一般形式为:
struct 结构体名
{
类型 成员变量名;
类型 成员变量名;
...
};
注意:定义最后使用分号结束。
构成结构体的每一个类型变量称为结构体成员,它象数组的元素一样,但数组中元素是
以下标来访问的,而结构体是按成员变量名字来访问成员的。定义一个结构体类型与定义一个
变量不同,定义结构体时系统不会分配内存单元来存放各数据项成员,而是告诉系统它由哪
些类型的成员构成,各占是什么数据类型,并把它们当作一个整体来处理。
下面就是一个结构体类型student的定义:
struct student
{ int num;
char name[10];
int score[2];
·140·
float aver;
};
其中,struct是定义结构体类型的关键字,student是结构体类型的名字,4个成员变
量组成一个结构体类型(student)。结构体类型定义了之后,student相当于系统提供的int、
float和char等类型说明符。
2.结构体变量的定义及初始化
通过用户定义的结构体类型student来定义结构体变量,系统为结构体变量分配存储单
元,可以将数据存放在结构体变量单元中。结构体变量的定义可以采用下面三种方法定义:
⑴ 在定义了一个结构体类型之后定义结构体变量。例如:
struct student
{ int num;
char name[10];
int score[2];
float aver;
};
struct student stu1,stu2;
上面定义了两个结构体变量stu1,stu2,它们是已定义的student结构体类型,系统为
每个结构体变量分配存储单元。使用student结构体类型定义结构体变量时,要在前面加上
struct关键字。
⑵ 在定义了一个结构体类型同时定义结构体变量。例如:
struct student
{ int num;
char name[10];
int score[2];
float aver;
}stu1,stu2;
在定义结构体类型同时可以直接定义结构体变量。
⑶ 直接定义结构体类型的变量。例如:
struct
{ int num;
char name[10];
int score[2];
float aver;
}stu1,stu2;
如果直接定义了stu1和stu2两个结构体变量,结构体类型的名字可以缺省。
在内存中,stu1占连续的一片存储单元,可以用sizeof(student)表达式测出一个结构
体类型数据的字节长度。
3. 在结构体定义中使用已定义过的结构体类型
结构体类型不允许嵌套定义,但可以在结构体成员表中出现另一个结构体类型变量定
·141·
义,而不能出现自身结构体变量定义。例如:
struct time
{ int house;
int min;
int sec;
};
struct date
{ int year;
int month;
int day;
struct time t;
};
在date结构体类型的定义中使用struct time t;定义结构体变量是合法的。
下面嵌套定义是非法的。
struct date
{ int year;
int month;
int day;
stuct time
{ int house;
int min;
int sec;
};
};
下面递归定义也是不允许的
struct date
{ int year;
int month;
int day;
stuct date d;
};
4. 结构体变量初始化
结构体变量的初始化规则与数组相同,例如:
struct student
{ int num;
char name[10];
int score[2];
float aver;
};
·142·
struct student stu1={10001,"Liming",{78,86},0};
10.2.2 结构体变量的使用
结构体类型变量的使用同其他类型变量一样,先定义后使用,但结构体类型变量中有不
同类型的成员,对结构体变量的使用从本质上来讲是对结构体变量成员的使用。
结构体变量包含多个成员,使用结构体成员时必须通过成员运算符(圆点),如
。圆点符号称为成员运算符,它的运算优先级别最高,与圆括号级别相同。
可以对结构体变量的成员进行各种有关的操作,可以将结构体成员看作是简单变量。
【例10.2】结构体成员的使用
【解题思路】
在结构体变量与它的成员之间用成员运算符分开,并把它作为一个“变量”来使用。
【程序代码】
#include
#include
struct student
{ int num;
char name[10];
int score[2];
float aver;
};
void main()
{ struct student stu;
=10001
strcpy(,"Liming");
[0]=78;
[1]=75;
=([0]+ [1])/2.0;
printf("%dn",);
printf("%sn",);
printf("%d,%d,%dn", [0], [1]);
printf("%fn", );
}
【运行结果】
10001
Liming
78,75,87
80.000000
·143·
10.3 结构体数组与结构体指针
10.3.1 结构体数组
一个结构体变量只能存放一个对象的一组相关信息,结构体数组可以存放多个同类型对
象的信息。上一节定义的结构体类型只能存放一名学生的信息,如果使用结构体数组就可以
存放多名学生信息。
定义结构体数组与定义一个一般的结构体变量一样,可采用直接定义、同时定义或先定
义结构体类型再定义结构体变量的方法。下面是含有30名学生成绩的结构体数组的定义:
方法一:直接定义法:
struct
{ int num;
char name[10];
int score[2];
float aver;
}stu[30];
方法二:先定义结构体类型再定义结构体变量法
struct student
{ int num;
char name[10];
int score[2];
float aver;
};
struct student stu[30];
方法三:同时定义结构体类型和结构体数组
struct student
{ int num;
char name[10];
int score[2];
float aver;
}stu[30];
结构体数组的每个元素相当于一个结构体变量,包括结构体中的各个成员项,它们在内
存中也是连续存放的。结构体数组的应用非常普遍,对它的初始化与对二维数组的初始化很
类似,只是在第二层花括号内的值为对应于结构体中各成员的不同数据类型的值。例如:
struct student stu[2]={{101,"Liming",{75,87},0},{102,"Wangli",{70,80},0}};
定义了结构体数组以后,要通过结构体数组元素访问其成员。例如,结构体数组stu中
第二名学生的平均成绩为stu[1].aver。
·144·
【例10.3】计算全班每个学生两门课的平均考试成绩,并在屏幕上显示学生学号、姓名
及其平均成绩。假设全班共有5名学生。
【解题思路】
把学生的信息定义为结构体类型,要处理多个学生的信息属同一结构体类型,所以用学
生结构体类型数组的方式解决此题。
【程序代码】
#include
#define N 5
void main()
{ struct student
{ int num;
char name[10];
int score[2];
float aver;
}stu[N];
int i;
printf("输入%d名学生姓名及2门考试成绩。n",N);
for(i=0;i {
printf("学号:");
scanf("%d",&stu[i].num);
printf("姓名:");
scanf("%s",stu[i].name);
printf("成绩1,成绩2: ");
scanf("%d,%d",&stu[i].score[0], &stu[i].score[1]);
stu[i].aver=(stu[i].score[0]+ stu[i].score[1])/2.0;
}
for(i=0;i printf("%d,%s,%fn", stu[i].num,stu[i].name,stu[i].aver);
}
10.3.2 结构体指针
可以定义一个指针变量指向一个结构体变量。结构体指针的定义与其他结构体变量的定
义方法相同,只需在指针变量前加星号“*”。
定义结构体类型为:
struct student
{ int num;
char name[10];
int score[2];
·145·
float aver;
};
定义结构体变量stu和结构体指针变量p:
struct student stu,*p;
下面为结构体指针变量赋值,p指向结构体变量stu
p=&stu;
用结构体指针访问结构体成员有两种方法:
第一种方法称为显示法,如(*p).name、(*p).score[0]、(*p).aver等。(*p).name中
圆括号不能省略,不能写成*,因为成员运算符的优先级高于指针运算符。
第二种方法使用结构体成员运算符“->”,“->”和“.”均为结构体成员运算符,“.”
只能用于结构体变量,“->”只能用于结构体指针,不能混用。当p 指向stu后,、
(*p).name和p->name三种表示形式是等价的。
下面用程序例子说明。
【例10.4】应用结构体指针处理学生的基本信息
【程序代码】
#include
void main()
{ struct student
{ int num;
char name[10];
int score[2];
float aver;
}stu,*p;
p=&stu;
scanf("%d",&p->num);
scanf("%s",p->name);
scanf("%d,%d",&p->score[0], &p->score[1]);
p->aver=(p->score[0]+ p->score[1])/2.0;
/* 下面分别使用两种结构体成员运算符输出数据 */
printf("num:%dn",p->num);
printf("name:%sn",p->name);
printf("score[0]=%d,score[1]=%dn", p->score[0],p->score[1]);
printf("aver=%fn",p->aver);
printf("num:%dn",(*p).num);
printf("name:%sn",(*p).name);
printf("score[0]=%d,score[1]=%dn", (*p).score[0],(*p).score[1]);
printf("aver=%fn",(*p).aver);
}
【运行结果】
输入数据:
·146·
10001
Liming
70,80
输出数据:
10001
Liming
score(0)=70,score(1)=80
aver=75.000000
10001
Liming
score(0)=70,score(1)=80
aver=75.000000
10.4 链表
结构体数组简单实用,但数组元素的物理地址是连续的,如果要在指定位置完成插入或
删除一个元素的操作是很麻烦的,它需要移动后面多个元素的操作,而且数组大小也不能改
变。为解决这个问题,可以使用本节介绍的链表数据结构。链表存储结构是一种动态数据结
构,其特点是它包含的数据对象的个数及其相互关系可以按需要改变,存储空间是程序根据
需要在程序运行过程中向系统申请获得,链表也不要求逻辑上相邻的元素在物理位置上也相
邻,它没有顺序存储结构所具有的弱点。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指
针域。使用链表处理数据信息时,不用事先考虑应用中元素的个数,当需要插入或删除元素
时可以随时申请或释放内存,并且不用移动其它元素。
10.4.1 链表概述
链表结构中的每一个元素都使用动态内存(堆内存),可以根据需要临时申请或释放,
各个元素不需要连续的存储单元。在链表结构中将为每一个元素申请的内存单元称为结点,
假定将结点结构类型定义如下:
struct list
{
int num; /* 学号 */
int score; /* 成绩 */
struct list *next;
};
用结点类型list建立一个链表listhead,链表中每个结点存放学生的学号(num)和考
试成绩(score)。先为第一名学生建立含有一个结点的链表,程序如下。
·147·
struct list *p,*listhead,*listp;
p=(struct list*)malloc(sizeof(struct list));
listhead=p;
listp=p;
listp->next=NULL;
listp->num=5;
listp->score=86;
sizeof(struct list)为结点内存大小的字节数,用malloc函数申请动态内存返回地址
是void无值型的,必须通过(struct list*)进行强制类型转换。p是指向新申请的结点指针。
listp总是指向链表当前结点listp=p,在后面继续插入其它结点时使用,是不可缺少的。
listp->next=NULL说明当前结点为链表的最后一个结点,NULL为链表尾标志。listhead为
链表的头指针,总是指向链表中第一个结点。
下面继续在链表尾部插入第二名学生的结点,程序如下:
p=(struct list*)malloc(sizeof(struct list));
listp->next=p;
listp=p;
listp->next=NULL;
listp->num=3;
listp->score=78;
如果继续在链表尾部插入其它学生结点,操作程序是一样的,下面程序完成了第三名学
生结点的插入:
p=(struct list*)malloc(sizeof(struct list));
listp->next=p;
listp=p;
listp->next=NULL;
listp->num=9;
listp->score=80;
到此为止,我们已经建立了一个包含三个结点的链表,每一个结点存放一名学生的数据,
并用一个指针指向下一个结点,最后一个结点的指针指向空NULL。图9-1是该链表的结构示
意图。
listhead
5号,86分
3号,78分 9号,80分 NULL
5号学生结点 3号学生结点 9号学生结点
图10-1 链表示意图
该链表是单向的,各个结点单元也不连续,只能通过当前结点的指针listp->next查找
下一个(后面的)结点,不能向数组那样直接处理指定位置的数据,必须通过头指针listhead
逐个查找各个结点的数据信息。链表结构最大的优点是在指定结点处插入新结点,或者删除
指定的结点,其操作将十分简单,在后面详细介绍。
通过下面一个简单的链表程序例子,初步了解链表的数据结构。
【例10.5】使用链表结构输入输出5名学生学号和考试成绩
·148·
【解题思路】
首先建立链表结点数据类型struct list,其中应包含数据域和指针域两个部分,然后
定义头结点listhead,指针域指向为NULL,接下来依次在链表尾部插入结点,每建立一个结
点,都要向数据域中输入数据。输出时,从头结点开始,直至当前结点的指针域的指向为NULL。
【程序代码】
#include
#include
struct list
{
int num; /* 学号 */
int score; /* 成绩 */
struct list *next;
};
void main()
{ int k;
struct list *p,*listhead,*listp;
for(k=1;k<=5;k++)
{
p=(struct list*)malloc(sizeof(struct list));
if(k==1)
listhead=p;
else
listp->next=p;
listp=p;
listp->next=NULL;
printf("输入第 %d 名学生学号和成绩:",k);
scanf("%d,%d",&listp->num,&listp->score);
}
/* 下面输出各个结点数据 */
p=listhead;
while(p!=NULL)
{
printf("%d号学生成绩=%d分n",p->num,p->score);
p=p->next;
}
/* 下面程序释放链表中各个结点的内存空间 */
p=listhead;
while(p!=NULL)
{
listp=p;
·149·
p=p->next;
free(listp);
}
}
【运行结果】
输入第 1 名学生学号和成绩:5,78
输入第 2 名学生学号和成绩:7,82
输入第 3 名学生学号和成绩:4,87
输入第 4 名学生学号和成绩:1,80
输入第 5 名学生学号和成绩:6,92
5号学生成绩=78分
7号学生成绩=82分
4号学生成绩=87分
1号学生成绩=80分
6号学生成绩=92分
10.4.2 链表的基本操作
为了进一步掌握链表的使用方法,下面结合程序例子介绍链表最常用的基本操作,包括
建立链表、输出链表中所有结点数据信息、在链表中插入一个结点和删除链表中的某个结点。
【例10.6】使用链表结构处理学生考试成绩,实现链表最基本的操作。
在这个例子中我们假定每个结点只存放一名学生的考试成绩,成绩为非负整数。结点类
型定义如下:
struct list
{ int data; /* 为了简单,假设一个结点只存放一个学生成绩(非负整数) */
struct list *next;
};
下面分别是实现链表各种基本操作的函数程序。
1. 建立链表
建立一个单向链表,从键盘逐个输入每名学生的考试成绩,当输入-1时结束,并返回链
表头指针(第一个结点的地址)。
设listhead为函数返回的链表头指针,p为新结点指针,listp为链表操作的当前指针。
建立链表函数如下:
struct list *create(void)
{ struct list *listhead,*p,*listp;
int x;
listhead=NULL;
printf("输入成绩:");
scanf("%d",&x);
·150·
}
while(x!=-1)
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
{ printf("内存申请错误!n");
exit(0);
}
if(listhead==NULL)
listhead=p;
else
listp->next=p;
listp=p;
listp->next=NULL;
printf("输入下一个成绩(-1结束):");
scanf("%d",&x);
}
return listhead;
2. 输出链表
输出链表就是将一个链表中的各结点数据依次输出,实现这个功能首先要将指针指向链
表的首地址,输出一个结点数据后,将指针移到下一个结点,直到指针为空为止,程序如下:
void display(struct list *listhead)
{ struct list *p;
if(listhead==NULL)
{ printf("链表为空!n");
return;
}
p=listhead;
while(p!=NULL)
{ printf("%dn",p->data);
p=p->next;
}
}
3. 在链表指定位置插入新结点
建立链表以后可以在任何位置插入新的结点,下面函数假定将新结点插入到第i个位置
(链表表头为第1个结点),新结点插入后原来第i个结点成为第i+1个结点,后面其它结点
类推。
为了容易理解插入操作的函数程序,需要重点掌握下面问题。
⑴ 在当前结点listp后插入新结点p的操作。
·151·
插入操作前listp指向存放成绩A的结点,其下一个是存放成绩B的结点,如图9-2所
示。
listp
成绩A 成绩B
图9-2 插入前链表示意图
现在p是指向新结点的指针,新结点存放成绩C。在listp结点后插入新结点需要使用
下面两条语句完成。
p->next=list->next;
listp->next=p;
必须先将listp的下一个结点地址保存在新结点p的指针p->next中,使得listp的下
一个结点成为新结点的下一个结点。然后,再将新结点地址存放到listp结点的指针
listp->next中,使得listp结点的下一个结点为p指向的新结点,如图9-3所示。
listp
成绩A 成绩B
p
成绩C
图9-3 插入后链表示意图
⑵ 确定插入位置前一个结点的地址,并验证插入位置的有效性。
根据前面插入操作的特点,要将新结点插入到链表中第i个位置,必须先确定第i-1个
结点地址,可以用下面控制循环次数的方法解决。
if(i<1)
printf(“插入位置错,不能小于1n”);
k=1;
listp=listhead;
while(listp!=NULL && k { listp=listp->next;
k=k+1;
}
if(k printf(“插入位置不能大于%dn”,k);
因为链表头指针指向的是第1个结点,所以插入位置不能小于1。如果循环结束后k成立,说明listp还没有指向第i-1个结点时链表就已经到尾了,因此无法完成插入操作。
如果插入位置有效,最后listp将指向插入位置的前一个结点。
⑶ 在链表表头位置插入新结点(i=1)
当插入位置i>1情况下,可以先确定第i-1个结点(前一个结点)的地址,然后完成插入
新结点的操作。如果插入新结点的位置i=1,那么无法按照前面方法查找到前一个结点的地
址,因此必须进行单独的特别处理,过程如下。
·152·
p->next=listhead;
listhead=p;
下面为插入新结点的函数程序:
struct list *insert(struct list *listhead)
{ struct list *p,*listp;
int k,x,i;
/* 输入插入结点数据 */
printf("输入新插入结点的数据和位置:");
scanf("%d,%d",&x,&i);
/* 如果链表为空 */
if(listhead==NULL)
{
printf("链表还没有建立!n");
return listhead;
}
k=1;
listp=listhead;
while(listp!=NULL && k { listp=listp->next;
k=k+1;
}
if(k printf(“插入位置必须大于0,小于%dn”,k+1);
else if(i==1)
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
{ printf("内存申请错误!n");
exit(0);
}
p->data=x;
p->next=listhead;
listhead=p;
}
else
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
{ printf("内存申请错误!n");
exit(0);
}
p->data=x;
·153·
}
p->next=listp->next;
listp->next=p;
}
return listhead;
4. 删除链表中的某个结点
首先在函数中输入待删除结点序号i,然后通过下面循环程序查找第i-1个结点的地址,
并用listp指向它。
k=1;
listp=listhead;
while(listp!=NULL && k { listp=listp->next;
k=k+1;
}
循环结束后,可以根据i和k的值按下面三种情况分别处理。
⑴ 当i<1或k⑵ 当i=1时,将删除的是表头结点,需要下面特别处理:
p=listhead; /* p指向第1个表头结点 */
listhead=p->next; /* 第2个结点变为第1个结点,原来第1个结点脱链 */
free(p); /* 释放删除结点 */
⑶ 当i>1并且k>=i-1时,listp已经指向第i-1个结点,再用p指向第i个要删除的
结点,然后将第i个结点从链中解除(脱链),最后释放删除结点,程序如下:
p=listp->next;
listp->next=p->next;
free(p);
脱链操作前p指向的删除结点是listp指向结点的下一个结点,脱链后p指向结点的下
一个结点变成listp指向结点的下一个结点。
下面为删除函数程序代码:
struct list *dele(struct list *listhead)
{ struct list *p,*listp;
int i,k;
if(listhead==NULL)
{ printf("链表为空n");
return listhead;
}
printf("输入删除结点序号:");
scanf("%d",&i);
k=1;
listp=listhead;
·154·
}
while(listp!=NULL && k{ listp=listp->next;
k=k+1;
}
if(k printf(“删除结点序号必须大于0,小于%dn”,k+1);
else if(i==1)
{
p=listhead; /* p指向第1个结点 */
listhead=p->next; /* 第2个结点变为第1个结点,原来第1个结点脱链 */
free(p); /* 释放删除结点 */
}
else if(i>1 && k>=i-1)
{
p=listp->next;
listp->next=p->next;
free(p);
}
return listhead;
10.4.3 链表操作完整程序
#include
#include
struct list
{ int data;
struct list *next;
};
struct list * dele(struct list *head);
struct list * insert(struct list *head);
void display(struct list *head);
struct list * create();
struct list *deleall(struct list *listhead); /* 释放链表全部结点 */
void main()
{
int ch;
struct list *head;
head=NULL;
·155·
while(1)
{ /* 屏幕输出菜单的字符提示信息 */
printf("1. 建立链表n");
printf("2. 插入结点n");
printf("3. 删除结点n");
printf("4. 显示结点n");
printf("0. 退出程序n");
printf("输入选择数字:");
scanf("%d",&ch);
switch(ch)
{
case 1:
head=create();
break;
case 2:
head=insert(head);
break;
case 3:
head=dele(head);
break;
case 4:
display(head);
break;
case 0:
/* 程序结束前,需要释放链表中所有的结点内存 */
head=deleall(head);
}
if(ch==0) break;
}
}
struct list *create()
{ struct list *listhead,*p,*listp;
int x;
listhead=NULL;
printf("输入成绩:");
scanf("%d",&x);
while(x!=-1)
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
·156·
{ printf("内存申请错误!n");
exit(0);
}
if(listhead==NULL)
listhead=p;
else
listp->next=p;
listp=p;
listp->data=x;
listp->next=NULL;
printf("输入下一个成绩(-1结束):");
scanf("%d",&x);
}
return listhead;
}
void display(struct list *listhead)
{ struct list *p;
if(listhead==NULL)
{ printf("链表为空!n");
return;
}
p=listhead;
while(p!=NULL)
{ printf("%dn",p->data);
p=p->next;
}
}
struct list *insert(struct list *listhead)
{ struct list *p,*listp;
int k,x,i;
if(listhead==NULL)
{
printf("链表还没有建立!n");
return listhead;
}
/* 输入插入结点数据 */
printf("输入新插入结点的数据和位置:");
scanf("%d,%d",&x,&i);
·157·
/* 如果链表为空 */
k=1;
listp=listhead;
while(listp!=NULL && k { listp=listp->next;
k=k+1;
}
if(k printf(“插入位置为无效值n”);
else if(i==1)
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
{ printf("内存申请错误!n");
exit(0);
}
p->data=x;
p->next=listhead;
listhead=p;
}
else
{
if((p=(struct list*)malloc(sizeof(struct list)))==NULL)
{ printf("内存申请错误!n");
exit(0);
}
p->data=x;
p->next=listp->next;
listp->next=p;
}
return listhead;
}
struct list *dele(struct list *listhead)
{ struct list *p,*listp;
int i,k;
if(listhead==NULL)
{ printf("链表为空n");
return listhead;
}
printf("输入删除结点序号:");
·158·
scanf("%d",&i);
k=1;
listp=listhead;
while(listp!=NULL && k { listp=listp->next;
k=k+1;
}
if(k printf(“删除结点序号无效n”);
else if(i==1)
{
p=listhead; /* p指向第1个结点 */
listhead=p->next; /* 第2个结点变为第1个结点,原来第1个结点脱链 */
free(p); /* 释放删除结点 */
}
else if(i>1 && k>=i-1)
{
p=listp->next;
listp->next=p->next;
free(p);
}
return listhead;
}
struct list *deleall(struct list *listhead)
{ struct list *p;
p=listhead;
while(p!=NULL)
{
listhead=p->next;
free(p);
p=listhead;
}
return p;
}
10.5 共用体
在进行某些C语言程序设计时,可能会需要使几种不同类型的变量存放到同一段内存单
·159·
元中,使几个变量互相覆盖,也就是使用覆盖技术。这种几个不同的变量共同占用一段内存
的结构,在C语言中,被称作共用体类型结构,简称共用体。使用共用体的目的,是为了节
省存储空间,尤其对于大型数组,把不同类型的几个变量共用同一地址单元,然后分阶段先
后使用。共用体类型各成员变量所占用的内存空间,不是其所有成员所需存储空间的总和,
而是其中所需存储空间最大的那个成员所占的空间。由于计算机的发展非常迅速,内存容量
越来越大,因此共用体的使用越来越少。现在,共用体主要应用于某些特殊的高级应用程序
中。
1. 共用体类型和共用体变量的定义
共用体类型的定义形式为:
union 共用体名
{ 成员项表
};
共用体变量的定义形式为:
union 共用体名
{ 成员项表
}共用体变量名表;
或
union
{ 成员项表
}共用体变量名表;
或
union 共用体名 共用体变量名表;
2. 共用体成员变量的引用
共用体的定义形式与结构体的定义形式是一样的,但必须特别注意它与结构体的区别,
特别是使用共用体成员的过程中。
假设我们定义了如下的共用体:
union student
{ char name[10];
int age;
float score;
}x;
则x在内存中需占用10个字节的内存空间。
共用体变量x的字符型数组name成员引用。
共用体变量x的整型成员变量age的引用。
共用体变量x的实型成员变量score的引用。
共用体变量中的各成员不能同时使用,起作用的成员只能是最后一次存放数据的成员,
存放进一个新成员的值后,原来的成员失去作用。例如有下列语句:
=78.5;
=23;
·160·
printf("age=%d,score=%fn",,);
则在执行完上述语句之后,只有的值正常输出23,而的值无效,即不是78.5,
也不是23。
还必须注意。即使是两个同类型的共用体变量之间也不能相互赋值;共用体变量不能作
为函数的参数;共用体指针可以作为函数参数,其用法于结构体指针类似;在共用体定义中,
可以使用结构体类型,共用体定义中也可以使用结构体类型。
【例10.7】共用体例子
【程序代码】
#include
struct ctag
{ char low;
char high;
};
union utag
{ struct ctag bacc;
short wacc;
}uacc;
void main()
{ =(short)0x1234;
printf("word value is : %04xn",);
printf("high byte is : %02xn",);
printf("low byte is : %02xn",);
=(char)0xFF;
printf("word value is : %04xn",);
}
【运行结果】
word value is : 1234
high byte is : 12
low byte is : 34
word value is : ff34
共用体的应用很多是在C语言的高级程序设计中,但使用并不复杂。
10.6 枚举
当一个变量只有几种可能的取值时,则可以定义为枚举类型的变量。
一般的枚举类型的定义的语法描述如下:
enum 枚举标识符{常量列表};
例如假定变量m,它的值是up、down、before、back、left、right六个方位之一,可
将其定义为下面枚举类型的变量:
·161·
enum direction
{ up,down,before,back,left,right }m;
其中enum为系统提供的定义枚举类型的关键字,Direction为用户定义的枚举名,up、
down、before、back、left、right为枚举元素,它们是常量,可以直接引用。C编译系统按
元素定义的顺序规定它们的值,up为0,down为1,before为2,back为3、left为4,right
为5。
另外,允许设定部分枚举常量对应的整数常量值,但是要求从左到右依次设定枚举常量
对应的整数常量值,并且不能重复。例如:
enum Direction{up,down=7,before,back=1,left,right};
则从第一没有设定值的常量开始,其整数常量值为前一枚举常量对应的整数常量值加1。
即up=0,down=7,before=8,back=1,left=2,right=3。
系统将枚举元素按常量处理,不能对其完成赋值操作,up=1;left=2;是错误的。对于枚
举变量,也不能直接赋值整数,语句m=1;是错误的。
【例10.8】枚举类型例子
【程序代码】
#include
enum direction
{up,down,before,back,left,right};
void main()
{ enum direction a,b;
a=down;
b=right;
printf("%d+%d=%dn",a,b,a+b);
}
【运行结果】
1+5=6
10.7 用typedef定义类型符
除了可以直接使用标准的类型符(如int、char、float、double、unsigned long等)
和用户自定义的结构体、共用体等类型符外,还可以用typedef声明新的类型符。如:
typedef int INTEGER;
typedef float REAL;
这样,在定义变量类型时可以用INTEGER替代int说明符,用REAL替代float说明符。
如:
INTEGER i,j;
REAL x,y;
声明结构体类型的例子:
typedef struct
·162·
{ char name;
int score[3];
float aver;
}STUDENT;
STUDENT为新类型符,它不是结构体变量,使用STUDENT定义结构体变量:
STUDENT x;
下面语句是声明一种新的类型符,而不是定义一个字符数组:
typedef char STRING[80];
声明之后,可以用STRING直接定义字符数组:
STRING dz1,dz2;
它与下面定义:
char dz1[80],dz2[80];
是等价的。
typedef是一个声明语句,不能定义新的数据类型,它可以将一些繁琐的说明符变为简
捷,或者更为直观易读。
10.8 本章小结
本章的要理解和掌握较多,其中包括以下内容:
1.引入结构体目的,是把不同类型的数据组合成一个整体对象来处理。
2.结构体变量的使用与其他变量一样,要先定义后使用。在定义结构体类型时,系统不
为各成员分配存储空间。结构体变量被定义说明后,结构变量虽然也可以象其它类型的变量
一样运算,但是结构变量以成员作为基本变量。结构成员的表示方式为:
结构变量.成员名
在使用时将“结构变量.成员名”看成一个整体,这个整体的数据类型与结构中该成员
的数据类型相同。
3.结构体数组是具有相同结构类型的变量集合,结构体数组成员的访问是以数组元素为
结构体变量的,其形式为:
结构数组元素.成员名
可以把结构体数组看作一个二维结构,第一维是结构数组元素,每个元素是一个结构体
变量,第二维是结构成员。
4.结构体指针是指向结构体的指针。它由一个加在结构变量名前的“*” 操作符来定义。
如:struct student * stu
使用结构体指针对结构成员的访问方法是:
结构指针名->结构成员
5.链表概念的引入,数组在物理存储单元上连续存放,插入与删除等操作的比较复杂,
而链表可以解决此问题,链表由一系列结点组成,结点可以在运行时动态生成。每个结点包
括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表的基本操作包括结点的插入、删除及对结点的访问。
·163·
6.共同体与结构体的定义与使用方法相类似,但共用体是为节省数据占用的内存空间而
采用的成员变量互相覆盖技术,即某一时刻只有一个成员起作用。
7.枚举类型用于解决某些变量的取值限定在一个有限的范围内的问题。枚举类型的定义
中列出所有可能的取值,它是一种基本数据类型,而不是构造类型。
8.typedef仅是一个声明语句,只能声明类型符号,并不能定义新的数据类型。
10.9 实训
实训1
【实训内容】结构体的概念。
【实训目的】掌握结构体类型的定义方法,掌握结构体变量的定义与使用方法。
【实训题目】以下程序,用于处理学生的基本信息,学生的信息包括:出生年份、姓名。
程序要求从键盘输入学生的数据,并输出成绩表。在读懂程序的基础上,上机运行验证,并
在/* */内填写相应的注释信息。
#include
struct student
{ int year; /* */
char name[10];
};
void main( )
{ struct student st1; /* */
scanf("%d",&); /* */
scanf("%s",);
printf("name=%s year=%dn",,) ; /* */
}
实训2
【实训内容】结构体数组。
【实训目的】理解结构体与数组的区别;掌握结构体类型数组的概念、定义和使用方法。
【实训题目】编写程序,用实训1所定义的学生的基本信息结构类型,建立包括3名学
生的数组,在定义的同时完成对数据的初始化;从这3名学生中查找闰年出生的学生,找到
则输出基本信息,否则输出“无此数据”的信息。
部分程序代码:
void main( )
{
·164·
struct student st[3]={{2000,"zhang"},{1999,"li"},{1980,"zhao"}};
int i;
for(i=0;i<3;i++)
if(st[i].year%4==0&&st[i].year%100!=0||st[i].year%400==0)
printf("name=%s year=%dn",st[i].name,st[i].year) ;
}
实训3
【实训内容】结构体指针。
【实训目的】掌握结构体指针的方法设计结构体程序。
【实训题目】试用结构体指针,重新编写实训2程序。
【实训指导】
用指针引用结构体变量成员有两种方法:
显示法,(*指针变量).成员名
成员运算符,指针变量->成员名
实训4
【实训内容】链表应用。
【实训目的】掌握结构体指针的方法设计结构体程序。
【实训题目】建立一个学生数据链表,每个结点数据域包括如下内容:出生年份、姓名。
对该链表作如下处理:(1)输出所有学生的信息;(2)把闰年出生的学生结点删除。(可参
照教材10.4.2和10.4.3)
【实训指导】
(1)链表结点结构定义时,注意包含数据域和指针域;
(2)结点的申请用要用到 malloc()函数;结点的删除用free()函数;
(3)建立链表,需定义链表头结点,链表当前结点,和申请空间的结点指针;
(4)访问链表时,从头结点开始,直至指针域为NULL;用p=p->next的方式向后查询;
(5)删除结点,用q->next=p->next的方法删除q结点后面的结点p。
习题
10.1阅读下列程序,写出程序运行结果:
(1)
#include
struct intxy
{int x;
int y;
·165·
};
void main( )
{ struct intxy m1,m2={2,7} ;
m1.x=1;m1.y=3;
printf("n%d",m1.y/m1.x*m2.x+m2.y);
}
(2)
#include
void main( )
{ struct
{ int x ;
int y ;
}s[2]={{1,2},{3,4}},*p=s;
printf("n %d",++p->x) ;
printf(" %d",(++p)->x) ;
}
(3)
#include
struct comm
{ char *name ;
int age ;
float sales ;
};
void main()
{ struct comm x[2],y,*p ;
int i ;
="Chan" ;
=30 ;
=200.0 ;
x[0].name="Liu";x[0].age=55;x[0].sales=350.0 ;
x[1].name="Li" ;x[1].age=45;x[1].sales=300.0 ;
p=&y;
printf("n %-5s %d %.2f",p->name,p->age,p->sales) ;
for(i=0;i<2;i++)
printf("n %-5s %d %.2f",x[i].name,x[i].age,x[i].sales) ;
}
(4)
#include
struct date
{ int y,m,d;
·166·
} ;
struct stu
{ char *name ;
struct date birthday ;
int s[3];
} ;
void main()
{ struct stu Li={"LiLan",1982,12,22,88,89,85.5};
printf("n name:%s",);
printf("n birthday:%d-%d-%d",
ay.y,ay.m,ay.d);
printf("n score:%d %d %d",Li.s[0],Li.s[1],Li.s[2]);
}
#include
#include
void main( )
{ int *a,*b,*c,*min;
}
#include
struct lst
{ int num ;
struct lst *next ;
};
void main()
{ struct lst a,b,c,*p;
=1 ;
=&b ;
=2 ;
(5)
a=(int *)malloc(sizeof(int)) ;
b=(int *)malloc(sizeof(int)) ;
c=(int *)malloc(sizeof(int)) ;
min=(int *)malloc(sizeof(int)) ;
scanf("%d %d %d",a,b,c);
*min=*a ;
if(*b<*min) *min=*b ;
if(*c<*min) *min=*c;
printf("n min=%d",*min) ;
free(a);free(b); free(c);free(min);
(6)
·167·
=&c ;
=3 ;
=NULL ;
p=&a ;
printf("n %d",p->num) ;
p=p->next ;
printf(" %d",p->num) ;
p=p->next ;
printf(" %d",p->num) ;
}
10.2 定义一个结构体变量,其成员包括职工的姓名、性别、年龄、工资和住址,然后
由键盘输入数据并输出到屏幕。
10.3 按第1题的结构体类型定义一个含有5名职工的结构体数组,从键盘输入每个结
构体元素所需的数据,计算平均工资,然后输出高于平均工资的职工姓名、工资和住址。
10.4 编一个含有4个学生(包括学号、姓名及数学成绩)的结构体数组,找出成绩最好
者并将其打印输出的程序。
10.5 建立一个链表,结点数据包括学生姓名、三门课程考试成绩,并实现下列功能:
⑴ 建立链表。
⑵ 输出成绩单。
⑶ 统计学生人数。
⑷ 计算平均成绩。
⑸ 按姓名查找学生成绩。
·168·
第11章 位运算
指针和位运算是C语言的重要特色,在许多方面满足了编写系统程序的需要。位运算功
能具有低级语言的特点,在检测和控制领域中广泛应用。
位运算是指进行二进制位的运算,C语言提供了下面六种位运算符:
& 按位与运算符
| 按位或运算符
∧ 按位异或运算符
~ 按位取反运算符
<< 左移运算符
>> 右移运算符
六种位运算符中,只有取反运算符为单目运算符,并具有右结合性,其它五种均为双目
运算符,具有左结合性。
关于优先级,取反运算符级别较高,仅次于括号和成员运算符。左移和右移运算符优先
级低于算术运算符,高于关系运算符。其它三种运算符优先级由高至低的顺序为按位与、按
位异或、按位或,三者优先级低于关系运算符,高于赋值运算符和条件运算符。详细说明参
考附录B。
1. 按位与运算符&
参加运算的两个数据按二进位进行与运算。即:
0&0=0; 0&1=0; 1&0=0; 1&1=1;
位运算操作结果的值一般没有参考意义,位运算操作更关心某些二进制位的值。按位与
运算的一般用途是:对某些位清零,采用的方法是使用0与这些为进行相与,若保留某些位
不变,则使用1与这些位相与。
编写位运算程序时,多数情况下将整型数据用八进制或十六进制表示。下面是两个八进
制数进行按位与运算:
例如:0125&017的结果为5,实现了左边4位置零。
0125=01010101
& 017=00001111
05=00000101
如果操作数为负数,将以补码形式参加运算。
2. 按位或运算符|
运算规则是:两个运算位,只要有一个为1,结果就为1。即:
0|0=0; 1|0=1; 0|1=1; 1|1=1;
按位或运算的一般用途是:对某些位置1,采用的方法是使用1与这些为进行相或,若
保留某些位不变,则使用0与这些位相或。
下面是两个八进制数进行按位或的运算。
例如:0101|017的结果为0117,将0101右4位置1。
01000001=0101
| 00001111=017
·169·
01001111=0117
3. 按位异或运算符∧
异或运算符的运算规则是:
0∧0=0; 0∧1=1; 1∧0=1; 1∧1=0;
如果两位数字相同结果为零,两位数字不同结果为1。通常与1异或完成对某位取反,
与0异或保留该位不变。
看下面简单应用例子。
例如:0172∧017=0163,将0172右4位翻转(0变1,1变0)
01111010=0172
∧ 00001111=017
01110101=0163
4. 按取反运算符~
按位取反是单目运算符,将每一位上的0变1,1变0。
例如:~0115=0262。
~ 01001101=0115
10110010=0262
5. 左移运算符<<
运算符的左边为移位对象,右边位左移位数。左移时,右端(低位)补零,左端(高位)
移出部分舍弃。
1
例如:06<<01=014,十进制表示6<<1=12,相当于6×2
=12。
00000110=06 <<01
000001100=014
.
例如:06<<02=030,十进制表示6<<2=24,相当于6×2=24。
00000110=06 <<02
=030
..
例如:0100<<2=0,十进制表示64<<2=0,唯一的一个非零数字移出高端。
01000000=0100 <<02
=030
..
6. 右移运算符>>
运算符的左边为移位对象,右边为右移位数。右移时,右端(低位)移出部分舍弃,左
端(高位)移入的二进制数分下面两种情况:
⑴对于无符号数或正整数,高位补零。
⑵对于负整数,高位补1,这种移位称为算术右移;在有的系统中仍然高位补零,称为
逻辑右移。Turbo C使用算术右移。
2
例如:030>>02=06,十进制表示24>>2=6,相当于24÷2
=6。
00011000=030 >>02
=06
..
例如:-071400>>2=-016300
二进制原码:1111
2
·170·
二进制补码:1000(机内存储形式)
右移两位后:111000 (算术右移,高位补1)
..
二进制原码:1000
八进制形式:-016300
当变量作为位运算符的操作数时,移位、按位取反等操作在运算器中进行,变量原来的
值不变。
【例10.1】在一个整型数a 中从右端开始,取出4至7位。
#include
void main()
{ unsigned a,b,c,d;
scanf("%o",&a);
b=a>>4;
c=~(~0<<4);
d=b&c;
printf("%o,%dn%o,%dn",a,a,d,d);
}
运行情况如下:
331
331,217
15,13
实训
【实训内容】位运算。
【实训目的】掌握位运算的概念和方法,掌握位运算符(按位与、按位或、按位非、左
移、右移)的使用方法,掌握有关位运算的用法。
【实训题目】阅读以下程序,分析程序的运行结果,并上机验证。
#include
void main()
{
char ch1='a',ch2;
int x=8,y,z;
ch2=ch1&223;
putchar(ch2);
ch2=ch1|3;
putchar(ch2);
ch2=(~ch1)&(~128);
putchar(ch2);
y=x<<2;
z=x>>1;
printf("nx=%d,y=%d,z=%dn",x,y,z);
}
·171·
习题
11.1 写一个函数,对一个8位的二进制数取出它的奇数位,即从左边开始第1、3、5、
7位。
11.2 计算下列表达式的值。
⑴ 3&&5 ⑵ 3&5 ⑶ 3||5
⑷ 3|5 ⑸ -2&3<<2|3 ⑹ 5∧3|2<<1
·172·
第11章 文 件
在处理实际问题时,常常需要处理大量数据,这些数据是以文件的形式存储在外部介
质(如磁盘)上,需要时从磁盘调入到计算机内存中,处理完毕后输出到磁盘上存储起来。本
章重点讲授文件的处理方法。
11.1 文件概述
11.1.1 文件概念和类型
通常 “文件” 的概念是指存储在外部介质上一组相关数据的集合。例如,程序文件是
程序代码的集合,数据文件是数据的集合。每个文件都有一个名称,称为文件名。一批数据
是以文件的形式存放在外部介质(如磁盘)上的,而操作系统以文件为单位对数据进行管理。
也就是说,如果想寻找保存在外部介质上的数据,必须先按文件名找到指定的文件,然后再
从该文件中读取数据。要向外部介质上存储数据也必须以文件名标识先建立一个文件,才能
向它输出数据。
在程序运行时,常常需要将一些数据(运行的最终结果或中间数据)输出到磁盘上存放
起来,以后需要时再从磁盘中输入到计算机内存,这就要用到磁盘文件。除磁盘文件外,操
作系统把每一个与主机相联的输入输出设备都看作是文件来管理。比如,键盘是输入文件,
显示屏和打印机是输出文件。
文件在C语言中被看成是由字符(字节)的数据顺序组成的一种序列,并将它们按数据
的组织方式分为二进制文件和ASCII码文件二种。二进制文件,即是把数据按内存的存储方
式直接存放在磁盘上的一种形式。
ASCII文件也称为文本文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存
放对应的ASCII码。例如,数5678的存储形式为:
ASC码: 00110101 00110110 00110111 00111000
↓ ↓ ↓ ↓
十进制码: 5 6 7 8 共占用4个字节。
ASCII码文件可在屏幕上按字符显示,例如源程序文件就是ASCII文件。由于是按字符
显示,因此能读懂文件内容。
二进制文件是按二进制的编码方式来存放文件的。 例如, 数5678的存储形式为:
00010110 00101110只占二个字节。二进制文件虽然也可在屏幕上显示, 但其内容无法读
懂。C系统在处理这些文件时,并不区分类型,都看成是字符流,按字节进行处理。输入输
出字符流的开始和结束只由程序控制而不受物理符号(如回车符)的控制。 因此也把这种文
件称作“流式文件”。
11.1.2 文件指针
每个被使用的文件都在内存中开辟一个区域,用来存放文件的有关信息,这些信息是保
·173·
存在一个结构体类型的变量中的,该结构体类型是由系统定义的,取名为FILE。
对FILE这个结构体类型的定义是在stdio.h头文件中由系统完成的,只要程序用到一
个文件,系统就为此文件开辟一个如上的结构体变量。有几个文件就开辟几个这样的结构体
变量,分别用来存放各个文件的有关信息。这些结构体变量不用变量名来标识,而通过指向
结构体类型的指针变量去访问,这就是“文件指针”。
例:FILE *fp;
其中fp是指向FILE结构的指针变量,把fp称为指向一个文件的指针。
11.2 文件基本操作
11.2.1 文件的打开和关闭
C语言同其它语言一样,规定对文件进行读写操作之前应该首先打开该文件,在操作结
束之后应关闭该文件。
1.文件的打开
标准输入输出函数库提供fopen函数完成文件打开操作,fopen函数的用法如下:
FILE *fp;
fp=fopen(文件名,文件操作方式);
文件操作方式见表11.1。
表11-1 文件操作方式
文件操作方式
“r”只读
“w”只写
“a”追加
“rb”
“wb”
“ab”
“r+”
“w+”
“a+”
“rb+”
“wb+”
“ab+”
含 义
打开一个文本文件只读
生成一个文本文件只写
对一个文本文件添加
打开一个二进制文件只读
生成一个二进制文件只写
对一个二进制文件添加
打开一个文本文件读/写
生成一个文本文件读/写
打开或生成一个文本文件读/写
打开一个二进制文件读/写
生成一个二进制文件读/写
打开或生成一个二进制文件读/写
指定文件不存在时
出错
建立新文件
建立新文件
出错
建立新文件
建立新文件
出错
建立新文件
建立新文件
出错
建立新文件
建立新文件
指定文件存在时
正常打开
原文件内容丢失
原文件尾部追加数据
正常打开
原文件内容丢失
原文件尾部追加数据
正常打开
原文件内容丢失
原文件尾部追加数据
正常打开
原文件内容丢失
原文件尾部追加数据
说明:
(1) 如果不能实现“打开”任务,fopen 函数将会带回一个出错信息。出错的原因可
能是用“r”方式打开一个并不存在的文件;磁盘出故障;磁盘已满无法建立新文件等。此
时 fopen 函数将带回一个空指针值NULL。
·174·
(2) 在向计算机输入文本文件时,将回车换行符转换为一个换行符,在输出时把换行
符转换成为回车和换行两个字符。在用二进制文件时,不进行这种转换,在内存中的数据形
式与输出到外部文件中的数据形式完全一致,一一对应。
【例11-1】如果想打开一个名为 texe .txt 文件并准备写操作。
可以用语句:
fp= fopen ( “ ”,“w”);
下面的用法比较常见。
if((fp=fopen(“text”,“w”))==NULL)
{
printf(“不能打开此文件 n”);
exit (1);
}
这种用法可以在写文件之前先检验已打开的文件是否成功。
2.文件的关闭
fclose()函数用来关闭一个已由fopen()函数打开的文件。
必须在程序结束之前关闭所有文件,文件未关闭会引起很多问题,如数据丢失、文件损
坏及其它一些错误
fclose()函数的调用形式为:
fclose(fp);
其中fp 是一个调用 fopen()时返回的文件指针。
若关闭文件成功,则fclose()函数返回值为0;若fclose()函数的返回值不为0,则说
明出错了。
11.2.2 文件的读写
文件打开后如何完成文件的读写操作。
11.2.2.1 字符读写操作
fputc()函数和fgetc()函数,是用来读写字符的。它们的调用形式是:
fputc(ch,fp);
ch=fgetc(fp);
其中:fp为文件型指针,ch为字符变量。
fputc(ch,fp) 函数的作用是将字符ch的值输出到fp所指向的文件中去。如果输出成
功则返回值就是输出的字符,如果输出失败,则返回一个EOF(或整型常量-1)。
fgetc 函数从指定的文件读入一个字符,赋给ch。如果在执行fgetc函数读字符时遇
到文件结束符,函数返回一个文件结束标志EOF(或整型常量-1)。
【例11-2】下面的程序段可以从文本文件头一直读到文件尾:
ch=fgetc(fp);
while(ch!=EOF);
{
ch=fgetc(fp);
}
·175·
文本文件可以用两种方法来判定文件结束:①读入的字符若是EOF(或整型常量-1),
则文件结束;②利用feof(fp)函数。若文件结束,feof函数返回非0值,否则返回0。
用二进制方式打开的文件,只能利用feof(fp)函数来判定文件结束。
【例11-3】下面的语句可以从二进制文件首一直读到文件尾:
while(! feof(fp))
ch=fgetc(fp);
11.2.2.2 字符串读写操作
fgets()函数和 fputs()函数,是用来读写字符串的。它们的调用形式是:
fgets(str,length,fp);
fputs(str,fp);
其中:str是字符指针,length是整型数值,fp是文件型指针。
函数fgets()从fp指定的文件中当前的位置上读取字符串,直至读到换行符或第
length-1个字符或遇到EOF为止。如果读入的是换行符,则它将作为字符串的一部分。操
作成功时,返回str;若发生错误或到达文件尾时,则fgets()都返回一个空指针。
fputs()函数用来向fp指定的文件中当前的位置上写字符串。操作成功时,fputs()函
数返回0,失败时返回非零值。
【例11-4】从指定文件读入一个字符串。
fgets(str,100,fp);
向指定的文件输出一个字符串。
fputs(“辽宁工程技术大学”,fp);
11.2.2.3 数据块读写操作
fread()函数 和fwrite()函数,是用来读写数据块的函数。它们的调用形式为:
fread(buffer,size,count,fp);
fwrite(buffer,size,count,fp);
其中:buffer是一个指针,它是读入数据的存放地址,或输出数据的地址(以上指的
是起始地址);size是 要读写的字节数;count是要进行读写多少次size字节的数据项;
fp是文件型指针。
fread()函数操作成功时,返回实际读取的字段个数count;到达文件尾或出现错误时,
返回值小于count。fwrite()函数操作成功时,返回实际所写的字段个数count;返回值小
于count,说明发生了错误。
如果文件以二进制形式打开,用 fread 和 fwrite 函数就可以读写任何类型的信息,
如:
fread(a,4,8,fp);
其中a是一个实型数组名。一个实型变量占4个字节。这个函数从fp所指向的文件读
入8次(每次4个字节)数据,存储到数组a中。
·176·
11.2.2.4 格式化读写操作
fprintf()函数 和fscanf()函数,这两个函数功能是与printf()和scanf()完全相同,
但其操作对象是磁盘文件。
调用方式为:
fprintf(fp“控制字符串”,参数表);
fscanf(fp“控制字符串”参数表);
其中:fp是文件型指针,控制字符串和参数表同printf()函数和scanf()函数一样。
这两个函数将其输入输出指向到由fp确定的文件。
fprintf ()函数操作成功,返回实际被写的字符个数;出现错误时,返回一个负数。
fscanf()函数操作成功,返回实际被赋值的参数个数;若返回EOF,则表示试图去读取超
过文件末尾的部分。
11.3 应用实例
【例11-5】从键盘输入一些字符,逐个把它们送到磁盘上去,直到输入一个“#”为止。
#include
#include
void main()
{ FILE *fp;
char ch,filename[10];
scanf("%s",filename);
if((fp=fopen(filename, "w"))==NULL)
{ printf("文件不能打开n");
exit(0);
}
ch=getchar();
while(ch!='#')
{ fputc(ch,fp);putchar(ch);
ch=getchar();
}
fclose(fp);
}
运行情况如下
file1.c (输入磁盘文件名)
computer and c# (输入一个字符串)
computer and c (输出一个字符串)
文件名由键盘输入,赋给字符数组filename,fopen函数中的第一个参数“文件名”可
以直接写成字符串常量形式,如“file1.c”,也可以用字符数组名,在字符数组中存放文件
名(如本例所用的方法)。
·177·
本例运行时,从键盘输入磁盘文件名“file1.c”,然后输入字符串“computer and c”,
“#”是表示输入结束,程序将“computer and c”写到以“file1.c”命名的磁盘文件中,
同时在屏幕上显示这些字符,以便核对。
【例11-6】在例11-5中建立的文本文件file1.c中追加一个字符串。
#include
#include
void main()
{ FILE *fp;
char ch,st[40];
if((fp=fopen(“file1.c”, "a+"))==NULL) /*以追加方式打开文件*/
{ printf("文件不能打开n");
exit(0);
}
gets(st);
fputs(st,fp);
fclose(fp);
}
运行情况如下
abcdefg (输入一个字符串)
file1.c文件内容为:
computer and cabcdefg
【例11-7】将一个磁盘文件中的信息复制到另一个磁盘文件中。
#include
#include
void main()
{ FILE *in,*out;
char infile[10],outfile[10];
printf("Enter the infile name :n");
scanf("%s",infile);
printf("Enter the outfile name :n");
scanf("%s",outfile);
if((in=fopen(infile, "r"))==NULL)
{ printf("文件不能打开n ");
exit(0);
}
if((out=fopen(outfile, "w"))==NULL)
{ printf("文件不能打开n ");
exit(0);
}
while(1)
{ fputc(fgetc(in),out);
if(!feof(in)) break ;
}
fclose(in);
·178·
fclose(out);
}
运行情况如下:
Enter the infile name :
file1.c (输入原有磁盘文件名)
Enter the outfile name:
file2.c (输入新复制的磁盘文件名)
程序运行结果是将file1.c文件中的内容复制到file2.c中去。
以上程序是按文本文件方式处理的。也可以用此程序来复制一个二进制文件,只需将两
个fopen函数中的“r”和“w”分别改为“rb”和“wb”即可。
【例11-8】从键盘输入4个学生的有关数据,然后把它们转存到磁盘文件。
#include
#define SIZE 4
struct student
{ char name[10];
int score[3];
}stud[SIZE];
void save( )
{ FILE * fp;
int i;
if((fp=fopen("stu_list","wb"))==NULL)
{ printf("cannot open filen");
return;
}
for(i=0;i if(fwrite(&stud[i],sizeof(struct student),1,fp)!=1)
printf("file write errorn");
}
void main()
{ int i;
for(i=0;i { //scanf("%s",stud[i].name);
scanf("%s%d%d%d",stud[i].name,&stud[i].score[0],
&stud[i].score[1],&stud[i].score[2]);
}
save();
}
在 main 函数中,从终端键盘输入4个学生的数据,然后调用save 函数,将这些数据
输出到以“stu_list”命名的磁盘文件中。fwrite函数的作用是将一个结构体数组元素数
据块送到 stu_list 文件中。
运行情况如下:
输入4个学生的姓名、成绩1、成绩2和成绩3:
Zhang 67 79 81
Fun 82 72 72
·179·
Tan 83 81 73
Ling 74 81 84
程序运行时,屏幕上并无任何信息,只是将从键盘输入的数据送到磁盘文件上。为了验
证在磁盘文件“stu_list”中是否已存在此数据可以用以下程序从“stu_list”文件中读人
数据,然后在屏幕上输出。
#include
#define SIZE 4
struct student
{ char name[10];
int score[3];
}stud[SIZE];
void main()
{ int i;
FILE *fp;
fp=fopen("stu_list", "rb");
for(i=0;i { fread(&stud[i],sizeof(struct student),1,fp);
printf("%s: %4d,%4d,%4dn",stud[i].name,
stud[i].score[0],stud[i].score[1],stud[i].score[2]);
}
}
程序运行时不需从键盘输入任何数据。屏幕上显示出以下信息:
Zhang 67 79 81
Fun 82 72 72
Tan 83 81 73
Ling 74 81 84
请注意从键盘输入4个学生的数据是ASCII 码字符,也就是文本文件。在送到计算机
内存时,回车和换行符转换成一个换行符。再从内存以“wb”方式(二进制写)输出到
“stu_list”文件,此时不发生字符转换,按内存中存储形式原样输出到磁盘文件上。在上
面验证程序中,又用fread函数从“stu_list”文件向内存读人数据,注意此时用的是“rb”
方式,即二进制方式,数据按原样输入,也不发生字符转换。也就是这时候内存中的数据恢
复到第一个程序向“stu_list”输出以前的情况。最后在验证程序中,用printf函数输出
到屏幕,printf是格式输出函数,输出ASCII码,在屏幕上显示字符。换行符又转换为回
车加换行符。
如果企图从“stu_list”文件中以“r”方式读入数据就会出错。因为它们是按数据块
的长度来处理输入输出的,在字符发生转换的情况下很可能出现与原设想的情况不同。
例如,如果写
fread(&stud[i],sizeof(struct student),1,stdin);
企图从终端键盘输入数据,这在语法上并不存在错误,编译能通过。如果用以下形式输
入数据:
Zhang 67 79 81
Fun 82 72 72
Tan 83 81 73
Ling 74 81 84
·180·
由于 fread 函数要求一次输入16个字节(而不问这些字节的内容),因此输入数据中的
空格也作为输入数据而不作为数据间的分隔符了。连空格也存储到stud[i]中了,显然是不
对的。
这个题目要求的是从键盘输入数据,如果已有的数据己以二进制形式存储在一个磁盘文
件“stu_list”中,要求从其中读入数据并输出到“stu_list”文件中,可以编写一个load
函数,从磁盘文件中读二进制数据。
void load()
{ FILE *fp;
int i;
if((fp=fopen(“stu_dat”, “rb”))==NULL)
{ printf(“cannot open infilen”);
return;
}
for(i=0;i if(fread(&stud[i],sizeof(struc student),1,fp)!=1)
{ if(feof(fp))
return ;
printf(“filereaderrorn”);
}
}
将load函数加到本题原来的程序文件中,并将main函数改为
#include
void main()
{ load();
save();
}
即可实现题目要求。
【例11-9】读文本文件
假设磁盘文件内容如下,包括五名学生姓名和三门考试成绩:
赵一 89 90 91
钱二 70 80 90
孙三 99 88 77
李四 72 82 92
周五 85 75 65
下面程序读取五名学生数据,计算平均分,最后输出到文本文件。
#include
#include
struct student
{ char name[10];
int score[3];
float aver;
};
void main()
{ int i;
·181·
struct student stu[5];
FILE *in,*out;
if((in=fopen("","r"))==NULL)
{ printf("cannot open ");
exit(0);
}
if((out=fopen("","w"))==NULL)
{ printf("cannot open ");
exit(0);
}
for(i=1;i<=5 && !feof(in);i++)
{ fscanf(in,”%s%3d%3d%3d”,stu[I].name,
&stu[I].x[0], &stu[I].x[1], &stu[I].x[2]);
stu[i].aver=(stu[i].x[0]+stu[i].x[1]+stu[i].x[2])/3.0;
printf("%s %3d %3d %3d %6.2fn",stu[i].name,stu[i].x[0],
stu[i].x[1], stu[i].x[2],stu[i].aver);
fprintf(out,"%s %3d %3d %3d %6.2fn",stu[i].name,
stu[i].x[0], stu[i].x[1], stu[i].x[2],stu[i].aver);
}
fclose(in);
fclose(out);
}
用 fprintf 和 fscanf 函数对磁盘文件读写,使用方便,容易理解,但由于在输入时
要将 ASCII 码转换为二进制形式,在输出时又要将二进制形式转换成字符,花费时间比较
多。因此,在内存与磁盘频繁交换数据的情况下,最好不用 fprintf 和 fscanf 函数,而
用 fread 和 fwrite 函数。
·182·
发表评论