第11章 复杂数据类型
11.1 结构体
11.1.1结构体类型的定义
- 定义
struct[结构体类型名]
{
数据类型名1 成员名1;
数据类型名2 成员名2;
...
};
//例如:
struct Date
{
int year;
int mouth;
int day;
};
注意: 结构类型只是用户自定义的一种数据类型, 与之间介绍的简单数据类型一样, 他本身不需要占用内存单元, 只有用它来定义某个变量时, 才会为该变量分配结构类型所需要大小的内存单元.
11.1.2结构体变量的定义和引用
- 结构体变量的定义
(1)间接定义法
struct 结构体类型名
{
数据类型名1 成员名1;
..
数据类型名N 成员名N;
};
struct 数据类型名 数据名列表;
(2)直接定义法
struct [结构体类型名]
{
数据类型名1 成员名1;
..
数据类型名N 成员名N;
}变量名列表;
// 例如: 下面的方法直接定义结构体变量 student1 和 student2
struct Student_Info
{
char no[9];
char name[20];
char sex;
unsigned int age;
unsigned int classno;
float grade;
}student1,student2;
struct //无名结构体定义变量只能一次
{
char no[9];
char name[20];
char sex;
unsigned int age;
unsigned int classno;
float grade;
}student1,student2;
说明:
结构体类型与结构体变量是两种不同的概念, 其区别同int 类型和int 型变量的区别一样. 结构体类型不分配内存; 而结构体变量分配内存; 结构体类型不能赋值、存取、运算; 但是结构体变量可以.
结构体可以嵌套.
- 结构体变量的引用
对于非指针型结构体变量,要通过成员运算符" ." , 逐一访问其成员.
结构体变量名.成员名
而对于指针型结构体变量来说, 需要用“–>” 来访问
结构体指针-->成员名 或(*结构体指针).成员名
如果某结构体成员本身又是一个结构体类型, 则只能通过多级的分量运算,对最低一级的成员进行引用.
结构体变量名.成员名.子成员名...... 最低级子成员名
struct Student_ Info stu;
struct Student_ Info *pstu;
pstu = &stu;//指针pstu指向stu
strcpy (stu.name, " zhangMing") ;//将" zhangMing "复制到stu的name成员中
stu.grade = 80;//対stu的成员grade赋值カ80
pstu->grade += 10;//將stu的成员grade的值増加10
printf ("%s %f", stu. name, (*pstu) .grade) ;//输出stu的成员name和grade的值
说明:
结构体变量不能整体引用, 只能引用变量成员
可以将一个结构体变量赋值给另一个结构体变量.
即可以应用结构体变量成员的地址,也可引用结构体变量的地址
11.1.3 结构体变量的赋值
- 结构体变量初始化赋值
(1)先定义结构体类型,在定义结构体变量时赋初值.
struct 结构体类型名
{
....
};
struct 结构体类型名 变量名={成员1的值,成员2的值....};
注意: 赋初值时, { }中间的数据顺序必须与结构体成员的定义顺序一致, 否则就会出现混乱.
(2)定义结构体类型的同时, 定义结构体变量并赋初值
struct [结构体类型名]
{
....
}变量名 ={成员1的值,成员2的值....};
// 例如:
struct Date
{
int year, month, day;
}birthday={1096,12,10};
- 结构体变量在程序中赋值
如果在定义结构体变量时并未对其赋初始值, 那么在程序中要对它赋值的话, 就只要一个一个地对其成员逐一赋值. 或者用已赋值的同类型的结构体变量对它赋值
//例如:
struct Student_info stu; // 只定义了结构体变量stu, 并未对其赋值
//一下通过语句对stu的各个成员逐一赋值
strcpy(stu.no,"20020306");
strcpy(stu.name,"ZhangMing");
stu.sex = 'M';
stu.age = 18;
stu.classno =1;
stu.grade=90;
//如果再定义一个结构体变量stu1, 那么也可以将上面赋值后的stu赋值给stu1
struct Student_Info stu1;
stu1 = stu;
//执行后的结果将是吧stu的各个成员的值赋值给stu1对应的成员,相当于执行下列语句:
strcpy(stu1.no,stu.no);
strcpy(stu1.name,stu.name);
stu1.sex = stu.sex;
stu1.age = stu.age;
stu1.classno = stu.classno;
stu1.grade = stu.grade;
//也可以用memcpy函数来替代,
memcpy(&stu1,&stu,sizeof(struct Student_Info));
注意:
对结构体变量不可整体赋值. stu1 ={ “20020306”,“ZhangMing”,‘M’,18,1,90}这是错误的语句
//计算学生5门课的平均成绩,最低分最高分
#include<stdio.h>
struct score
{
float grade[5];
float avergrade, maxgrade, mingrade;
};
void main()
{
int i;
struct score m;
printf("input the grade of five course:\n");
for(i=0;i<5;i++)
scanf("%f",m.grade[i])
m.avegrade = 0;
m.maxgrade = m.grade[0];
m.mingrade = m.grade[0];
for(i=0;i<5;i++) //求平均分、最高分、最低分
{
m.avegrade += m.grade[i];
m.maxgrade = (m.grade[i] > m.maxgrade)?m.grade[i]:m.maxgrade;
m.mingrade = (m.grade[i] < m.mingrade)?m.grade[i]:m.mingrade;
}
m.avegraed /=5;
printf("avegrade = %5.lf maxgrade = %5.1lf mingrade = %5.1f\n",m.avefrade, m.amxgrade, m.mingrade);
}

