1.1 什么是结构体?
结构体是一个复杂对象,我们都知道,C语言的基本数据类型有 int,char,float,double …等等。 int类型可以表示年龄,double类型可以表示考试分数。但是如果我们要描述一个人,人有各种各样的特征,是一个复杂对象。这时基本数据类型就无法满足我们的需求,所以我们需要用结构体来描述一个复杂对象。
1.2 结构体的定义
那么我们如何定义一个结构体呢?结构体的定义格式:
struct 结构体类型名
{
结构体成员;
…
}结构体变量名(可以不写,写了该变量默认全局变量);
那我们用代码演示一下。
struct Student //结构体类型名
{
//结构体成员
int age;
char neme[20];
double score;
}stu1; //结构体变量名(也是创建结构体对象)
这样,我们的一个结构体类型就定义好了,但仅仅只是定义的这个类型,如果需要用到这个类型,我们还需要创建变量。 就像 int 这个类型本身存在,我们要使用的时候需要 int i 来创建,结构体也是一样的。
struct Student
{
int age;
char neme[20];
double score;
}stu1;
int main()
{
int i ; //创建一个整形变量
struct Student stu2;//创建一个结构体变量
return 0;
}
实际上stu1 和stu2唯一的区别就是,stu1是全局变量,stu2是局部变量
1.3 结构成员的访问
当我们创建了一个结构体的时候,我们需要去访问结构体里面的成员,访问结构体成员有两种方式。
1.结构体变量名.结构体成员名
2.结构体指针->结构体成员名
那么我们用代码来演示一下。
#include<stdlib.h>
#include<string.h>
struct Student
{
int age;
char name[20];
double score;
}stu1;
int main()
{
struct Student stu2;
//1.结构体变量名.结构体成员名
stu1.age = 10; //给结构体成员赋值
strcpy(stu1.name,"zhangsan");
stu1.score = 88.5;
printf("%d %s %.1lf\n", stu1.age, stu1.name, stu1.score);
//2.结构体指针->结构体成员名
struct Student* ps = &stu2;
ps->age = 18; //通过结构体指针给成员赋值
strcpy(ps->name, "lisi");
ps->score = 85.5;
printf("%d %s %.1lf\n", ps->age, ps->name, ps->score);
return 0;
}
这样我们就成功的访问到了结构体成员,并且还能给结构体成员赋值。
1.4 结构体的内存对齐
那么一个结构体的大小是多少呢?我们先来看看下面2个结构体的大小。
#include<stdlib.h>
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
char c2;
int i;
};
int main()
{
printf("s1 = %d\n",sizeof(struct s1));
printf("s2 = %d\n", sizeof(struct s2));
return 0;
}
我们可以看到,s1结构体的大小是12,s2结构体的大小是 8 ,可是它们的成员明明一样,为什么大小不一样? 那是因为结构体存在内存对齐。
结构体的内存对齐规则
1.第一个成员在结构体变量偏移量为0的地址处
2.其他成员变量要对齐某个数字(对齐数)的整数倍地址处
3. 结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍
4.如果结构体嵌套,嵌套的结构体对齐到它本身的最大对齐数的整数倍
我们了解了对齐规则,那么现在就来解释一下 为什么s1是12,s2是8。
c1是结构体的第一个成员,所以放在偏移量为0的位置
随后我们要存放 i ,因为i是4个字节,而vs默认的对齐数是8个字节,取较小的那一个,所以 i的对齐数是4,因此我们要把i放4的整数倍地址处,而偏移量1,2,3都不是4个整数倍,所以int应该放在偏移量为4的位置。也就是说上面三块空间浪费了,如下图。
现在已经放进了c1和i,接下来我们放c2,c2是char类型,占1个字节,所以对齐数是1,所以存放在1的倍数的地址偏移量处(任意)。
这时候我们的结构成员全部放完,占了9个字节,可是第三点说,结构体的总大小必须是成员中最大对齐数的整数倍。而
char c1 1个字节,对齐数为1
int i 4个字节 对齐数为4
char c2 1个字节,对齐数为1
所以成员中的最大对齐数是4,也就是结构体的总大小必须是4的倍数。所以下面9,10,11偏移量 的空间就浪费了。
那们我们在根据这个方法,来算s2的大小。
首先,我们把c1 放进偏移量为0的位置。
然后我们放c2,因为c2是一个字节,vs默认对齐数为8,所以c2的对齐数是1,可以放在所有1 的倍数的偏移量处。所以c2可以直接放在偏移量为1 的地方。
然后我们放i,i 是一个int类型,占四个字节,所以对齐数是4,要放在4的整数倍的地址偏移处。也就是偏移量为4的地方。
这样我们的成员就放完了,此时整个结构体的大小是8个字节,最大对其实是int,四个字节,总大小是最大对对齐数的整数倍。所以,s2的字节大小是8。
1.5 位段
位段的声明和结构体是类似的,但有两点不同。
1.位段的成员必须是int,unsigned int,signed int
2.位段的成员名后面有一个冒号和一个数字,数字代表占的比特位。
以下就是位段的创建
#include<stdio.h>
struct s1
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
return 0;
}
我们可以看到这个位段,占3个字节
接下来我们分析一下,它为什么只占三个字节。
首先,我们说过,冒号后面的数字代表的是所占的比特位。
而一开始的成员类型是char , 所以我们会开辟一个字节的空间。
然后我们把a放进,a占了3个比特位,因为是小端存储,所以低字节序存放在低地址处,所以后面三个比特位是a
接下来b占4个比特位。
然后c占5个比特位,但是这只有1比特位了,不够,那么就在开辟一个字节的空间。
那么d占4个比特位,又不够了怎么办?我先把d改成3个比特位,看大小是不是2。
d改成3后,那么就是2个字节,d是4的时候,是3个字节。那么当d4个字节不够放进第二个字节时。那么会继续开辟一个字节的空间,而第一个字节剩下的一个和第二个字节剩下的3个,不会再被利用,也就是说这四个比特位浪费了。
上图就是面位段再内存中的样子。
联合体
2.1 联合体的概念
联合体也被称为共用体,联合体的成员在内存中是共用一块空间的,所以联合体的大小,至少是最大成员的大小。因为联合体必须有能力保存这个最大成员。
2.2 联合体的共用性
我们先来看看这段代码
#include<stdio.h>
union un1
{
char ch[3];
int i;
};
int main()
{
printf("%d", sizeof(union un1));
return 0;
}
再看看它在内存中的大小。
四个字节,这说明 ch和 i 是共用同一块内存空间,这样的话,我们修改了 i ,那么ch 就会发生改变,我们修改了ch,i的值就会被改变。那我们测试一下。我们不给ch初始化,我们把i的值赋值成0x11223344,然后输出ch的所有值。
因为当前存储模式是小端存储,所以低字节序会存放于低地址处,而数组的地址是随着下标的增长而增加的。所以 就读到了 44 33 22 ,这也更加的说明,联合体的成员是共用同一块内存空间的。那我们这回不修改i的值,把ch数组的三个值赋值,然后打印i的值。
#include<stdio.h>
union un1
{
char ch[3];
int i;
};
int main()
{
union un1 u;
u.ch[0] = 0x44;
u.ch[1] = 0x33;
u.ch[2] = 0x22;
printf("%x",u.i);
return 0;
}
因为 i 是未初始化的,而ch数组只有3个元素,所以只把i的低字节序赋值,高字节序依旧是随机值。
2.3联合体的内存计算
结构体存在内存对齐,那么联合体也存在内存对齐。那我们来看看以下代码。
#include<stdio.h>
union un1
{
char ch[5];
int i;
};
int main()
{
printf("%d",sizeof(union un1));
return 0;
}
那我们来计算一下它的内存大小
首先,char ch 是char类型,它的对齐数是1,所以从偏移量为0的位置开始。
因为这时联合体,所以 i 也是存放于0的位置
但是i是4个字节,vs默认对齐数是8,所以i的对齐数是4,ch是char类型,对齐数是1,那么联合体的总大小应该为最大对齐数4的整数倍,而此时联合体的大小是5个字节,不是4的整数倍,所以会浪费下面3个字节的空间。
所以这个联合体的的大小应该为8。