C语言之自定义类型——结构体
文章目录
前言
本文重点讲解结构体的知识点。结构体、枚举和联合属于自定义类型。与自定义类型相对应的是内置类型,比如int、short、char等都属于内置类型。
1 结构体
我们描述一个事物通常需要描述其不同的特性,比如描述人,需要知道每个人的姓名、年龄和性别等。如果这些值能够存储在一起,访问起来会简单一些。在C中,使用结构体可以把不同类型的值存储在一起1。结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1 结构体声明
1.1.1 一般格式
结构体类型的声明一般格式:
struct tag {
member-list
member-list
member-list
...
};
tag 是结构体标签。
member-list 是成员变量的定义,比如 int i; 或者 float f,或者其他有效的变量定义。
结构体的声明就是创造一个结构体类型,用这个类型可以描述真实世界事物。例如我们想描述一个学生,一般可以用以下格式进行声明一个结构体类型,然后用这个类型描述学生。
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//分号不能丢
其中Stu是一个结构体类型,name、age、sex、id都是结构体类型的成员。我们利用这个类型可以定义结构体变量,这一点下文会讲。
1.1.2 特殊结构体声明
匿名结构体类型的声明格式:
struct {
member-list
member-list
member-list
...
} variable-list ;
可以发现没有匿名结构体标签,variable-list 是结构体变量。下面例程声明两个匿名结构体类型,进行详细说明。
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
以上程序在声明结构的时候省略了结构体标签(tag),这种不完全的声明,称为匿名结构体类型(anonymous struct),虽然两个匿名结构体类型的成员变量相同,但属于两种不同的结构体类型。由于无法利用匿名结构体类型定义结构体变量,所以必须在声明的时候定义结构体变量,即variable-list不可以省略。
匿名结构体与共用体(union)联合使用时,可以像使用结构体成员一样直接使用其中联合体的成员2。
1.2 结构的自引用
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等3,例程如下所示。
struct Node
{
int data;
struct Node* next;
};
重定义类型名的一个注意点:
利用typedef可以重新定义类型的名字,这样可以使用类型定义变量时可以很方便,但是在结构体的自引用中需要注意,否则容易出错。
错误例程:
typedef struct
{
int data;
Node* next;
}Node;
结构体内部不能使用重新定义的结构体名字,所以上面的程序会出错。下面对以上程序进行修改。
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.3 结构体变量的定义和初始化
前面我们讲了如何声明结构体类型,那么如何定义结构体变量呢?定义有两种方式,见例程。
struct Point
{
int x;
int y; }p1; //第一种:声明类型的同时定义变量p1
struct Point p2; //第二种:定义结构体变量p2
结构体初始化有两种方式,见例程。
struct Point
{
int x;
int y;
}p1 = { 1,2 }; //第一种:结构体嵌套初始化
struct Point p2 = { 3, 4 };//第二种:定义结构体变量p2的同时赋初值。
1.4 结构体内存对齐
通过以上内容,我们已经掌握了结构体的简单使用,下面来点难的。结构体内存对齐是指结构体在内存中存储时会进行对其,以方便处理器进行访问。如果我们想计算结构体的大小,首先您必须掌握结构体的对齐规则。
1.4.1 结构体对齐规则
- 第一个成员变量的偏移量为0,即第一个成员变量和结构体变量的地址相同。
- 其他成员变量的偏移量等于某个数字(对齐数)的最小正整数倍。
- 结构体总大小为最大对齐数的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
先对以上提到的两个概念进行解释:
偏移量:成员变量与结构体变量相差的字节数。
对齐数 = 编译器默认对齐数 与 该成员大小中的较小值。(Visual Studio 编译器的默认对齐数为8(字节))
1.4.2 计算结构体的大小
掌握了以上规则,就可以计算结构体的大小,下面举例说明。
练习1
struct S1
{
char c1;
int d1;
char c2;
};
分析:c1是第一个成员变量,故偏移量为0,c1的存储起始地址与结构体变量S1的地址相同。c1的大小为1字节,故c1的存储位置如图1所示。(注意:变量的起始地址就是变量的地址)
d1的大小为4,编译器默认的对齐数为8,故可得d1的对齐数取两者之中的最小值4。d1的偏移量为对齐数4的最小正整数倍,这个最小正整数只能是1,因此偏移量为1*4 = 4。d1的存储地址就是偏移量为4的位置。d1的大小为4字节,故d1的存储位置如图2所示。
成员变量c2的大小为1,编译器默认的对齐数为8,故可得c2的对齐数取两者之中的最小值1。c2的偏移量为对齐数1的最小正整数倍,由图2可以看出,结构体变量S1内存已经存到第八个字节,故可得对齐数最小正整数倍为8,故偏移量为8*1 = 8。所以c2的存储地址就是偏移量为8的位置。c2的大小为1字节,故c2的存储位置如图3所示。
经过以上分析,我们知道了每个成员变量的存储状况,那么结构体的总大小为多少呢?其实可以根据规则的第三条进行计算。由以上可知c1的对齐数为0,d1的对齐数为4,c2的对齐数为1,所以成员变量的最大对齐数为4,又当前结构体内存已经存到了第9个字节,但9除以4的商不是整数,9/4 = 2.25,2.25向无穷大取整,可得到3,所以可以得到结构体的大小为3*4 = 12,即结构体变量S1占12个字节。下面通过程序验证一下。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int d1;
char c2;
};
int main()
{
printf("S1成员c1偏移量:%d\n", offsetof(struct S1, c1));
printf("S1成员d1偏移量:%d\n", offsetof(struct S1, d1));
printf("S1成员c2偏移量:%d\n", offsetof(struct S1, c2));
printf("S1结构体的大小为:%d\n", sizeof(struct S1));
return 0;
}
运行结果:
其中宏offsetof可以计算成员变量的偏移量,其存在的头文件为<stddef.h>,可以看到图4运行的结果与我们上面分析的一样。下面再来个程序,进行分析练习。
练习2
struct S2
{
char c1;
char c2;
int d1;
};
这一次简化分析,c1的偏移量为0,占用S2的第1个字节,c2偏移量为1,占用S2的第2个字节,d1的偏移量为4,占用S2的第5~8字节,由于最大的对齐数为4,S2的内存已经存储到第8个字节,并且8/4 = 2,2是个整数,所以可以得出S2的大小为8个字节,十分细心的人可能已经发现,上文的结构体变量S1和本例程的S2的成员变量相同,但是两者占用的内存却不同这说明,我们在声明结构体的时候,调整成员变量的位置,可以节省内存,即如果我们做到让占用空间小的成员尽量集中在一起,就可以节省空间。S2的存储位置如图5所示。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S2
{
char c1;
char c2;
int d1;
};
int main()
{
printf("S2成员c1偏移量:%d\n", offsetof(struct S2, c1));
printf("S2成员c2偏移量:%d\n", offsetof(struct S2, c2));
printf("S2成员d1偏移量:%d\n", offsetof(struct S2, d1));
printf("S2结构体的大小为:%d\n", sizeof(struct S2));
return 0;
}
运行结果:
为了熟悉,再来个练习
练习3
struct S3
{
double d;
char c;
int i;
};
分析:
上面提供了一个表格,利用这个表格可以简化我们的分析,希望诸位能学会。
下面分析一个嵌套结构体的情况,嵌套结构体就选择上文分析的S3,我们已经知道S3的大小为16字节。那么请看练习4,试着分析结构体变量S4的大小。
练习4-结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
分析:
可以看到结构体S3作为S4的成员,S3的最大对齐数变成了8,这一点需要特别注意。我们可以简化表2分析,如表3所示。
程序验证S4
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("S4成员c1偏移量:%d\n", offsetof(struct S4, c1));
printf("S4成员s3偏移量:%d\n", offsetof(struct S4, s3));
printf("S4成员d偏移量:%d\n", offsetof(struct S4, d));
printf("S4结构体的大小为:%d\n", sizeof(struct S4));
return 0;
}
运行结果:
上文提到Visual Studio 编译器的默认对齐数为8字节,其实利用#pragma 这个预处理指令可以修改默认对齐数,下面对这一点进行介绍。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
//visual studio 2019的默认对齐数为8
struct s1
{
char c1;
double d;
int i;
};
#pragma pack(4)//设置默认对齐数为4
struct s2
{
char c1;
double d;
int i;
};
#pragma pack()//取消设置对齐数,对齐数被还原为默认值
int main()
{
printf("s1成员c1偏移量:%d\n", offsetof(struct s1, c1));
printf("s1成员c2偏移量:%d\n", offsetof(struct s1, d));
printf("s1成员i偏移量:%d\n", offsetof(struct s1, i));
printf("s1结构体的大小为:%d\n", sizeof(struct s1));
printf("s2成员c1偏移量:%d\n", offsetof(struct s2, c1));
printf("s2成员c2偏移量:%d\n", offsetof(struct s2, d));
printf("s2成员i偏移量:%d\n", offsetof(struct s2, i));
printf("s2结构体的大小为:%d\n", sizeof(struct s2));
return 0;
}
运行结果:
1.5 内存对齐的原因
上文讲述了内存对齐的一点知识,那么为什么要内存对齐呢?主要有以下两点。
- 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总结
本文讲述了结构体的声明两种方式,结构体定义和初始化的方式,介绍了匿名结构体,并给出了结构体的自引用方法。结构体的内存对齐是本文的重点内容,掌握结构体内存对齐的规则是计算结构体大小的基础。以上内容如有错误,还请各位看官多多批评。