返回列表 发帖

C语言初学者入门讲座 第十一讲 指针的慨念(2)

(2)加减算术运算

  对于指向数组的指针变量,可以加上或减去一个整数n。设pa是指向数组a的指针变量,则pa+n,pa-n,pa++,++pa,pa--,--pa 运算都是合法的。指针变量加或减一个整数n的意义是把指针指向的当前位置(指向某数组元素)向前或向后移动n个位置。应该注意,数组指针变量向前或向后移动一个位置和地址加1或减1 在概念上是不同的。因为数组可以有不同的类型, 各种类型的数组元素所占的字节长度是不同的。如指针变量加1,即向后移动1 个位置表示指针变量指向下一个数据元素的首地址。而不是在原地址基础上加1。

  例如:

int a[5],*pa;
pa=a; /*pa指向数组a,也是指向a[0]*/
pa=pa+2; /*pa指向a[2],即pa的值为&pa[2]*/ 指针变量的加减运算只能对数组指针变量进行, 对指向其它类型变量的指针变量作加减运算是毫无意义的。(3)两个指针变量之间的运算只有指向同一数组的两个指针变量之间才能进行运算, 否则运算毫无意义。

  ①两指针变量相减

  两指针变量相减所得之差是两个指针所指数组元素之间相差的元素个数。实际上是两个指针值(地址) 相减之差再除以该数组元素的长度(字节数)。例如pf1和pf2 是指向同一浮点数组的两个指针变量,设pf1的值为2010H,pf2的值为2000H,而浮点数组每个元素占4个字节,所以pf1-pf2的结果为(2000H-2010H)/4=4,表示pf1和 pf2之间相差4个元素。两个指针变量不能进行加法运算。 例如, pf1+pf2是什么意思呢?毫无实际意义。

  ②两指针变量进行关系运算

  指向同一数组的两指针变量进行关系运算可表示它们所指数组元素之间的关系。例如:

pf1==pf2表示pf1和pf2指向同一数组元素
pf1>pf2表示pf1处于高地址位置
pf1<pf2表示pf2处于低地址位置
main(){
 int a=10,b=20,s,t,*pa,*pb;
 pa=&a;
 pb=&b;
 s=*pa+*pb;
 t=*pa**pb;
 printf("a=%d\nb=%d\na+b=%d\na*b=%d\n",a,b,a+b,a*b);
 printf("s=%d\nt=%d\n",s,t);
}
......

  说明pa,pb为整型指针变量

  给指针变量pa赋值,pa指向变量a。

  给指针变量pb赋值,pb指向变量b。

  本行的意义是求a+b之和,(*pa就是a,*pb就是b)。

  本行是求a*b之积。

  输出结果。

  输出结果。

  ......

  指针变量还可以与0比较。设p为指针变量,则p==0表明p是空指针,它不指向任何变量;p!=0表示p不是空指针。空指针是由对指针变量赋予0值而得到的。例如: #define NULL 0 int *p=NULL; 对指针变量赋0值和不赋值是不同的。指针变量未赋值时,可以是任意值,是不能使用的。否则将造成意外错误。而指针变量赋0值后,则可以使用,只是它不指向具体的变量而已。

main(){
 int a,b,c,*pmax,*pmin;
 printf("input three numbers:\n");
 scanf("%d%d%d",&a,&b,&c);
 if(a>b){
  pmax=&a;
  pmin=&b;
 }
 else{
  pmax=&b;
  pmin=&a;
 }
 if(c>*pmax) pmax=&c;
 if(c<*pmin) pmin=&c;
 printf("max=%d\nmin=%d\n",*pmax,*pmin);
}
......  

  pmax,pmin为整型指针变量。

  输入提示。

  输入三个数字。

  如果第一个数字大于第二个数字...

  指针变量赋值

  指针变量赋值

  指针变量赋值

  指针变量赋值

  判断并赋值

  判断并赋值

  输出结果

  ......

  数组指针变量的说明和使用

  指向数组的指针变量称为数组指针变量。 在讨论数组指针变量的说明和使用之前,我们先明确几个关系。

  一个数组是由连续的一块内存单元组成的。 数组名就是这块连续内存单元的首地址。一个数组也是由各个数组元素(下标变量) 组成的。每个数组元素按其类型不同占有几个连续的内存单元。 一个数组元素的首地址也是指它所占有的几个内存单元的首地址。 一个指针变量既可以指向一个数组,也可以指向一个数组元素, 可把数组名或第一个元素的地址赋予它。如要使指针变量指向第i号元素可以把i元素的首地址赋予它或把数组名加i赋予它。

  设有实数组a,指向a的指针变量为pa,从图6.3中我们可以看出有以下关系:

  pa,a,&a[0]均指向同一单元,它们是数组a的首地址,也是0 号元素a[0]的首地址。pa+1,a+1,&a[1]均指向1号元素a[1]。类推可知a+i,a+i,&a

  指向i号元素a。应该说明的是pa是变量,而a,&a都是常量。在编程时应予以注意。

main(){
 int a[5],i;
 for(i=0;i<5;i++){
  a=i;
  printf("a[%d]=%d\n",i,a);
 }
 printf("\n");
}

  主函数

  定义一个整型数组和一个整型变量

  循环语句

  给数组赋值

  打印每一个数组的值

  ......

  输出换行

  ......

  数组指针变量说明的一般形式为:

  类型说明符 * 指针变量名

  其中类型说明符表示所指数组的类型。 从一般形式可以看出指向数组的指针变量和指向普通变量的指针变量的说明是相同的。
引入指针变量后,就可以用两种方法来访问数组元素了。

  第一种方法为下标法,即用a形式访问数组元素。 在第四章中介绍数组时都是采用这种方法。

  第二种方法为指针法,即采用*(pa+i)形式,用间接访问的方法来访问数组元素。

main(){
 int a[5],i,*pa;
 pa=a;
 for(i=0;i<5;i++){
  *pa=i;
  pa++;
 }
 pa=a;
 for(i=0;i<5;i++){
  printf("a[%d]=%d\n",i,*pa);
  pa++;
 }
}

  主函数

  定义整型数组和指针

  将指针pa指向数组a

  循环

  将变量i的值赋给由指针pa指向的a[]的数组单元

  将指针pa指向a[]的下一个单元

  ......

  指针pa重新取得数组a的首地址

  循环

  用数组方式输出数组a中的所有元素

  将指针pa指向a[]的下一个单元

  ......
  ......

  下面,另举一例,该例与上例本意相同,但是实现方式不同。

main(){
 int a[5],i,*pa=a;
 for(i=0;i<5;){
  *pa=i;
  printf("a[%d]=%d\n",i++,*pa++);
 }
}

  主函数

  定义整型数组和指针,并使指针指向数组a

  循环

  将变量i的值赋给由指针pa指向的a[]的数组单元

  用指针输出数组a中的所有元素,同时指针pa指向a[]的下一个单元

  ......
  ......
 
  数组名和数组指针变量作函数参数

  在第五章中曾经介绍过用数组名作函数的实参和形参的问题。在学习指针变量之后就更容易理解这个问题了。 数组名就是数组的首地址,实参向形参传送数组名实际上就是传送数组的地址, 形参得到该地址后也指向同一数组。 这就好象同一件物品有两个彼此不同的名称一样。同样,指针变量的值也是地址, 数组指针变量的值即为数组的首地址,当然也可作为函数的参数使用。

float aver(float *pa);
main(){
 float sco[5],av,*sp;
 int i;
 sp=sco;
 printf("\ninput 5 scores:\n");
 for(i=0;i<5;i++) scanf("%f",&sco);
 av=aver(sp);
 printf("average score is %5.2f",av);
}
float aver(float *pa)
{
 int i;
 float av,s=0;
 for(i=0;i<5;i++) s=s+*pa++;
 av=s/5;
 return av;
}

TOP

C语言初学者入门讲座 第十二讲 多维数组的指针变量

一、多维数组地址的表示方法

  设有整型二维数组a[3][4]如下:

  0 1 2 3
  4 5 6 7
  8 9 10 11

  设数组a的首地址为1000,各下标变量的首地址及其值如图所示。  

在前面曾经介绍过, C语言允许把一个二维数组分解为多个一维数组来处理。因此数组a可分解为三个一维数组,即a[0],a[1],a[2]。每一个一维数组又含有四个元素。例如a[0]数组,含有a[0][0],a[0][1],a[0][2],a[0][3]四个元素。 数组及数组元素的地址表示如下:a是二维数组名,也是二维数组0行的首地址,等于1000。a[0]是第一个一维数组的数组名和首地址,因此也为1000。*(a+0)或*a是与a[0]等效的, 它表示一维数组a[0]0 号元素的首地址。 也为1000。&a[0][0]是二维数组a的0行0列元素首地址,同样是1000。因此,a,a[0],*(a+0),*a,&a[0][0]是相等的。同理,a+1是二维数组1行的首地址,等于1008。a[1]是第二个一维数组的数组名和首地址,因此也为1008。 &a[1][0]是二维数组a的1行0列元素地址,也是1008。因此a+1,a[1],*(a+1),&a[1][0]是等同的。 由此可得出:a+i,a,*(a+i),&a[0]是等同的。 此外,&a和a也是等同的。因为在二维数组中不能把&a理解为元素a的地址,不存在元素a

  C语言规定,它是一种地址计算方法,表示数组a第i行首地址。由此,我们得出:a,&a,*(a+i)和a+i也都是等同的。另外,a[0]也可以看成是a[0]+0是一维数组a[0]的0号元素的首地址, 而a[0]+1则是a[0]的1号元素首地址,由此可得出a+j则是一维数组a的j号元素首地址,它等于&a[j]。由a=*(a+i)得a+j=*(a+i)+j,由于*(a+i)+j是二维数组a的i行j列元素的首地址。该元素的值等于*(*(a+i)+j)。

[Explain]

#define PF "%d,%d,%d,%d,%d,\n"
main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
printf(PF,a,*a,a[0],&a[0],&a[0][0]);
printf(PF,a+1,*(a+1),a[1],&a[1],&a[1][0]);
printf(PF,a+2,*(a+2),a[2],&a[2],&a[2][0]);
printf("%d,%d\n",a[1]+1,*(a+1)+1);
printf("%d,%d\n",*(a[1]+1),*(*(a+1)+1));
}

  二、多维数组的指针变量

  把二维数组a 分解为一维数组a[0],a[1],a[2]之后,设p为指向二维数组的指针变量。可定义为: int (*p)[4] 它表示p是一个指针变量,它指向二维数组a 或指向第一个一维数组a[0],其值等于a,a[0],或&a[0][0]等。而p+i则指向一维数组a。从前面的分析可得出*(p+i)+j是二维数组i行j 列的元素的地址,而*(*(p+i)+j)则是i行j列元素的值。

  二维数组指针变量说明的一般形式为: 类型说明符 (*指针变量名)[长度] 其中“类型说明符”为所指数组的数据类型。“*”表示其后的变量是指针类型。 “长度”表示二维数组分解为多个一维数组时, 一维数组的长度,也就是二维数组的列数。应注意“(*指针变量名)”两边的括号不可少,如缺少括号则表示是指针数组(本章后面介绍),意义就完全不同了。

