結構最大的優點是它的域可以含有不同的數據類型,包括數組和指向自己的指針。因此,常常設計使用鍵盤完成人機交互。從理論上講,賦值很簡單。但是,如果使用鍵盤賦值,則不是語法意義的正確與否所能解決的,還存在著如何克服鍵盤抖動所帶來的一系列問題。
實際上,對一個簡單的結構變量,關鍵是要注意字符和指針。對結構數組,則還要兼顧數組的特點。常常需要為結構申請動態內存,這都與賦值相關聯。
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種簡單。