C语言程序设计课件 第8章_第1页
C语言程序设计课件 第8章_第2页
C语言程序设计课件 第8章_第3页
C语言程序设计课件 第8章_第4页
C语言程序设计课件 第8章_第5页
已阅读5页,还剩140页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第8章结构、共用和枚举类型8.1结构类型8.2共用类型8.3枚举类型8.4自定义类型8.5结构类型的综合示例本章小结

前面介绍的整型、浮点型、字符型等数据类型都是单一数据类型,即便是包含多个元素的数组,也只能存储同一种数据类型的数据。但在实际问题中,常常需要把一些属于不同数据类型的数据作为一个整体来处理。如一个学生的通信地址(包括姓名、部门、地址、邮编、电话号码、电子邮箱等)或一个学生的成绩表(包括班级、学号、姓名、性别、数学成绩、数据库成绩、英语成绩等),如图8-1所示。这些表集合了各种标准类型数据,无法用前面学过的任一种数据类型完全描述,因此,C语言引入一种能集不同数据类型于一体的非单一数据类型—结构类型。此外,本章还将介绍另两种非单一数据类型—共用类型和枚举类型。

图8-1结构类型数据示例

8.1结构类型

8.1.1结构类型和结构类型变量的声明及定义1.结构类型和结构类型变量的声明到目前为止介绍的唯一一种数据结构(非单一数据类型)就是数组。数组有两个重要特性。首先,数组的所有元素具有相同的类型;其次,为了选择数组元素需要指明元素的位置(作为整数下标)。

一般来说,结构所具有的特性与数组有很大不同。结构的元素(在C语言中的说法是结构的成员)可能具有不同的类型。而且,每个成员都有名字,因此为了选择特定的成员需要指明成员的名字而不是它的位置。

由于大多数编程语言提供类似的特性,因此结构可能听起来很熟悉。在其他一些语言中,经常把结构称为记录(Record),把结构的成员称为字段(Field)。

当需要存储相关数据项的集合时,结构是一种合乎逻辑的选择。例如,假设需要记录存储在仓库中的零件。每种零件需要存储的信息可能包括零件的编号(整数)、零件的名称(字符串)以及现有零件的数量(整数)。为了产生一个可以存储这三种数据项的变量,可以使用类似下面这样的声明:

struct是一个关键字,表示结构类型定义的开始。结构名的命名要求符合标识符命名规则。结构成员可以是char、int、float、double、数组、指针、结构等各种数据类型。成员名的命名跟普通变量一样,所有成员用花括号括起来,构成一个整体。结构类型定义语句以分号作为结束符。注意,这里的声明格式和C语言中其他变量的声明格式一样。part1和part2是具有这种类型的变量,称为结构类型变量。

结构的成员在内存中是按照声明的顺序存储的。为了说明part1在内存中存储的形式,现在假设:(1)part1存储在地址为2000的内存单元中;(2)每个整数在内存中占4字节;(3)NAME_LEN的值为25;(4)成员之间没有间隙。根据这些假设,part1在内存中的样子如图8-2所示。图8-2part1在内存中样子

每个结构代表一种新的作用域。任何声明在此作用域内的名称都不会和程序中的其他名称冲突。(用C语言的术语可表述为,每个结构都为它的成员设置了独立的命名空间。)例如,下列声明可以出现在同一程序:

和数组一样,结构类型变量也可以在声明的同时进行初始化。为了对结构进行初始化,要把待存储到结构中的值的列表准备好并用花括号把它括起来。例如

初始化中的值必须按照结构成员的顺序进行显示。在此例中,结构part1的成员number值为528,成员name则是"Diskdrive",依次类推。结构part1初始化后的样子如图8-3所示。图8-3part1初始化

2.结构类型和结构类型变量的定义

虽然上节说明了声明结构类型变量的方法,但是没有讨论一个重要的问题:命名结构。假设程序需要声明几个具有相同成员的结构类型变量。如果一次可以声明全部变量,那么没有什么问题。但是,如果需要在程序中的不同位置声明变量,那么问题就复杂了。

如果在某处编写了

那么问题出现了。重复的结构信息会使程序膨胀。因为难以确保这些声明会保持一致,将来修改程序会有风险。

此外,根据C语言的规则,part1和part2不具有兼容的类型,因此不能把part1赋值给part2,反之亦然。而且,因为part1和part2的类型都没有名字,所以也就不能把它们用作函数调用的参数。

为了解决这些问题,需要定义表示结构(而不是特定的结构)的名称。结构类型定义的一般形式为

在一个结构类型中,将一些不同类型的数据组合成一个整体,虽然各个成员数据分别有着不同的数据类型,但是它们之间密切相关。下面先看一个联系人的结构类型定义。