[Explain]

main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int(*p)[4];
int i,j;
p=a;
for(i=0;i<3;i++)
for(j=0;j<4;j++) printf("%2d ",*(*(p+i)+j));
}

  'Expain字符串指针变量的说明和使用字符串指针变量的定义说明与指向字符变量的指针变量说明是相同的。只能按对指针变量的赋值不同来区别。 对指向字符变量的指针变量应赋予该字符变量的地址。如: char c,*p=&c;表示p是一个指向字符变量c的指针变量。而: char *s="C Language";则表示s是一个指向字符串的指针变量。把字符串的首地址赋予s。

  请看下面一例。

main(){
char *ps;
ps="C Language";
printf("%s",ps);
}

  运行结果为:

  C Language

  上例中,首先定义ps是一个字符指针变量, 然后把字符串的首地址赋予ps(应写出整个字符串,以便编译系统把该串装入连续的一块内存单元),并把首地址送入ps。程序中的: char *ps;ps="C Language";等效于: char *ps="C Language";输出字符串中n个字符后的所有字符。

main(){
char *ps="this is a book";
int n=10;
ps=ps+n;
printf("%s\n",ps);
}

  运行结果为:

  book 在程序中对ps初始化时,即把字符串首地址赋予ps,当ps= ps+10之后,ps指向字符“b”,因此输出为"book"。

main(){
char st[20],*ps;
int i;
printf("input a string:\n");
ps=st;
scanf("%s",ps);
for(i=0;ps!='\0';i++)
if(ps=='k'){
printf("there is a 'k' in the string\n");
break;
}
if(ps=='\0') printf("There is no 'k' in the string\n");
}

  本例是在输入的字符串中查找有无‘k’字符。 下面这个例子是将指针变量指向一个格式字符串,用在printf函数中,用于输出二维数组的各种地址表示的值。但在printf语句中用指针变量PF代替了格式串。 这也是程序中常用的方法。

main(){
static int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
char *PF;
PF="%d,%d,%d,%d,%d\n";
printf(PF,a,*a,a[0],&a[0],&a[0][0]);
printf(PF,a+1,*(a+1),a[1],&a[1],&a[1][0]);
printf(PF,a+2,*(a+2),a[2],&a[2],&a[2][0]);
printf("%d,%d\n",a[1]+1,*(a+1)+1);
printf("%d,%d\n",*(a[1]+1),*(*(a+1)+1));
}

  在下例是讲解,把字符串指针作为函数参数的使用。要求把一个字符串的内容复制到另一个字符串中,并且不能使用strcpy函数。函数cprstr的形参为两个字符指针变量。pss指向源字符串,pds指向目标字符串。表达式:

(*pds=*pss)!=`\0'
cpystr(char *pss,char *pds){
while((*pds=*pss)!='\0'){
pds++;
pss++; }
}
main(){
char *pa="CHINA",b[10],*pb;
pb=b;
cpystr(pa,pb);
printf("string a=%s\nstring b=%s\n",pa,pb);
}  

  在上例中,程序完成了两项工作:一是把pss指向的源字符复制到pds所指向的目标字符中,二是判断所复制的字符是否为`\0',若是则表明源字符串结束,不再循环。否则,pds和pss都加1,指向下一字符。在主函数中,以指针变量pa,pb为实参,分别取得确定值后调用cprstr函数。由于采用的指针变量pa和pss,pb和pds均指向同一字符串,因此在主函数和cprstr函数中均可使用这些字符串。也可以把cprstr函数简化为以下形式:

cprstr(char *pss,char*pds)
{while ((*pds++=*pss++)!=`\0');}

  即把指针的移动和赋值合并在一个语句中。 进一步分析还可发现`\0'的ASCⅡ码为0,对于while语句只看表达式的值为非0就循环,为0则结束循环,因此也可省去“!=`\0'”这一判断部分,而写为以下形式:

cprstr (char *pss,char *pds)
{while (*pdss++=*pss++);}  

  表达式的意义可解释为,源字符向目标字符赋值, 移动指针,若所赋值为非0则循环,否则结束循环。这样使程序更加简洁。简化后的程序如下所示。

cpystr(char *pss,char *pds){
while(*pds++=*pss++);
}
main(){
char *pa="CHINA",b[10],*pb;
pb=b;
cpystr(pa,pb);
printf("string a=%s\nstring b=%s\n",pa,pb);
}

  使用字符串指针变量与字符数组的区别

  用字符数组和字符指针变量都可实现字符串的存储和运算。 但是两者是有区别的。在使用时应注意以下几个问题:

  1. 字符串指针变量本身是一个变量,用于存放字符串的首地址。而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以‘\0’作为串的结束。字符数组是由于若干个数组元素组成的,它可用来存放整个字符串。

  2. 对字符数组作初始化赋值,必须采用外部类型或静态类型,如: static char st[]={“C Language”};而对字符串指针变量则无此限制,如: char *ps="C Language";

  3. 对字符串指针方式 char *ps="C Language";可以写为: char *ps; ps="C Language";而对数组方式:

static char st[]={"C Language"};

  不能写为:

char st[20];st={"C Language"};

  而只能对字符数组的各元素逐个赋值。

  从以上几点可以看出字符串指针变量与字符数组在使用时的区别,同时也可看出使用指针变量更加方便。前面说过,当一个指针变量在未取得确定地址前使用是危险的,容易引起错误。但是对指针变量直接赋值是可以的。因为C系统对指针变量赋值时要给以确定的地址。因此,

char *ps="C Langage";

  或者

char *ps;
ps="C Language";

  都是合法的。

TOP

C语言初学者入门讲座 第十二讲 结构(1)

在实际问题中,一组数据往往具有不同的数据类型。例如, 在学生登记表中,姓名应为字符型;学号可为整型或字符型; 年龄应为整型;性别应为字符型;成绩可为整型或实型。 显然不能用一个数组来存放这一组数据。 因为数组中各元素的类型和长度都必须一致,以便于编译系统处理。为了解决这个问题,C语言中给出了另一种构造数据类型——“结构”。 它相当于其它高级语言中的记录。





  “结构”是一种构造类型,它是由若干“成员”组成的。 每一个成员可以是一个基本数据类型或者又是一个构造类型。 结构既是一种“构造”而成的数据类型, 那么在说明和使用之前必须先定义它,也就是构造它。如同在说明和调用函数之前要先定义函数一样。

  结构的定义

  定义一个结构的一般形式为:

struct 结构名
{
 成员表列
};

  成员表由若干个成员组成, 每个成员都是该结构的一个组成部分。对每个成员也必须作类型说明,其形式为:

  类型说明符 成员名;

  成员名的命名应符合标识符的书写规定。例如:

struct stu
{
 int num;
 char name[20];
 char sex;
 float score;
};  

  在这个结构定义中,结构名为stu,该结构由4个成员组成。 第一个成员为num,整型变量;第二个成员为name,字符数组;第三个成员为sex,字符变量;第四个成员为score,实型变量。 应注意在括号后的分号是不可少的。结构定义之后,即可进行变量说明。 凡说明为结构stu的变量都由上述4个成员组成。由此可见, 结构是一种复杂的数据类型,是数目固定,类型不同的若干有序变量的集合。

  结构类型变量的说明

  说明结构变量有以下三种方法。以上面定义的stu为例来加以说明。

  1. 先定义结构,再说明结构变量。如:

struct stu
{
 int num;
 char name[20];
 char sex;
 float score;
};
struct stu boy1,boy2;

  说明了两个变量boy1和boy2为stu结构类型。也可以用宏定义使一个符号常量来表示一个结构类型,例如:

#define STU struct stu
STU
{
 int num;
 char name[20];
 char sex;
 float score;
};
STU boy1,boy2;

  2. 在定义结构类型的同时说明结构变量。例如:

struct stu
{
 int num;
 char name[20];
 char sex;
 float score;
}boy1,boy2;

  3. 直接说明结构变量。例如:

struct
{
 int num;
 char name[20];
 char sex;
 float score;
}boy1,boy2;  

  第三种方法与第二种方法的区别在于第三种方法中省去了结构名,而直接给出结构变量。说明了boy1,boy2变量为stu类型后,即可向这两个变量中的各个成员赋值。在上述stu结构定义中,所有的成员都是基本数据类型或数组类型。成员也可以又是一个结构, 即构成了嵌套的结构。例如:

struct date{
 int month;
 int day;
 int year;
}
struct{
 int num;
 char name[20];
 char sex;
 struct date birthday;
 float score;
}boy1,boy2;

  首先定义一个结构date,由month(月)、day(日)、year(年) 三个成员组成。 在定义并说明变量 boy1 和 boy2 时, 其中的成员birthday被说明为data结构类型。成员名可与程序中其它变量同名,互不干扰。结构变量成员的表示方法在程序中使用结构变量时, 往往不把它作为一个整体来使用。

  在ANSI C中除了允许具有相同类型的结构变量相互赋值以外, 一般对结构变量的使用,包括赋值、输入、输出、 运算等都是通过结构变量的成员来实现的。

  表示结构变量成员的一般形式是: 结构变量名.成员名 例如:boy1.num 即第一个人的学号 boy2.sex 即第二个人的性别 如果成员本身又是一个结构则必须逐级找到最低级的成员才能使用。例如:boy1.birthday.month 即第一个人出生的月份成员可以在程序中单独使用,与普通变量完全相同。

  结构变量的赋值

  前面已经介绍,结构变量的赋值就是给各成员赋值。 可用输入语句或赋值语句来完成。

  [例7.1]给结构变量赋值并输出其值。

main(){
 struct stu
 {
  int num;
  char *name;
  char sex;
  float score;
 } boy1,boy2;
 boy1.num=102;
 boy1.name="Zhang ping";
 printf("input sex and score\n");
 scanf("%c %f",&boy1.sex,&boy1.score);
 boy2=boy1;
 printf("Number=%d\nName=%s\n",boy2.num,boy2.name);
 printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);
}
struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy1,boy2;
boy1.num=102;
boy1.name="Zhang ping";
printf("input sex and score\n");
scanf("%c %f",&boy1.sex,&boy1.score);
boy2=boy1;
printf("Number=%d\nName=%s\n",boy2.num,boy2.name);
printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);

  本程序中用赋值语句给num和name两个成员赋值,name是一个字符串指针变量。用scanf函数动态地输入sex和score成员值,然后把boy1的所有成员的值整体赋予boy2。最后分别输出boy2 的各个成员值。本例表示了结构变量的赋值、输入和输出的方法。

  结构变量的初始化

  如果结构变量是全局变量或为静态变量, 则可对它作初始化赋值。对局部或自动结构变量不能作初始化赋值。

  [例7.2]外部结构变量初始化。