11.1.4 简化结构体类型名
可以使用typedef
来为结构体类型起别名
格式: typedef 类型名 类型名的别名;
其中:
类型名必须是已经定义的数据类型名或C语言提供的基本类型名
类型名的别名必须是合法的标识符, 通常用大写字符来表示
typedef 语句要以分号结尾
11.1.5 结构体数组
结构体数组的每一个元素都是具有相同结构体类型的下标结构变量. 在实际应用中,经常用结构体数组来表示具有相同数据结构的一个群体.
从某种意义上来讲, 结构体数组就相当于一张二维表. 一个表的框架对应的就是某种结构体类型,标中的每一列对应该结构体的成员, 表中每一行信息对应该结构体数组元素各成员的具体值, 表中的行数对应结构体数组的大小.
- 结构体数组的定义
struct Student_Info
{
char no[9],name[20],sex;
unsigned int age, classno;
float grade;
}stu[10];
//或者
struct Student_Info stu[10];
//结构体数组stu的每个元素所占内存大小为: sizeof(struct Student_Info)

- 结构体数组的初始化
struct 结构体类型名
{
....
};
struct 结构体类型名 结构体数组[size] = {{初值表1},{初值表2},....};
或者:
struct [结构体类型名]
{
...
}结构体数组[size] = {{初值表1},{初值表2}....};
- 结构体数组的引用
结构体数组名[下标].成员名;
struct Student_Info stu[3];
strcpy(stu[0].name,"Wangfei"); //对数组元素stu[0]中的成员name赋值
stu[1].grade++; //对数组元素stu[1]中的成员grade值
printf("%s",stu[0].name); //显示数组元素stu[0]中的成员name的值
- 结构体数组的应用
统计候选人选票
#include<stdio.h>
#include<string.h>
struct person
{
char name[20]; //候选人姓名
int count; //得票数
}leader[3]={"LI",0,"zhang",0,"wang",0};
void main()
{
int i, j;
char leader_name[20];
while(1)//统计候选人得票数
{
scanf("%s",leader_name);
if(strcmp(leader_name,"0")==0); //输入为“0”结束
break;
for(j=0;j<3;j++)
if(strcmp(leader_name,leader[j].name)==0); //合法
leader[j].count++;
}
for(i=0;i<3;i++)
printf("%5s : %d\n",keader[i].name,leader[i].count);
}
//定义一个结构体类型person, 其中包含两个成员,候选人姓名name和得票数count,以此结构体类型定义了候选人数组leader,并对它赋初值.
11.2 线性链表
- 线性链表概述和结构
当一组数据元素形成了“前后”关系时, 称为线性表.
线性表再内存有两种存放形式:
- 顺序表 : 以数组的形式存放,数组元素在内存中是连续存放的
- 线性链表: 数据元素在内存中不需要连续存放,而是通过指针将各个数据元素连接起来,像一条链子一样将数据单元前后元素链接起来.
对于线性链表来说,它的存放形式不需要提供连续的内存块 , 当插入或删除一个数据元素时,不需要移动其他数据元素,因而其实用性更广.
线性链表逻辑结构图:
线性链表的数据单元包含两个部分 : 指针域和数据域
链表的头节点的数据域不存放有效数据,它的指针域存放实际数据链表的第一个数据单元的地址.
尾节点的指针域置为NULL,作为链表结束的标志
最开始的实际数据链表建立之初, head = tail 表示实际数据链表为空, 即链表中没有任何有效数据.
链表中定义聂店的结构体类型一般格式:
struct 节点结构体类型名
{
数据成员定义;
struct 节点结构体类型名 *指针变量名;
}
- 线性链表的基本操作
基本操作: 创建、插入、删除、输出和销毁
(1) 链表的创建操作
链表创建是指从无到有建立起一个链表, 往空链表中依次插入若干个节点,并保持节点之间的前驱和后继的关系.
基本思想:首先创建—个头节点,让头指针head和尾指针tail都指向该节点,并设置该节点的指针域为NULL(链尾标志);
然后为实际数据创建—个节点,用指针pnew指向它,并将实际数据放在该节点的数据域,其指针域置为NULL ;
最后将该节点插入到tail所指向节点的后面,同时使tail指向pnew所指向的节点。其具体操作见下面的函数Create_ LinkList。
//链表创建操作函数函数Create_LinkList
NODE *Create_ LinkList ( )
{
NODE *head, *tail, *pnew;
int score;
head = (NODE *)malloc (sizeof (NODE) ) ;//创建头节点
if (head == NULL) //创建失败,则返回
{
printf ("no enough memory! \n") ;
return (NULL) ;
}
head->next = NULL;//头节点的指针域置NULL
tail = head;//开始时尾指针指向头节点
printf ("input the score of students: \n") ;
while (1) //创建学生成绩线性链表
{
scanf("号d", &score) ;//输入成绩
if (score < 0)//成绩为负,循环退出
break;
pnew = (NODE *)malloc (sizeof (NODE)); //创建一新节点
if (pnew == NULL)//创建新节点失败,则返回
{
printf ("no enough memory! \n") ;
return (NULL) ;
}
pnew->score = score;//新节点数据域放输入的成绩
pnew->next = NULL;//新节点指针域置NULL
tail->next = pnew; //新节点插入到链表尾
tail = pnew; //尾指针指向当前的尾节点
}
return (head); //返回创建的链表的头指针
}

