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