在联系人的结构类型定义中,将一个联系人的姓名、部门、地址、邮编、电话号码和电子邮箱组合成了一个整体,虽然各个成员数据分别有着不同的数据类型,但是各个成员数据都属于同一个联系人的。

结构类型定义并没有说明任何实际的变量,它仅仅是定义一种特殊的数据类型,它与系统标准类型(如int、char、float、longint等)一样可以用来定义变量。

在定义了一个结构类型后,还需要对相应的变量进行定义,定义一个结构类型变量的方法有三种方式。

1)先定义结构类型再定义变量

先定义结构类型。

结构类型变量stu1和stu2均为结构类型structscore的变量,即它们具有structscore定义的结构,如图8-4所示。

图8-4stu1和stu2结构类型变量

2)在定义结构类型的同时定义变量

同时定义结构类型和结构类型变量,是一种双重定义,其定义的一般形式为

下面的定义同时定义了一个结构类型structscore和两个结构类型变量stu1与stu2。

3)直接定义结构类型变量

若只需定义结构类型变量,则定义时可不出现结构名。其定义的一般形式为

下面的定义直接定义了两个结构类型变量stu1与stu2。

关于结构类型变量,有以下几点要说明:

(1)成员可以与程序中的变量同名,但两者不代表同一对象

(2)结构类型与结构类型变量的概念不同

①对结构类型变量来说,一般先定义一个结构类型,然后定义该类型的变量。

②只能对结构类型变量赋值、存取或运算,不能对一个结构类型赋值、存取或运算。

③在编译时,不对结构类型分配存储空间,只对结构类型变量分配存储空间。C编译器将自动地分配适当的内存区域给结构类型变量的各个成员变量。

(3)结构类型变量的内存分配与编译器有关。

表8-1是结构类型变量stu1在两种不同编译环境中的内存分配情况。

VisualC++2010Express为结构类型变量分配内存与TurboC不同。TurboC中是按需分配,即实际需要多大内存就分配多大内存。而VisualC++2010Express中为结构类型变量分配内存是按单位长度分配的,即先分配一单位长度(该单位长度的大小等于结构成员中占内存最多的数据类型长度),然后在该单位长度空间中依次为结构类型变量中的成员变量分配存储空间,直至剩余空间不够分配给一个完整的成员变量时为止,接着为该结构类型变量分配另一个单位长度的存储空间。

3.结构类型变量的引用规则

(1)对结构类型变量中各个成员的访问,用操作符“.”表示,其格式为

结构类型变量名.成员名

操作符“.”称为成员运算符,具有最高优先级。C语言允许直接赋值给一个结构类型变量成员,而不能将一个结构类型变量作为一个整体进行输入和输出。例如,若系统已经进行了如图8-4所示的定义,则将30301赋给其结构类型变量stu1中的number成员的正确语句是

stu1.number=30301;

而以下语句是错误的:

scanf("%s,%ld,%s,%c,%f,%f,%f",&stu1);

printf("%s,%ld,%s,%c,%f,%f,%f\n",stu1);

(2)对成员变量可以像普通变量一样进行各种运算。

下列运算是正确的:

stu1.number++;

++stu1.number;

(3)可以引用成员的地址,也可以引用结构类型变量的地址。

例如,对于如图8-4所示的结构类型,有以下引用:

4.结构类型变量的初始化

在定义结构类型变量的同时,可以对结构类型变量中的各个成员进行初始化。初始化时注意数据类型的一致性。

例8-1在定义结构类型的同时进行结构类型变量的定义及初始化的示例。

例8-2先定义结构类型,再进行结构类型变量的定义及初始化的示例。

8.1.2结构类型数组

一个结构类型变量只能存放表格中的一个记录(如一个学生的学号、姓名和成绩等数据)。若一个班有数十名学生的记录,则需要数十个结构类型变量来存放全部的记录,那样就太麻烦了。较好的方法就是用结构类型数组(简称结构数组)来描述。

结构类型数组在定义结构类型变量时指定数组下标,下标从0开始。结构类型数组与普通数组区别在于它的每个数组元素都是一个结构类型数据,它们都分别包括各个成员项。

1.结构类型数组的定义

与定义结构类型变量的方法相似,这里只需说明其为数组即可。例如,定义一个能保存35个同学的通信录结构类型数组,格式为

以上定义了一个有35个元素的结构类型数组stu,其元素为structperson类型数据。为了访问数组中的某同学的通信录,可以使用下标方式。例如,为了显示存储在位置i的通信录,可以写成

print_person(stu[i]);

