前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【熟视C语言】自定义类型之结构体详解(内存对齐,位段)

【熟视C语言】自定义类型之结构体详解(内存对齐,位段)

作者头像
Crrrush
发布2023-06-23 14:32:18
1850
发布2023-06-23 14:32:18
举报
文章被收录于专栏:后端开发练级指南

写在前面

本篇文章为自定义类型系列讲解的第一篇,而本篇文章讲解的时自定义类型的第一部分内容——结构体。同时,本篇文章也是结构体内容的详解,希望对你的结构体学习有所帮助。

1 结构体的声明

代码语言:javascript
复制
//结构体声明例1
struct MyStruct
{
	int a;
	char b;
};

这是一个简单的结构体声明,相信不少读者对这个声明都能信手拈来,但是结构体声明的知识远没有那么浅显。

1.1 结构体基础概念

简单地说,结构体是几个类型的值的集合,这些类型被称为成员变量,成员变量的类型可以不一样,而这个集合组成一个结构,组成一个新的类型。

1.2 声明

声明的方法和例1一样,很简单直白。这里举两个例子:

代码语言:javascript
复制
//例2
struct Student
{
       char name[20];
       int age;
       char sex[8];
       char phnum[20];
};
//例3
struct Commodity
{
       char name[30];
       int price;
       int num;
};

但是有一种特殊声明,可以在声明结构时,进行不完全的声明。例如

代码语言:javascript
复制
//例4
struct 
{
       int a;
       char b;
       int c;
}a;
//例5
struct 
{
       int a;
       char b;
       int c;
}*p;

这种结构体被称为匿名结构体,省略了结构体标签,这也意味着除了一开始声明时定义的变量,后面是无法再想创建这个类型的变量的。 此外,例4和例5的成员变量是一样的,那么是否可以认为这两个结构体是同一个类型呢?

代码语言:javascript
复制
p = &a;

这一句代码在vs2022上运行会出现变量类型不兼容的警告。说明即使是成员变量相同,编译器也认为这是两个完全不同的结构体。

1.3 自引用

代码语言:javascript
复制
//例6
struct Example
{
       int num;
       struct Example next;
};
//例7
struct Example
{
       int num;
       struct Example* next;
};

例6的自引用似乎看起来比例7的更方便直接,但是实际上是否可行呢?试想一下,当在以例6的方法定义时,用sizeof(struct Example)求结构体大小时,这个大小该是多少。所以例6的写法其实是错误的。 注意:无法使用typedef重命名后的结构体变量名称来定义结构体成员变量。例子:

代码语言:javascript
复制
//错误示范
typedef struct
{
       int num;
       Example* next;
}Example;
//正确示范
typedef struct Example
{
       int num;
       struct Example* next;
}Example;

1.4 定义与初始化

这部分比较简单,下面我放几个例子解释一下如何定义和初始化。

代码语言:javascript
复制
struct Stu
{
       char name[20];
       int age;
}a;//声明结构体类型并定义一个变量
struct Stu b;//定义一个结构体变量


struct Candy
{
       char name[30];
       int price;
       int num;
}n = { "chewing gum",10,1 };//声明结构体类型,定义并初始化一个变量
struct Candy m = { "Mints",6,1 };//定义并初始化一个变量


struct Reward
{
       struct Stu stu;
       struct Candy candy;
};
struct Reward a = { {"judy",6},{"lollipop",20,5} };//嵌套定义初始化

1.5 结构体内存对齐

研究结构体的内存对齐自然是用来计算结构体内存大小的,同时,这也是一个非常热门的考点,一定要掌握好。下面贴出几个代码做实验,如果可以的话,建议将先代码复制到编译器运行看是什么结果。

代码语言:javascript
复制
//实验1
struct S1
{
       char a;
       int b;
       char c;
};
//printf("%d\n", sizeof(struct S1));//计算结构体S1的大小

//实验2
struct S2
{
       char a;
       char b;
       int c;
};
//printf("%d\n", sizeof(struct S2));//计算结构体S2的大小

下图是这两个实验代码运行的结果。

很明显,虽然这两个结构体的成员变量相同,但是显示的的所占空间大小却不是一样的。所以很明显,在一个结构体变量中,结构体的成员变量的内存分布并不是紧挨着的,而是存在一定程度上的间隔,而这些间隔正是内存对齐这个机制所产生的。 那么,要想知道结构体的内存大小是如何计算的,就要先理解内存对齐的规则:

  1. 成员变量的存储位置总是在结构体对齐数整数倍的偏移量的位置。由于偏移量是从0开始计算起的,所以第一个变量的对齐数的整数倍是0,也就是说第一个成员变量在结构体偏移量为0的位置。 其中,对齐数 = 自身内存大小和默认对齐数两个数中的较小值 (vs的默认值是8,且据笔者所知,大部分编译器是没有设置这个默认对齐数的)
  2. 结构体的大小为结构体成员变量中的最大对齐数的整数倍。
  3. 如果是嵌套了结构体的情况,那么嵌套结构体的对齐数是自身成员变量中最大对齐数,这个结构体的大小依然是成员(含嵌套结构体)变量的最大对齐数的整数倍

以上就是内存对齐的规则。其中,结构体成员变量的偏移量可以用宏offsetof求得。S1,S2内存对齐的情况如图所示:

