【C】深度剖析结构体@自定义类型 —— 结构体内存对齐 | 结构体实现位段

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 结构体内存对齐

@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.位段的成员必须是 intunsigned intsigned intchar,即整形家族。
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.位段的成员必须是 intunsizgned intsigned intchar,即 整型家族。
: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 位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器 -- int 2byte = 16bit,32位机器最大32,假如写成27,在16位机器会出问题。
  3. 位段中的成员一个整型/字节内部在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,而第一个位段剩余的位无法容纳时,是浪费还是利用剩余的位,这是不确定的。

与结构相比,位段作用相似,结构能使用的地方,位段设计合理也能实现。可以很好的节省空间,但是也有跨平台的问题存在。

2.4 位段的应用

在这里插入图片描述
本文完

接下来的文章将介绍有关枚举与联合的相关内容。

相关文章
|
6月前
|
编译器 Linux C语言
结构体内存对齐
结构体内存对齐
53 0
|
6月前
|
编译器 Linux C语言
详解结构体内存对齐及结构体如何实现位段~
详解结构体内存对齐及结构体如何实现位段~
|
6月前
|
存储 C语言
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(下)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
42 0
|
6月前
|
编译器 C语言 C++
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(上)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
22 0
|
6月前
|
存储 C语言
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)(中)
C语言进阶⑮(自定义类型)(结构体+枚举+联合体)(结构体实现位段)
36 0
|
6月前
|
存储 网络协议 编译器
【C语言】自定义类型:结构体深入解析(三)结构体实现位段最终篇
【C语言】自定义类型:结构体深入解析(三)结构体实现位段最终篇
|
6月前
|
存储 编译器 C语言
自定义类型:结构体(自引用、内存对齐、位段(位域))
自定义类型:结构体(自引用、内存对齐、位段(位域))
|
测试技术 编译器 C语言
自定义类型知识详解(结构体,位段,枚举,联合体)
结构体是一些值的集合,这些值可以是不同类型的变量,被称为成员变量。
79 0
|
编译器 Linux C++
结构体的内存对齐
结构体的内存对齐
|
编译器 C++
结构体内存对齐问题
结构体重点😃 1.结构体内存对齐问题,是在计算结构体的大小时,对结构体成员在内存中的位置进行研究的问题。