结构最大的优点是它的域可以含有不同的数据类型,包括数组和指向自己的指针。因此,常常设计使用键盘完成人机交互。从理论上讲,赋值很简单。但是,如果使用键盘赋值,则不是语法意义的正确与否所能解决的,还存在着如何克服键盘抖动所带来的一系列问题。
实际上,对一个简单的结构变量,关键是要注意字符和指针。对结构数组,则还要兼顾数组的特点。常常需要为结构申请动态内存,这都与赋值相关联。
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 ,0x4257d4 men ,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.56 1003 张玉萍 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种简单。