【C-Primer-Plus读书笔记】第14章:结构和其他数据形式
示例程序
- C提供了结构变量提高你表示数据的能力,它能让你创造新的形式。
- 结构的每个部分都称为成员或字段。
使用结构的3个技巧:
- 为结构建立一个格式或样式;
- 声明一个适合该样式的变量;
- 访问结构变量的各个部分。
建立结构声明
struct book
{
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
- 结构声明描述了一个结构的组织布局。如上示例程序。
- 该声明并未创建实际的数据对象,只描述了该对象由什么组成。
细节分析:
- 首先是关键字
struct
,它表明跟在其后的是一个结构; - 后面是一个可选的标记(
book
),稍后程序中可以使用该标记引用该结构; - 在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用自己的声明来描述。成员可以是任意一种C的数据类型,甚至可以是其他结构。
- 右花括号后面的分号是声明所必须的,表示结构布局定义结束。
- 首先是关键字
- 结构声明放置的位置可以是函数外或函数内,作用域规则与普通变量声明一致。
- 结构的标记名是可选的。但是在一处定义结构布局,在另一处定义实际的结构变量必须使用标记。
定义结构变量
结构有两层含义:
- 一层含义是“结构布局”,如上述所讲。结构布局告诉编译器如何表示数据,但它并未让编译器为数据分配空间;
- 下一步是创建一个结构变量,即是结构的另一层含义。
- 如此,便创建了一个结构变量(
library
)。编译器使用book
模板为该变量分配空间。这些存储空间(成员)都与一个名称library
结合在一起。 struct book
所起的作用相当于一般声明中的int
或float
。
struct book library;
- 从本质上看,结构声明创建了一种新类型。
- 声明结构的过程和定义结构变量的过程可以组合成一个步骤。即花括号左边可以不用定义标记,花括号右边则需要定义变量名。
struct //无结构标记
{
char title[MAXTITL];
char author[MAXAUTL];
float value;
} library; //声明的右花括号后跟变量名
- 初始化结构
初始化结构变量与初始化数组的语法类似:
struct book library = {
"The Pious Pirate and The Devious Damsel",
"Renee Vivotte",
1.95
};
简而言之,我们使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。
第12章讲过,如果初始化静态存储器的变量,必须使用常量值。这同样适用于结构。
访问结构成员
- 结构类似于一个“超级数组”,这个超级数组可以存储各种数据类型不同的元素。
- 数组中可以使用下标单独访问数组中的各元素;而结构中使用结构成员运算符——点(
.
)访问结构中的成员。
- 结构的初始化器
C99和C11为结构提供了指定初始化器,其语法与数组的指定初始化器类似。但是,结构的指定初始化器使用点运算符和成员名标识特定的元素。
struct book library = { .value = 10.99 };
结构数组
- 声明结构数组
声明结构数组和声明其他类型的数组类似:
struct book library[MAXBKS];
以上代码是数组里包含结构,而不是结构里包含数组。library
本身不是结构名,它是一个数组名,该数组每个元素都是struct book
类型的结构变量。
- 标识结构数组的成员
为了标识结构数组中的成员,可以采用访问单独结构的规则:在结构名后面加一个点运算符,再在点运算符后面写上成员名。
library; //一个book结构的数组
library[3]; //一个数组元素,该元素是book结构
library[0].value; //一个整数值————第1个数组元素(结构)下的value字段(整数)
library[4].title; //一个字符串————第5个数组元素(结构)下的title字段(字符数组)
library[2].title[4]; //一个字符————第3个数组元素(结构)下的title字段(字符数组)的第5个字符
点运算符右侧的下标作用于各成员,点运算符左侧的下标作用于结构数组。
嵌套结构
有时,在一个结构中包含另一个结构(即嵌套结构)很方便。
- 首先,在结构声明中创建嵌套结构,和声明
int
类型变量一样; - 其次,访问嵌套结构的成员,这需要使用两次(两层嵌套)点运算符。
指向结构的指针
使用指向结构的指针的4个理由:
- 就像指向数组的指针比数组本身更容易操纵一样,结构同理;
- 在一些早期C实现中,结构不能作为参数传递给函数,但可以传递指向结构的指针;
- 即使能传递一个结构,传递指针通常更有效率;
- 一些用于表示数据的结构中包含指向其他结构的指针。
声明和初始化结构指针
- 声明结构指针很简单,语法同声明其他指针变量一样。
要点:
- 该声明并未创建一个新的结构,但是该指针现在可以指向任意现有的book类型的结构。
- 和数组不同的是,结构名并不是结构的地址,因此取地址要在结构名前面加上
&
运算符。 - 结构指针加 1 指的是该指针地址加整个结构的大小(字节),这和其他指针变量也同理。
- 在某些系统中,一个结构的大小可能大于它各成员大小之和,这是因为系统对数据进行校准的过程中产生了一些“缝隙”。如把成员放在偶数或4的倍数的地址上。
struct book * pt;
pt = &library;
用指针访问成员
两种方法:- 使用
->
运算符。该运算符由一个连接号(-)后跟一个大于号(>)组成; - 可以使用
*
运算符解引用后就可以如使用正常结构变量那样使用了。必须使用圆括号,因为.
比*
运算符优先级高。
- 使用
pt->value; //即是 library.value
(*pt).value; //即是 library.value
向函数传递结构的信息
- 传递结构成员
只要结构成员是一个具有单个值的数据类型,便可把它作为参数传递给接受该特定类型的函数。这就跟传递普通变量一样,没什么好说的。 - 传递结构的地址
如同上面介绍的结构指针,只需要把该指针传递进去(或直接对结构取地址,用&
运算符)即可,这和传递普通变量的地址也没什么区别。而对于如何使用结构指针见上小节。 - 传递结构
对于允许把结构作为参数的编译器,才可以传递结构本身。函数内的值是原来结构的副本,这和传递普通变量也没有差别。在函数内就正常使用结构即可。 其他结构特性
- 现在的C允许把一个结构值赋值给另一个结构,即使成员是数组也能完成赋值,但是数组不能这样做;
- 另外,还可以把一个结构初始化为相同类型的另一个结构;
- 函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。
结构和结构指针的选择
指针作为参数
优点:
- 无论是以前还是现在的C实现都能使用这种方法;
- 而且执行速度快,只需要传递一个地址。
缺点:
- 无法保护数据。不过ANSI C新增的
const
限定符解决了这个问题。
- 无法保护数据。不过ANSI C新增的
结构作为参数
优点:
- 函数处理的是原始数据的副本,这保护了原始数据;
- 另外,代码风格也更清楚。
缺点:
- 较老的C实现可能无法处理这样的代码;
- 而且传递结构浪费时间和存储空间。
总结:通常情况下,为了追求效率会使用结构指针作为函数参数,如需保护原始数据使用const限定符;按值传递结构是处理小型结构最常用的方法。
结构中的字符数组和字符指针
在结构中可以使用字符数组来存储字符串,也可以使用指向char的指针来代替字符数组。但需要注意以下几点:- 存储位置上,字符数组是将字符存储在该数组中(结构成员,即结构中),字符指针则只存储了指向存储在别处的字符串首地址,所以所占用的空间也大不相同;
- 结构声明后,其成员如果存在字符指针,则这是一个未经初始化的指针变量,地址可以是任何值,对其进行操作需特别注意
scanf("%s", lib.title); //成员title是一个未初始化的字符指针
- 结构、指针和
malloc()
接上,如果使用malloc()
分配内存并使用指针存储该地址,那么在结构中使用指针处理字符串就比较合理。
这样做的优点是:
- 可以请求`malloc()`为字符串分配合适的存储空间;
- 结构内只需要存储一个固定大小(一般为4字节)的地址。
pst->fname = (char *) malloc(STRLEN); //成员fname是一个未初始化的字符指针,该操作将分配的内存地址赋给成员fname,即初始化了
- 复合字面量和结构(C99)
C99的复合字面量特性可用于结构和数组。复合字面量常用于需要一个临时值的场景。
语法是把类型名放在圆括号中,后面紧跟一个花括号括起来的初始化列表:
(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99}
伸缩型数组成员(C99)
C99新增了一个特性:伸缩性数组成员。利用这项特性声明的结构,其最后一个数组成员具有一些特性:
- 该数组不会立即存在;
- 使用这个伸缩性数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。
声明一个伸缩性数组成员有如下规则:
- 伸缩性数组成员必须是结构的最后一个成员;
- 结构中必须至少有一个成员;
- 伸缩数组的声明类似于普通数组,只是它的方括号中是空的。
- 声明一个该类型的结构变量时,没有给这个数组预留存储空间所以不能使用该数组做任何事。C99的意图并不是让你声明一个该类型的变量,而是希望你声明一个指向该类型的指针,然后用
malloc()
来分配足够的空间,以存储该类型结构的常规内容和伸缩性数组成员所需的额外空间。
struct flex
{
int count;
double average;
double scors[]; // 伸缩性数组成员
};
struct flex * pf; //声明一个指针
pf = malloc(sizeof(struct flex) + 5 * sizeof(double)); //请求为一个结构和一个数组分配存储空间
分配完毕后就可以正常使用该结构指针。
带伸缩型数组成员的结构有一些特殊的处理要求:
- 不能用结构进行赋值或拷贝;
- 不要以按值方式把这种结构传递给结构;
- 不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。
- 匿名结构(C11)
匿名结构是一个没有名称的结构成员。在C11中,可以用嵌套的匿名成员定义嵌套结构:
int id;
struct {char first[20]; char last[20];}; //匿名结构
而访问该匿名结构成员时,只需把该匿名结构成员看作是外层结构的成员那样使用它即可。
使用结构数组的函数
- 可以把数组名作为数组中第 1 个结构的地址传递给函数(
jones
和&jones[0]
的地址相同); - 然后可以用数组表示法访问数组中的其他结构,访问其结构成员都可以使用点(
.
)运算符; - 如果函数不能改变原始数据,请使用ANSI C的限定符
const
。
- 可以把数组名作为数组中第 1 个结构的地址传递给函数(
把结构内容保存到文件中
- 由于结构可以存储不同类型的信息,所以它是构建数据库的重要工具。存储在一个结构中的整套信息被称为记录,单独的项被称为字段。
存储记录最没效率的是用
fprintf()
一项一项地去处理值,更好的方案是使用fread()
和fwrite()
函数读写结构大小的单元。前面学过这两个函数使用与程序相同的二进制表示法。不过得注意以下几点:- 二进制表示法是一次读写整个记录,而不是一个字段;
- 不同系统使用的不同的二进制表示法,所以数据文件可能不具可移植性。甚至同一系统不同编译器也可能导致不同的二进制布局。
链式结构
- 结构的多种用途之一:创建新的数据形式。
- 计算机用户已经开发出的一些数据形式比数组和简单结构更有效地解决特定问题。这些形式包括队列、二叉树、堆、哈希表和图表。许多这样的形式都由链式结构组成。
- 通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。
联合简介
- 联合是一种数据类型,它能在同一个内存空间中存储不同的数据类型(不是同时存储)。
union hold
{
int digit;
double bigfl;
char letter;
};
需要注意的是,联合只能存储一个值,这与结构不同。编译器分配足够的空间以便它能存储联合声明中占用最大字节的类型。
初始化联合有3种方法:
- 把一个联合初始化为另一个同类型的联合;
- 初始化联合的第1个元素;
- 或根据C99标准,使用指定初始化器。
union hold valA;
valA.letter = 'R';
union hold valB = valA; //用另一个联合来初始化
union hold valC = {88}; //初始化联合的digit成员
union hold valD = { .bigfl = 118.2}; //指定初始化器
- 使用联合
联合中的点运算符表示正在使用哪种数据类型,所以当对某个数据类型(即成员)赋值后,后面读取该数据也得保证是同一个数据类型。当又对另一个数据类型赋值后则覆盖了前一个数据类型的数据。
联合常见的两种使用场景:
1. 用一个成员把值存储在一个联合中,然后用另一个成员查看内容
2. 在结构中存储与其成员有从属关系的信息
- 匿名联合(C11)
匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。
总结:结构和联合运算符
成员运算符:.
。该运算符与结构变量或联合变量一起使用,指定结构变量或联合变量的一个成员
间接成员运算符:->
。该运算和指向结构或联合的指针一起使用,标识结构变量或联合变量的一个成员。
枚举类型
可以用枚举类型声明符号名称来表示整型常量。使用enum
关键字,可以创建一个新“类型”并指定它可具有的值。
它的语法与结构的语法相同:
enum spectrum {red, orange, yellow, green, blue, violet};
enum spectrum color;
花括号中的值是符号常量,这些符号常量被称为枚举符。且它是整数类型。
enum
常量
前面提到枚举符是整数类型的符号常量,从技术层面看它们是int
类型的常量。只要是能使用整型常量的地方就可以使用枚举常量(如可以表示数组大小、switch语句的标签等)。- 默认值
默认情况下,枚举列表中的常量都被赋予0、1、2等。 - 赋值
在枚举声明中,可以为枚举常量指定整数值。如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。 enum
的用法
枚举类型的目的是为了提高程序的可读性和可维护性。因为字母标识比数字的含义更直观。- 共享名称空间
C语言使用名称空间标识程序中的各部分,即通过名称来识别。
作用域是名称空间概念的一部分:
- 两个不同作用域的同名变量不冲突;
- 两个相同作用域的同名变量冲突。
名称空间是分类别的。即在相同作用域中变量和标记的名称可以相同。尽管如此使用相同的标识会造成混乱。
typedef简介
typedef
工具是一个高级数据特性,利用typedef
可以为某一类型自定义名称。
这方面与
#define
类似,但有3处不同:- 与
#define
不同,typedef
创建的符号名只受限于类型,不能用于值; typedef
由编译器解释,不是预处理器;- 在其受限范围内,
typedef
比#define
更灵活。
- 与
typedef
的工作原理,如下定义:
typedef unsigned char BYTE;
BYTE x, y[10], * z;
使用typedef
时要注意,typedef
并没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签。
其他复杂的声明
在一些复杂的声明中,常包含下面的符号:
符号 | 含义 |
---|---|
* | 表示一个指针 |
() | 表示一个函数 |
[] | 表示一个数组 |
记住下面几条规则:
- 数组名后面的
[]
和函数名后的()
具有相同的优先级; []
和()
都是从左往右结合(优先级相同);[]
和()
比*
(解引用运算符)的优先级高。
- 数组名后面的
函数和指针
可以声明一个指向函数的指针。通常,函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数。
什么是函数指针:
- 函数和变量一样也有地址,因为函数的机器语言实现由载入内存的代码组成,指向函数的指针中存储着函数代码的起始处地址;
- 同数据指针,声明函数指针时,必须声明该指针指向的函数类型(返回的数据类型)。为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型;
void ToUpper(char *); //函数
void (*pf)(char *); //指向(上述类型,包括返回类型和参数类型)函数的指针
pf = ToUpper; //指向(上述)函数的指针,不用`&`符
- 函数指针的使用
有两种方法:
char mis[] = "Nina Metier";
pf = ToUpper;
(*pf)(mis); //解引用方式
pf(mis); //直接使用指针
【个人理解】由此发现,函数名是一个指针,且机器执行函数时就是从该地址开始执行的。从某种意义上来说
pf == ToUpper
并且*pf == ToUpper
,那么即pf == *pf
。这有点矛盾但都正确。