目录
前言
结构体
1. 结构体的基本含义
2. 结构体的声明
3. 特殊的结构体声明
4. 结构体的自引用
5. 结构体的初始化
6. 结构体内存对齐
总结
如果我们想要用程序保存一个学生的信息,该如何表示呢?首先,我们需要明确一个学生的信息都有哪些:姓名、年龄、性别、地址……可见,一个学生包含着多种信息,单纯用一个变量是无法表述的,所以就需要借助C语言中的一种自定义类型——结构体。
结构体,就是一些值的集合,这些值也被称为成员变量。虽然听上去与数组有些相似,因为数组也是一些值的集合。但是与其截然不同的是:结构体中的每个成员可以是不同类型的变量。
struct student
{
char name[10];
int age;
char sex[5];
}s1, s2;
上面代码就是一个结构体的声明示例。
其中 struct 是固定不变的,student 是结构体的标签,是由我们根据不同场景设定的。
大括号内部的 name 、age 、sex 这三个变量即为成员变量,大括号内的内容也被称为成员列表。成员列表中的成员可以一个也可以多个。
而大括号后面的 s1 、s2 就是这个已经定义好了的结构体类型创建出来的两个结构体变量,它们也被合称为变量列表。此处定义的结构体变量,可以一个也可以多个,也可以不定义,在 main 函数中定义也可以,在其他地方定义也可以。但不同地方所定义的结构体变量的作用域不同。在 main 方法之外定义结构体变量为全局变量(上面代码中的 s1 和 s2 也是如此),在 main 方法之内的是局部变量,它们除了作用域不同之外,其他作用都相同。
定义结构体类型时,大括号后的分号不能忘记。
如果不使用上面的方法创建变量,定义格式如下:
struct student s3;
切记,定义时不能将前面的 struct 丢弃。
如果你觉得 struct student 这样一个结构体类型名字过长,可以使用 typedef 将其重命名,具体格式如下:
可以在定义结构体类型时就进行重命名:
typedef struct student
{
char name[10];
int age;
char sex[5];
}student;
注意此处的 student 不是变量列表,而是重命名之后的 struct student 的名字。
也可以在定义好结构体类型之后再重命名:
struct student
{
char name[10];
int age;
char sex[5];
}s1, s2;
typedef struct student student;
重命名之后,下次想要创建结构体变量时,类型名就可以简写为 student 。
在声明结构体的时候,可以不完全的声明
例如:声明匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
这两种结构体声明时都省略了结构体标签,所以是匿名结构体类型。
匿名结构体类型如果不在声明时创建变量,往后就无法再使用了。
并且,虽然上面两种声明时,结构体内部的成员变量都一模一样,但是不能认为上面两种类型就一模一样,编译器仍旧认为它们是两种不同的类型,所以下面的代码是不合法的。
p = &x;//不合法
匿名结构体类型也是可以进行重命名的:
typedef struct
{
int a;
char b;
float c;
}s;
结构体的自引用,就是结构体中包含一个类型为该结构本身的成员。
现在我们就举一个例子来引申出结构体自引用的正确方式:
假设现在处在网络中的一个节点,需要存储自身的数据,并且还需要包含下一个节点的信息,那么此时就需要在结构体中进行自引用了。
下面是一种最容易想到的一种自引用方式,但却是错误的。
struct Node
{
int data;
struct Node next;
};
假设节点中的数据使用 data 表示,上述代码中直接在结构体中创建一个自身类型的变量。这显然是我们正常情况下最容易想到的:直接将下一个节点结构体变量存储在本节点中。但是这样是不允许的,因为这样一直嵌套下去,那么第一个节点所占据的内存是无法估算的,因为它包含自身的数据,还包含下个节点,而下个节点也有自身的数据,也包含着下下个节点的数据……这样下去,所占据的内存十分庞大,因此我们需要一种更加简便的方法:
struct Node
{
int data;
struct Node* next;
};
只需存储下一节点的地址即可,需要使用时进行解引用即可。
所以,我们也了解到了自引用的正确形式:只需在结构体内包含一个类型该结构体本身的指针作为成员即可。
此处还有一个需要注意的易错点:
typedef struct Node
{
int data;
Node* next;
}Node;
上面代码中,在自引用的时候,直接使用重命名后的名字作为类型名创建变量,这样是不合法的,因为在大括号之后,struct Node 重命名为 Node 这个过程才算是完成了,而在大括号内就使用重命名后的类型名创建变量,相当于 Node 未产生前就使用了,所以是不合法的,仍旧应该先使用原来类型名进行创建变量。
typedef struct Node
{
int data;
struct Node* next;
}Node;
1)可以在定义变量的同时初始化
struct Point
{
int x;
int y;
}p1 = { 10,10 };
struct Point p2 = { 10, 20 };
2)可以在变量定义好之后再初始化
struct Point
{
int x;
int y;
}p3;
p3 = { 20,10 };
3)如果结构体内还有结构体成员,需要嵌套初始化:
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL };
struct Node n2 = { 20, {5,6}, NULL };
4)上面初始化形式都为按照成员变量顺序初始化,也可以乱序初始化,但是需要写清楚是对应哪个成员变量:
struct Node n3 = { .data = 30, .p.x = 10, .p.y = 20, .next = &n2 };
struct A
{
int a;
char c;
double d;
};
当你还没学习到下面的内容,你会认为上面的结构体所占内存是多少?相信很多人都会认为是所有成员变量所占内存总和:13 。
struct B
{
char c;
double d;
int a;
};
那么对于这一个结构体,你又会认为它是占据多大内存呢?还是 13 吗?
int main()
{
printf("%d\n", sizeof(struct A));
printf("%d\n", sizeof(struct B));
return 0;
}
通过代码验证之后,我们发现答案并不是 13 ,并且两个结构体的成员列表明明完全一样,顺序不同而已,所占内存居然也不同。这就涉及到了结构体知识中,最重要也是最难的一个知识点:计算结构体大小。这就需要先需要了解结构体中的数据在内存中是如何存放的,这就得引申出一个概念:结构体内存对齐。
首先得掌握结构体的对齐规则:
1)第一个成员在结构体变量偏移量为 0 的地址处;
2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值(VS 中默认的对齐数是 8 );
3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍;
4)如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
那么上述这些规则具体是什么意思呢,下面我们通过一个例子来说明一下:
假如现在有一个结构体类型:
struct S2
{
char c1;
char c2;
char c3;
char c4;
};
struct S1
{
char c;
int a;
struct S2 s2;
}s1;
S1 中包含着另一个结构体类型 S2,并且已知 S2 在内存中占 4 个字节,定义 S1 的同时创建好变量 s 。现在我们来看看 s 中的数据在内存中是如何存储的,占多少个字节空间:
s 的起始处就是该变量偏移量为 0 的地方,然后以字节为单位,后续是偏移量为 1、2、3、4、5、6……
s 中的第一个变量是 char 类型的 c ,应该从偏移量为 0 的地方开始存储,字节数为 1 。 所以这就是第一条规则所讲述的内容。
接着需要存储 int 类型的 a ,从第二个变量开始,就应该存储到对齐数的整数倍的地址处。int 类型为 4 个字节,VS 中默认对齐数为 8 ,所以取它们的较小值是 4 ,即对齐数是 4 ,所以应该对齐到偏移量为 4 的地方,字节数为 4。中间空缺的字节全都不使用,即为浪费掉。这就是第二条规则所讲述的内容。
然后存放变量 s2 ,这是个结构体类型,所以需要对齐到它自身的最大对齐数的整数倍处。
我们可以看到 struct S2 中存放的是四个 char 类型的成员,所以它们的对齐数都为 1 .(所占字节数 1 ,默认对齐数 8 ,所以对齐数为 1)所以 s2 的最大对齐数是 1 ,它在内存中对齐到 1 的整数倍即可,也就是任意位置都可以。这就是第四条规则所讲述的内容。
最后,由于是嵌套结构体,所以结构体总大小是所有最大对齐数的整数倍处(包含嵌套在内的结构体中的最大对齐数)。
char c 的对齐数是 1 (这个变量虽然是对齐到偏移量为 0 就行,但是仍需计算其对齐数)
int a 的对齐数是 4
struct S2 s2 的最大对齐数是 1
所以整个结构体 struct S1 s1 的最大对齐数是 4 ,其总大小应该是 4 的整数倍。上面所占的内存是12 个字节,已经是 4 的倍数了,所以总大小是 12 个字节。如果不是 4 的整数倍,就还需空缺浪费几个字节,直至大小是 4 的倍数。这就是第三条规则所讲述的内容。
int main()
{
printf("%d", sizeof(struct S1));
}
通过代码验证之后,struct S1 的大小确实是 12 。
综上所述,我们明白了结构体大小并不只是简单计算其中所有成员变量的大小之和,而是要根据它们在内存中的对齐规则进行计算,而且就算是完全一样的成员列表,其变量的顺序不同,结构体占据的内存也可能不同。
那为什么要存在内存对齐,而不能简单进行成员变量所占内存相加总和作为该结构体的内存大小呢?
大部分参考资料都是如是说的:
1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台的只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
2. 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问。
对于第二点,我们来通过一个例子来探究其所说的意思是什么:
struct S
{
char c;
int i;
}s;
当上面的结构体在内存中是未对齐进行存放时:
在 32 位机器中,内存每次只能访问 32 比特位的数据,也就是 4 个字节
在上图中可以看到,如果我们需要访问变量 i 时,需要两次访问,第一次访问 i 所占的前三个字节,第二次访问 i 所占的最后一个字节。
而如果是对齐进行存放时:
可以看到这样就只需一次访问就可以完整地访问到 i 的数据,无需拆分。
总体来说,结构体的内存对齐就是拿空间来换取时间的做法。 所以在设计结构体的时候,我们既要满足对齐,又要尽量地节省空间,就应该让占用空间小的成员尽量集中在一起。
结构体在复杂的程序编写中经常会使用到,因为我们的日常生活存在着大量多属性化的变量,所以都需要借助结构体进行表示,而“ 了解结构体在内存是如果存放的 ”是最为重要的,充分掌握之后有助于我们更好地理解结构体如何使用。