struct stu /*定义结构*/
{
 int num;
 char *name;
 char sex;
 float score;
} boy2,boy1={102,"Zhang ping",'M',78.5};
main()
{
 boy2=boy1;
 printf("Number=%d\nName=%s\n",boy2.num,boy2.name);
 printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);
}
struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy2,boy1={102,"Zhang ping",'M',78.5};
main()
{
 boy2=boy1;
 ……
}

  本例中,boy2,boy1均被定义为外部结构变量,并对boy1作了初始化赋值。在main函数中,把boy1的值整体赋予boy2, 然后用两个printf语句输出boy2各成员的值。

  [例7.3]静态结构变量初始化。

main()
{
 static struct stu /*定义静态结构变量*/
 {
  int num;
  char *name;
  char sex;
  float score;
 }boy2,boy1={102,"Zhang ping",'M',78.5};
 boy2=boy1;
 printf("Number=%d\nName=%s\n",boy2.num,boy2.name);
 printf("Sex=%c\nScore=%f\n",boy2.sex,boy2.score);
}
static struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy2,boy1={102,"Zhang ping",'M',78.5};  

  本例是把boy1,boy2都定义为静态局部的结构变量, 同样可以作初始化赋值。

TOP

C语言初学者入门讲座 第十二讲 结构(2)

结构数组

  数组的元素也可以是结构类型的。 因此可以构成结构型数组。结构数组的每一个元素都是具有相同结构类型的下标结构变量。 在实际应用中,经常用结构数组来表示具有相同数据结构的一个群体。如一个班的学生档案,一个车间职工的工资表等。

  结构数组的定义方法和结构变量相似,只需说明它为数组类型即可。例如:

struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy[5];  

  定义了一个结构数组boy1,共有5个元素,boy[0]~boy[4]。每个数组元素都具有struct stu的结构形式。 对外部结构数组或静态结构数组可以作初始化赋值,例如:

struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy[5]={
{101,"Li ping","M",45},
{102,"Zhang ping","M",62.5},
{103,"He fang","F",92.5},
{104,"Cheng ling","F",87},
{105,"Wang ming","M",58};
}

  当对全部元素作初始化赋值时,也可不给出数组长度。

  [例7.4]计算学生的平均成绩和不及格的人数。

struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy[5]={
{101,"Li ping",'M',45},
{102,"Zhang ping",'M',62.5},
{103,"He fang",'F',92.5},
{104,"Cheng ling",'F',87},
{105,"Wang ming",'M',58},
};
main()
{
 int i,c=0;
 float ave,s=0;
 for(i=0;i<5;i++)
 {
  s+=boy.score;
  if(boy.score<60) c+=1;
 }
 printf("s=%f\n",s);
 ave=s/5;
 printf("average=%f\ncount=%d\n",ave,c);
}

  本例程序中定义了一个外部结构数组boy,共5个元素, 并作了初始化赋值。在main函数中用for语句逐个累加各元素的score 成员值存于s之中,如score的值小于60(不及格)即计数器C加1, 循环完毕后计算平均成绩,并输出全班总分,平均分及不及格人数。

  [例7.5]建立同学通讯录

#include"stdio.h"
#define NUM 3
struct mem
{
 char name[20];
 char phone[10];
};
main()
{
 struct mem man[NUM];
 int i;
 for(i=0;i<NUM;i++)
 {
  printf("input name:\n");
  gets(man.name);
  printf("input phone:\n");
  gets(man.phone);
 }
 printf("name\t\t\tphone\n\n");
 for(i=0;i<NUM;i++)
 printf("%s\t\t\t%s\n",man.name,man.phone);
}

  本程序中定义了一个结构mem,它有两个成员name和phone 用来表示姓名和电话号码。在主函数中定义man为具有mem 类型的结构数组。在for语句中,用gets函数分别输入各个元素中两个成员的值。然后又在for语句中用printf语句输出各元素中两个成员值。

  结构指针变量

  结构指针变量的说明和使用一个指针变量当用来指向一个结构变量时, 称之为结构指针变量。

  结构指针变量中的值是所指向的结构变量的首地址。 通过结构指针即可访问该结构变量, 这与数组指针和函数指针的情况是相同的。结构指针变量说明的一般形式为:

  struct 结构名*结构指针变量名

  例如,在前面的例7.1中定义了stu这个结构, 如要说明一个指向stu的指针变量pstu,可写为:

struct stu *pstu;  

  当然也可在定义stu结构时同时说明pstu。与前面讨论的各类指针变量相同,结构指针变量也必须要先赋值后才能使用。赋值是把结构变量的首地址赋予该指针变量, 不能把结构名赋予该指针变量。如果boy是被说明为stu类型的结构变量,则: pstu=&boy是正确的,而: pstu=&stu是错误的。

  结构名和结构变量是两个不同的概念,不能混淆。 结构名只能表示一个结构形式,编译系统并不对它分配内存空间。 只有当某变量被说明为这种类型的结构时,才对该变量分配存储空间。 因此上面&stu这种写法是错误的,不可能去取一个结构名的首地址。 有了结构指针变量,就能更方便地访问结构变量的各个成员。

  其访问的一般形式为: (*结构指针变量).成员名 或为:

  结构指针变量->成员名

  例如: (*pstu).num或者: pstu->num

  应该注意(*pstu)两侧的括号不可少, 因为成员符“.”的优先级高于“*”。如去掉括号写作*pstu.num则等效于*(pstu.num),这样,意义就完全不对了。 下面通过例子来说明结构指针变量的具体说明和使用方法。

  [例6]

struct stu
{
 int num;
 char *name;
 char sex;
 float score;
} boy1={102,"Zhang ping",'M',78.5},*pstu;
main()
{
 pstu=&boy1;
 printf("Number=%d\nName=%s\n",boy1.num,boy1.name);
 printf("Sex=%c\nScore=%f\n\n",boy1.sex,boy1.score);
 printf("Number=%d\nName=%s\n",(*pstu).num,(*pstu).name);
 printf("Sex=%c\nScore=%f\n\n",(*pstu).sex,(*pstu).score);
 printf("Number=%d\nName=%s\n",pstu->num,pstu->name);
 printf("Sex=%c\nScore=%f\n\n",pstu->sex,pstu->score);
}

  本例程序定义了一个结构stu,定义了stu类型结构变量boy1 并作了初始化赋值,还定义了一个指向stu类型结构的指针变量pstu。在main函数中,pstu被赋予boy1的地址,因此pstu指向boy1 。然后在printf语句内用三种形式输出boy1的各个成员值。 从运行结果可以看出:

  结构变量.成员名

  (*结构指针变量).成员名

  结构指针变量->成员名

  这三种用于表示结构成员的形式是完全等效的。结构数组指针变量结构指针变量可以指向一个结构数组, 这时结构指针变量的值是整个结构数组的首地址。 结构指针变量也可指向结构数组的一个元素,这时结构指针变量的值是该结构数组元素的首地址。设ps为指向结构数组的指针变量,则ps也指向该结构数组的0号元素,ps+1指向1号元素,ps+i则指向i号元素。 这与普通数组的情况是一致的。

  [例7.7]用指针变量输出结构数组。

struct stu
{
 int num;
 char *name;
 char sex;
 float score;
}boy[5]={
{101,"Zhou ping",'M',45},
{102,"Zhang ping",'M',62.5},
{103,"Liou fang",'F',92.5},
{104,"Cheng ling",'F',87},
{105,"Wang ming",'M',58},
};
main()
{
 struct stu *ps;
 printf("No\tName\t\t\tSex\tScore\t\n");
 for(ps=boy;ps<boy+5;ps++)
 printf("%d\t%s\t\t%c\t%f\t\n",ps->num,ps->name,ps->sex,ps->score);
}

  在程序中,定义了stu结构类型的外部数组boy 并作了初始化赋值。在main函数内定义ps为指向stu类型的指针。在循环语句for的表达式1中,ps被赋予boy的首地址,然后循环5次,输出boy数组中各成员值。 应该注意的是, 一个结构指针变量虽然可以用来访问结构变量或结构数组元素的成员,但是,不能使它指向一个成员。 也就是说不允许取一个成员的地址来赋予它。因此,下面的赋值是错误的。

  ps=&boy[1].sex;而只能是:ps=boy;(赋予数组首地址)

  或者是:

  ps=&boy[0];(赋予0号元素首地址)

TOP

C语言初学者入门讲座 第十二讲 结构(3)

结构指针变量作函数参数

  在ANSI C标准中允许用结构变量作函数参数进行整体传送。 但是这种传送要将全部成员逐个传送, 特别是成员为数组时将会使传送的时间和空间开销很大,严重地降低了程序的效率。 因此最好的办法就是使用指针,即用指针变量作函数参数进行传送。 这时由实参传向形参的只是地址,从而减少了时间和空间的开销。

  [例7.8]题目与例7.4相同,计算一组学生的平均成绩和不及格人数。

  用结构指针变量作函数参数编程。

struct stu
{
 int num;
 char *name;
 char sex;
 float score;}boy[5]={
  {101,"Li ping",'M',45},
  {102,"Zhang ping",'M',62.5},
  {103,"He fang",'F',92.5},
  {104,"Cheng ling",'F',87},
  {105,"Wang ming",'M',58},
 };
main()
{
 struct stu *ps;
 void ave(struct stu *ps);
 ps=boy;
 ave(ps);
}
void ave(struct stu *ps)
{
 int c=0,i;
 float ave,s=0;
 for(i=0;i<5;i++,ps++)
 {
  s+=ps->score;
  if(ps->score<60) c+=1;
 }
 printf("s=%f\n",s);
 ave=s/5;
 printf("average=%f\ncount=%d\n",ave,c);
}  

  本程序中定义了函数ave,其形参为结构指针变量ps。boy 被定义为外部结构数组,因此在整个源程序中有效。在main 函数中定义说明了结构指针变量ps,并把boy的首地址赋予它,使ps指向boy 数组。然后以ps作实参调用函数ave。在函数ave 中完成计算平均成绩和统计不及格人数的工作并输出结果。与例7.4程序相比,由于本程序全部采用指针变量作运算和处理,故速度更快,程序效率更高。.

  topoic=动态存储分配

  在数组一章中,曾介绍过数组的长度是预先定义好的, 在整个程序中固定不变。C语言中不允许动态数组类型。例如: int n;scanf("%d",&n);int a[n]; 用变量表示长度,想对数组的大小作动态说明, 这是错误的。但是在实际的编程中,往往会发生这种情况, 即所需的内存空间取决于实际输入的数据,而无法预先确定。对于这种问题, 用数组的办法很难解决。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态地分配内存空间, 也可把不再使用的空间回收待用,为有效地利用内存资源提供了手段。 常用的内存管理函数有以下三个:

  1.分配内存空间函数malloc

  调用形式: (类型说明符*) malloc (size) 功能:在内存的动态存储区中分配一块长度为"size" 字节的连续区域。函数的返回值为该区域的首地址。 “类型说明符”表示把该区域用于何种数据类型。(类型说明符*)表示把返回值强制转换为该类型指针。“size”是一个无符号数。例如: pc=(char *) malloc (100); 表示分配100个字节的内存空间,并强制转换为字符数组类型, 函数的返回值为指向该字符数组的指针, 把该指针赋予指针变量pc。

  2.分配内存空间函数 calloc

  calloc 也用于分配内存空间。调用形式: (类型说明符*)calloc(n,size) 功能:在内存动态存储区中分配n块长度为“size”字节的连续区域。函数的返回值为该区域的首地址。(类型说明符*)用于强制类型转换。calloc函数与malloc 函数的区别仅在于一次可以分配n块区域。例如: ps=(struet stu*) calloc(2,sizeof (struct stu)); 其中的sizeof(struct stu)是求stu的结构长度。因此该语句的意思是:按stu的长度分配2块连续区域,强制转换为stu类型,并把其首地址赋予指针变量ps。

  3.释放内存空间函数free

  调用形式: free(void*ptr); 功能:释放ptr所指向的一块内存空间,ptr 是一个任意类型的指针变量,它指向被释放区域的首地址。被释放区应是由malloc或calloc函数所分配的区域:[例7.9]分配一块区域,输入一个学生数据。