访问结构person内的成员可结合下标和成员名。例如为了给stu[i]中的成员department赋值883,可以写成

stu[i].department=883;

访问通信录名单中的单个字符要求先取下标(选择特定的名单),然后选择成员(选择成员name),再取下标(选择零件名中的字符)。为了使存储在stu[i]中的名字变为空字符串,可以写成

stu[i].name[0]='\0';

结构类型数组各元素在内存中连续存放,如图8-5所示。结构类型数组也可以直接定义,例如

图8-5结构类型数组

2.结构类型数组的初始化

初始化结构类型数组可以用类似于结构类型变量初始化的方法。其中每个数组元素的值要用花括号“{}”括起来,各数组元素之间以逗号“,”隔开。一般形式为

struct结构名数组名[数组元素数]={初值表列};

3.结构类数组的应用

为了说明实际应用中数组和结构是如何嵌套的,现在编写一个相对大一点的程序,此程序用来维护仓库存储的零件的信息的数据库。程序围绕一个结构类型数组构建,且每个结构包含以下信息:零件的编号、名称以及数量。程序将支持下列操作。

(1)添加新零件的编号、名称和初始的现货数量。如果零件已经在数据库中,或者数据库已满,那么程序必须显示出错信息。

(2)给定零件编号,显示出零件的名称和当前的现货数量。如果零件编号不在数据库中,那么程序必须显示出错信息。

(3)显示数据库中全部信息的表格。零件必须按照录入的顺序显示出来。

(4)终止程序的执行。

使用i(插入)、s(搜索)、u(更新)、p(显示)和q(终止)分别表示这些操作。与程序的会话可能有以下几种情况。

程序将在结构中存储每种零件的信息。这里限制数据库的大小为100种零件,这使得用数组来存储结构成为可能,这里称此数组为inventory。(如果这里的限制值太小,可以改变它。)为了记录当前存储在数组中的零件数,使用名为num_parts的变量。因为这个程序是以菜单方式驱动的,所以十分容易写出主循环结构:

为了方便起见,将分别设置不同的函数执行插入、搜索、更新和显示操作。因为这些函数都需要访问inventory和num_parts,所以可以把这些变量设置为外部变量。或者把变量声明在main()函数内,然后把它们作为实际参数传递给函数。从设计角度来说,使变量局部于函数通常比把它们外部化更好。然而,在此程序中,把inventory和num_parts放在main()函数中只会使程序复杂化(后面会加以解释的)。

这里决定把程序分割为三个文件:inventory.c文件,它包含程序的大部分内容;readline.h文件,它包含read_line函数的原型;readline.c文件,它包含read_line函数的定义。本节后面将讨论后两个文件,现在先集中讨论inventory.c文件。

在main()函数中,格式串"%c" 允许scanf()函数在读入操作码之前跳过空白字符。格式串中的空格是至关重要的,如果没有它,scanf()函数有时会读入前一输入行末尾的换行符。

程序包含一个名为find_part()的函数,main()函数不调用此函数。这个“辅助”函数用于避免多余的代码和简化更重要的函数。通过调用find_part(),insert()函数、search()函数和update()函数可以定位数据库中的零件(或者简单地确定零件是否存在)。

上述程序现在还剩下一个细节:read_line()函数。这个函数用来读取零件名称。请思考当用户插入零件时会发生什么?例如:

Enteroperationcode:i↙

Enterpartnumber:528↙

8.1.3结构类型指针

为了方便对结构类型变量或结构类型数组进行操作,可以定义结构类型指针变量,用以指向结构类型变量或结构类型数组。

1.指向结构类型变量的指针

结构类型变量指针要求先定义、后使用。基本步骤如下。

(1)定义结构:

struct结构名

{

}结构类型变量名表;

(2)用类似下面的语句来定义指向结构类型变量的指针变量:

struct结构名*p;

随后可将所定义的指针变量p指向结构类型变量(注意:它只能指向一个结构类型变量,而不能指向结构类型变量的某一成员),如:

p=&结构类型变量;

(3)通过p引用结构类型变量成员,形式如下:

(*指针变量).成员或指针变量->成员

其中算符“->”称为指向运算符,由一个减号“-”和一个大于号“>”组成。表8-2给出了与指向运算符相关的几种运算。

例8-3输出一个同学的通信录。

例8-3是一指向结构类型变量的指针变量的应用举例。例中指针指向的示意图如图8-6所示。在这段程序中,前两个printf函数是输出stu1的各个成员的值,后两个printf函数使用p->num的形式来输出stu1各成员的值,结果是相同的。图8-6指针指向示意图

在C语言中,当用一个指针变量p指向某个结构类型变量后,以下3种结构成员的引用是等价的:

①结构类型变量.成员名

② p->成员名

③ (*p).成员名

2.指向结构类型数组的指针

可以用指针或指针变量来指向结构类型数组及其元素,其定义和使用原则与指向结构类型变量的指针相同。

例8-4编写程序,要求用指向结构类型数组的指针来连续输出通信录中的3条记录。

解题思路利用循环结构,先使p指向数组stu的首地址,如图8-7所示。在第一次循环中输出stu[0]各个成员值,然后执行p++,使p指向stu[1]的首地址;在第二次循环中输出stu[1]的各成员值……直到p不小于stu+3时,停止循环。图8-7通过指针访问结构类型数组

程序运行结果为

3.用指向结构的指针作为函数参数

将一个结构类型变量(或结构类型数组)的值传给另一个函数有两个方法:

(1)用结构类型变量(或结构类型数组)作为函数形参和实参。

(2)用指向结构类型变量(或结构类型数组)的指针作为形参,用结构类型变量(或结构类型数组)的首地址作为实参。

将一个完整的结构类型变量(或结构类型数组)作为实参传递给形参,需要将全部成员值一个一个传递,时间与空间开销大,程序运行效率低;而将结构类型变量(或结构类型数组)的地址作为实参传递给形参,减少了时间与空间开销,提高了程序运行效率。

例8-5将结构类型变量(或结构类型数组)作为实参传递给形参的示例。

当用整个结构作为函数参数传递时,必须保证实参与形参的类型相同。例8-5中,形参parm和实参arg被说明为同一结构类型(structmy)。在函数f1()调用时,形参“parm”需要单独分配空间,且需要将实参arg的全部成员值一个一个传递给形参“parm”的各个成员,时间与空间开销大,程序运行效率低。

例8-6有一个结构类型变量stu,内含学生学号、姓名和三门课的成绩。要求编写一个程序,在main()函数中赋值,在另一函数print()中将它们输出。

例8-6中的函数print()在调用时,将结构类型变量stu的首地址传送给形参p(p是指针变量),p指向结构类型变量stu后间接引用stu的内存和成员值,由于不需额外给结构类型变量分配内存并传递各个成员的值,从而减少了时间与空间开销,提高了程序运行效率。

8.1.4结构类型的嵌套

结构和数组的组合没有限制。数组可以用结构作为元素,结构也可用数组和其他结构作为成员。前面已经介绍了数组嵌套在结构内部的示例(结构part的成员name)。下面再来探讨其他的可能性:结构成员是其他结构的结构和数组元素是结构的数组。

例如,在下面的程序中先定义了一个structsubject结构类型,它代表“科目”,包括3个成员:math、database和english。然后在定义structscore类型时,其成员course的类型定义为structsubject类型。于是结构类型structscore的构成如图8-8所示。图8-8结构类型structscore的构成

由于成员本身又属一个结构类型,在访问成员时,则要用若干个成员运算符,逐级找到最低一级的成员。系统只能对最低的成员进行赋值、存储或运算。例如,对上面定义的structscore结构类型变量stu1,可以这样访问各成员:

① 

② stu1.course.math

③ stu1.course.database

④ stu1.course.english

8.1.5用指针处理链表

1.链表概述

链表是一种常见的重要数据结构。它是一种动态存储分配结构,可避免因无法确定所需长度(即元素个数)而把数组定义得较大,导致浪费内存资源的情况。

图8-9表示一种单向链表结构。链表有一个“头指针”变量,图中以“head”表示,它存放一个地址,该地址指向一个结构元素。链表中每一个元素称为“结点”,每个结点都应包括两个部分:第一部分为用户需要用的实际数据,第二部分为下一个结点的地址。“head”指向第一个元素,第一个元素又指向第二个元素……直到最后一个元素。最后一个元素不再指向其他元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”)。链表到此结束。

图8-9单向链表结构

图8-9的另一层含义是:链表中的各结点在内存中的存储地址可以不连续。其各结点的地址在需要时向系统申请分配,系统根据内存的当前情况,既可以连续分配地址,也可以跳跃式分配地址。无论在表中访问哪一个结点,都需要从链表的头指针开始,顺序向后查找。

为了建立链表,首先需要一个表示链表中单个结点的结构。简单起见,先假设结点只包含一个整数(即结点的数据)和指向链表中下一个结点的指针。

关于node结构,有一点需要特别注意,通常可以选择使用标记或者用typedef来定义一种特殊的结构类型名。但是,在结构有一个指向相同结构类型的指针成员时(就像node中那样),要求使用结构标记。没有node标记,就没有办法声明next的类型。