(2)链表的插入操作
插入操作是在第i个节点Ni与第i+1节点Ni+1之间插入一个新的节点N ,使线性表
的长度增1,且Ni与Ni+1的逻辑关系发生如下变化:插入前,Ni是Ni+1的前驱,Ni+1
是Ni的后继;插入后,新插入的节点N成为Ni的后继、Ni+1的前驱。
基本思想:通过单链表的头指针head ,首先找到链表的第一个节点 ;然后顺着节点
的指针域找到第i个节点,最后将pnew指向的新节点插入到第i个节点之后。插入时首先将新节点的指针域指向第i个节点的后继节点,然后再将第i个节点的指针域指向新节点。注意顺序不可颠倒。当i=0时,表示头节点。
void Insert_ LinkList (NODE *head, NODE *pnew, int i)
{
NODE *p;
int j;
p = head;
for(j = 0;j< i &&p!= NULL; j++) // 将P指向要插入的第i个节点
p = p->next;
if (p == NULL) //表明链表中第i个节点不存在
{
printf ("the 号d node not foundt! \n",i) ;
return;
}
pnew- > next = P-> next ; //将插入节点的指针域指向第i个节点的后继节点
p -> next = pnew;//将第i个节点的指针域指向插入节点
}

(3) 链表的删除操作
删除操作是删除链表中的第i个节点Ni ,使线性表的长度减1。删除前,节点Ni-1
是Ni的前驱,Ni+1是Ni的后继 ; 删除后,节点Ni+1成为Ni+1的后继。
基本思想 : 通过单链表的头指针head ,首先找到链表中指向第i个节点的前驱节点
的指针p和指向第i个节点的指针q ;然后删除第i个节点。删除时只需执行p ->next = q->next即可,当然不要忘了释放节点i的内存单元。注意当i=0时,表示头节点,是不
可删除的。
void Delete_ LinkList (NODE *head,int i)
{
NODE *p, *q;
int j;
if(i==0)//删除的是头指针,则返回
return;
P = head;
for (j =1; j <i && p->next != NULL; j++)
P = p->next; //将p指向要删除的第i个节点的前驱节点
if (p->next == NULL) / /表明链表中第i个节点不存在
{
printf ("the 8d node not foundt! \n",i) ;
return;
}
q = p->next; // q指向待删除的节点i
p->next = q->next;//删除节点I,也可写成p->next = p->next->next;
free(q) ;//释放节点I的内存单元
}