main()
{
 struct stu
 {
  int num;
  char *name;
  char sex;
  float score;
 } *ps;
 ps=(struct stu*)malloc(sizeof(struct stu));
 ps->num=102;
 ps->name="Zhang ping";
 ps->sex='M';
 ps->score=62.5;
 printf("Number=%d\nName=%s\n",ps->num,ps->name);
 printf("Sex=%c\nScore=%f\n",ps->sex,ps->score);
 free(ps);
}

  本例中,定义了结构stu,定义了stu类型指针变量ps。 然后分配一块stu大内存区,并把首地址赋予ps,使ps指向该区域。再以ps为指向结构的指针变量对各成员赋值,并用printf 输出各成员值。最后用free函数释放ps指向的内存空间。 整个程序包含了申请内存空间、使用内存空间、释放内存空间三个步骤, 实现存储空间的动态分配。链表的概念在例7.9中采用了动态分配的办法为一个结构分配内存空间。每一次分配一块空间可用来存放一个学生的数据, 我们可称之为一个结点。有多少个学生就应该申请分配多少块内存空间, 也就是说要建立多少个结点。当然用结构数组也可以完成上述工作, 但如果预先不能准确把握学生人数,也就无法确定数组大小。 而且当学生留级、退学之后也不能把该元素占用的空间从数组中释放出来。 用动态存储的方法可以很好地解决这些问题。 有一个学生就分配一个结点,无须预先确定学生的准确人数,某学生退学, 可删去该结点,并释放该结点占用的存储空间。从而节约了宝贵的内存资源。 另一方面,用数组的方法必须占用一块连续的内存区域。 而使用动态分配时,每个结点之间可以是不连续的(结点内是连续的)。 结点之间的联系可以用指针实现。 即在结点结构中定义一个成员项用来存放下一结点的首地址,这个用于存放地址的成员,常把它称为指针域。可在第一个结点的指针域内存入第二个结点的首地址, 在第二个结点的指针域内又存放第三个结点的首地址, 如此串连下去直到最后一个结点。最后一个结点因无后续结点连接,其指针域可赋为0。这样一种连接方式,在数据结构中称为“链表”。

  在链表中,第0个结点称为头结点, 它存放有第一个结点的首地址,它没有数据,只是一个指针变量。 以下的每个结点都分为两个域,一个是数据域,存放各种实际的数据,如学号num,姓名name,性别sex和成绩score等。另一个域为指针域, 存放下一结点的首地址。链表中的每一个结点都是同一种结构类型。例如, 一个存放学生学号和成绩的结点应为以下结构:

struct stu
{
 int num;
 int score;
 struct stu *next;
}

  前两个成员项组成数据域,后一个成员项next构成指针域, 它是一个指向stu类型结构的指针变量。链表的基本操作对链表的主要操作有以下几种:

  1.建立链表;

  2.结构的查找与输出;

  3.插入一个结点;

  4.删除一个结点;

  下面通过例题来说明这些操作。

  [例7.10]建立一个三个结点的链表,存放学生数据。 为简单起见, 我们假定学生数据结构中只有学号和年龄两项。

  可编写一个建立链表的函数creat。程序如下:

#define NULL 0
#define TYPE struct stu
#define LEN sizeof (struct stu)
struct stu
{
 int num;
 int age;
 struct stu *next;
};
TYPE *creat(int n)
{
 struct stu *head,*pf,*pb;
 int i;
 for(i=0;i<n;i++)
 {
  pb=(TYPE*) malloc(LEN);
  printf("input Number and Age\n");
  scanf("%d%d",&pb->num,&pb->age);
  if(i==0)
   pf=head=pb;
  else pf->next=pb;
  pb->next=NULL;
  pf=pb;
 }
 return(head);
}

  在函数外首先用宏定义对三个符号常量作了定义。这里用TYPE表示struct stu,用LEN表示sizeof(struct stu)主要的目的是为了在以下程序内减少书写并使阅读更加方便。结构stu定义为外部类型,程序中的各个函数均可使用该定义。

  creat函数用于建立一个有n个结点的链表,它是一个指针函数,它返回的指针指向stu结构。在creat函数内定义了三个stu结构的指针变量。head为头指针,pf 为指向两相邻结点的前一结点的指针变量。pb为后一结点的指针变量。在for语句内,用malloc函数建立长度与stu长度相等的空间作为一结点,首地址赋予pb。然后输入结点数据。如果当前结点为第一结点(i==0),则把pb值 (该结点指针)赋予head和pf。如非第一结点,则把pb值赋予pf 所指结点的指针域成员next。而pb所指结点为当前的最后结点,其指针域赋NULL。 再把pb值赋予pf以作下一次循环准备。

  creat函数的形参n,表示所建链表的结点数,作为for语句的循环次数。图7.4表示了creat函数的执行过程。

  [例7.11]写一个函数,在链表中按学号查找该结点。