声明了node结构之后,还需要记录链表开始的位置。换句话说,需要有一个始终指向链表中第一个结点的变量。这里把此变量命名为first:

structnode*first=NULL;

把first初始化为NULL表明链表初始为空。

下面是一个链表的示例,利用在结构类型变量中所包含的指针类型成员来指向其他结构类型数据,或指向与自身所在结构类型相同的结构类型数据:

其中next是指针类型的成员名,它指向structperson类型数据(即next所在的结构类型数据)。在该结构类型的定义中,非常特殊的一点就是结构类型为递归结构类型,构成的链表结构如图8-10所示。该链表中每一个结点都属于structperson类型,其成员next存放下一结点的首地址。图8-10的存储形式与图8-7所示的结构类型数组存储的形式相比,主要的不同是各个结点之间并不需要连续存储。

图8-10递归结构类型的链表结构

2.建立与输出链表

在定义结构类型时,系统并未实际分配存储空间。为能让链表在需要时动态地分配和

释放一个结点的存储单元,可以使用C语言编译系统提供的以下库函数。

(1) void*malloc(unsignedsize):在内存的动态存储区中分配一长度为size的连续空间。分配空间成功,则返回指向被分配内存的无类型指针,否则返回NULL。在实际编程时,无类型指针(void*)可以通过类型转换强制转换为任何其他类型的指针。

(2) void*calloc(unsignedn,unsignedsize):在内存的动态存储区中分配n个长度为size的连续空间。分配空间成功,则返回指向被分配内存的无类型指针,否则返回NULL。

(3) voidfree(void*ptr):释放由ptr指向的内存空间。ptr是调用malloc或calloc函数时的返回值。

1)链表的建立

链表建立主要有以下几个步骤(见图8-11)。

(1)定义链表数据结构。

(2)创建一个空表head=NULL。

(3)输入数据有效时,重复以下操作:

①利用malloc()函数向系统申请分配一个结点p1,并初始化p1的数据项和指针项。

②判断链表明是否为空表,若是空表,将新结点置为头结点;若是非空表,将新结点接到表尾。

图8-11链表建立算法

为了创建结点,需要一个变量临时指向该结点(直到该结点插入链表中为止)。设此变量为new_node,例如:

structnode*new_node;

用malloc函数为新结点分配内存空间,并且把返回值保存在new_node中,语句如下:

new_node=malloc(sizeof(structnode));

现在new_node指向了一个内存块,且此内存块正好能放下一个node结构。如图8-12所示。图8-12new_node的指向

注意,传给sizeof的是待分配的名字,而不是指向此类型的名字,下面的语句便是错误的:

new_node=malloc(sizeof(new_node));/***WRONG***/

上面的代码仍然能通过编译,但是malloc()函数将只为指向node结构的指针分配足够的内存单元。当程序试图把数据存储到new_node可能指向的结点中时,可能会引起系统崩溃。接下来,将把数据存储到新结点的成员value中,语句如下:

(*new_node).value=10;

图8-13给出了赋值后的情形。图8-13new_node的赋值

2) ->运算符

在介绍链表的输出和往链表中插入新结点之前,先来讨论一种有用的捷径。利用指针访问结构中的成员是很普遍的,因此C语言针对此目的专门提供了一种运算符“->”。例如

new_node->value=10;

可代替语句

(*new_node).value=10;

运算符“->”是运算符“*”和运算符“.”的组合,它先对new_node间接寻址以定位所指向的结构,然后再选择结构的成员value。

由于运算符“->”产生左值,所以可以在任何允许普通变量的地方使用它。刚才已经看到一个new_node->value出现在赋值运算左侧的例子,在scanf()调用中也很常见,如:

scanf("%d",&new_node->value);

注意,new_node是一个指针,运算符“&”仍然是需要的。如果没有运算符“&”,就会把new_node->value的值传递给scanf()函数,而这个值是int类型。

3)链表的输出

首先找到链表头指针head,若链表是空表则立即退出输出过程;若链表是非空链表,则通过指针的不断向后移动,逐个输出链表中各个结点的值,直到表尾。算法如图8-14所示。图8-14链表的输出算法

(1)找到表头(p=head)。

(2)当链表非空(p!=NULL)时,重复以下操作:

①输出结点p数据(p->num)。

②跟踪p的下一结点(p=p->next)。

例8-7创建一个存放正整数(以-1作为结束标志)的单向链表,并打印输出。

解题思路如图8-11和图8-14所示。

3.链表的删除与插入操作

链表的好处之一就是可以在表中的任何位置添加结点:在表头、在表尾或者表中的任何位置。表头是最常见插入结点的地方,所以这里集中讨论这种情况。

