@TOC
:key:引:
:star:C语言有许多内置类型,如 char, short, int, long, float, double等,
:star:而 描述书(书名+作者+出版社+定价+书号)
或者人(名字+年龄+身高+身份证号码)
这种 复杂对象,则要用 可自定义的结构体类型来描述。
:snowflake:本文介绍的结构体内存对齐,是面试考点,很重要,小伙伴们都搞起来!
正文开始
一、结构体基础知识
1.结构体声明与变量的定义
struct tag
{
member-list;//成员列表
}variable-list;//变量列表(可以在这里直接创建变量也可不写)
例如描述一本书:(
特殊的声明:
声明结构时,可以不完全的声明。如,匿名结构体类型
//匿名结构体类型
struct
{
int a;
char b;
float c;
}s1,s2;//只能用一次,顺带着就直接创建变量s1,s2
int main()
{
struct s3;//这样是不行的的,连名字都没有,没法创建
return 0;
}
若两个结构在声明时都省略掉结构体标签:
struct
{
int a;
char b;
float c;
}s;
struct
{
int a;
char b;
float c;
}a[10], *p;
问:那么在上面代码基础上,这样的代码合法吗?
p = &x;
可见,尽管两结构体成员一模儿一样儿,编译器还是会把上面的两个声明当做完全不同的类型,所以是这段代码是非法的。
2.结构体自引用
:green_heart:自己如何找到和自己同类型的对象呢?
引:
那么从 节点1 如何找到 节点2 ?
//思考:像这样在一个结构体内包含为结构体本身的成员可以吗?
struct Node
{
int data;
struct Node n;
};
//如果可以,那sizeof(struct Node);是多少?
:x:不可以的,会无限递归下去,sizeof(struct Node);
也会无限大。
:star:正确的自引用方式
struct Node
{
int data;//数据域
struct Node* next;//指针域-->可以找到下一个节点
};
:white_check_mark:自己能够找到同类型的另一个对象---要存同类对象的地址
3.结构体变量的初始化和使用
:green_heart:初始化:
:green_heart:使用:
#include<stdio.h>
struct Point
{
int x;
int y;
};
struct S
{
double d;
struct Point p;
char name[20];
};
int main()
{
struct S s = { 3.14, { 3, 4 }, "laowang" };
struct S* ps = &s;
printf("%lf\n", s.d);
printf("%lf\n", ps->d);
printf("----------------\n");
printf("%d %d\n", s.p.x, s.p.y);
printf("%d %d\n", ps->p.x, ps->p.y);
printf("----------------\n");
printf("%s\n", s.name);
printf("%s\n", ps->name);
return 0;
}
运行结果:
二、结构体内存对齐
:star:掌握了结构体的基本使用后,将深入探讨如何 计算结构体大小。
:snowflake:这是特别热门的考点,大家 都搞起来!思考:这个结构体类型大小是多少?
没关系,不太知道为什么小伙伴往下读就一定懂了!
1.结构体的对齐规则
:snowflake:1. 结构体的 第一个成员永远放在==结构体起始位置偏移量为0==的地址处。
:snowflake:2. 第二个成员开始,总是放在==偏移量为对齐数整数倍的地址==处。
:snowman: 对齐数 == ==编译器默认的对齐数== 与 ==变量自身大小的== 较小值 ( vs的默认值为8
):snowflake:3. 结构体的总大小必须是各成员的对齐数中==最大对齐数的整数倍==。
看完规则你可能还是有点蒙,不过没关系,下面
上代码边分析边学习:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
:key:解析:
做几道练习来巩固一下吧!
练习2:
根据结构体内存对齐规则,思考下面结构体大小:
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
:key:解析:
练习3:
根据结构体内存对齐规则,思考下面结构体大小:
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
:key:解析
练习4---结构体嵌套问题
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d\n", sizeof(struct S4));
return 0;
}
这就先需要补充第四条规则:
:snowflake: 4. 如果嵌套了结构体,嵌套的结构体==对齐到自己的最大对齐数==。 整个结构体的大小是==所有成员最大对齐数==(含嵌套结构体的对齐数) 的整数倍。
:key:解析:
2. 结构体内存对齐的意义:
这个没有官方解释,大部分的参考资料如是说:
:fish: 1.平台原因
不是所有 硬件平台都能访问任意地址的数据;某些硬件平台只能在某些地址处读取特定类型的数据,否则会抛出硬件异常。
:fish: 2.性能原因
数据结构(尤其是栈)应该尽可能在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器可能要做两次内存访问;而对齐的内存访问仅需要一次访问, 提升了效率。(画图说明)
结构体的内存对齐是用时间换取空间的一种做法,那在设计结构体时,可以稍稍动一点脑筋,既满足对齐,也不要浪费太多空间。
:key:让占用空间小的成员尽量集中在一起。
例如:
struct S1
{
char c1;
int i;
char c2;
};//12byte
struct S2
{
char c1;
char c2;
int i;
};//8byte
尽管s1和s2类型的成员一模一样,但是占用空间的大小还是有一定区别。
3. 修改默认对齐数
之前我们见过#pragma
这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数:(vs默认对齐数为8,Linux没有默认对齐数)
上代码:
#include<stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置默认对齐数,恢复默认
#pragma pack(1)//相当于没有对齐(没有空间浪费,当然效率也比较低)
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置默认对齐数,恢复默认
int main()
{
printf("%d\n", sizeof(struct S1));//12byte
printf("%d\n", sizeof(struct S2));//6byte
return 0;
}
运行结果:
在结构体对齐方式不合适时,我们可以自己修改默认对齐数,但也不能乱改,一般为
2^n^ 。
4. 百度笔试题
题目:写一个宏,计算结构体中某成员相对结构体首地址的偏移量
考察:offsetof
宏的实现
这里小边还没有写有关宏的文章,在此只介绍offsetof
函数的使用。
#include<stdio.h>
#include<stddef.h>//@offsetof
struct S1
{
char c1;
int i;
char c2;
};
//offsetof是一个宏:这里居然是类型传参,后面还会聊
int main()
{
printf("%u\n", offsetof(struct S1, c1));
printf("%u\n", offsetof(struct S1, i));
printf("%u\n", offsetof(struct S1, c2));
return 0;
}
运行结果:
三、结构体传参
上代码:
思考:哪种打印函数更好些?
#include<stdio.h>
struct S
{
int data[1000];
int num;
};
void print1(struct S tmp)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", tmp.data[i]);
}
printf("\n%d\n ", tmp.num);
}
void print2(struct S* ps)//为了使ps指向内容不被修改,可以写成(const struct S* ps)
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n%d\n ", ps->num);
}
int main()
{
struct S s = { { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, 100 };
print1(s);
print2(&s);
return 0;
}
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候, 结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降, 效率低下。
:snowflake: 因此结构体传参时,最好还是穿结构体的地址。
四、结构体实现位段
1. 什么是位段
在此之前,你可能只听说过段位,没有听说过位段哈哈哈哈哈,不过呢,聊完结构体就要聊一聊结构体实现位段的能力:
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int
、unsigned int
、signed int
或char
,即整形家族。
2.位段的成员名后边有一个冒号和一个数字。比如:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
这是什么意思呢?那我们先来测一测它的大小:
printf("%d\n", sizeof(struct A));
运行结果:
好像与结构体相比,位段A变小了。
其实呀,位段中的"位"代表的就是二进制位,:key:位段就是可以节省空间的!
生活中的有些值,不需要太多的存储空间,比如表示性别,男00--女01--保密11,只需要两个比特位即能表示,
struct A
{
int _a : 2;//_a 2个bit位
int _b : 5;//_b 5个bit位
int _c : 10;//_c 10个bit位
int _d : 30;//_d 30个bit位
};//47个bit位
看起来,好像6byte就足够了,那位段A的大小为什么是8byte?
那位段的内存分配究竟是怎样的呢?
2. 位段的内存分配
:snowflake:1.位段的成员必须是int
、unsizgned int
、signed int
或char
,即 整型家族。
:snowflake:2.==位段的空间==是按照需要以 4个字节( int)或者 一个字节( char)的方式来开辟。
:snowflake:3.位段涉及了很多不确定因素,位段是 不跨平台的,注重可移植的程序应该避免使用位段。那我们再来分析一下
struct A
的内存分配过程。
再分析一段代码:
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;
}
在此,我们先按照如下猜想进行内存分配:
为了探寻究竟,我们调试起来监视一下:(vs2013环境测试数据)
由此可见,接过与我刚刚分析出来的一样:
:snowman: 开辟空间 --- 一次一个字节
:snowman: 放数据 --- 从低位到高位使用,紧挨着使用;若高位空间不够用,则浪费掉,重新开辟新的字节
但这也仅仅是在vs上成立。
2.3 位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器 -- int 2byte = 16bit,32位机器最大32,假如写成27,在16位机器会出问题。
- 位段中的成员一个整型/字节内部在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,而第一个位段剩余的位无法容纳时,是浪费还是利用剩余的位,这是不确定的。
与结构相比,位段作用相似,结构能使用的地方,位段设计合理也能实现。可以很好的节省空间,但是也有跨平台的问题存在。
2.4 位段的应用
本文完
接下来的文章将介绍有关枚举与联合的相关内容。