结构最大的优点是它的域可以含有不同的数据类型,包括数组和指向自己的指针。因此,常常设计使用键盘完成人机交互。从理论上讲,赋值很简单。但是,如果使用键盘赋值,则不是语法意义的正确与否所能解决的,还存在着如何克服键盘抖动所带来的一系列问题。
实际上,对一个简单的结构变量,关键是要注意字符和指针。对结构数组,则还要兼顾数组的特点。常常需要为结构申请动态内存,这都与赋值相关联。
21.2.1 为结构变量赋值
本节不涉及结构数组,仅针对结构变量。对结构变量用scanf语句赋值时,一定要注意成员的数据类型。如果成员是普通变量,则要使用地址符号“&”。如果成员是数值数组,为它的各个元素赋值时,可以对每个元素使用“&”号构成显式表示方法。因为数组本身代表地址,也可以使用以数组首地址为基准的表示法。对于字符串,因其作为整体而无需使用“&”号(与显式表示等效)。要特别留意单个字符变量的赋值,以免引发其他问题。
【例21.4】为结构变量赋值的例子。
#include <stdio.h>const double K=0.5;struct List{ char name[12]; char sex; int num; double score[2]; double total; double mean;}a;int main( ){ int i=0; char st[12]={"语文分数:","算数分数:"}; a.total=0.0; printf("性别(F/M):"); scanf("%c",&a.sex); printf("学号:"); scanf("%d",&a.num); printf("名字:"); scanf("%s",a.name); //&a.name等效 for(i=0;i<2;i++) { printf(st[i]); scanf("%lf",(a.score+i)); //a.score错误,等效&a.score[i] a.total=a.total+a.score[i]; //不能用a.total=a.total+(a.score+i); } a.mean=a.total*K; printf("%s,%c,%d,%lf,%lf,%lf,%lf/n", a.name,a.sex,a.num,a.score[0],a.score[1],a.total,a.mean); printf("%s,%c,%d,%lf,%lf,%lf,%lf/n", a.name,a.sex,a.num,*(a.score),*(a.score+1),a.total,a.mean); return 0;}
运行示范如下:
性别(F/M):F学号:205名字:王莹莹语文分数:87算数分数:94王莹莹,F,205,87.000000,94.000000,181.000000,90.500000王莹莹,F,205,87.000000,94.000000,181.000000,90.500000
【解释】字符的读入最麻烦,如果不相信,可以换一下顺序。例如,将性别换到学号之后,典型的运行为
学号:234性别(F/M):名字:F语文分数:
回答学号之后,它跳过这一项,直接询问名字!这就是本程序首先输入性别的原因。因为赋值的顺序与结构定义变量的顺序无关,所以可以充分利用数据类型的特点预防干扰。
如果一定要求按名字和性别的顺序输入,那就要采取措施。例如,可以在scanf语句之前加一条语句“getchar();”,或者将scanf语句改为
scanf(" %c",&a.sex);
形式,均可以解决这个问题。
还有一种办法可以尝试,那就是将字符设计为字符串。例如,将sex声明为
char sex[4];
形式。不过要验证测试,因为有时也会受到干扰产生错误。但要注意,使用getchar有时反而真的需要输入空行。最简单有效的方法可能是在“%c”之前增加一个空格。
在读分数时,对数组score,使用如下两种格式是等效的。
scanf("%lf",(a.score+i));scanf("%lf",&a.score[i]);
对total而言,对应的格式分别为
a.total=a.total+*(a.score+i);a.total=a.total+a.score[i];
一定要分清地址和数据,例如使用
a.total=a.total+(a.score+i); //错误
则是错误的。因为加的是地址,不是地址里的内容。
按此推理,printf的输出格式就很容易掌握了。字符串数组name的名字是数组的首地址,也可以用显式的方式,所以“a.name”和“&a.name”是等效的。实数数组a.score是首地址,也就是第一个元素的地址,第二个元素的地址则是a.score+1。与之等效的显式表示分别为“&a.score[0]”和“&a.score[1]”。如果用数组首地址的方式输出其值,第1个元素为*(a.score),第2个为*(a.score+1)。
21.2.2 为结构指针变量赋值
假设定义如下一个List结构和结构变量a。
struct List{ char *name; char sex[6]; int age;}a;
如何给指针变量name赋值呢?关键是使用scanf语句赋值时,需要给出变量的地址。在使用结构变量a的成员时,“a.sex”和“&a.sex”确实分别代表字符串的值和地址值,而且“a.sex”又代表存储字符串的首地址,这在编译时就由系统予以分配。
使用结构变量a的指针成员时,“a.name”和“&a.name”确实也分别代表指针变量的值和地址值,但“a.name”代表的地址却与“&a.name”不一样。“a.name”所代表的地址应该是它指向的存储字符串的首地址值。由于这个指针没有被初始化,所以不能直接给它赋值,下面的例子将验证这一点。
【例21.5】为结构指针变量赋值的例子。
#include <stdio.h>#include <string.h>struct List{ char *name; char sex[6]; int age;}a;int main( ){ char c[ ]="men"; printf("%s,0x%x,0x%x,%s,0x%x,0x%x/n", a.name,a.name,&a.name, a.sex,a.sex,&a.sex); a.name=c; strcpy(a.sex,c); printf("%s,0x%x,0x%x,%s,0x%x,0x%x/n", a.name,a.name,&a.name, a.sex,a.sex,&a.sex); printf("%s,0x%x,0x%x/n", c,c,&c); printf("姓名:"); scanf("%s",a.name); //注意字符串中不能有空格 printf("%s,0x%x,0x%x,%s,0x%x,0x%x/n", a.name,a.name,&a.name,a.sex,a.sex,&a.sex); printf("%s,0x%x,0x%x/n", c,c,&c); return 0;}
例中分别使用%s和%x输出a.name的值和地址并与a.sex的情况进行比较。程序运行后,第1行输出如下:
(null),0x0,0x4257d0,,0x4257d4,0x4257d4
由此可见,指针没有初始化,指向的地址值为0,所以很危险,必须尽快初始化。而且a.name的地址确实与&a.name不一样。对于a.sex而言,a.sex和&a.sex是一样的。所以说对结构变量a的字符指针域的处理,不能搬用字符域的处理方法。
当用字符串c初始化a.name之后,a.name指向的地址就是存储变量c的地址0x12ff7c。以后重新改变指针内容时,仍然使用这个地址,但字符串的长度受初始化字符串长度的限制。对照下面第2-3行的输出以加深理解。
men,0x12ff7c,0x4257d0,men,0x4257d4,0x4257d4men,0x12ff7c,0x12ff7c
&a.name的地址是固定不变的,仍为0x4257d0,不过这个地址并没有用处。另外,对字符串c初始化时可以有空格,但用键盘赋值时不能有空格(为了使用空格,可以使用gets函数接收键盘输入),下面是键盘赋值的示范。
姓名:张三张三,0x12ff7c,0x4257d0,men,0x4257d4,0x4257d4张三,0x12ff7c,0x12ff7c
思考:为何分别使用“a.name=c;”和“strcpy(a.sex,c);”两种形式?
也可以使用一个字符串数组接收输入,然后再赋给name。也可以申请动态内存初始化指针变量。下面分别给出三种完整的程序。
【例21.6】使用字符串初始化程序的例子。
#include <stdio.h>#include <string.h>struct List{ char *name; char sex[6]; int age;}a;int main( ){ char c[ ]="12345678910w"; //要满足预定长度 a.name=c; printf("姓名:"); gets(a.name); //注意字符串中可以有空格 printf("性别:"); scanf(" %s",&a.sex); printf("年龄:"); scanf("%d",&a.age); printf("/n姓 名 性别 年龄/n"); printf("%6s %3s %4d/n", a.name,a.sex,a.age); return 0;}
程序运行示范如下。
姓名:王 平性别:男年龄:16姓 名 性别 年龄王 平 男 16
【例21.7】使用字符串变量中转实现的程序实例。
#include <stdio.h>#include <string.h>struct List{ char *name; char sex[6]; int age;}a;int main( ){ char c[12]; printf("姓名:"); gets(c); // a.name=c; //不推荐在此位置赋值 printf("性别:"); // getchar(); //根据情况设置,本程序在scanf里面解决 scanf(" %s",a.sex); //注意空格的用途 printf("年龄:"); scanf("%d",&a.age); a.name=c; //推荐的位置 printf("/n姓 名 性别 年龄/n"); printf("%6s %3s %4d/n", a.name,a.sex,a.age); return 0;}
【例21.8】使用动态内存初始化指针实现的程序实例。
#include <stdio.h>#include <stdlib.h>#include <string.h>struct List{ char *name; char sex[6]; int age;}a;int main( ){ char *p=(char *)malloc (12*sizeof(char)); printf("姓名:"); gets(p); //注意字符串中可以有空格 printf("性别:"); scanf(" %s",&a.sex); printf("年龄:"); scanf("%d",&a.age); a.name=p; printf("/n姓 名 性别 年龄/n"); printf("%6s %3s %4d/n", a.name,a.sex,a.age); return 0;}
21.2.3 为链表赋值
为链表赋值仍然需要克服为字符串和字符赋值的干扰。请仔细研读这个赋值的例子和采取的措施。
【例21.9】使用键盘为链表赋值的例子。
#include<stdio.h>#include<stdlib.h>struct person * CreateList(void); //声明返回结构指针的建表函数void PrintList(struct person *); //输出链表内容struct person { int num; //职工编号 char name[12]; float salary; //职工工资 struct person *next; //指向自身的结构指针(指向下一个)};int main(){ struct person *head; head=CreateList(); //建立链表 PrintList(head); //遍历链表 return 0;}struct person * CreateList(void) //返回结构指针的建表函数{ int number; struct person *head; //头指针 struct person *rear; //尾指针 struct person * p ; //新结点指针 head=NULL; //置空链表 printf("输入职工编号,输入0结束. /n"); printf("编号:"); scanf("%d",&number); //读入第一个职工号 if(number==0) return head; //退出建表函数 while(number!=0) //读入职工号不是结束标志(0)时做循环 { p=(struct person *)malloc(sizeof(struct person)); //申请新结点 p->num=number; //数据域赋值 printf("姓名:"); scanf(" %s",p->name); //输入职工姓名 printf("工资:"); scanf("%f",&p->salary); //输入职工工资 if(head==NULL)head=p; //将p指向的新结点插入空表 else rear->next=p; //新结点插入到表尾结点(rear指向的结点)之后 rear=p; //表尾指针指向新的表尾结点 printf("编号:"); scanf(" %d",&number); //读入下一个职工号 } if(rear!=NULL) rear->next=NULL; //终端结点置空 printf("/n建表结束!/n"); return head; //返回表头指针}void PrintList(struct person *head){ struct person *p=head; //p指向表头 while(p!=NULL) { printf("%d %s %6.2f/n", p->num, p->name, p->salary); //输出职工的信息 p=p->next; //使p指向下一个结点 }}
程序运行示范如下。
输入职工编号,输入0结束.编号:1002姓名:李一鸣工资:3455.56编号:1003姓名:张玉萍工资:2356.45编号:0建表结束!1002 李一鸣 3455.561003 张玉萍 2356.45
21.2.4 为结构数组的变量赋值
结构数组是由若干组相同的结构组成,所以重点就是如何表示结构数组的问题。如wk[3]表示有3个相同的结构wk[0]、wk[1]和wk[2]。结构数组的名称就是数组存储的首地址,每个结构的名称就是各个结构的存储首地址。结构数组2的名称是wk[2],也就是结构数组2的首地址。wk[2]类似于单个结构的名称,余下的问题也就迎刃而解了。
注意区分它们域的类型,也就是数组域与变量域的表示方法,对于数组域,数组名就是存储的首地址,但使用显式表示法更容易理解,wk[2].score[0]就是score数组的第1个元素的地址,显式表示为&wk[2].score[0],两者是完全等效的。至于其他数值型,则将&号冠于数组元素名之前即可。
【例21.10】为结构数组变量赋值的例子。
#include <stdlib.h>#include <stdio.h>struct wkrs{ char num[6]; char name[10]; int score[3];}wk[3];void main ( ){ int i=0,j=0; char *c[4]={"序号","姓名","数学","语文"}; printf("准备输入信息/n"); for( i=0; i<3; i++) { printf("序号:"); scanf("%s",wk[i].num); //使用数组名表示 printf("姓名:"); scanf("%s",wk[i].name); printf("成绩:"); { for(j=0;j<2;j++) scanf("%d",&wk[i].score[j]); //使用显式表示 } } printf("/n%8s/t%8s/t%6s/t%4s/n",c[0],c[1],c[2],c[3]); for(i=0;i<3;i++) printf("%8s/t%8s/t%6d/t%4d/n",wk[i].num, wk[i].name, wk[i].score[0],wk[i].score[1]); //使用数组名表示}
运行示例如下:
准备输入信息序号:1001姓名:张晓红成绩:65 78序号:1002姓名:李小刚成绩:76 88序号:1003姓名:黄小华成绩:86 89序号 姓名 数学 语文 1001 张晓红 65 78 1002 李小刚 76 88 1003 黄小华 86 89
21.2.5 为含有指针域的结构数组赋值
【例21.11】这个程序是为使用指针的结构数组赋值,但得到错误的结果。分析错在何处并改正之。
//含有错误的源程序#include <stdlib.h>#include <stdio.h>#include <string.h>struct wkrs{ int num; char *name; int score[3];}wk[3];int main ( ){ int i=0; char s[12]; char *c[4]={"序号","姓名","数学","语文"}; printf("准备输入信息/n"); for( i=0; i<3; i++) { printf("序号:"); scanf(" %d",&wk[i].num); printf("姓名:"); scanf(" %s",s); wk[i].name=s; for( j=0; j<2; j++) { printf("成绩:"); scanf("%d",&wk[i].score[i]); } } printf("/n%8s/t%8s/t%6s/t%4s/n",c[0],c[1],c[2],c[3]); for(i=0;i<3;i++) printf("%8s/t%8s/t%6d/t%4d /n",wk[i].num, wk[i].name, wk[i].score[0],wk[i].score[1]); return 0;}
【解答】程序声明一个字符串数组s作为中转站,但是每次执行程序段
scanf(" %s",s);wk[i].name=s;
的时候,又会将上一个数组元素的wk也更新为新输入的名字。这样一来,数组的所有name均等于最后一次赋给的名字。对数组来说,必须每次使用新的字符串数组中转。本程序的数组分量是3,所以需要声明
char s[3][12];
将接收键盘输入部分改为
printf("姓名:");scanf(" %s",s[i]);wk[i].name=s[i];
即可。另外,有些头文件是多余的,可以去掉。这个程序取消输入成绩的内循环语句,简化了设计。
//改正错误后的源程序#include <stdio.h>struct wkrs{ int num; char *name; int score[3];}wk[3];int main ( ){ int i=0,j=0; char s[3][12]; char *c[4]={"序号","姓名","数学","语文"}; printf("准备输入信息/n"); for( i=0; i<2; i++) { printf("序号:"); scanf(" %d",&wk[i].num); printf("姓名:"); scanf(" %s",s[i]); wk[i].name=s[i]; printf("成绩:"); scanf("%d%d",&wk[i].score[0],&wk[i].score[1]); } printf("/n%8s/t%8s/t%6s/t%4s/n",c[0],c[1],c[2],c[3]); for(i=0;i<3;i++) printf("%8s/t%8s/t%6d/t%4d /n",wk[i].num, wk[i].name, wk[i].score[0],wk[i].score[1]); return 0;}
【例21.12】下面这个程序也是为了给使用指针的结构数组赋值,采用动态内存作为中转,但也得到了错误的结果。分析错在何处并改正之。
#include <stdlib.h>#include <stdio.h>struct wkrs{ int num; char *name; int score[3];}wk[3];void main ( ){ int i=0; char *p; char *c[4]={"序号","姓名","数学","语文"}; p=(char *)malloc (12*sizeof(char)); printf("准备输入信息/n"); for( i=0; i<2; i++) { printf("序号:"); scanf(" %d",&wk[i].num); printf("姓名:"); scanf(" %s",p); wk[i].name=p; printf("成绩:"); scanf("%d%d",&wk[i].score[0],&wk[i].score[1]); } printf("/n%8s/t%8s/t%6s/t%4s/n",c[0],c[1],c[2],c[3]); for(i=0;i<2;i++) printf("%8s/t%8s/t%6d/t%4d /n",wk[i].num, wk[i].name, wk[i].score[0],wk[i].score[1]);}
【解答】与上题犯的错误一样。最简单的方法就是将语句
p=(char *)malloc (12*sizeof(char));
移到for语句下面,作为循环体的第1条语句即可。这就能保证每次重新申请内存,不会覆盖原来的存储信息。
实际上,可以一次申请足够的内存,把它当做数组使用。例如,这里的数组是3个元素,一个元素申请12个字符,现在申请36个字符的内存,即
char *p=(char *)malloc (36*sizeof(char));
然后像使用指针那样使用这块内存。例如:
printf("姓名:");scanf(" %s",p+i);wk[i].name=p+i;
还可以申请对等的字符指针数组,如:
char *p[3];
至于在哪里初始化,都是可以的。例如,在使用前先完成初始化
for( i=0; i<3; i++) p[1]=(char *)malloc (12*sizeof(char));
在for语句里像下面那样使用。
scanf(" %s",p[i]);wk[i].name=p[i];
当然,也可以放到for循环里初始化,但都不如第1种简单。