将一个结点插入一个链表中也有3种情况,如图8-15中虚线所示的操作。插入的结点可以在表头、表中或表尾。当插入时,插入的结点依次与链表中结点相比较,找到插入位置。若插入结点在链表的头部,则要对链表的头指针进行修改,所以定义插入结点函数的返回值为结构类型指针(用于返回头指针)。

图8-15插入结点的3种情况

1)在表头插入结点

如果new_node正指向要插入的结点,并且first正指向链表中的首结点,那么为了把结点插入链表将需要两条语句。首先,修改结点的成员next,使其指向先前表头的结点:

new_node->next=first;

接下来,使first指向新结点:

first=new_node;

如果当插入结点时链表为空,那么这些语句是否还能可以作用呢?幸运的是,可以。为了验证这一点,一起来跟踪一下在空链表中插入两个结点的过程。如图8-16所示,首先插入含有数10的结点,然后插入含有数20的结点。图中空指针用斜线表示。图8-16插入结点情况图

往链表中插入结点是经常用到的操作,所以希望为此目的编写一个函数。把此函数命

名为add_to_list。此函数有两个形式参数:list(指向旧链表中首结点的指针)和n(需要存储在新结点中的整数)。

下列函数用add_to_list()来创建一个含有用户录入数的链表:

该函数创建的链表内的数会发生顺序倒置,因为first始终指向包含最后录入的数的结点。

一旦创建了链表,可能就需要为某个特殊的数据段而搜索链表。虽然while循环可以用于搜索链表,但是for语句常常是首选。人们习惯于在编写含有计数操作的循环时使用for语句,因为for语句更加灵活。下面是一种访问链表中结点的习惯方法,使用了指针变量p来跟踪“当前”结点:

for(p=first;p!=NULL;p=p->next)

现在编写名为search_list()的函数,此函数为找到整数n而搜索链表(由形式参数list指向)。如果找到n,那么search_list()函数是将返回指向含有n的结点的指针;否则,它会返回空指针。下面的第一版search_list()函数依赖于“链表搜索”惯用法:

2)从链表中删除结点

把数据存储到链表中一个很大的好处就是可以轻松删除不需要的结点。类似创建结点,删除结点也包含3个步骤。

(1)定位要删除的结点。

从链表中删除一个结点有3种情况:删除表头结点、删除链中结点和删除表尾结点,如图8-17中虚线所示的操作。由于删除的结点可能是链表的表头,会对链表的头指针进行修改,所以删除结点函数的返回值定义为返回结构类型的指针(用于返回头指针)。图8-17删除结点的3种情况(p为被删除结点temp的前驱)

(2)改变前一个结点,使它“绕过”删除结点。

(3)调用free()函数收回删除结点占用的内存空间。

第(1)步并不像看起来那么容易。如果按照显而易见的方式搜索链表,那么将在指针指向要删除的结点时终止搜索。但是,这样做就不能执行第(2)步了,因为第(2)步要求改变结点。

这个问题有各种不同的解决办法。这里将使用“追踪指针”的方法:在第(1)步搜索链表时,将保留一个指向前一个结点的指针(prev),还有指向当前结点的指针(cur)。如果list指向待搜索的链表,并且n是要删除的整数,那么下列循环就可以实现第(1)步:

for(cur=list,prev=NULL;cur!=NULL&&cur->value!=n;prev=cur,cur=cur->next);

为了看清楚这个删除结点的循环的工作过程,现在假设list指向依次含有30、40、20和10的链表,如图8-18所示。图8-18含有30、40、20和10的链表

假设n为20,那么目标就是删除此链表中的第3个结点。在执行完cur=list,prev=NULL后,cur指向了链表中的第1个结点,如图8-19所示。图8-19cur指向第1个结点

因为cur正指向一个结点,且此结点不含有20,所以判定表达式cur!=NULL&&cur->value!=n为真。在执行完prev=cur,cur=cur->next后,指针prev跟踪在指针cur的后边,如图8-20所示。图8-20prev跟随cur

判定表达式cur!=NULL&&cur->value!=n再次为真,所以再次执行prev=cur,cur=cur->next,此时cur指向含有20的结点,如图8-21所示。图8-21cur指向含有20的结点

接下来,将根据第(2)步的要求执行绕过操作。语句

prev->next=cur->next;

使前一个结点中的指针指向了当前结点的下一结点,如图8-22所示。图8-22绕过当前结点

例8-8创建一个单链表。结点信息包括学号与姓名,结点数不限,表以学号为序,学号小的在前,学号大的在后,若输入姓名为空则结束。在此链表中,要求删除一个给定姓名的结点,并插入一个给定学号和姓名的结点。

