结构体和联合体(也称共用体)是自定义的数据类型,用于构造非数值数据类型,在处理实际问题中应用非常广泛。数据结构中的链表、队列、树、图等结构都需要用到结构体。本节主要介绍结构体、联合体及其应用。
一个教职工基本情况表包括编号、姓名、性别、职称、学历和联系电话等信息,每个数据信息的类型并不相同,使用前面学过的数据类型不能将这些信息有效组织起来。每一个教职工都包含编号、姓名、性别、职称、学历和联系电话等数据项,这些数据项放在一起构成的信息称为一个记录。例如,一个教师基本情况表如表2-1所示。
表2-1 教师基本情况表
要用C语言描述表中的某一条记录,需要定义一种特殊的类型,这种类型就是结构体类型。它的定义如下。
struct teacher /* 结构体类型*/ { int no; /* 编号*/ char name[20]; /* 姓名*/ char sex[4]; /* 性别*/ char headship[8]; /* 职称*/ char degree[6]; /* 学历*/ long int phone; /* 联系电话*/ };
其中,struct teacher就是新的数据类型──结构体类型,no、name、sex、headship、degree和phone为结构体类型的成员,表示记录中的数据项。这样,结构体类型struct teacher就可以完整地表示一个教师信息了。
定义结构体类型的一般格式如下。
struct 结构体名 { 成员列表; };
struct与结构体名合在一起构成结构体类型,结构体名与变量名的命名规则一样。前面的student就是结构体名。使用一对花括号将成员列表括起来,在右花括号外使用一个分号作为定义结构体类型的结束。前面的no、name、sex等都是结构体类型的成员,每个成员需要说明其类型,就像定义变量一样。
注意 在定义结构体类型时,struct不可以省略。struct和结构体名一起构成结构体类型,例如,前面的struct teacher是结构体类型,而teacher只是结构体名。不要遗漏结构体外的分号,这是非常容易出错的地方。
像上面介绍的struct teacher类型是无法用C语言中的整型、浮点型、字符型等基本数据类型描述的,只能用其他数据类型构造出新的数据类型。例如,一个学生记录可能包括学号、姓名、性别、年龄及成绩等数据项,学生的记录可以用以下的结构体类型描述。
struct student { char *no; int *name; char sex; int age; float score; };
其中,struct是关键字,表示是一个结构体类型,大括号里面是结构体的成员;struct student是一个类型名,如果要定义一个结构体变量,例如如下语句。
struct student stu1;
stu1就是类型为结构体struct student类型的变量。如果给结构体变量stu1的成员分别赋值,语句如下。
stu1.no="13001"; stu1.age=20; stu1.name="Zhu Tong"; stu1.sex='m'; stu1.score=89.0;
则stu1的结构如图2-51所示。
图2-51 stu1的结构
结构体的变量定义也可以和定义结构体类型同时定义。例如如下语句。
struct student { char *no; int *name; char sex; int age; float score; }stu1;
同样,也可以定义结构体数组类型。结构体变量的定义与初始化可以分开进行,也可以在结构体数组的定义的时候初始化,例如如下语句。
struct student { char *no; char *name; char sex; int age; float score; }stu[2]={{"13001","Zhu Tong",'m',22,90}, {"13002","Guo Jing",'f',21,82}};
数组中各个元素在内存中的情况如图2-52所示。
图2-52 结构体数组stu在内存中的结构
指针可以指向整型、浮点型、字符等基本类型变量,同样也可以指向结构体变量。指向结构体变量的指针的值是结构体变量的起始地址。指针可以指向结构体,也可以指向结构体数组。指向结构体的指针和指向变量和指向数组的指针的用法类似。
【例2-16】 利用指向结构体数组的指针输出学生基本信息。
【说明】 指向结构体的指针与指向数组的指针一样,结构体中的成员变量地址是连续的,将指针指向结构体数组,就可直接访问结构体中的所有成员。程序实现如下。
#include <stdio.h> /* 包含输入输出函数*/ #define N 10 /* 结构体类型及变量定义、初始化*/ struct student { char *no; char *name; char sex; int age; float score; }stu[3]={{"13001","Zhu Tong",'m',22,90.0}, {"13002","Li Hua",'f',21,82.0}, {"13003","Yang Yang",'m',22,95.0}}; void main() { struct student *p; printf(" 学生基本情况表:\n"); printf(" 学号 姓名 性别 年龄 成绩\n"); for(p=stu;p<stu+3;p++) /* 通过指向结构体的指针输出学生信息*/ printf("%-8s%12s%8c%8d%8.1f\n",p->no,p->name,p->sex,p->age,p->score); }
程序运行结果如图2-53所示。
首先定义了一个指向结构体的指针变量p,在循环体中,指针指向结构体数组p=stu,即指针指向了结构体变量的起始地址。通过p->no、p->name等访问各个成员。如果p+1,表示数组中第2个元素stu[1]的起始地址。p+2表示数组中的第3个元素地址,如图2-54所示。
图2-53 通过结构体指针输出学生信息
图2-54 指向结构体数组的指针在内存的情况
通常情况下,在定义结构体类型时,使用关键字typedef为新的数据类型起一个好记的名字。typedef是C语言中的关键字,它的主要作用是为类型重新命名,一般形式如下。
typedef 类型名1 类型名2
其中,类型名1是已经存在的类型,如int、float、char、long等;也可以是结构体类型,如struct student。类型名2是你重新起的名字,命名规则与变量名的命名规则类似,必须是一个合法的标识符。
例如如下语句。
typedef int COUNT; /* 将int 型重新命名为COUNT*/ typedef float SCORE; /* 将float 型重新命名为SCORE*/
经过以上重新定义变量,COUNT就代表了int,SCORE就表示了float。这样,以上语句与如下语句等价。
int a,b,c; /* 定义int 型变量a 、b 、c*/ COUNT a,b,c; /* 定义COUNT 型变量a 、b 、c*/
例如如下代码是将NUM定义数组类型。
typedef int NUM[20]; /*NUM 被定义为新的数组类型*/
代码表示NUM被定义为数组类型,该数组的长度为20,类型为int。可以使用NUM定义int型数组,代码如下。
NUM a; /* 使用NUM 定义int 型数组*/
a表示长度为20的int型数组,它与如下代码等价。
int a[20]; /* 使用int 定义数组*/
使用typedef为指针类型变量重新命名与重新命名数组类型的方法是类似的。例如如下语句。
typedef float *POINTER; /*POINT 被定义为指针类型*/
POINTER表示指向float类型的指针类型。如果要定义一个float类型的指针变量p,代码如下。
POINTER p; /* 使用POINTER 定义指针变量*/
p被定义为指向float类型的指针变量。同样,也可以使用typedef重新为指向函数的指针类型命名,例如定义一个函数指针类型,代码如下。
typedef int (*PTR)(int,int); /*PTR 被定义为函数指针类型*/
PTR被定义为函数指针类型,PTR是指向返回值为int且有两个int型参数的函数指针。如下语句使用PTR定义变量。
PTR pm; /* 使用PTR 定义一个函数指针变量pm*/
pm被定义为一个函数指针变量。
用户自己定义的数据类型主要包括结构体、联合体、枚举类型,最为常用的是为结构体类型重新命名,联合体和枚举类型的命名方法与结构体的重新命名方法类似。例如,将一个结构体命名为DATE,代码如下。
typedef struct /* 为结构体类型重新命名*/ { int year; int month; int day; }DATE;
从类型名DATE可以很容易看出,DATE是表示日期的类型。上面的类型重新定义是在定义结构体类型的同时为结构体命名;也可以先定义结构体类型,然后重新为结构体命名,代码如下。
struct date /* 定义结构体类型*/ { int year; int month; int day; }; typedef date DATE; /* 为结构体类型重新命名*/
以上两段代码是等价的。注意,date和DATE是两个不同的名字,C语言是区分大小写的。接下来,就可以使用DATE定义变量了,代码如下。
DATE d; /* 定义变量d*/
上面的变量定义与如下变量定义等价。
struct date d;
与结构体一样,联合体也是一种派生的数据类型。但是与结构体不同的是,联合体的成员共享同一个存储空间。
定义联合体的方式与定义结构体的方式类似,定义联合体的一般形式如下。
union 共用体名 { 成员列表; } 变量列表;
其中,union是C语言的关键字,用来定义联合体类型。例如,一个联合体的类型定义如下。
union data /* 定义联合体类型和变量abc*/ { int a; float b; char c; double d; }abc;
以上代码是将联合体类型与变量同时定义,当然也可以先定义联合体类型,然后定义变量,例如如下语句。
union data /* 定义联合体类型union abc*/ { int a; float b; char c; double d; }; union data abc; /* 定义联合体类型的变量abc*/
当然也可以省略联合体名data,代码如下。
union /* 省略了联合体名data*/ { int a; float b; char c; double d; }; union data abc; /* 定义联合体类型的变量abc*/
以上3段代码是等价的。
从联合体的类型定义可以看出,联合体与结构体的定义非常相似。但是联合体与结构体在存储方式上却是不同的。联合体中的成员在同一时刻只有一个有效,联合体中的成员占用同一块内存单元。上面定义的变量abc在内存中的情况如图2-55所示。
图2-55 变量abc在内存中的情况
其中,变量abc中包含4个成员,它以占用内存单元最长的成员作为变量的长度。因此,abc占用8个字节,而不是4+4+1+8=17个字节。
引用联合体成员变量的方式与引用结构体成员变量的方式相同。例如,前面定义了联合体变量abc,引用变量中的成员的代码如下。
abc.a /* 引用联合体变量中的成员a*/ abc.b /* 引用联合体变量中的成员b*/ abc.c /* 引用联合体变量中的成员c*/
不可以整体对联合体变量进行输入和输出,以下代码的写法是错误的。
scanf("%d",&abc); /* 错误!试图整体为联合体变量输入数据*/ printf("%d",&abc); /* 错误!试图整体输出联合体变量的值*/
正确的写法应该是分别输入和输出成员变量的值,代码如下。
scanf("%d",&abc.a); /* 正确!为联合体变量的成员a 输入数据*/ printf("%f",&abc.b); /* 正确!输出联合体变量的成员b 的值*/
在使用联合体时,需要注意以下几个问题。
(1)联合体变量中的成员共同占有同一块内存单元,同时只能有一个成员存放其中。同一时刻,只能有一个成员起作用,其他成员不起作用。
(2)联合体变量有效的成员是最后被赋值的成员,每存入一个新的数据,前面的成员的值不起作用。例如如下代码。
abc.a=5; abc.b=7.5; abc.c='r';
经过3次赋值之后,只有最后一个赋值语句有效,即联合体变量abc中的成员c的值为字符'r',成员a、b、d中的值没有意义。
(3)联合体变量中的各个成员的地址都是相同的,每个成员的存放都是从这个地址开始存放。例如&abc.a、&abc.b、&abc的值都是相同的。
(4)不能对联合体变量像结构体一样地赋值。这是因为同一时刻只能有一个成员有效。例如如下代码是错误的:
union data /* 定义联合体类型*/ { int a; float b; char c; double d; }abc={3,7.9,'x',5.6}; /* 错误!同一时刻只能有一个成员有效,不能这样赋值*/
(5)不能将联合体变量作为函数的参数。
【例2-17】 建立一个教师和学生基本情况登记表,其中,教师基本情况由编号、姓名、性别、年龄、职业和职称构成,学生基本情况由编号、姓名、性别、年龄、职业和班级构成。
【分析】 教师和学生的基本情况基本相同,只有一项不同,教师有职称而没有班级,学生有班级而没有职称,因此可以将职称和班级放在一起,构成一个联合体。将这个联合体类型与编号、姓名、性别、年龄、职业放在一起构成一个结构体,结构体的定义如下。
struct STAFFER /* 定义结构体类型*/ { int num; char name[20]; char sex[4]; int age; int job; union /* 定义联合体类型*/ { int class; char prof[20]; }category; /* 定义联合体变量*/ };
结构体类型为struct STAFFER,为了简便,可以将结构体类型重新命名,结构体类型的定义可以写成如下形式。
typedef struct { int num; char name[20]; char sex[4]; int age; int job; union /* 定义联合体类型*/ { int class; char prof[20]; }category; /* 定义联合体变量*/ }STAFFER; /* 结构体类型为STAFFER*/
使用typedef重新对结构体定义后,结构体类型为STAFFER。完整的程序代码如下。
#include<stdio.h> /*------------------------ 定义结构体类型STAFFER----------------------*/ typedef struct { int num; char name[20]; char sex[4]; int age; int job; union { int class; char prof[20]; }category; }STAFFER; /*---------------------------- 结构体类型定义结束--------------------------*/ void main() { STAFFER staf[20]; /* 定义结构体数组staf*/ int i; /* 定义变量i*/ /*--------------------------------- 输入3 条记录-------------------------------*/ for(i=0;i<3;i++) { printf(" 编号:"); scanf("%d",&staf[i].num); /* 输入编号*/ printf(" 姓名:"); scanf("%s",staf[i].name); /* 输入姓名*/ printf(" 性别:"); scanf("%s",staf[i].sex); /* 输入性别*/ printf(" 年龄:"); scanf("%d",&staf[i].age); /* 输入年龄*/ printf(" 职业(1 表示教师,0 表示学生) :"); scanf("%d",&staf[i].job); /* 输入职业*/ if(staf[i].job==1) /* 如果输入的是1 ,表示教师*/ { printf(" 职称:"); scanf("%s",staf[i].category.prof); /* 输入职称*/ } else if(staf[i].job==0) /* 如果输入的是0 ,则表示学生*/ { printf(" 班级:"); scanf("%d",&staf[i].category.class); /* 输入班级*/ } } /*------------------------------- 记录输入结束-----------------------------*/ /*------------------------------------ 输出记录------------------------------*/ printf(" 编号 姓名 性别 年龄 职业 班级/ 职称\n"); for(i=0;i<3;i++) { if(staf[i].job==1) /* 如果是教师*/ printf("%7d%7s%5s%6d%6s%8s\n",staf[i].num,staf[i].name,staf[i].sex, staf[i].age," 教师",staf[i].category.prof); else if(staf[i].job==0) /* 如果是学生*/ printf("%7d%7s%5s%6d%6s%8d\n",staf[i].num,staf[i].name,staf[i].sex, staf[i].age," 学生",staf[i].category.class); } } /*---------------------------------- 记录输出结束----------------------------*/
程序运行结果如图2-56所示。
图2-56 程序运行结果