(4)链表的输出操作
输出操作是指,将链表中节点的数据域的值显示出来。如果在输出过程中,对数据
进行相应的比较,则可实现对链表的检索操作。
基本思想:通过单链表的头指针head ,使指针p指向实际数据链表的第一个节点,
输出其数据值, 接着p又指向下一个节点,输出其数据值,如此进行下去,直到尾节点
的数据项输出完为止,即p为NULL为止。
void Display_LinkList(NODE *head)
{
NODE *p;
for(p = head->next; p!=NULL; p=p->next)
printf("%d",p->score);
printf("\n");
}
(5)链表的销毁操作
销毁操作是将创建的链表从内存中释放掉, 达到销毁的目的.
基本思想:每次删除头节点的后继节点 , 最后删除头节点。注意 , 不要以为只要删
除了头节点就可以删除整个链表,要知道链表是一个节点一个节点建立起来的, 所以销毁它也必须一个一个节点的删除才行。
void Free_LinkList(NODE *head)
{
NODE *p, *q;
p=head;
while(p->next !=NULL)
{
q=p->next;
p->next = q->next;
free(q);
}
free(head);
}
- 线性链表应用举例
建立一个学生成绩的线性链表,然后对其进行插入、删除、显示, 最后销毁该链表.
#include<stdio.h>
#include<stdlib.h>
struct Grade_Info
{
int score;
struct Gread_Info *next;
}
typedef struct Grade_Info NODE;
NODE *Create_LinkList();
void Insert_LinkList(NODE *head, NODE *pnew, int i);
void Delete_LinkList(NODE *head, int i);
void Display_LinkList(NODE *head);
void Free_LinkList(NODE *head);
void main()
{
NODE *head, *pnew;
head = Create_ LinkList();//创建链表
if(head == NULL)//创建失败
return;
printf ("after create: ");
Display_LinkList(head);//输出链表中的值
pnew = (NODE *) malloc (sizeof (NODE)); //新建一插入的节点
if (pnew == NULL)//创建失败,则返回
{
printf ("no enough memory! \n") ;
return;
}
pnew->score = 88;
Insert_ LinkList (head, pnew, 3); //将新节 点插入节点3的后面
printf ("after insert: ") ;
Display_ LinkList (head) ;//输出链表中的值
Delete_ LinkList (head,3);//删除链表中节点3
printf ("after delete: ");
Display_ LinkList(head);//输出链表中的值
Free_LinkList(head);//销毁链表
}
11.3 联合体
11.3.1 联合体类型的定义
联合体类型的定义格式
union [联合体类型名]
{
数据类型名1 成员名1;
数据类型名2 成员名2;
....
数据类型名n 成员n;
};
union UData
{
short i;
char ch;
float f;
};
//联合体UData包含三个成员,他们使用同一地址的内存.
//联合体的大小是成员中占内存最大的成员的大小.
//UData的大小由最大的成员确定, float f 的大小为4, 所以联合体的大小也为4

结构体:
struct SData
{
short i;
char ch;
float f;
}