解题思路链表定义与输出函数可以参见前文。对于插入函数,利用malloc()函数向系统申请分配一个结点存放输入的姓名和学号,并让新的学号与各结点的学号比较,以确定插入的结点位于表头、表中或表尾。对于删除函数,在链表中从头到尾依此查找各结点,将指定的学生姓名与各结点的学生姓名比较,若查找成功(相同)则修改指针并释放内存空间,否则提示找不到结点,之后退出程序。

8.2共用类型8.2.1共用类型和共用类型变量的定义所谓共用类型是指将不同类型(也可以相同)的数据项组织成一个整体,它们在内存中占用同一段存储单元。共用类型与结构类型的差异在于它们成员的存储方式不同。在一个结构类型里,各成员顺序存储,每个成员都有自己独立的存储位置。而共用类型的所有成员共享同一片存储区,例如,可把一个整型变量、一个字符型变量、一个实型变量放在同一个地址开始的内存单元中,也就是使用覆盖技术,将几个变量互相重叠,共用内存空间,如图8-14所示。

图8-23共用类型在内存中的存储形式

例8-9共用类型所占内存字节数的计算方法。

C语言规定,共用类型采用与开始地址对齐的方式分配内存空间。如本例中的共用类型i占2个字节,ch占1个字节,f占4个字节,于是f的前1个字节就是为ch分配的内存空间,而f的前2个字节就是为i分配的内存空间,如图8-24所示。共用类型使用覆盖技术来实现内存的共用,即当对成员f进行赋值操作时,成员i的内容将被改变,于是i失去其自身的意义,若再对ch进行赋值操作,f的内容被改变,于是f又失去其自身的意义。

由于同一内存单元的第一瞬时只能存放其中一种类型的成员,即同一时刻只有一个成员是有意义的,因此,在每一瞬时起作用的成员就是最后一次被赋值的成员。正因如此,不能对共用类型的所有成员同时进行初始化,只能对第一个成员进行初始化(可以指定成员初始化)。此外,共用类型不能进行比较操作,也不能作为函数参数。

图8-24共用类型与结构类型的内存分配及其占用字节数

共用类型变量的定义与结构类型变量的定义相似,包括如下三种形式。

(1)先定义共用类型,再定义共用类型变量。这里,共用类型变量定义的一般形式为

(2)在定义共用类型的同时定义共用类型变量。其一般形式为

(3)直接定义共用类型变量。其一般形式为

由此可见,共用类型与结构类型在形式上非常相似,但其表示的含义及存储方式是完全不同的,结构类型变量所占内存长度大于或等于(TurboC中是等于)各成员所占的内存长度之和,每个成员分别占有其自己的内存单元。共用类型变量所占的内存长度等于最长的成员的长度,其各数据成员共用低地址空间。

设存储空间起始单元地址为3000,若有定义

则结构类型变量s_egl在内存中的存储情况如图8-25所示。若有定义

图8-25s_eg1在内存中的存储情况

则共用类型变量u_egl在内存中的存储情况如图8-26所示。

如图8-25所示,结构类型变量s_eg1在内存中所占字节数是各成员所占字节数之和,即8+4+4=16。其中,数组c从地址为3000的单元开始存储,占用地址为3000~3007的单元;i从地址为3008的单元开始存储,占用地址为3008~3011的单元;f从地址为3012的单元开始存储,占用地址为3012~3015的单元。图8-26u_eg1在内存中的存储情况

如图8-26所示,共用类型变量u_egl在内存中所占字节数是字节数最多的成员所占的字节数,即字符数组c所占的字节数8。数组c、i、f都从同一个地址3000开始存储,其中数组c占用地址为3000~3007的单元,i占用地址为3000~3003的单元,f占用地址为3000~3003的单元。数组c、i、f共享同一段存储空间,每次只能保存其中一个成员的值。

关于共用类型变量,有以下几点要注意:

(1)由于共用类型变量各成员共用同一段存储空间,因此共用类型变量的起始地址与其各成员的起始地址都相同。

(2)共用类型变量在某一个时刻只有一个成员起作用,本质上是指共用类型变量的存储空间只能保存其中一个成员的值,不能同时保存所有成员的值。

(3)结构类型定义中可以包含共用类型成员或者共用类型数组成员,共用类型定义中也可以包含结构类型成员或者结构类型数组成员。

8.2.2共用类型变量的引用和初始化

1.共用类型变量成员的引用

共用类型变量中每个成员的引用方式与结构变量完全相同,也有3种形式。其各成员引用格式为:

(1)共用类型变量名.成员名

(2)指针变量名->成员名

