映月读书网 > C语言解惑 > 21.2 使用键盘赋值 >

21.2 使用键盘赋值

结构最大的优点是它的域可以含有不同的数据类型,包括数组和指向自己的指针。因此,常常设计使用键盘完成人机交互。从理论上讲,赋值很简单。但是,如果使用键盘赋值,则不是语法意义的正确与否所能解决的,还存在着如何克服键盘抖动所带来的一系列问题。

实际上,对一个简单的结构变量,关键是要注意字符和指针。对结构数组,则还要兼顾数组的特点。常常需要为结构申请动态内存,这都与赋值相关联。

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种简单。