TYPE * search (TYPE *head,int n)
{
 TYPE *p;
 int i;
 p=head;
 while (p->num!=n && p->next!=NULL)
  p=p->next; /* 不是要找的结点后移一步*/
  if (p->num==n) return (p);
  if (p->num!=n&& p->next==NULL)
  printf ("Node %d has not been found!\n",n
}

  本函数中使用的符号常量TYPE与例7.10的宏定义相同,等于struct stu。函数有两个形参,head是指向链表的指针变量,n为要查找的学号。进入while语句,逐个检查结点的num成员是否等于n,如果不等于n且指针域不等于NULL(不是最后结点)则后移一个结点,继续循环。如找到该结点则返回结点指针。 如循环结束仍未找到该结点则输出“未找到”的提示信息。

  [例7.12]写一个函数,删除链表中的指定结点。删除一个结点有两种情况:

  1. 被删除结点是第一个结点。这种情况只需使head指向第二个结点即可。即head=pb->next。其过程如图7.5所示。

  2. 被删结点不是第一个结点,这种情况使被删结点的前一结点指向被删结点的后一结点即可。即pf->next=pb->next。

  函数编程如下:

TYPE * delete(TYPE * head,int num)
{
 TYPE *pf,*pb;
 if(head==NULL) /*如为空表, 输出提示信息*/
 {
  printf("\nempty list!\n");
  goto end;
 }
 pb=head;
 while (pb->num!=num && pb->next!=NULL)
  /*当不是要删除的结点,而且也不是最后一个结点时,继续循环*/
 {
  pf=pb;pb=pb->next;}/*pf指向当前结点,pb指向下一结点*/
  if(pb->num==num)
  {
   if(pb==head) head=pb->next;
    /*如找到被删结点,且为第一结点,则使head指向第二个结点,
     否则使pf所指结点的指针指向下一结点*/
   else pf->next=pb->next;
   free(pb);
   printf("The node is deleted\n");}
  else
   printf("The node not been foud!\n");
   end:
  return head;
 }  

  函数有两个形参,head为指向链表第一结点的指针变量,num删结点的学号。 首先判断链表是否为空,为空则不可能有被删结点。若不为空,则使pb指针指向链表的第一个结点。进入while语句后逐个查找被删结点。找到被删结点之后再看是否为第一结点,若是则使head指向第二结点(即把第一结点从链中删去),否则使被删结点的前一结点(pf所指)指向被删结点的后一结点(被删结点的指针域所指)。如若循环结束未找到要删的结点, 则输出“末找到”的提示信息。最后返回head值。

  [例7.13]写一个函数,在链表中指定位置插入一个结点。在一个链表的指定位置插入结点, 要求链表本身必须是已按某种规律排好序的。例如,在学生数据链表中, 要求学号顺序插入一个结点。设被插结点的指针为pi。 可在三种不同情况下插入。

  1. 原表是空表,只需使head指向被插结点即可。

  2. 被插结点值最小,应插入第一结点之前。这种情况下使head指向被插结点,被插结点的指针域指向原来的第一结点则可。即:

pi->next=pb;
head=pi;

  3. 在其它位置插入。这种情况下,使插入位置的前一结点的指针域指向被插结点,使被插结点的指针域指向插入位置的后一结点。即为:pi->next=pb;pf->next=pi;

  4. 在表末插入。这种情况下使原表末结点指针域指向被插结点,被插结点指针域置为NULL。即:

pb->next=pi;
pi->next=NULL; TYPE * insert(TYPE * head,TYPE *pi)
{
 TYPE *pf,*pb;
 pb=head;
 if(head==NULL) /*空表插入*/
  (head=pi;
  pi->next=NULL;}
 else
 {
  while((pi->num>pb->num)&&(pb->next!=NULL))
  {
   pf=pb;
   pb=pb->next;
  }/*找插入位置*/
  if(pi->num<=pb->num)
  {
   if(head==pb)head=pi;/*在第一结点之前插入*/
   else pf->next=pi;/*在其它位置插入*/
   pi->next=pb; }
  else
  {
   pb->next=pi;
   pi->next=NULL;
  } /*在表末插入*/
 }
 return head;
}


  本函数有两个形参均为指针变量,head指向链表,pi 指向被插结点。函数中首先判断链表是否为空,为空则使head指向被插结点。表若不空,则用while语句循环查找插入位置。找到之后再判断是否在第一结点之前插入,若是则使head 指向被插结点被插结点指针域指向原第一结点,否则在其它位置插入, 若插入的结点大于表中所有结点,则在表末插入。本函数返回一个指针, 是链表的头指针。 当插入的位置在第一个结点之前时, 插入的新结点成为链表的第一个结点,因此head的值也有了改变, 故需要把这个指针返回主调函数。

  [例7.14]将以上建立链表,删除结点,插入结点的函数组织在一起,再建一个输出全部结点的函数,然后用main函数调用它们。

#define NULL 0
#define TYPE struct stu
#define LEN sizeof(struct stu)
struct stu
{
 int num;
 int age;
 struct stu *next;
};
TYPE * creat(int n)
{
 struct stu *head,*pf,*pb;
 int i;
 for(i=0;i<n;i++)
 {
  pb=(TYPE *)malloc(LEN);
  printf("input Number and Age\n");
  scanf("%d%d",&pb->num,&pb->age);
  if(i==0)
   pf=head=pb;
  else pf->next=pb;
  pb->next=NULL;
  pf=pb;
 }
 return(head);
}
TYPE * delete(TYPE * head,int num)
{
 TYPE *pf,*pb;
 if(head==NULL)
 {
  printf("\nempty list!\n");
  goto end;
 }
 pb=head;
 while (pb->num!=num && pb->next!=NULL)
 {
  pf=pb;pb=pb->next;
 }
 if(pb->num==num)
 {
  if(pb==head) head=pb->next;
  else pf->next=pb->next;
  printf("The node is deleted\n");
 }
 else
  free(pb);
  printf("The node not been found!\n");
 end:
 return head;
}
TYPE * insert(TYPE * head,TYPE * pi)
{
 TYPE *pb ,*pf;
 pb=head;
 if(head==NULL)
 {
  head=pi;
  pi->next=NULL;
 }
 else
 {
  while((pi->num>pb->num)&&(pb->next!=NULL))
  {
   pf=pb;
   pb=pb->next;
  }
  if(pi->num<=pb->num)
  {
   if(head==pb) head=pi;
   else pf->next=pi;
   pi->next=pb;
  }
  else
  {
   pb->next=pi;
   pi->next=NULL;
  }
 }
 return head;
}
void print(TYPE * head)
{
 printf("Number\t\tAge\n");
 while(head!=NULL)
 {
  printf("%d\t\t%d\n",head->num,head->age);
  head=head->next;
 }
}
main()
{
 TYPE * head,*pnum;
 int n,num;
 printf("input number of node: ");
 scanf("%d",&n);
 head=creat(n);
 print(head);
 printf("Input the deleted number: ");
 scanf("%d",&num);
 head=delete(head,num);
 print(head);
 printf("Input the inserted number and age: ");
 pnum=(TYPE *)malloc(LEN);
 scanf("%d%d",&pnum->num,&pnum->age);
 head=insert(head,pnum);
 print(head);
}

  本例中,print函数用于输出链表中各个结点数据域值。函数的形参head的初值指向链表第一个结点。在while语句中,输出结点值后,head值被改变,指向下一结点。若保留头指针head, 则应另设一个指针变量,把head值赋予它,再用它来替代head。在main函数中,n为建立结点的数目, num为待删结点的数据域值;head为指向链表的头指针,pnum为指向待插结点的指针。 main函数中各行的意义是:

  第六行输入所建链表的结点数;

  第七行调creat函数建立链表并把头指针返回给head;

  第八行调print函数输出链表;

  第十行输入待删结点的学号;

  第十一行调delete函数删除一个结点;

  第十二行调print函数输出链表;

  第十四行调malloc函数分配一个结点的内存空间, 并把其地址赋予pnum;

  第十五行输入待插入结点的数据域值;

  第十六行调insert函数插入pnum所指的结点;

  第十七行再次调print函数输出链表。

  从运行结果看,首先建立起3个结点的链表,并输出其值;再删103号结点,只剩下105,108号结点;又输入106号结点数据, 插入后链表中的结点为105,106,108。联合“联合”也是一种构造类型的数据结构。 在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据。 这在前面的各种数据类型中都是办不到的。例如, 定义为整型的变量只能装入整型数据,定义为实型的变量只能赋予实型数据。

TOP

C语言初学者入门讲座 第十三讲 联合

“联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和。而在“联合”中,各成员共享一段内存空间, 一个联合变量的长度等于各成员中最长的长度。应该说明的是, 这里所谓的共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值。如前面介绍的“单位”变量, 如定义为一个可装入“班级”或“教研室”的联合后,就允许赋予整型值(班级)或字符串(教研室)。要么赋予整型值,要么赋予字符串,不能把两者同时赋予它。联合类型的定义和联合变量的说明一个联合类型必须经过定义之后, 才能把变量说明为该联合类型。

  一、联合的定义

  定义一个联合类型的一般形式为:

union 联合名
{
 成员表
};

  成员表中含有若干成员,成员的一般形式为: 类型说明符 成员名 成员名的命名应符合标识符的规定。

  例如:

union perdata
{
 int class;
 char office[10];
};

  定义了一个名为perdata的联合类型,它含有两个成员,一个为整型,成员名为class;另一个为字符数组,数组名为office。联合定义之后,即可进行联合变量说明,被说明为perdata类型的变量,可以存放整型量class或存放字符数组office。

  二、联合变量的说明

  联合变量的说明和结构变量的说明方式相同, 也有三种形式。即先定义,再说明;定义同时说明和直接说明。以perdata类型为例,说明如下:

union perdata
{
 int class;
 char officae[10];
};
union perdata a,b; /*说明a,b为perdata类型*/

  或者可同时说明为:

union perdata
{
 int class;
 char office[10];
}

 a,b;或直接说明为:

union
{
 int class;
 char office[10];
}
a,b  

  经说明后的a,b变量均为perdata类型。 它们的内存分配示意图如图7—8所示。a,b变量的长度应等于 perdata 的成员中最长的长度, 即等于office数组的长度,共10个字节。从图中可见,a,b变量如赋予整型值时,只使用了2个字节,而赋予字符数组时,可用10个字节。

  联合变量的赋值和使用

  对联合变量的赋值,使用都只能是对变量的成员进行。 联合变量的成员表示为: 联合变量名.成员名 例如,a被说明为perdata类型的变量之后,可使用 a.class a.office 不允许只用联合变量名作赋值或其它操作。 也不允许对联合变量作初始化赋值,赋值只能在程序中进行。还要再强调说明的是,一个联合变量, 每次只能赋予一个成员值。换句话说,一个联合变量的值就是联合变员的某一个成员值。

  [例7.15]设有一个教师与学生通用的表格,教师数据有姓名,年龄,职业,教研室四项。学生有姓名,年龄,职业,班级四项。

  编程输入人员数据, 再以表格输出。

main()
{
 struct
 {
  char name[10];
  int age;
  char job;
  union
  {
   int class;
   char office[10];
  } depa;
 }body[2];
 int n,i;
 for(i=0;i<2;i++)
 {
  printf("input name,age,job and department\n");
  scanf("%s %d %c",body.name,&body.age,&body.job);
  if(body.job=='s')
   scanf("%d",&body.depa.class);
  else
   scanf("%s",body.depa.office);
 }
 printf("name\tage job class/office\n");
 for(i=0;i<2;i++)
 {
  if(body.job=='s')
   printf("%s\t%3d %3c %d\n",body.name,body.age,body.job,body.depa.class);
  else
   printf("%s\t%3d %3c %s\n",body.name,body.age,
   body.job,body.depa.office);
 }
}

  本例程序用一个结构数组body来存放人员数据, 该结构共有四个成员。其中成员项depa是一个联合类型, 这个联合又由两个成员组成,一个为整型量class,一个为字符数组office。在程序的第一个for语句中,输入人员的各项数据,先输入结构的前三个成员name,age和job,然后判别job成员项,如为"s"则对联合depa·class输入(对学生赋班级编号)否则对depa·office输入(对教师赋教研组名)。

  在用scanf语句输入时要注意,凡为数组类型的成员,无论是结构成员还是联合成员,在该项前不能再加"&"运算符。如程序第18行中
body.name是一个数组类型,第22行中的body.depa.office也是数组类型,因此在这两项之间不能加"&"运算符。程序中的第二个for语句用于输出各成员项的值。

TOP

C语言初学者入门讲座 第十四讲 枚举与位运算(1)

在实际问题中, 有些变量的取值被限定在一个有限的范围内。例如,一个星期内只有七天,一年只有十二个月, 一个班每周有六门课程等等。如果把这些量说明为整型, 字符型或其它类型显然是不妥当的。 为此,C语言提供了一种称为“枚举”的类型。在“枚举”类型的定义中列举出所有可能的取值, 被说明为该“枚举”类型的变量取值不能超过定义的范围。应该说明的是, 枚举类型是一种基本数据类型,而不是一种构造类型, 因为它不能再分解为任何基本类型。

  枚举类型的定义和枚举变量的说明

  一、枚举的定义

  枚举类型定义的一般形式为:

enum 枚举名
{
 枚举值表
};

  在枚举值表中应罗列出所有可用值。这些值也称为枚举元素。

  例如:

enum weekday
{
 sun,mou,tue,wed,thu,fri,sat
};

  该枚举名为weekday,枚举值共有7个,即一周中的七天。 凡被说明为weekday类型变量的取值只能是七天中的某一天。

  二、枚举变量的说明

  如同结构和联合一样,枚举变量也可用不同的方式说明, 即先定义后说明,同时定义说明或直接说明。设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式:

enum weekday
{
......
};
enum weekday a,b,c;或者为: enum weekday
{
......
}a,b,c;或者为: enum
{
......
}a,b,c;

  枚举类型变量的赋值和使用

  枚举类型在使用中有以下规定:

  1. 枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值。例如对枚举weekday的元素再作以下赋值: sun=5;mon=2;sun=mon; 都是错误的。

  2. 枚举元素本身由系统定义了一个表示序号的数值,从0 开始顺序定义为0,1,2…。如在weekday中,sun值为0,mon值为1, …,sat值为6。

main(){
 enum weekday
 {
  sun,mon,tue,wed,thu,fri,sat
 } a,b,c;
 a=sun;
 b=mon;
 c=tue;
 printf("%d,%d,%d",a,b,c);
}

  3. 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量。如: a=sum;b=mon; 是正确的。而: a=0;b=1; 是错误的。如一定要把数值赋予枚举变量,则必须用强制类型转换,如: a=(enum weekday)2;其意义是将顺序号为2的枚举元素赋予枚举变量a,相当于: a=tue; 还应该说明的是枚举元素不是字符常量也不是字符串常量, 使用时不要加单、双引号。

main(){
 enum body
 {
  a,b,c,d
 } month[31],j;
 int i;
 j=a;
 for(i=1;i<=30;i++){
  month=j;
  j++;
  if (j>d) j=a;
 }
 for(i=1;i<=30;i++){
  switch(month)
  {
   case a:printf(" %2d %c\t",i,'a'); break;
   case b:printf(" %2d %c\t",i,'b'); break;
   case c:printf(" %2d %c\t",i,'c'); break;
   case d:printf(" %2d %c\t",i,'d'); break;
   default:break;
  }
 }
 printf("\n");
}

  位运算

  前面介绍的各种运算都是以字节作为最基本位进行的。 但在很多系统程序中常要求在位(bit)一级进行运算或处理。C语言提供了位运算的功能, 这使得C语言也能像汇编语言一样用来编写系统程序。

  一、位运算符C语言提供了六种位运算符:

  & 按位与
  | 按位或
  ^ 按位异或
  ~ 取反
  << 左移
  >> 右移

  1. 按位与运算 按位与运算符"&"是双目运算符。其功能是参与运算的两数各对应的二进位相与。只有对应的两个二进位均为1时,结果位才为1 ,否则为0。参与运算的数以补码方式出现。

  例如:9&5可写算式如下: 00001001 (9的二进制补码)&00000101 (5的二进制补码) 00000001 (1的二进制补码)可见9&5=1。

  按位与运算通常用来对某些位清0或保留某些位。例如把a 的高八位清 0 , 保留低八位, 可作 a&255 运算 ( 255 的二进制数为0000000011111111)。

main(){
 int a=9,b=5,c;
 c=a&b;
 printf("a=%d\nb=%d\nc=%d\n",a,b,c);
}

  2. 按位或运算 按位或运算符“|”是双目运算符。其功能是参与运算的两数各对应的二进位相或。只要对应的二个二进位有一个为1时,结果位就为1。参与运算的两个数均以补码出现。

   例如:9|5可写算式如下:

00001001|00000101
00001101 (十进制为13)可见9|5=13
main(){
 int a=9,b=5,c;
 c=a|b;
 printf("a=%d\nb=%d\nc=%d\n",a,b,c);
}

  3. 按位异或运算 按位异或运算符“^”是双目运算符。其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。参与运算数仍以补码出现,例如9^5可写成算式如下:

00001001^00000101 00001100 (十进制为12)
main(){
 int a=9;
 a=a^15;
 printf("a=%d\n",a);
}

  4. 求反运算 求反运算符~为单目运算符,具有右结合性。 其功能是对参与运算的数的各二进位按位求反。例如~9的运算为: ~(0000000000001001)结果为:1111111111110110

  5. 左移运算 左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干位,由“<<”右边的数指定移动的位数,

  高位丢弃,低位补0。例如: a<<4 指把a的各二进位向左移动4位。如a=00000011(十进制3),左移4位后为00110000(十进制48)。6. 右移运算 右移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若干位,“>>”右边的数指定移动的位数。

  例如:设 a=15,a>>2 表示把000001111右移为00000011(十进制3)。 应该说明的是,对于有符号数,在右移时,符号位将随同移动。当为正数时, 最高位补0,而为负数时,符号位为1,最高位是补0或是补1 取决于编译系统的规定。Turbo C和很多系统规定为补1。

main(){
 unsigned a,b;
 printf("input a number: ");
 scanf("%d",&a);
 b=a>>5;
 b=b&15;
 printf("a=%d\tb=%d\n",a,b);
}

  请再看一例!

main(){
 char a='a',b='b';
 int p,c,d;
 p=a;
 p=(p<<8)|b;
 d=p&0xff;
 c=(p&0xff00)>>8;
 printf("a=%d\nb=%d\nc=%d\nd=%d\n",a,b,c,d);
}

TOP

C语言初学者入门讲座 第十四讲 枚举与位运算(2)

位域

  有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

  struct 位域结构名

  { 位域列表 };

  其中位域列表的形式为: 类型说明符 位域名:位域长度

  例如:

struct bs
{
 int a:8;
 int b:2;
 int c:6;
};  

  位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:

struct bs
{
 int a:8;
 int b:2;
 int c:6;
}data;

  说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。对于位域的定义尚有以下几点说明:

  1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:

struct bs
{
 unsigned a:4
 unsigned :0 /*空域*/
 unsigned b:4 /*从下一单元开始存放*/
 unsigned c:4
}

  在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。

  2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。

  3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k
{
 int a:1
 int :2 /*该2位不能使用*/
 int b:3
 int c:2
};  

  从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

  二、位域的使用

  位域的使用和结构成员的使用相同,其一般形式为: 位域变量名·位域名 位域允许用各种格式输出。

main(){
 struct bs
 {
  unsigned a:1;
  unsigned b:3;
  unsigned c:4;
 } bit,*pbit;
 bit.a=1;
 bit.b=7;
 bit.c=15;
 printf("%d,%d,%d\n",bit.a,bit.b,bit.c);
 pbit=&bit;
 pbit->a=0;
 pbit->b&=3;
 pbit->c|=1;
 printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
}  

  上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。

  程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=", 该行相当于: pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=", 相当于: pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

  类型定义符typedef

  C语言不仅提供了丰富的数据类型,而且还允许由用户自己定义类型说明符,也就是说允许由用户为数据类型取“别名”。 类型定义符typedef即可用来完成此功能。例如,有整型量a,b,其说明如下: int aa,b; 其中int是整型变量的类型说明符。int的完整写法为integer,

  为了增加程序的可读性,可把整型说明符用typedef定义为: typedef int INTEGER 这以后就可用INTEGER来代替int作整型变量的类型说明了。 例如: INTEGER a,b;它等效于: int a,b; 用typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如:

typedef char NAME[20]; 表示NAME是字符数组类型,数组长度为20。

  然后可用NAME 说明变量,如: NAME a1,a2,s1,s2;完全等效于: char a1[20],a2[20],s1[20],s2[20]
又如:

typedef struct stu{ char name[20];
 int age;
 char sex;
} STU;

  定义STU表示stu的结构类型,然后可用STU来说明结构变量: STU body1,body2;

  typedef定义的一般形式为: typedef 原类型名 新类型名 其中原类型名中含有定义部分,新类型名一般用大写表示, 以便于区别。在有时也可用宏定义来代替typedef的功能,但是宏定义是由预处理完成的,而typedef则是在编译时完成的,后者更为灵活方便。

TOP

C语言初学者入门讲座 第十五讲 预处理

概述

  在前面各章中,已多次使用过以“#”号开头的预处理命令。如包含命令# include,宏定义命令# define等。在源程序中这些命令都放在函数之外, 而且一般都放在源文件的前面,它们称为预处理部分。

  所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理是C语言的一个重要功能, 它由预处理程序负责完成。当对一个源文件进行编译时, 系统将自动引用预处理程序对源程序中的预处理部分作处理, 处理完毕自动进入对源程序的编译。

  C语言提供了多种预处理功能,如宏定义、文件包含、 条件编译等。合理地使用预处理功能编写的程序便于阅读、修改、 移植和调试,也有利于模块化程序设计。本章介绍常用的几种预处理功能。

  宏定义

  在C语言源程序中允许用一个标识符来表示一个字符串, 称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换, 这称为“宏代换”或“宏展开”。

  宏定义是由源程序中的宏定义命令完成的。 宏代换是由预处理程序自动完成的。在C语言中,“宏”分为有参数和无参数两种。 下面分别讨论这两种“宏”的定义和调用。

  无参宏定义

  无参宏的宏名后不带参数。其定义的一般形式为: #define 标识符 字符串 其中的“#”表示这是一条预处理命令。凡是以“#”开头的均为预处理命令。“define”为宏定义命令。 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。在前面介绍过的符号常量的定义就是一种无参宏定义。 此外,常对程序中反复使用的表达式进行宏定义。例如: # define M (y*y+3*y) 定义M表达式(y*y+3*y)。在编写源程序时,所有的(y*y+3*y)都可由M代替,而对源程序作编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去置换所有的宏名M,然后再进行编译。

#define M (y*y+3*y)
main(){
 int s,y;
 printf("input a number: ");
 scanf("%d",&y);
 s=3*M+4*M+5*M;
 printf("s=%d\n",s);
}

  上例程序中首先进行宏定义,定义M表达式(y*y+3*y),在s= 3*M+4*M+5* M中作了宏调用。在预处理时经宏展开后该语句变为:s=3*(y*y+3*y)+4(y*y+3*y)+5(y*y+3*y);但要注意的是,在宏定义中表达式(y*y+3*y)两边的括号不能少。否则会发生错误。

  当作以下定义后: #difine M y*y+3*y在宏展开时将得到下述语句: s=3*y*y+3*y+4*y*y+3*y+5*y*y+3*y;这相当于; 3y?2+3y+4y?2+3y+5y?2+3y;显然与原题意要求不符。计算结果当然是错误的。 因此在作宏定义时必须十分注意。应保证在宏代换之后不发生错误。对于宏定义还要说明以下几点:

  1. 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

  2. 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起置换。

  3. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结 束。如要终止其作用域可使用# undef命令,例如:

# define PI 3.14159
main()
{
……
}

  # undef PIPI的作用域

  f1()

  ....表示PI只在main函数中有效,在f1中无效。

  4. 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。

#define OK 100
main()
{
 printf("OK");
 printf("\n");
}

  上例中定义宏名OK表示100,但在printf语句中OK被引号括起来,因此不作宏代换。程序的运行结果为:OK这表示把“OK”当字符串处理。

  5. 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层代换。例如:

#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/对语句: printf("%f",s);

  在宏代换后变为: printf("%f",3.1415926*y*y);

  6. 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。

  7. 可用宏定义表示数据类型,使书写方便。例如: #define STU struct stu在程序中可用STU作变量说明:

STU body[5],*p;#define INTEGER int
  
  在程序中即可用INTEGER作整型变量说明: INTEGER a,b; 应注意用宏定义表示数据类型和用typedef定义数据说明符的区别。宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换, 而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。请看下面的例子: #define PIN1 int* typedef (int*) PIN2;从形式上看这两者相似, 但在实际使用中却不相同。下面用PIN1,PIN2说明变量时就可以看出它们的区别: PIN1 a,b;在宏代换后变成 int *a,b;表示a是指向整型的指针变量,而b是整型变量。然而:PIN2 a,b;表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟是作字符代换。在使用时要分外小心,以避出错。

  8. 对“输出格式”作宏定义,可以减少书写麻烦。例9.3 中就采用了这种方法。

#define P printf
#define D "%d\n"
#define F "%f\n"
main(){
int a=5, c=8, e=11;
float b=3.8, d=9.7, f=21.08;
P(D F,a,b);
P(D F,c,d);
P(D F,e,f);
}
  带参宏定义

  C语言允许宏带有参数。在宏定义中的参数称为形式参数, 在宏调用中的参数称为实际参数。对带参数的宏,在调用中,不仅要宏展开, 而且要用实参去代换形参。

  带参宏定义的一般形式为: #define 宏名(形参表) 字符串 在字符串中含有各个形参。带参宏调用的一般形式为: 宏名(实参表);
例如:

#define M(y) y*y+3*y /*宏定义*/
:
k=M(5); /*宏调用*/
: 在宏调用时,用实参5去代替形参y, 经预处理宏展开后的语句
为: k=5*5+3*5
#define MAX(a,b) (a>b)?a:b
main(){
int x,y,max;
printf("input two numbers: ");
scanf("%d%d",&x,&y);
max=MAX(x,y);
printf("max=%d\n",max);
}

  上例程序的第一行进行带参宏定义,用宏名MAX表示条件表达式(a>b)?a:b,形参a,b均出现在条件表达式中。程序第七行max=MAX(x,
y)为宏调用,实参x,y,将代换形参a,b。宏展开后该语句为: max=(x>y)?x:y;用于计算x,y中的大数。对于带参的宏定义有以下问题需要说明:

  1. 带参宏定义中,宏名和形参表之间不能有空格出现。

  例如把: #define MAX(a,b) (a>b)?a:b写为: #define MAX (a,b) (a>b)?a:b 将被认为是无参宏定义,宏名MAX代表字符串 (a,b)(a>b)?a:b。

  宏展开时,宏调用语句: max=MAX(x,y);将变为: max=(a,b)(a>b)?a:b(x,y);这显然是错误的。

  2. 在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值。要用它们去代换形参,因此必须作类型说明。这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中,只是符号代换,不存在值传递的问题。

  3. 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

#define SQ(y) (y)*(y)
main(){
int a,sq;
printf("input a number: ");
scanf("%d",&a);
sq=SQ(a+1);
printf("sq=%d\n",sq);
}

  上例中第一行为宏定义,形参为y。程序第七行宏调用中实参为a+1,是一个表达式,在宏展开时,用a+1代换y,再用(y)*(y) 代换SQ,得到如下语句: sq=(a+1)*(a+1); 这与函数的调用是不同的, 函数调用时要把实参表达式的值求出来再赋予形参。 而宏代换中对实参表达式不作计算直接地照原样代换。

  4. 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。 在上例中的宏定义中(y)*(y)表达式的y都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:

#define SQ(y) y*y
main(){
int a,sq;
printf("input a number: ");
scanf("%d",&a);
sq=SQ(a+1);
printf("sq=%d\n",sq);
}

  运行结果为:input a number:3

  sq=7 同样输入3,但结果却是不一样的。问题在哪里呢? 这是由于代换只作符号代换而不作其它处理而造成的。 宏代换后将得到以下语句: sq=a+1*a+1; 由于a为3故sq的值为7。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:

#define SQ(y) (y)*(y)
main(){
int a,sq;
printf("input a number: ");
scanf("%d",&a);
sq=160/SQ(a+1);
printf("sq=%d\n",sq);
}

  本程序与前例相比,只把宏调用语句改为: sq=160/SQ(a+1); 运行本程序如输入值仍为3时,希望结果为10。但实际运行的结果如下:input a number:3 sq=160为什么会得这样的结果呢?分析宏调用语句,在宏代换之后变为: sq=160/(a+1)*(a+1);a为3时,由于“/”和“*”运算符优先级和结合性相同, 则先作160/(3+1)得40,再作40*(3+1)最后得160。为了得到正确答案应在宏定义中的整个字符串外加括号, 程序修改如下

#define SQ(y) ((y)*(y))
main(){
 int a,sq;
 printf("input a number: ");
 scanf("%d",&a);
 sq=160/SQ(a+1);
 printf("sq=%d\n",sq);
}

  以上讨论说明,对于宏定义不仅应在参数两侧加括号, 也应在整个字符串外加括号。

  5. 带参的宏和带参函数很相似,但有本质上的不同,除上面已谈到的各点外,把同一表达式用函数处理与用宏处理两者的结果有可能是不同的。

main(){
int i=1;
while(i<=5)
printf("%d\n",SQ(i++));
}
SQ(int y)
{
 return((y)*(y));
}#define SQ(y) ((y)*(y))
main(){
 int i=1;
 while(i<=5)
  printf("%d\n",SQ(i++));
}  

  在上例中函数名为SQ,形参为Y,函数体表达式为((y)*(y))。在例9.6中宏名为SQ,形参也为y,字符串表达式为(y)*(y))。 两例是相同的。例9.6的函数调用为SQ(i++),例9.7的宏调用为SQ(i++),实参也是相同的。从输出结果来看,却大不相同。分析如下:在例9.6中,函数调用是把实参i值传给形参y后自增1。 然后输出函数值。因而要循环5次。输出1~5的平方值。而在例9.7中宏调用时,只作代换。SQ(i++)被代换为((i++)*(i++))。在第一次循环时,由于i等于1,其计算过程为:表达式中前一个i初值为1,然后i自增1变为2,因此表达式中第2个i初值为2,两相乘的结果也为2,然后i值再自增1,得3。在第二次循环时,i值已有初值为3,因此表达式中前一个i为3,后一个i为4, 乘积为12,然后i再自增1变为5。进入第三次循环,由于i 值已为5,所以这将是最后一次循环。计算表达式的值为5*6等于30。i值再自增1变为6,不再满足循环条件,停止循环。从以上分析可以看出函数调用和宏调用二者在形式上相似, 在本质上是完全不同的。

  6. 宏定义也可用来定义多个语句,在宏调用时,把这些语句又代换到源程序内。看下面的例子。

