本节视频链接:点击这里
上篇中讲述的数组是复合数据类型中最简单的一种,一个数组使用一段连续的内存保存了若干个类型相同的数据元素。由于类型和长度相同,数组的每个元素通过数组下标和指针变量访问。如果我们希望一个结构保存多个不同类型的数据元素,那么数组将无能为力。为了实现这样的功能,C语言提供了结构体和联合体。
1、结构体基本概念
(1)结构体的定义
假设我们需要定义一个图形中的点的概念。在一个使用笛卡尔坐标系表示图像的系统中,点的位置使用两个坐标分量表示,即横坐标x和纵坐标y。那么为了定义点这个变量,我们需要将坐标的两个分量定义于一起。将坐标x和坐标y定义在结构体中的方法为:
struct Point { int x; int y; };
定义一个结构体使用关键字struct实现。关键字struct定义了结构体名称,并在其后用大括号{ }指明了结构体的成员。每一个结构体成员是该结构体实例中所包含的数据。如我们定义的Point结构中就包含了x和y两个int型数据成员。在定义结构体的同时可以定义结构体的实例,如:
struct Point { int x; int y; } pt1, pt2, *ppt1;
另一种方法也可以将定义结构体类型和定义结构体实例分开:
struct Point { int x; int y; }; struct Point pt1, pt2, *ppt1;
在定义结构体时,更常用是使用typedef定义一种新的类型,这样后面再定义新的结构体实例时,便不需要再添加关键字struct,使得我们定义的结构体更像C语言提供的其他基本数据类型一样,使用更加简洁。
typedef struct _Point { int x; int y; } Point; Point pt1, pt2, *ppt1;
(2)结构体成员的访问
当我们定义了结构体实例后,可以轻松访问包含在结构体中的各种变量。例如我们定义了以下的Point变量:
Point pt1 = {1,3}, pt2 = {4, 8}, *ppt1 = &pt1;
使用结构体成员运算符“.”,可以按照结构体成员的名称获取其数值:
printf(“Point 1: (%d, %d), Point 2: (%d, %d)\n”, pt1.x, pt1.y, pt2.x, pt2.y); float distance = sqrt(pow(pt1.x-pt2.x, 2) + pow(pt1.y-pt2.y, 2)); printf(“Distance of two points: %f”, distance);
对于结构体实例,可以直接访问使用其内部成员,而使用指向结构体实例的指针也可以间接访问结构体的成员,其方法不再是使用”.”而是”->”。例如:
printf(“Point 1: (%d, %d)\n”, ppt1->x, pt1->y);
对于使用指针访问结构体成员,其效果同使用实例访问结构体成员是一致的。在实际使用时,使用指针的情况还更加频繁。
(3)结构体成员的初始化
从前面的程序中可以看出,结构体的初始化方法同数组的初始化方法比较类似,使用的是一对大括号所包括的、由逗号分隔的多个初始值。同初始化数组不同的是,由于结构体中的成员可以是不同的数据类型,初始化结构体的数据也可以依据结构体成员的定义类型不同而不同。结构体成员类型相同时的初始化方法已在上一节有所体现,现在我们来定义一个更复杂的结构体:
typedef struct _Student { long long student_id; char *student_name; char student_gender; int student_age; float student_height; float student_weight; } Student;
定义两个结构体实例,并将其初始化:
Student Jack = {201601001, “Jack”, ‘F',17, 180.5, 190.0}, Alice = {201601002, “Alice”, ‘M’, 16, 165.0, 140.8};
在这两个实例进行初始化时,我们根据定义的数据不同,分别包含了整型、字符串、字符型和浮点型等数据类型,并在初始化时按照不同类型对其成员变量进行了赋初值。如果我们只初始化了前面一部分的成员,那么剩余的结构体成员将被初始化为缺省值。
2、结构体的存储结构
当我们定义了一个结构体时,有时需要对其存储结构有一定了解。通常情况下,一个结构体的实例的大小等同于结构体内部各个成员大小的总和,因为程序编译时编译器会按照成员列表的顺序一个一个给每一个成员分配内存,但是这只有在满足内存边界对其时才是这种情况。如果定义的结构体成员之间大小关系不满足边界对其要求,那么成员之间可能会出现用于填充的额外内存空间。
例如我们定义一个结构体:
typedef struct { char ChrVal1; int IntVal; char ChrVal2; } MemAlign; MemAlign ma1 = {10, ‘A’, ‘b'};
结构体实例ma1在内存中的实际大小因机器不同而异。如果某个机器的的整型值长度诶4 Byte,且起始存储位置必须能被4整除,则这一结构在内存中的大小为12,而不是1+4+1=6。
结构体成员的存储必须满足几个原则:
- 某一个结构体对象的起始存储位置必须是最大的成员大小的整数倍;
- 每个结构体成员相对于本结构体起始位置的偏移量必须是自身大小的整数倍;
- 结构体对象所占据的总大小必须是最大数据成员体积的整数倍。如果以上三个条件有任何一个不满足,都将会在相应位置补充填充字节使之满足条件。
鉴于此,如果不涉及到程序可读性和可维护性的前提下,我们可以重新排列结构体成员的顺序来提高内存的利用效率。如MemAlign结构按照如下方式声明,将只占据8个字节,相对于上文的声明方法存储效率提高了1/3。
typedef struct { int IntVal; char ChrVal1; char ChrVal2; } MemAlign;
3、位段
位段是结构体中一种特殊的成员变量类型。位段的声明方式同普通的结构体成员类似,但是位段所代表的是一个或多个位(bit)的数据。所有的位段成员必须声明为int/signed/unsigned int类型。合适地使用位段可以根据需要更高效地利用内存,但是可能会降低程序的可移植性。这主要是由于以下情况根据系统的不同而不同:
- int位段被作为有符号还是无符号;
- 一个位段中允许的最大bit数;高位系统上允许的程序在低位系统上可能无法运行;
- 位段成员在内存中分配的顺序(从左到右/从右到左);
- 体积较大的后续位段的存放位置(直接紧邻前一个/从下一个字开始);
例如,一个使用位段的结构体声明如下:
typedef struct { unsigned ch: 7; unsigned font: 6; unsigned size: 19; } CHAR; CHAR ch1;
4、联合体
(1)联合体的概念和使用
联合体是C语言中提供的另一种结构。联合体的声明方式同结构体类似,但是它的结构和行为方式同结构体完全不同。结构体中使用不同的内存位置包含了不同的成员,而联合体则是使用相同的内存位置代表不同的联合体成员,而其中内存实际的数据都是相同的。例如,定义一个简单的联合体:
union { float f; int i; } fi;
该联合体实例只占用1个32位的内存空间,其中的f和i成员指代的都是同一段内存。如果我们对其中的一个成员赋值,那么联合体其他成员所指代的含义通常是用其他的格式表示内存中相同的位:
fi.f = 3.14159; int x = fi.i;
如果一个联合体中各成员的长度不同,那么联合体的长度是其最长的成员的长度。
(2)联合体实例如何初始化:
初始化一个联合体实例必须采用联合体第一个成员的类型,而且该值必须位于大括号内部,例如:
union { float f; int i; } fi = {3.14};