对于S1char a在偏移量为0的位置,之后三个位置由于都不是int b对齐数4的整数倍,所以直接跳过,从偏移量为4的位置开始存入int b,存完数据后到了偏移量为8的位置,为对齐数1的整数倍,存入char c,但此时结构体整体的内存并没有停止,由于此时总大小为9,不是成员变量中最大对齐数4的整数倍,所以继续往下占用空间,直到总大小为4的整数倍为止。 对于S2第一个成员变量char a在偏移量为0的位置,而第二个成员变量char b则直接在偏移量为1的位置,之后跳过两个位置在偏移量为4的位置存储int c。此时总大小为8,是结构体成员变量最大对齐数的整数倍,所以不再继续往下占用空间,结构体总大小为8。 下图是用offsetof求成员变量的偏移量验证刚刚的推导,事实与结论符合。

再给出下面两个实验代码,我会给出分析图和运行结构,请自行分析。

代码语言:javascript
复制
//实验3
struct S3
{
       double a;
       char b;
};
//printf("%d\n", sizeof(struct S3));//计算结构体S3的大小

//实验4
struct S4
{
       struct S3 a;
       int b;
       char c;
};
//printf("%d\n", sizeof(struct S4));//计算结构体S4的大小

在分析完以上几个结构体的存储时,我们发现内存对齐有时可能会浪费大量的空间,可能会有人会有疑问,既然内存对齐会造成那么大的空间浪费,那为什么当初设计的时候要这样设计呢? 内存对齐的设计,大体上考虑以下两个因素:

  1. 不是所有的平台都能访问任意位置的任意类型的数据的。某些硬件平台只能在特定位置取出特定数据,若不存在内存对齐则会出现硬件异常。这是考虑了代码的泛用性。
  2. 数据结构(尤其是栈)应该让数据尽可能靠近自然边界。 这个比较难理解,由于处理数据时要用处理器上的寄存器,而寄存器访问内存数据时并不是一个一个字节访问的,一个寄存器的宽度是32位或者64位(根据平台而定),也就是说是一块一块空间访问的,而内存不对齐是必然会出现某一块空间的数据是属于两个变量的,这时候需要寄存器重新访问一遍空间以获取另外一个数据。这就造成了效率上的降低。

总的来说,内存对齐是一个以空间换时间的设计。

1.6修改默认对齐数

代码语言:javascript
复制
#pragma pack(4)//设置默认对齐数为8
struct S5
{
       double a;
       int b;
       char c;
};
#pragma pack()//取消设置,还原默认对齐数

在对齐方式不合适时,可以这样修改默认对齐数。

2 位段

2.1 位段是什么

位段和结构体类似,只是位段的成员类型必须是int\unsigned int\signed int\char(整型家族)中的一种,并且每个成员变量后面会有一个冒号加数字。例如:

代码语言:javascript
复制
//位段演示
struct Se
{
       int a : 3;
       char b : 3;
       int c : 5;
};

这个数字的意义就是为这个成员变量开辟的空间,单位是bit。

2.2位段的内存分配

位段的内存开辟是按一个字节(char)或者四个字节(int)来的。且位段涉及很多不确定因素,不跨平台,设计可移植的程序时应该避免使用位段。下面是位段的空间使用说明。

代码语言:javascript
复制
struct Se
{
       char a : 3;//3bit
       char b : 4;//4bit
       char c : 5;//5bit
       char d : 4;//4bit
};

int main()
{
       struct Se s = { 0 };

       s.a = 10;//二进制:1010
       s.b = 12;//二进制:1100
       s.c = 3;//二进制:11
       s.d = 4;//二进制:100

       printf("%d\n", s.a);//3bit 1010 -> 010 -> 2 
       printf("%d\n", s.b);//4bit 1100 -> 1100(最高位为符号位,进行原反补计算) -> -4
       printf("%d\n", s.c);//5bit 11 -> 00011 -> 3
       printf("%d\n", s.d);//4bit 100 -> 0100 -> 4

       return 0;
}

代码运行结果

2.3位段的跨平台问题

  1. int被当成有符号数还是无符号数是不确定的。上文代码中的情况就是被编译器当作有符号数。
  2. 位段的最大位数不确定。如16位的机器最多16和32位的机器最多,写有27位的成员的位段显然不能在16位的机器上运行。
  3. 位段成员的数据是从左到右还是从右到左尚未又标准规定。
  4. 例如上文位段代码,成员a,b并未将第一个字节的空间占满,那么成员c是否会利用起来那个剩下的空间是不确定的。

结语

好的,到这里,自定义类型的第一部分也就是最长最难啃的部分就完结了,非常感谢各位读者能读完这篇文章,如果你觉得做的还不错的话,可以点赞收藏分享,让更多的朋友知道,当然,如果你觉得有什么问题的话也欢迎在评论区留言或私信告诉我哦!下期再会!

彩蛋

在下方的两个仓库可以获取我这篇文章的分析图和源码哦。 gitee:路径:Custom type\structs GitHub:路径:Custom type\structs

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 1 结构体的声明
    • 1.1 结构体基础概念
      • 1.2 声明
        • 1.3 自引用
          • 1.4 定义与初始化
            • 1.5 结构体内存对齐
              • 1.6修改默认对齐数
              • 2 位段
                • 2.1 位段是什么
                  • 2.2位段的内存分配
                    • 2.3位段的跨平台问题
                    • 结语
                    • 彩蛋
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档