#define SSSV(s1,s2,s3,v) s1=l*w;s2=l*h;s3=w*h;v=w*l*h;
main(){
 int l=3,w=4,h=5,sa,sb,sc,vv;
 SSSV(sa,sb,sc,vv);
 printf("sa=%d\nsb=%d\nsc=%d\nvv=%d\n",sa,sb,sc,vv);
}

  程序第一行为宏定义,用宏名SSSV表示4个赋值语句,4 个形参分别为4个赋值符左部的变量。在宏调用时,把4 个语句展开并用实参代替形参。使计算结果送入实参之中。

  文件包含

  文件包含是C预处理程序的另一个重要功能。文件包含命令行的一般形式为: #include"文件名" 在前面我们已多次用此命令包含过库函数的头文件。例如:

#include"stdio.h"
#include"math.h"  

  文件包含命令的功能是把指定的文件插入该命令行位置取代该命令行, 从而把指定的文件和当前的源程序文件连成一个源文件。在程序设计中,文件包含是很有用的。 一个大的程序可以分为多个模块,由多个程序员分别编程。 有些公用的符号常量或宏定义等可单独组成一个文件, 在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量, 从而节省时间,并减少出错。

  对文件包含命令还要说明以下几点:

  1. 包含命令中的文件名可以用双引号括起来,也可以用尖括号括起来。例如以下写法都是允许的: #include"stdio.h" #include<math.h> 但是这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的), 而不在源文件目录去查找; 使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。 用户编程时可根据自己文件所在的目录来选择某一种命令形式。

  2. 一个include命令只能指定一个被包含文件, 若有多个文件要包含,则需用多个include命令。3. 文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

  条件编译

  预处理程序提供了条件编译的功能。 可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。 这对于程序的移植和调试是很有用的。 条件编译有三种形式,下面分别介绍:

  1. 第一种形式:

#ifdef 标识符
程序段1
#else
程序段2
#endif  

  它的功能是,如果标识符已被 #define命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有, 即可以写为:

#ifdef 标识符
程序段 #endif
#define NUM ok
main(){
 struct stu
 {
  int num;
  char *name;
  char sex;
  float score;
 } *ps;
 ps=(struct stu*)malloc(sizeof(struct stu));
 ps->num=102;
 ps->name="Zhang ping";
 ps->sex='M';
 ps->score=62.5;
 #ifdef NUM
  printf("Number=%d\nScore=%f\n",ps->num,ps->score);
 #else
  printf("Name=%s\nSex=%c\n",ps->name,ps->sex);
 #endif
 free(ps);
}  

  由于在程序的第16行插入了条件编译预处理命令, 因此要根据NUM是否被定义过来决定编译那一个printf语句。而在程序的第一行已对NUM作过宏定义,因此应对第一个printf语句作编译故运行结果是输出了学号和成绩。在程序的第一行宏定义中,定义NUM表示字符串OK,其实也可以为任何字符串,甚至不给出任何字符串,写为: #define NUM 也具有同样的意义。 只有取消程序的第一行才会去编译第二个printf语句。读者可上机试作。

  2. 第二种形式:

#ifndef 标识符
程序段1
#else
程序段2
#endif  

  与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译, 否则对程序段2进行编译。这与第一种形式的功能正相反。

  3. 第三种形式:

#if 常量表达式
程序段1
#else
程序段2
#endif  

  它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下,完成不同的功能

#define R 1
main(){
 float c,r,s;
 printf ("input a number: ");
 scanf("%f",&c);
 #if R
  r=3.14159*c*c;
  printf("area of round is: %f\n",r);
 #else
  s=c*c;
  printf("area of square is: %f\n",s);
 #endif
}

  本例中采用了第三种形式的条件编译。在程序第一行宏定义中,定义R为1,因此在条件编译时,常量表达式的值为真, 故计算并输出圆面积。上面介绍的条件编译当然也可以用条件语句来实现。 但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2, 生成的目标程序较短。如果条件选择的程序段很长, 采用条件编译的方法是十分必要的。

TOP

C语言初学者入门讲座 第十六讲 文件(1)

所谓“文件”是指一组相关数据的有序集合。 这个数据集有一个名称,叫做文件名。 实际上在前面的各章中我们已经多次使用了文件,例如源程序文件、目标文件、可执行文件、库文件 (头文件)等。文件通常是驻留在外部介质(如磁盘等)上的, 在使用时才调入内存中来。从不同的角度可对文件作不同的分类。从用户的角度看,文件可分为普通文件和设备文件两种。

  普通文件是指驻留在磁盘或其它外部介质上的一个有序数据集,可以是源文件、目标文件、可执行程序; 也可以是一组待输入处理的原始数据,或者是一组输出的结果。对于源文件、目标文件、 可执行程序可以称作程序文件,对输入输出数据可称作数据文件。

  设备文件是指与主机相联的各种外部设备,如显示器、打印机、键盘等。在操作系统中,把外部设备也看作是一个文件来进行管理,把它们的输入、输出等同于对磁盘文件的读和写。 通常把显示器定义为标准输出文件, 一般情况下在屏幕上显示有关信息就是向标准输出文件输出。如前面经常使用的printf,putchar 函数就是这类输出。键盘通常被指定标准的输入文件, 从键盘上输入就意味着从标准输入文件上输入数据。scanf,getchar函数就属于这类输入。

  从文件编码的方式来看,文件可分为ASCII码文件和二进制码文件两种。

  ASCII文件也称为文本文件,这种文件在磁盘中存放时每个字符对应一个字节,用于存放对应的ASCII码。例如,数5678的存储形式为:
ASC码:  00110101 00110110 00110111 00111000
     ↓     ↓    ↓    ↓