(3)(*指针变量名).成员名

例如,在前面定义的前提下,有“p=&x;”,则x1.i、x1.ch、x1.f、或p->i、p->ch、p->f、(*p).i、(*p).ch、(*p).f、都是合法的引用形式。

2.共用类型变量的整体引用

如果两个共用类型变量具有相同的共用类型,则可以将一个共用类型变量整体赋给另外一个共用类型变量。例如,两个类型相同的共用类型变量x1、x2,可以用

x1=x2;

在如下几种情况中,不能对共用类型变量进行整体引用。

(1)不能对共用类型变量整体进行输入或者输出,但可以对共用类型变量成员进行输入或者输出。例如,以下两条语句都是错误的:

scanf("%c",&xl);

printf("%c",xl);

以下两条语句都是正确的:

scanf("%c",&xl.ch);

printf("%c",xl.ch);

(2)除了相同共用类型数值以外,不能给共用类型变量赋予其他类型的值。例如:

x1=1;

(3)除了给相同类型的共用类型变量赋值以外,共用类型变量的值不能赋给其他类型的变量。例如:

uniondataxl;

intk;

k=xl;

(4)共用类型变量不能作为函数参数,函数返回值也不能是共用类型变量值,但可以用共用类型指针变量实现。

共用类型的特点如下:

(1)内存空间共用。

(2)成员变量覆盖。为共用类型变量赋值时,最新的成员值有效,其他成员的值被覆盖。例如:

覆盖顺序为x.i覆盖x.ch[1],x.a覆盖x.i,被覆盖的数据无意义,只有成员x.a的数据有意义。如果现在使用x.ch[1]或x.i,则会发现它们存储着成员x.a的部分内容(空间共享的内容)。如果占用字节短的成员覆盖了占用字节长的成员,例如:

x.a=56.3;

x.ch[0]='p';

那么成员x.ch[0]有效,未覆盖部分的数据无使用价值。

例8-12分析下面程序中共用类型变量成员的取值情况。

例8-13通过共用类型成员显示其在内存的存储情况。

解题思路定义一个名为time的结构类型,再定义共用类型dig。

分析一下最后一行的输出结果。2003、4和25分别占2个字节,总共占6个字节,现在若将这6个字节以字节为单位单独输出,则需要分析一下存储状态。2003、4和25这三个数对应的十六进制数为0x07d3、0x0004和0x0019。由于多字节数的存储顺序是低位优先,6个字节中依次存储的十六进制数为0xd3、0x07、0x04、0x00、0x19、0x00,转换成对应的有符号单字节十进制数依次便是-45、7、4、0、25、0。

8.3枚举类型

8.3.1枚举类型的定义和枚举类型变量的说明如果一个变量只有几种可能的值,可以定义为枚举类型,例如,一个星期内有七天,一年有十二个月,有七种基本颜色(红、黄、蓝,绿、紫、橙、白)。所谓“枚举”是指将变量的值一一列举出来,变量的取值只限于列举出来的那些值。

定义枚举类型用enum开头。其格式为

enum枚举类型名{枚举值表};

枚举值表中的各值称为枚举元素或枚举常量,是用户定义的标识符。例如,定义一个枚举类型enumweekday可以是

enumweekday{sun,mon,tue,wed,thu,fri,sat};

其中sun、mon……是枚举元素。

可以用枚举类型来定义枚举类型变量,枚举类型变量的定义包括如下三种形式:

(1)先定义枚举类型,再定义枚举类型变量。例如:

enumweekday{sun,mon,tue,wed,thu,fri,sat};

enumweekdayworkday,week_end;

(2)在定义枚举类型的同时定义枚举类型变量。例如:

enumweekday{sun,mon,tue,wed,thu,fri,sat}workday,week_end;

(3)在无名枚举类型之后,直接定义枚举类型变量。例如:

enum{sun,mon,tue,wed,thu,fri,sat}workday,week_end;

8.3.2枚举类型变量的赋值和使用

枚举类型变量的取值只限于列举出来的那些值。例如,workday和week_end为枚举类型变量,其值只能是sun~sat之中的某个值。

workday=mon;

week_end=sun;

是正确的。当然,也可以直接定义枚举类型变量,如:

enum{sun,mon,tue,wed,thu,fri,sat}workday,week_end;

枚举类型在使用中有以下规定。

(1)枚举元素是常量,不是变量,不能在程序中用赋值语句再对它赋值。例如,对枚举weekday的元素再作以下赋值:sun=5;mon=2;sun=mon;都是错误的。

(2)枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2,…如在weekday中,sun值为0,mon值为1,sat值为6。可以从下面的程序得到证明。

例8

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论