我们知道,C语言是允许我们自己来创造类型的,这些类型就叫做——自定义类型。
自定义类型又包括结构体类型,联合体类型还有枚举类型。
今天的文章,我们就着重讲解这其中的结构体类型。
结构体的声明
1.1结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2结构的声明
struct tag
{
member-list;
}variable-list;
我们以这种方式来描述一个结构体。下面是简单的示范,我们来描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; //分号不能丢
定义局部变量和全局变量的关系:
#define _CRT_SECURE_NO_WARNINGS struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }s1,s2,s3; //全局变量 int main() { struct Stu s4; struct Stu s5;//局部变量 return 0; }
1.3 匿名结构体的情况
也可以省略不写结构体标签,不过这样会导致一个结果,结构体只能定义一次类型。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> struct { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }s1; //全局变量 struct { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }*ps; //全局变量 int main() { s1.age = 1; printf("%d", s1. age); return 0; }
在上述的代码中,体现为定义结构体变量s1之后,无法再次定义诸如s2,s3等结构体类型。
不过要是你本来就准备只用一次结构体的话,定义一个匿名结构体也不错就是了。
上面的两个结构在声明的时候省略掉了结构体标签, 那么问题来了?
//在上面代码的基础上,下面的代码合法吗?
ps=&s1;
答案是否定的,及时两个结构体里面的元素都相同,编译器也会他们当成两个完全不同的类型,所以是非法的。
1.4结构的自引用
我们想要使用结构体实现类似于链表的功能。
在结构中包含一个类型为该结构本身的成员是否可以呢?
#include<stdio.h> struct Node { int data; struct Node n; }; int main() { return 0; }
我们开动小脑筋,立马就发现了错误。
struct Node这个节点它所占用的空间有多大呢?
它不仅要存放一个整形,还要存放一个n。
这就无限循环下去了,struct Node里面还有一个struct Node。
大小是无法得出的,这是一个错误示范。
我们转变战略,用指针来实现。
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> struct Node { int data;//4 struct Node *next;//4/8 }; int main() { struct Node n1; struct Node n2; n1.next = &n2; return 0; }
创建两个节点n1,n2,把它们像链条一样串起来。
编译器没有报错,这样的写法是正确的,同时我们发现,struct Node的大小可以轻而易举地算出,我们得出一个结论:
不是在自己的类型里面包含一个自己类型的变量,而是在自己的类型里面包含一个自己类型的指针。这样的实现方式才是可行的。
1.5重命名匿名结构体的情况
下面的代码是否可行呢?
#include<stdio.h> typedef struct { int data; }S; int main() { return 0; }
可行,不过S不再是匿名结构体的变量,而是变成了匿名结构体类型。
怎么用呢?这么用:
#include<stdio.h> typedef struct { int data; }S; int main() { S s; s.data = 1; printf("%d", s.data); return 0; }
能用这种方式模拟实现上面的链表呢?
这样写行吗?
typedef struct { int data; Node* next; }Node;
不行,在没有重命名出Node时就调用了Node。
在这种情况下,我们只能老老实实地写出类型名了!
typedef struct Node { int data; struct Node* next; }Node;
1.6 结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单。
int x; int y; }p1; //声明类型的同时定义变量p1 struct Point p2; //定义结构体变量 struct Point { p2 //初始化:定义变量的同时赋初值。 struct Point p3 = {x, y}; struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s = {"zhangsan", 20};//初始化 struct Node { int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //结构体嵌套初始化 struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
1.7 结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这就到了本文的重中之重: 结构体内存对齐。
计算以下的结构体大小。
#include<stdio.h> int main() { struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1)); //练习2 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2)); //练习3 struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3)); //练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4)); }
运行结果如下:
是不是跟想的完全不一样?
没错,结构体的大小并不是成员大小的简单相加,而是有自己的一套规则的。
结构体的第一个成员永远是放在零偏移处。
从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
这个对齐数是成员自身大小和默认对齐数的较小值。
VS中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如果不够,则浪费空间来对齐。
我们以s1为例子来试验一下上述规则,如图所示。
因为从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。
所以1,2,3三个字节被浪费,int类型的存储从4开始到7,char类型存到8处。
最后结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
S1中最大对齐数为4,结构体总大小要为最大对齐数(每个成员变量都有一个对齐数)的整数倍。而现在大小为9,为了让其变为4的倍数,结构体S1的总大小变为12。
再看S4的情况:
白色为浪费部分,黄色为char,绿色是double,粉色是int。
1.8为什么存在内存对齐?
1.不同硬件平台不一定支持访问任意内存地址数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。使用内存对齐可以保证每次访问都从块内存地址头部开始存取
2.提高cpu内存访问速度,内存是分块的,如两字节一块,四字节一块,考虑这种情况:一个四字节变量存在一个四字节地址的后三位和下一个四字节地址的前一位,这样cpu从内存中取数据便需要访问两个内存并将他们组合起来,降低cpu性能
用内存对齐达到了用空间换时间的效果
1.9我们可以耍些小聪明达到节省空间的效果。
让占用空间小的成员尽量集中在一起。
//例如: struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; };
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。