十进制码: 5     6    7    8 共占用4个字节。ASCII码文件可在屏幕上按字符显示, 例如源程序文件就是ASCII文件,用DOS命令TYPE可显示文件的内容。 由于是按字符显示,因此能读懂文件内容。

  二进制文件是按二进制的编码方式来存放文件的。 例如, 数5678的存储形式为: 00010110 00101110只占二个字节。二进制文件虽然也可在屏幕上显示, 但其内容无法读懂。C系统在处理这些文件时,并不区分类型,都看成是字符流,按字节进行处理。 输入输出字符流的开始和结束只由程序控制而不受物理符号(如回车符)的控制。 因此也把这种文件称作“流式文件”。

  本章讨论流式文件的打开、关闭、读、写、 定位等各种操作。文件指针在C语言中用一个指针变量指向一个文件, 这个指针称为文件指针。通过文件指针就可对它所指的文件进行各种操作。 定义说明文件指针的一般形式为: FILE* 指针变量标识符; 其中FILE应为大写,它实际上是由系统定义的一个结构, 该结构中含有文件名、文件状态和文件当前位置等信息。 在编写源程序时不必关心FILE结构的细节。例如:FILE *fp; 表示fp是指向FILE结构的指针变量,通过fp 即可找存放某个文件信息的结构变量,然后按结构变量提供的信息找到该文件, 实施对文件的操作。习惯上也笼统地把fp称为指向一个文件的指针。文件的打开与关闭文件在进行读写操作之前要先打开,使用完毕要关闭。 所谓打开文件,实际上是建立文件的各种有关信息, 并使文件指针指向该文件,以便进行其它操作。关闭文件则断开指针与文件之间的联系,也就禁止再对该文件进行操作。

  在C语言中,文件操作都是由库函数来完成的。 在本章内将介绍主要的文件操作函数。

  文件打开函数fopen

  fopen函数用来打开一个文件,其调用的一般形式为: 文件指针名=fopen(文件名,使用文件方式) 其中,“文件指针名”必须是被说明为FILE 类型的指针变量,“文件名”是被打开文件的文件名。 “使用文件方式”是指文件的类型和操作要求。“文件名”是字符串常量或字符串数组。例如:

FILE *fp;
fp=("file a","r");

  其意义是在当前目录下打开文件file a, 只允许进行“读”操作,并使fp指向该文件。

  又如:

FILE *fphzk
fphzk=("c:\\hzk16',"rb")

  其意义是打开C驱动器磁盘的根目录下的文件hzk16, 这是一个二进制文件,只允许按二进制方式进行读操作。两个反斜线“\\ ”中的第一个表示转义字符,第二个表示根目录。使用文件的方式共有12种,下面给出了它们的符号和意义。

文件使用方式        意 义
“rt”      只读打开一个文本文件,只允许读数据
“wt”      只写打开或建立一个文本文件,只允许写数据
“at”      追加打开一个文本文件,并在文件末尾写数据
“rb”      只读打开一个二进制文件,只允许读数据
“wb”       只写打开或建立一个二进制文件,只允许写数据
“ab”       追加打开一个二进制文件,并在文件末尾写数据
“rt+”      读写打开一个文本文件,允许读和写
“wt+”      读写打开或建立一个文本文件,允许读写
“at+”      读写打开一个文本文件,允许读,或在文件末追加数 据
“rb+”      读写打开一个二进制文件,允许读和写
“wb+”      读写打开或建立一个二进制文件,允许读和写
“ab+”      读写打开一个二进制文件,允许读,或在文件末追加数据

  对于文件使用方式有以下几点说明:

  1. 文件使用方式由r,w,a,t,b,+六个字符拼成,各字符的含义是:

  r(read): 读
  w(write): 写
  a(append): 追加
  t(text): 文本文件,可省略不写
  b(banary): 二进制文件
  +: 读和写

  2. 凡用“r”打开一个文件时,该文件必须已经存在, 且只能从该文件读出。

  3. 用“w”打开的文件只能向该文件写入。 若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件。

  4. 若要向一个已存在的文件追加新的信息,只能用“a ”方式打开文件。但此时该文件必须是存在的,否则将会出错。

  5. 在打开一个文件时,如果出错,fopen将返回一个空指针值NULL。在程序中可以用这一信息来判别是否完成打开文件的工作,并作相应的处理。因此常用以下程序段打开文件:

if((fp=fopen("c:\\hzk16","rb")==NULL)
{
printf("\nerror on open c:\\hzk16 file!");
getch();
exit(1);
}

  这段程序的意义是,如果返回的指针为空,表示不能打开C盘根目录下的hzk16文件,则给出提示信息“error on open c:\ hzk16file!”,下一行getch()的功能是从键盘输入一个字符,但不在屏幕上显示。在这里,该行的作用是等待, 只有当用户从键盘敲任一键时,程序才继续执行, 因此用户可利用这个等待时间阅读出错提示。敲键后执行exit(1)退出程序。

  6. 把一个文本文件读入内存时,要将ASCII码转换成二进制码, 而把文件以文本方式写入磁盘时,也要把二进制码转换成ASCII码,因此文本文件的读写要花费较多的转换时间。对二进制文件的读写不存在这种转换。

  7. 标准输入文件(键盘),标准输出文件(显示器 ),标准出错输出(出错信息)是由系统打开的,可直接使用。文件关闭函数fClose文件一旦使用完毕,应用关闭文件函数把文件关闭, 以避免文件的数据丢失等错误。

  fclose函数

  调用的一般形式是: fclose(文件指针); 例如:

  fclose(fp); 正常完成关闭文件操作时,fclose函数返回值为0。如返回非零值则表示有错误发生。文件的读写对文件的读和写是最常用的文件操作。

  在C语言中提供了多种文件读写的函数:

  ·字符读写函数 :fgetc和fputc

  ·字符串读写函数:fgets和fputs

  ·数据块读写函数:freed和fwrite

  ·格式化读写函数:fscanf和fprinf

  下面分别予以介绍。使用以上函数都要求包含头文件stdio.h。字符读写函数fgetC和fputC字符读写函数是以字符(字节)为单位的读写函数。 每次可从文件读出或向文件写入一个字符。

  一、读字符函数fgetc

  fgetc函数的功能是从指定的文件中读一个字符,函数调用的形式为: 字符变量=fgetc(文件指针); 例如:ch=fgetc(fp);其意义是从打开的文件fp中读取一个字符并送入ch中。

  对于fgetc函数的使用有以下几点说明:

  1. 在fgetc函数调用中,读取的文件必须是以读或读写方式打开的。

  2. 读取字符的结果也可以不向字符变量赋值,例如:fgetc(fp);但是读出的字符不能保存。

  3. 在文件内部有一个位置指针。用来指向文件的当前读写字节。在文件打开时,该指针总是指向文件的第一个字节。使用fgetc 函数后, 该位置指针将向后移动一个字节。 因此可连续多次使用fgetc函数,读取多个字符。 应注意文件指针和文件内部的位置指针不是一回事。文件指针是指向整个文件的,须在程序中定义说明,只要不重新赋值,文件指针的值是不变的。文件内部的位置指针用以指示文件内部的当前读写位置,每读写一次,该指针均向后移动,它不需在程序中定义说明,而是由系统自动设置的。

  [例10.1]读入文件e10-1.c,在屏幕上输出。

#include<stdio.h>
main()
{
FILE *fp;
char ch;
if((fp=fopen("e10_1.c","rt"))==NULL)
{
printf("Cannot open file strike any key exit!");
getch();
exit(1);
}
ch=fgetc(fp);
while (ch!=EOF)
{
putchar(ch);
ch=fgetc(fp);
}
fclose(fp);
}

  本例程序的功能是从文件中逐个读取字符,在屏幕上显示。 程序定义了文件指针fp,以读文本文件方式打开文件“e10_1.c”, 并使fp指向该文件。如打开文件出错, 给出提示并退出程序。程序第12行先读出一个字符,然后进入循环, 只要读出的字符不是文件结束标志(每个文件末有一结束标志EOF)就把该字符显示在屏幕上,再读入下一字符。每读一次,文件内部的位置指针向后移动一个字符,文件结束时,该指针指向EOF。执行本程序将显示整个文件。

  二、写字符函数fputc

  fputc函数的功能是把一个字符写入指定的文件中,函数调用的 形式为: fputc(字符量,文件指针); 其中,待写入的字符量可以是字符常量或变量,例如:fputc('a',fp);其意义是把字符a写入fp所指向的文件中。

  对于fputc函数的使用也要说明几点:

  1. 被写入的文件可以用、写、读写,追加方式打开,用写或读写方式打开一个已存在的文件时将清除原有的文件内容,写入字符从文件首开始。如需保留原有文件内容,希望写入的字符以文件末开始存放,必须以追加方式打开文件。被写入的文件若不存在,则创建该文件。

  2. 每写入一个字符,文件内部位置指针向后移动一个字节。

  3. fputc函数有一个返回值,如写入成功则返回写入的字符, 否则返回一个EOF。可用此来判断写入是否成功。

  [例10.2]从键盘输入一行字符,写入一个文件, 再把该文件内容读出显示在屏幕上。

#include<stdio.h>
main()
{
FILE *fp;
char ch;
if((fp=fopen("string","wt+"))==NULL)
{
printf("Cannot open file strike any key exit!");
getch();
exit(1);
}
printf("input a string:\n");
ch=getchar();
while (ch!='\n')
{
fputc(ch,fp);
ch=getchar();
}
rewind(fp);
ch=fgetc(fp);
while(ch!=EOF)
{
putchar(ch);
ch=fgetc(fp);
}
printf("\n");
fclose(fp);
}

  程序中第6行以读写文本文件方式打开文件string。程序第13行从键盘读入一个字符后进入循环,当读入字符不为回车符时, 则把该字符写入文件之中,然后继续从键盘读入下一字符。 每输入一个字符,文件内部位置指针向后移动一个字节。写入完毕, 该指针已指向文件末。如要把文件从头读出,须把指针移向文件头, 程序第19行rewind函数用于把fp所指文件的内部位置指针移到文件头。 第20至25行用于读出文件中的一行内容。

  [例10.3]把命令行参数中的前一个文件名标识的文件, 复制到后一个文件名标识的文件中, 如命令行中只有一个文件名则把该文件写到标准输出文件(显示器)中。

#include<stdio.h>
main(int argc,char *argv[])
{
FILE *fp1,*fp2;
char ch;
if(argc==1)
{
printf("have not enter file name strike any key exit");
getch();
exit(0);
}
if((fp1=fopen(argv[1],"rt"))==NULL)
{
printf("Cannot open %s\n",argv[1]);
getch();
exit(1);
}
if(argc==2) fp2=stdout;
else if((fp2=fopen(argv[2],"wt+"))==NULL)
{
printf("Cannot open %s\n",argv[1]);
getch();
exit(1);
}
while((ch=fgetc(fp1))!=EOF)
fputc(ch,fp2);
fclose(fp1);
fclose(fp2);
}

  本程序为带参的main函数。程序中定义了两个文件指针 fp1 和fp2,分别指向命令行参数中给出的文件。如命令行参数中没有给出文件名,则给出提示信息。程序第18行表示如果只给出一个文件名,则使fp2指向标准输出文件(即显示器)。程序第25行至28行用循环语句逐个读出文件1中的字符再送到文件2中。再次运行时,给出了一个文件名(由例10.2所建立的文件), 故输出给标准输出文件stdout,即在显示器上显示文件内容。第三次运行,给出了二个文件名,因此把string中的内容读出,写入到OK之中。可用DOS命令type显示OK的内容:

TOP

返回列表