目录
今天,我们还是先讲解结构体
问题1:如何求结构体大小
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct S1
{
char c1;
int a;
char c2;
};
int main()
{
printf("%d", sizeof(struct S1));
return 0;
}
按照正常的情况,c1占一个字节,a占四个字节,c2占一个字节,一共应该占6个字节,我们进行编译,查看结果。
一共占12个字节。
接下来,我们再写一串代码:
#include<stdio.h>
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d", sizeof(struct S2));
return 0;
}
我们再写一个相同的结构体S2,S2的位置在S1的后面,我们打印对应S2所占的字节数。
一共占8个字节,我们可以发现,即使是相同元素的结构体,所占空间的大小也不一样。
结构体内存对齐
我们进行绘图讲解
如图,左边代表我们的结构体,右边代表我们的内存。
我们讲第一个规则:第一个成员在结构体变量偏移量为0的地址处。
假设我们的s1指向的空间在0处,下面的一个单元格就表示一个偏移量,因为第一个成员在结构体变量偏移量为0的地址处,又因为c1只占一个字节,所以c1如图所示
第二条规则:其他成员变量要对齐到对齐数的整数倍的地址处
第三条:对齐数是编译器默认的对齐数与该成员大小的较小值。(vs编译器默认的对齐数是八个字节)
假如我们求i变量默认的对齐数:i变量的类型是int,大小占四个字节,编译器默认的对齐数是八个字节,两个之间的较小值是四个字节,所以i变量的对齐数是4.
对齐数的整数倍也是4,所以我们的变量i要对齐到4的地址处,如图所示:
我们的变量i的地址从4的地址处开始,变量i占四个字节,向后数四个字节,对应到8的地址处
1,2,3这三个字节也是为我们开辟的,但是我们并没有使用
又因为: 第二条规则:其他成员变量要对齐到对齐数的整数倍的地址处
c2成员变量所占的字节数1个字节,vs默认的编译器的对齐数是8,则较小值为1,所以c2变量的对齐数是1,对应的整数倍也是1,因为8也是1的倍数,所以c2直接存在变量i的后一位即可
如图所示
第四条规则:结构体总大小为最大对齐数的整数倍。
c1所占空间大小为1,默认对齐数为8,对齐数为1
i所占空间大小为4,默认对齐数为8,对齐数为4
c2所占空间大小为1,默认对齐数为8,对齐数为1
所以最大对齐数为4,而我们计算出的数为9
因为必须是最大对齐数的整数倍,所以对应的结果为12.所以s1所占空间大小为12个字节。
所以对应的,我们又浪费了三个字节的空间
我们介绍一个函数
offsetof:可以求出某个函数的偏移量,第一个参数是结构体类型,第二个参数是成员变量名
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;
int a;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s1;
struct S2 s2;
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, a));
printf("%d\n", offsetof(struct S1, c2));
return 0;
return 0;
}
我们进行编译:
打印出来的结果是0,4,8.正好对应我们三个成员变量偏移量。
我们再研究一下s2:
第一个成员在结构体变量偏移量为0的地址处,对应的图像:
c2变量的大小是一个字节,默认对齐数是8,所以较小值为1,对齐数为1,所以对应结果如图
i变量的大小是四个字节,默认对齐数是8,所以较小值为4,对齐数为4,所以对应的结果如图:
i变量要对齐到对齐数的整数倍,对齐数为4,整数倍为4.所以跳过2,3.
我们的i占四个字节,所以一共占用了8个字节。
第四条规则:结构体总大小为最大对齐数的整数倍。
c1的对齐数为1
c2的对齐数为4
c3的对齐数为1,所以最大的对齐数为4,结构体总大小是最大对齐数的整数倍,一共占了8个字节,所以结构体总大小为8个字节。
默认对齐数:只有vs上有。其他的默认为自身大小
接下来,我们来自己算一下结构体大小
struct S1
{
double d;
char c;
int i;
};
这个结构体的大小是几个字节呢?
第一个成员变量存放在0位置处,占八个字节
变量c的对齐数为1.
变量i的对齐数为4,对应图像:
成员变量的对齐数分别为8,1,4,最高对齐数为8,8的整数倍是16,所以结构体类型struct S1占16个字节.
我们进行检测
#include<stdio.h>
struct S1
{
double d;
char c;
int i;
};
int main()
{
printf("%d", sizeof(struct S1));
return 0;
}
编译:
和我们计算的结果相同。
下一道题目:
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d", sizeof(struct S4));
return 0;
}
打印的结果是多少?
我们进行口算:c1占的字节数为1,s3占的字节数为16,s3结构体内的最大对齐数为8,所以对齐到8位置开始,向后占用16个字节,到达第24位,d占用的字节数为8,默认的对齐数为8,对齐数就为8,所以可以对齐到第24位,向后占用8个字节,一共占用32个字节。所有的成员变量包括嵌套结构体内部的成员变量的最大对齐数为8,所以占用32个字节。
为什么要对齐,有以下两种原因
第一种:
这种话原因就属于硬件原因了。
第二种:
我们画图进行解释
对于这个结构体
这是没有对齐的情况
我们的平台是32为平台,也就是四个字节,所以我们访问一次时,最多访问四个字节
假如我们要访问变量i的内容,我们从c1开始访问,第一次访问
我们只读取了i变量的3个字节
我们再次访问
我们这才访问完变量i。
假如我们提前进行对齐
我们可以直接访问变量i
一次就可以访问完变量i。
总的来说:我们提升了效率,却浪费了空间。所以结构体的内存对齐是拿空间换取时间的做法
那么,在设计结构体时,我们既想要满足对齐,又想要节省空间,有没有好的做法呢?
答:我们可以让占用小的变量尽量集中在一起。
修改默认对齐数
struct s
{
int i;
double d;
};
int main()
{
printf("%d\n", sizeof(sizeof(struct s)));
return 0;
}
我们可以很轻松的知道,struct s类型所占的字节数是16,最大对齐数是8.
当我们修改默认对齐数为4时,最后的结果是什么呢?
#pragma pack(4)
struct s
{
int i;
double d;
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct s));
return 0;
}
我们进行编译
对应的结果为12,原因是double的对齐数由原来的8变成了4,所以导致少了四个字节。
假如我们不想让结构体再对齐,我们可以把默认对齐数修改成1.
#pragma pack(1)
struct s1
{
int i;
char b;
char c;
};
int main()
{
printf("%d\n", sizeof(struct s1));
return 0;
}
修改为1后,相当于不用对齐,那么结构体的大小应该就是对应变量类型所占字节数的和,也就是6.
如何使用结构体传参,我们先用结构体传参打印一下结构体内部的元素
struct S
{
int data[1000];
int a;
};
print(struct S s)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", s.data[i]);
}
printf("%d ", s.a);
}
int main()
{
struct S s = { { 1, 2, 3 }, 20 };
print(s);
return 0;
}
我们进行编译:
我们还可以使用指针的方法:
#include<stdio.h>
struct S
{
int data[1000];
int a;
};
print1(struct S s)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", s.data[i]);
}
printf("%d ", s.a);
}
void print2(struct S *ps)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d ", ps->a);
}
int main()
{
struct S s = { { 1, 2, 3 }, 20 };
print1(s);
print2(&s);
return 0;
}
进行编译:
print1方法(传值调用)和print2方法(传址调用)哪一个更好。
print1方法的缺点:在使用print1方法传参时,我们需要创建一个对应的临时结构体来接受我们传递的结构体参数,并且还要在栈上开辟空间存储我们传递的数据,既浪费了空间,又浪费了时间。
print2方法的缺点:print2方法不够安全,print1方法虽然浪费,但是我们并不会改变结构体的内容,但是print2方法因为传递的说地址,所以可能该改变结构体的内容。
所以我们可以优化print2方法,
void print2(const struct S *ps)
{
加上const修饰,防止ps解引用导致结构体内容发生改变。
位段:
讲完结构体就得讲讲结构体实现位段的能力。
备注:位段的成员必须是整型家族。
这就是位段
位段是什么意思呢?
答:位段的意思就是说,比如第一行的int_a:10,这里的表示_a占用的空间是10个比特位,_b占用的 空间是20个比特位。
有人会说,_a是整型啊,一个整型等于32个比特位,为什么会少呢?
struct s
{
int _a : 1;
int _b : 4;
};
比如说,假如a就表示真假,所以a只取1或者0.所以占用1个比特位就可以了
假如我们的b要表示的值为0,1,2,3那我们的b只取两个比特位就行了,对应的值为00 01 10 11.
struct s
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
那么,这个结构体所占的空间又是多少呢?
struct s
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d", sizeof(struct s));
return 0;
}
我们想想最理想的情况:这四个元素加起来所占的比特位是47,因为打印的是字节数,那么大概就是6个字节,我们进行编译
可以发现,结果是8个字节
证明:虽然位段能够节省空间,但是只是在一定程度上,并不能完完全全的不浪费空间。
struct s
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
首先,编译器识别我们的位段的空间第一个元素是int类型的,所以开辟4个比特位,这四个比特位能够承载成员_a,_b,_c。到d的时候,编译器识别到我们是int类型的,再开辟4个比特位,用来存储_d. 所以我们算出的结果是8个字节。
位段一般是相同类型的元素。
位段的可移植性不好。
struct s
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
这个结构体一共需要占用多少空间,我们进行分析:
答:因为是char类型,我们首先开辟1个字节,也就八个比特位,八个比特位能够存储_a和_b,并且剩一1个比特位,下一个元素_c,占用了5个比特位,所以我们再开辟一个字节,存储过_c之后,假如我们之前剩下的一个比特位在存储c时使用了,那我们就剩下四个比特位,是能够存储下_d的,假如我们的之前剩下的一个比特位浪费掉了,那么我们只剩下三个比特位,不能存储_d,所以需要再开辟一个字节。
假如没有浪费,那我们就需要开辟两个字节
假如浪费掉了,那我们就需要开辟三个字节。
struct s
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
int main()
{
printf("%d", sizeof(struct s));
return 0;
}
我们进行编译,最后打印的结果是
struct S
{
char _a : 3;
char _b : 4;
char _c : 5;
char _d : 4;
};
int main()
{
struct S s = { 0 };
s._a = 10;
s._b = 12;
s._c = 3;
s._d = 4;
return 0;
}
我们画一下s在内存中可能的布局情况
假设我们的s在存储时,是由低位向高位进行存储,我们进行尝试:
我们的_a占三个比特位,但是我们_a初始化的结果为10,对应的二进位制是1010,占三个比特位,所以我们只能把010存储到对应的位置。
12要存储在四个比特位里,对应的结果为1100
最后一位,我们直接浪费,要把3存储到5个比特位里,对应的结果就是00011.
这剩下的三个比特位,我们直接浪费。要把4存储到4个比特位里面,对应的结果是0100
所以存储完毕后,对应的二进位制实际上是:
因为内存中显示的是字节,我们将其转化为字节
所以对应内存中的空间应该是62 03 04,我们进行检测
如图所示。
第一条的意思:
例如,我们的int位段并不清楚是有符号数还是无符号数,在c语言中,int表示的类型并不总是有符号整型,所以最高位是不是符号位也不确定
第二条的意思:我们的机器是32位平台的,我们可以输入30,有的机器是16位平台,写30就会出现问题
第三条的意思:我们的vs2013版本下,位段中的成员在内存中从右向左分配。但是其他编译器位段中的成员在内存中怎么分配是不确定的。
第四条的意思:例如
struct S
{
char _a : 3;
char _b : 6;
char _c : 5;
char _d : 4;
};
我们第一个位段存储后的剩余位有5位,无法容纳下一个位段b的6位,那么这剩余的5位究竟是要浪费还是利用是不确定的。
诸多不确定因素导致我们最好不要跨平台使用位段。
总结:和结构相比,假如位段可以达到同样的效果,是可以实现节省空间的,但是有跨平台的问题所在。
位段在网络中经常应用。
实现一个通讯录