讀古今文學網 > 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種簡單。