不知不觉已经写到了第九章了,还有三章就结束《C 语言学习》了。我在这里可以先预告一下剩下三章的标题,分别是——《第十章 文件与图形》、《第十一章 预处理和 C 库》、《第十二章 高级数据表示》。
-
1. 结构和结构变量
-
设计程序最重要的一个步骤就是选择一个表示数据的好方法。在多数情况下,使用简单的变量甚至数组都是不够的 。C 使用结构变量(structure variable) 进一步增强了表示数据的能力 。C 的结构的基本形式就足以灵活地表示多种数据,并且它还使您能够创建新的形式。
-
1.1 结构的声明
- 结构声明 (structurede claration)是描述结构如何组合的主要方法。声明就像下面这样:
#define MAXTITL 20 #define MAXAUTL 50 struct book{ char title[MAXTITL]; char author[MAXAUTL]; float value; };
该声明描述了一个由两个字符数组和一个 float 变量组成的结构。它并没有创建一个实际的数据对象, 而是描述了组成这类对象的元素(有时候,我们也把结构声明叫做模板,因为它勾勒出数据该如何存储。 如果您已经听过 C++中的模板,那只是这个词的不同用法)。我们来看看细节。首先使用关键字 struct , 它表示接下来是一个结构。后面是一个可选的标记(单词 book ),它是用来引用该结构的快速标记。因此,以后我们就可以这样声明:
struct book library;
当然,还可以直接在 book 的定义后面直接创建变量:
#define MAXTITL 20 #define MAXAUTL 50 struct book{ char title[MAXTITL]; char author[MAXAUTL]; float value; }library;
-
1.2 结构的初始化
- 要初始化一个结构变量 (对于ANSIC,可以是任何一种存储类;但对于ANSI 之前的C,不能是自动变量) ,可以使用与初始化数组相似的语法:
当然,也和数组一样,C99 支持结构的指定初始化项目,其语法与数组的指定初始化项目相似。 只是,结构的指定初始化项目使用点运算符和成员名 (而不是方括号和索引值) 来标识具体的元素。比如:struct book libaray={ "The Pirate and the Devious Damsel", "Renee Vivotte", 1.95 };
同时,对特定成员的最后一次赋值时它实际获得的值。例如:struct book libaray={ .value=2; .author="Renee Vivotte", };
struct book libaray={ .value=2; .author="Renee Vivotte", 1.95 };
*结构初始化和存储类时期
在上章中我们曾提到,如果初始化一个具有静态存储时期 (比如静态外部链接、静态内部链接或静态空链接) 的变量,只能使用常量值。这条规则同样也适用于结构。如果初始化一个具有静态存储时期的结构,初始化项目列表中的值必须是常量表达式。如果存储时期是自动的,列表中的值就不必是常量了。 -
1.3 访问结构成员
- 结构就像是一个 “超级数组” 。在这个超级数组内,一个元素可以是 char 类型,下一个元素可以是 float 类型,再下一个可以是 int 数组。使用下标可以访问一个数组的各个元素。而结构是用结构成员运算符点(.)达到访问结构成员的目的。如:
gets(libaray.title); scanf("%f",&libaray.value);
在本质上,.title、.author 和 .value 在 book 结构中扮演了下标的⻆色。
注意,虽然 library 是一个结构,但是 library.value 是 float 类型,可以像使用其他任何 float 类型变量那样使用它 。
-
-
2.结构数组与结构指针
-
2.1 结构数组
-
声明结构数组
- 声明结构数组和声明其他任何类型的数组一样:
struct book libraries[10];
-
标识结构数组的成员
- 为了标识结构数组的成员,可以采用适用于各个结构的规则: 在结构名后加一个点运算符,然后是成员名。例如:
libraries[0].value; // 正确 libraries[4].title; // 正确 libraries[3].title[1]; // 访问 title 成员数组中的一个字符 libraries.value[2]; // 错误
-
2.2 结构指针
- 喜欢使用指针的人会高兴地得知能够使用指向结构的指针。至少有三个原因可以解释为什么使用指问结构的指针是个好主意。第一,就像指向数组的指针比数组本身更容易操作 (例如在一个排序问题中) 一样, 指问结构的指针通常都比结构本身更容易操作。第二 ,在一些早期的 C 实现中,结构不能作为参数被传递给函数,但指向结构的指针可以。第三,许多奇妙的数据表示都使用了包含指向其他结构的指针的结构。
-
声明结构指针
- 声明和其他类型的指针声明相似:
struct guy*him;
首先是关键字 struct,其次是结构标记 guy,然后是一个*号,紧跟着是指针名。
这个声明不是建立一个新的结构,而是意味着指针 him 现在可以指向任何现有的guy 类型的结构。 -
初始化结构指针
- 假设 barney 是一个 guy 类型的结构。那么,结构指针的初始化可以这么做:
struct guy*him=&barney;
和数组不同,一个结构的名字不是该结构的地址,必须使用 & 运算符。
-
使用指针访问结构成员
- 使用指针访问结构成员有两种方法:
1. 使用一个新运算符: -> 。这个运算符由一个连接号 (-) 后跟一个大于符号 (>) 组成。
2. 先对指针解引用,再用点运算符访问结构成员。下面程序 1 是这两种的示例。 - 程序 1:
#include<stdio.h> #define MAXTITL 50 #define MAXAUTL 20 #define MAXBKS 10 struct book{ char title[MAXTITL]; char author[MAXAUTL]; float value; }; int main(void) { struct book libaray={ "The Pirate and the Devious Damsel", "Renee Vivotte", 1.95 }; struct book *pb1; pb1=&libaray; puts(pb1->title); puts((*pb1).author); printf("%g",pb1[0].value); return 0; }
结果:
The Pirate and the Devious Damsel
Renee Vivotte
1.95 -
2.3 函数中结构的使用
-
结构作为参数传递
- ANSI C 支持在调用函数时,把结构作为参数传递给函数,采取的是 “值传递” 的方式,将结构体变量所占的内存单元的内容全部顺序传递给形参。形参也必须是同类型的结构体变量。在函数调用期间,形参也要占用内存单元。(占用的内存相当与一个结构占用的内存,不建议使用)
-
结构指针作为参数传递
- 用结构指针作为函数的形参与用指针变量作为形参在函数间传递方式是一样的,即采用 “址传递” 的方式,把结构变量的存储首地址或结构数组名作为实参向函数传递,函数的形参是指向相同结构类型的指针接收该地址值。(占用的内存相当于一个指针占用的内存)
-
使用结构指针的优点和缺点
- 把指针作为参数的方法的两个优点是:它既工作在较早的 C 实现上,也工作在较新的C 实现上,而且执行起来很快; 只须传递一个单个地址。缺点是缺少对数据的保护。被调用函数中的一些操作可能不经意地影响到原来结构中的数据。不过,ANSI C 新增的 const 限定词就可以解决这个问题。
-
2.4 伸缩型结构数组成员(C99)
- C99 具有一个称为伸缩型数组成员 (flexiblearay member ) 的新特性。利用这一新特性可以声明最后一个成员是一个具有特殊属性的数组的结构。该数组成员的特殊属性之一是它不存在,至少不立即存在。 第二个特殊属性是您可以编写适当的代码使用这个伸缩型数组成员,就像它确实存在并且拥有您需要的任何数目的元素一样。可能这听起来有些奇怪,因此计我们开始一步一步地创建和使用具有伸缩型数组成员的结构。
首先看看声明一个伸缩型数组成员的规则:
• 伸缩型数组成员必须是最后一个数组成员。
• 结构中必须至少有一个其他成员。
• 伸缩型数组就像普通数组一样被声明,除了它的方括号内是空的。 - 例如:
struct flex{ int count; double average; double scores[]; // 伸缩型数组成员 };
如果声明了一个 struct flex 类型的变量,您不能使用 scores 做任何事情,因为没有为它分配任何内存空间 。实际上,C99 的意图并不是让您声明 struct flex 类型的变量:而是希望您声明一个指问 struct flex 类型的指针,然后使用 malloc() 来分配足够的空间,以存放 struct flex 结构的常规内容和伸缩型数组成员需要的任何额外空间。例如,假设想要用 scores 表示含有 5 个 double 型数值的数组,那么就要这样做:
struct flex*pf; // 声明一个指针 // 请求一个结构和一个数组的空间 pf=malloc(sizeof(struct flex)+5*sizeof(doble));
-
-
3. 嵌套结构
- 结构是允许嵌套的。例如:
struct book // 第一个结构 { char title[MAXTITL]; char author[MAXAUTL]; float value; }; struct libaray // 第二个结构 { struct book books[BOOK]; // 嵌套结构 int n=BOOK; };
而对于该结构的访问,可以是:
int main(void) { struct libaray l; l.books[0].title="红楼梦"; printf("%s",l.books->title); return 0; }
-
4. 位字段结构
- 位字段数据是一种数据压缩形式,数据整体没有具体意义。因此,在处理位字段数据时,总是以组成它的位字段为处理对象。在 C 语言程序中,可以使用位操作(位逻辑与操作、位逻辑或操作等)对位字段进行处理。此外,C语言还提供了处理位字段的另一种构造类型——位字段结构。
位字段结构是一种特殊形式的结构,它的成员项是二进制位字段。且位字段结构定义中的每个成员项一般书写格式是:
unsigned 成员名 : 位数; - 位字段结构定义的规则
1)位字段以单个 unsigned 型数据为单位,其内存放着一连串相邻的二进制位。
2)若某一位段要从另一个字开始存放。可以使用下列形式:struct test { unsigned a:1; // 成员 a 和 b 共占一个存储单元 unsigned b:2; unsigned :0; // 成员 c 占用另一个存储单元 unsigned c:3; };
在位字段结构体内若有不带名字的位字段时,冒号后面的位数应写 0,它具有特殊的含义,表示位字段间的填充物,用无名的位字段作填充物占满整个 unsigned 型数据的其他位,使得下一个成员项 c 分配在相邻的下一个 unsigned 型中。
3)位字段结构在程序中的处理方式和表示方法等,与普通结构相同。
4)一个位段必须存储在同一个存储单元中,不能跨两个单元。如果一个单元空间不能容纳下一位段,则不用该空间,而从另一个单元起存放该位段。位段的长度不能大于存储单元的长度,也不能定义位段数组。
5)位字段结构的成员项可以和一个变量一样参加各种运算。但一般不能对位字段结构的成员项作取地址 & 运算。位段可以在数值表达式中引用,它会被系统自动地转换成整型数。 -
5. 联合
- 在 C 语言中,不同数据类型的数据可以使用共同的存储区域,这种数据构造类型称为联合体,简称联合。联合体在定义、说明和使用形式上与结构相似。两者本质上的不同仅在于使用内存的方式上。
-
5.1 联合的定义
union 联合名 { 数据类型 成员名 1; 数据类型 成员名 2; ... 数据类型 成员名 n; };
-
5.1 联合的初始化
-
因为联合只能存储一个值,所以初始化的规则与结构的初始化不同。具体地,有 3 种选择:可以把一个联合初始化为同样类型的另一个联合;可以初始化联合的第一个元素;或者,按照 C99 标准,可以使用一个指定初始化项目。例如:
union hold { short digit; double bigfl; char letter; }; int main(void) { union hold val1; val1.letter='R'; union hold val2=val1; // 把一个联合初始化为另一个联合 union hold val3={88}; // 初始化联合的 digit 成员 union hold val4={.bigfl=118.2}; // 指定初始化项目 return 0; }
-
5.3 访问联合的成员
- 联合可以使用点运算符进行访问联合体的成员。但在同一个时间只能存储一个值。即使有足够的空间,也不能同时保存一个 char 类型和一个 int 类型的值。由您负责记住当前保存在联合中的数据的类型。例如:
val1.digit=20; // 把 20 存储在 val1 中,使用 2 个字节 val1.bigfl=2.0; // 清除 20,存储 2.0;使用 8 个字节 val1.letter='h'; // 清除 2.0,存储 'h';使用 1 个字节 val2.letter='a'; double n=3.02*fit.bigfl; // 错误
当然,联合体的指针也可以用(->) 来做到联合的成员访问。例如:
union hold *p=&val1; p->digit=2;
-
6. 类型定义语句 typedef
- typedef 工具是一种高级数据特性,它使您能够为某一类型创建您自己的名字。在这个方面,它和 #define 相似,但是它们具有 3 个不同之处:
• 与 #define 不同,typedef 给出的符号名称仅限于对类型,而不是对值 。
• typedef 的解释由编译器,而不是预处理器执行。
• 虽然它的范围有限,但在其受限范围内,typedef 比 #define 更灵活。
我们来看看 typedef 是怎样工作的。假设要对 1 字节的数值使用术语 BYTE,您只须像定义一个 char 变量那样定义 BYTE,然后在这个定义前面加上关键字 typedef,如:
随后您就可以使用 BYTE 来定义变量了:typedef unsigned char BYTE;
该定义的作用域取决于 typedef 语句所在的位置。如果定义是在一个函数内部,它的作用域就是局部的, 限定在那个函数里。如果定义是在函数外部,它将具有全局作用域。 通常,这些定义使用大写字母,以提醒用户这个类型名称实际上是一个符号缩写。不过,您也可以使用小写字母:BYTE x, y[10], *z;
管理变量名的同样规则也用来管理 typedef 使用的名字。typedef unsigned char byte;
-
7. 枚举
-
可以使用枚举类型 (enumerated type) 声明代表整数常量的符号名称。通过使用关键字 enum,可以创建一个新 “类型” 并指定它可以具有的值 (实际上,enum 常量是 int 类型的,因此在使用 int 类型的任何地方都可以使用它)。枚举类型的目的是提高程序的可读性 。它的语法与结构的语法相同。例如:
enum spectrum {red,orange,yellow,green,blue,violet}; enum spectrum color;
顺便提一下,C 的某些枚举属性不能延至 C++ 中。例如,C 允许对枚举变量使用运算符++,而 C++ 不允许。
-
7.1 enum 常量
- blue 和 red 都是 int 类型的常量。在使用整数常量的任何地方都能使用枚举常量。例如,在数组声明中可以把它们作为数组大小,在switch 语句中,可用它们来作为标签。
-
7.2 默认值
- 默认时,枚举列表中的常量被指定为整数值 0、1、2 等等。因此,上面的 red、orange、yellow 等分别是 0、1、2 等。
-
7.3 指定值
- 您可以选择常量具有的整数值。只须在声明中包含期望的值:
如果只对一个常量賦值,而没有对后面的常量赋值,那么这些后面的常量会被赋予后续的值。例如,假设有这样的声明:enum levels (low=100, medium=500, high=2000};
那么,cat 的值默认为 0,lynx、puma 和 tiger 的值分别是 10、11 和 12。enum feline {cat, lynx = 10, puma, tiger);
-
7.4 enum 用法
- 回忆一下,枚举类型的目的是提高程序的可读性。如果是处理颜色,采用 red 和blue 要比使用 0 和 1 更显而易⻅。注意,枚举类型是内部使用的。如果想输入 color 值orange,只能输入1,而不是单词 orange。 或者,可以读入字符串 "orange”,并让程序将它转换成值 orange。
-
7.5 共享的名字空间
- C 使用术语名字空间 (namespace) 来表示识别一个名字的程序部分。作用域是这个概念的一部分: 名字相同但具有不同作用域的两个变量不会冲突;而名字相同并在相同作用域中的两个变量就会冲突。名字空间是分类别的。在一个特定作用域内的结构标记、联合标记以及枚举标记都共享同一个名字空间,并且这个名字空间与普通变量使用的名字空间是不同的。这意味着,可以在同一个作用域内对一个变量和一个标记使用同一个名字,而不会产生错误;但是不能在同一作用域内使用名字相同的两个标记或名字相同的两个变量。例如,在 C 中下面的语句不会产生冲突:
然而,用两种不同的方式使用同一标识符会造成混乱:而且,C++ 不允许在同一个作用域内对一个变量和一个标记使用同一个名字,因为它把标记和变量名放在同一个名字空间中。struct rect ( double x; double y; }; int rect; //在C中不会引起冲突
-
参考书籍:《C Primer Plus》【美】 Stephen Prata 著
《程序设计教程 用 C/C++ 语言编程》 周纯杰 何顶新 周凯波 彭刚 张惕远 编著