2 结构、指针和成员
直接或者通过指针访问结构体是相当简单的,因为这样的做法和数组非常类似,但是在稍微复杂一点的情形下,我们又该如何访问该结构体成员呢?
为了更好地阐述结构体,结构体指针,结构体成员之间的关系,先定义相关的结构体。
typedef struct { int a; short b[2]; } Ex2; typedef struct { int a; char b[3]; Ex2 c; struct EX *d; }EX;
再定义相关的结构体变量。
EX x = { 10, "Hi", {5, { -1, 25}}, 0}; EX *px = &x;
2.1 访问指针
来看看px
的含义。
表达式px的右值是整个结构体的内容。如下图所示:
左值很好理解,就是可以接受一个新的值。
2.2 访问结构
要想访问结构,也很简单,直接用间接访问操作符即可,所以表达式*px
的右值就是px所指向的整个结构。
表达式的左值,同样也可以接受新值。
2.3 访问结构成员
访问结构成员也是一样,我们先来访问结构体变量x
中的变量a
和变量b
。
访问a
很简单,可以直接使用表达式px->a
b是个数组,所以px->b
表示的是b首元素的地址,表达式*(px->b)
和px->b[0]
访问该数组第一个元素的值,访问后续元素和数组的访问方式类似。如下图所示:
2.4 访问嵌套的结构
C也是个结构体,要想访问C中的元素,要先通过px->c
访问到c结构体,然后再访问其中的元素即可。
比如,px->c.a
是访问结构体c的a元素,px->c.b
与上述说法一样,是一个指针常量,指向结构体c
中b
数组的首地址,访问b中的元素同样有两种方式,下标访问(px->c.b[0]
)和间接访问(*(px->c.b)
)。如下图所示:
2.5 访问指针成员
现在我们的结构体成员d尚未指向任何结构体,所以先建一个结构体,并把x.d指向它。
EX y = { 20, "mm", {12, { 5, 7}}, 0 }; x.d = &y;
现在y也指向了一个结构,整体变成了这样的结构:
那么要想访问结构体y中的元素,则先要通过px->y
访问到y
结构体。结构体y
中一些元素的访问方法如下:
px->d->a; px->d->b[0]; px->d->c.a; px->d->c.b[0];
3 结构的存储分配
结构的存储分配也是一个非常有意思的话题。毕竟C语言偏向底层的语言,很多数据的定义直接关系到分配内存的大小。
例如:
#include<stdio.h> struct ALIGN1 { char a; int b; char c; }; struct ALIGN2 { int b; char a; char c; }; int main() { printf("struct ALIGN1占%d个字节内存\n",sizeof(struct ALIGN1)); printf("struct ALIGN2占%d个字节内存\n",sizeof(struct ALIGN2)); return 0; }
打印输出:
可以看到,两个基本相同的结构体,仅仅因为数据存储顺序的不同,会导致其占不同的内存。具体的内存分配如下图所示:
绿色部分表示没有具体含义的空间。
在声明中对结构的成员列表重新排列,让那些对边界要求最严格的成员首先出现,对边界要求最弱的成员最后出现。这种做法可以最大限度地减少因边界对齐而带来的空间损失。
4 作为函数参数的结构
结构体也可以作为函数的参数进行传递。直接传递结构体是合法的,但这种操作并不是很“优雅”。同数组一样,往往采用指针的方式进行传递,不过数组是默认以指针的方式进行传递,而结构体却不是这样。如果直接将结构体变量名称当做实参传入,会直接将整个结构体传入该函数,比较浪费栈空间。
所以我们在定义自定义函数的时候,形参就定义为结构体指针,到时候传入结构体指针即可。比如:
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <string.h> typedef struct { char name[10]; short age; }people_info; void get_info(people_info *info) { strcpy(info->name, "Mystic"); info->age = 22; } int main() { //定义结构体变量并初始化 people_info p1 = {"No Name",25}; //定义结构体指针 people_info *p = &p1; //重新给结构体定义新值 get_info(p); //打印输出 printf("%s\n", p->name); printf("%d\n", p->age); system("pause"); return 0; }
我们定义了一个结构体,然后调用get_info
函数来给结构体录入个人信息。再返回主函数,验证我们录入的信息是否正确。
打印输出:
可以看出,我们的函数运行是没有问题的。
5 位段
位段是一个神奇的存在,仅仅从这一点上,就可以看出C语言的设计师为了紧密联系内存,到底花了多少心思。这种设计,就相当于将一个完整的数据分成了若干个部分,每个单独的部分可以表示不同的含义。
有两点需要注意:首先,位段成员必须声明为int、signed int或unsigned int类型。其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占的数目。
举个例子:
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <string.h> typedef struct { unsigned int class_1 : 5; unsigned int class_2 : 5; unsigned int class_3 : 5; unsigned int class_4 : 5; unsigned int class_5 : 5; }class_name; int main() { class_name normal; normal.class_1 = 26; normal.class_2 = 23; normal.class_3 = 30; normal.class_4 = 31; normal.class_5 = 29; printf("%d\n", normal.class_1); printf("%d\n", normal.class_2); printf("%d\n", normal.class_3); printf("%d\n", normal.class_4); printf("%d\n", normal.class_5); printf("结构体占内存大小为%d个字节\n", sizeof(normal)); system("pause"); return 0; }
比方说我们想存储5
个班级的人数,每个班级最多为31
人,我们就可以定义个结构体来实现位段,如果不使用位段,即使每个班级的人数定义为char
类型的变量,总共也需要5
个字节,当我们定义了位段,就可以省去一个字节。
打印输出:
当然,不仅仅是节省存储空间这么简单,在实际中还有其他的妙用。比方说我们需要两个设备之间的通信,发送PWM波,那么我们就需要变量来保存该PWM
波的基本属性,包括每组的脉冲数num
、占空比duty
和总共发送组的数量group
。然后需要先将该信息发送给另一个设备,方便该设备做好接收准备,每个数据帧有两个字节,构成如下:
如果没有位段我们就需要一系列的移位运算(和其他运算),才能得到最终想要发送的数据,但是有了位段,就简单多了,而且不易出错,程序如下所示:
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #include <string.h> union { struct signal_info { unsigned int num : 4; //每组发送的PWM波数量 unsigned int duty : 7; //占空比(%) unsigned int group : 5; //总共发送几组 }; unsigned int signal_information; }signal_info_union; int main() { signal_info_union.num = 1; signal_info_union.duty = 50; signal_info_union.group = 10; printf("%d\n", signal_info_union.signal_information); system("pause"); return 0; }
注:上述例子用到了联合体,如果对联合体不熟悉,可以先看本文后面的章节。
打印输出:
我们手动的计算结果也是21281
,二者相吻合。
6 联合
在C语言中,变量的定义是分配存储空间的过程。一般每个变量都具有其独有的存储空间,那么可不可以在同一个内存空间中存储不同的数据类型呢?
答案是可以的,使用联合体就可以达到这样的目的。较之于结构体,联合体在实际的开发中出现的频率并不是很高,但这并不是说联合体不重要。
可见,在内存方面,C语言的设计者可谓是下足了功夫,这也是C语言虽饱经风霜,却从未在计算机语言的发展长河中销声匿迹的原因之一。
6.1 变体记录
变体记录可以看作联合的升级版, 变体记录中联合成员是比int
和float
更为复杂的结构。
那么,究竟什么是变体记录呢?在网上我找到了这样的一番描述。
若记录是由一部分固定不变和另一部分变化部分是随固定部分中的某个数据项的具体取值而定的数据项所组成的称为记录变体。
大概就可以知道是什么意思了,所以这个所谓的变体记录并不是联合体独有的概念。只是一种数据的记录形式。
考虑下面的情况:
仓库储存两种货物, 一种是零件(part), 一种是装配件(subassembly), 装配件由一些零件组成。一个零件信息包括零件成本,零件供应商编号;一个装配件信息包括组成装配件的零件数, 以及零件信息。显然, 仓库的一条存货记录(inventory)可能是零件, 也可能是装配件, 并且包含入库日期和操作员编号, 可以用变体记录实现。
// 零件 struct PARTINFO { int cost; // 零件成本 int supplier; // 供应商编号 }; // 装配件 struct SUBASSYINFO { int n_parts; // 零件数 PARTINFO parts[MAXPARTS]; // 每个零件信息 }; // 存货记录 struct INVREC { char date[9]; // 入库日期 int oper; // 操作员编号 enum (PART, SUBASSY} type; union { struct PARTINFO part; struct SUBASSYINFO subassy; } info; } record;
我们可以通过以下方式访问存货记录。
record.oper 获取存货操作员编号
if(record.type == PART) { record.info.part.cost获取存货零件成本 record.info.part.supplier 获取存货零件供应商编号 } else if(record.type == SUBASSY) { record.info.subassy.n_parts 获取存货装配件包含零件数 record.info.subassy.parts[0].cost 获取存货装配件第一个零件的成本 }
6.2 联合的初始化
联合变量可以被初始化,但这个初始值必须是联合第一个成员的类型,而且它必须位于一对花括号里面。例如:
union { int a; float b; char c[4]; }x = { 5 };
我们不能将其初始化为一个浮点值或者字符值。如果给出的初始值是任何其他类型,它就会转换(如果可能的话)为一个整数并赋值给x.a
。
7 总结
结构的成员可以是标量、数组或指针。结构也可以包含本身也是结构的成员(除了自己,但它的成员可以是指向这个结构的指针)。
一个联合的所有成员都存储于同一个内存位置。通过访问不同类型的联合成员,内存中相同的位组合可以被解释位不同的东西。联合变量也可以进行初始化,但初始化值必须与联合第1
个变量的类型匹配。
在大型项目的开发中,往往会将结构体,联合体,数组等数据类型联合起来使用,还有各种各样让人眼花缭乱的指针,这对我们C
语言的技术功底提出了较高的要求。
---END---