11.3.2 联合体变量的定义和引用
-
联合体变量的定义
-
联合体变量的引用
对联合体成员的引用格式与对结构体成员的引用格式相同,如果通过联合体变量来引用成员,则要使用”.”, 如果是通过联合体指针来引用成员,则要使用”->”。例如:union UData data, *p,d[10]
11.3.3 联合体变量的赋值
- 联合体变量的赋初值
定义联合体变量时可以对变量赋初值, 但只能对变量的第-一个成员赋初值,不可像结构体变量那样对所有的成员赋初值。
union UData data = {10} ;//10赋给成员i
union UData data = {'A'} ;//' A'赋给成员i,即i的值为65 ( ' A'的ASCII码)
union UData data = {10, 'A', 12.5}; //错误, {}中只能有一个值
union UData data = 10;//错误,初值必须用{ }括起来
- 联合体变量在程序中赋值
- 定义了联合体变量以后, 如果要对其赋值, 则只能通过对其成员赋值, 不可对其整体赋值.
union UData data, *p,d[10];
data = [10];//错误
data = 10;//错误
data.i =10;//正确,将10赋给data的成员i
p = &data; // p指向Ddata
p->f =12.5; //正确,将12.5赋给data的成员f
d[0].ch='A' //正确,将'A'赋给d[0]的成员ch
- 像相同结构体类型的变量之间可以彼此赋值一样, 具有相同联合体类型的变量之间也可以相互赋值.
union UData data1 = {10}, data2;
data2 = data1; //正确
几点说明:
1)由于联合体变量的各成员共享同一地址的内存单元,所以在对其成员赋值的某一时刻, 存放的和起作用的将是最后一次存入的成员值.
union UData data;
data.i=10;
data.ch='a';
data.f=12.5;
2)对联合体变量的某个成员赋值时, 也该变量其他成员的值, 因为他们共享一个内存地址.
3)由于联合体变量所有成员共享同一内存空间,因此联合体变量与其各成员的地址相同。
11.4 位域
如果要访问结构体或联合体中的成员所对应内存单元的若干位,就需要用位域
对位域成员的引用方法与结构体或联合体成员引用方法- -样,用”."或”->”来引用这些成员。
#include <stdio.h>
struct MyStruct
{
unsigned char a : 1 ;
unsigned char b : 5;
unsigned short c : 10;
};
union MyUnion
{
unsigned short x;
struct MyStruct y ;
];
void main ( )
{
union MyUnion m = { (unsigned short) 0XFFF1] };
printf("m.y.a = %uln", m.y.a) ;
printf("m.y.b = %uln", m.y.b) ;
printf("m.y.c = %uln", m.y.c) ;
m.y.b=0;
printf ("%Xn", m.x) ;

在VC下, C的数据类型是short, 与 a 、b 的数据类型 char不一致, 所以C不可存取者16位的第7位到第15位的数据,而是起默认值0
11.5 枚举类型变量的定义和引用
枚举就是吧这种类型数据可取的值一一列举出来,一个枚举型变量取值仅限于列出值的范围.
枚举数据类型定义:
enum 枚举类型名
{
枚举元素表
};
//例子: 定义表示日期的枚举类型 weekday
enum weekday{sun, mon, tue, wed, thu, fir, sat};
遇到枚举元素列表时, 编译程序就把其中第一个标识符赋0值, 第二、第三…个标识符依次赋1,2…
因此枚举值赋给枚举变量时,该变量实际得到一个整数值.
enum weekday{sun, mon, tue, wed, thu, fir, sat};
today = sun;
printf("today = %d",today);
//输出的结果将是L today = 0;
同时也可以在枚举类型定义时指定枚举元素的值.
enum weekday(sun =7;mon=1,tue,wed,thu,fri,sat);
//这时, sun =7, mon =1, tue 以后的元素的值 从mon =1, tue =2, wed =3 依次类推
枚举元素是常量,在程序中不可对它赋值
11.6 总结、容易错误的地方
11.6.1 常见错误
- 把数据类型名当作变量名
struct Date
{
int year,month,day ;
};
Date.year=2004;
Date.month=10;
Date.day=18;
//上面是错误的,需要创建一个变量来进行复制
struct Date d;
d.year = 2004;
d.month = 10;
d.day = 18;
- 不能对结构体变量进行整体赋值
- 结构体类型定义在一个函数内,而其他函数中用其定义结构体变量
void main()
{
struct Date
{
int year,month,day;
};
....
}
void func(){
struct Date d; //错误, Date没有定义
}
/*
在编译时会指出func中的Date没有定义, 结构类型名也是标识符,函数中定义的标识符的作用域是它所处的函数,这个函数外不可见,所以对于func()函数中的Date没有定义
*/
- 在typedef语句中漏掉分号