椋鸟C语言笔记#28:匿名结构体、结构体的内存对齐、结构体传参、位段

2023-12-18 21:28

本文主要是介绍椋鸟C语言笔记#28:匿名结构体、结构体的内存对齐、结构体传参、位段,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

萌新的学习笔记,写错了恳请斧正。


目录

在定义结构体时起别名

匿名结构体

结构体的自引用

结构体的内存对齐

offsetof

内存对齐练习

为什么要内存对齐

平台原因

性能原因

书写规范

修改默认对齐数

结构体传参

位段(位域)

位段的概念

位段的声明

位段的内存分配

位段的特殊声明

位段的跨平台性

位段注意事项


关于结构体的基本内容(包括结构体的声明、创建、初始化、结构成员访问)已经在笔记#15中讲述,不再赘述。

在定义结构体时起别名

定义结构体时前面直接加typedef进行起别名的操作不会影响结构体的创建。

typedef struct a
{int a;float b;char c;
} sta;

这就是定义了一个结构体类型struct a,然后给它起别名为类型sta

但是这样就不能在定义结构体类型的时候直接创建结构体变量了

注意(相关内容看下面):

  • 匿名结构体可以起别名,这样就能正常使用了
  • 自引用同时起别名不能用别名自引用(创建的优先顺序高于起别名)

匿名结构体

结构体在声明时,其实可以省略结构体标签(名称)。如下:

struct
{int a;float b;char c;
} a;

这就是匿名的创建了一个结构体并声明了一个该类型的结构体变量。

注意,在不起别名的情况下:

  • 匿名创建结构体如果没有直接声明几个对应的结构体变量,之后就再也不能申请了。
  • 匿名创建结构体之后也再也无法找到这个结构体类型了。
  • 两个成员相同的匿名结构体不会被认为是同一种结构体,比方说:
struct
{int a;float b;char c;
} a;struct
{int a;float b;char c;
} b, *p;

在上面这种情况下,如果令p等于&a就是非法的,因为两个匿名结构体类型不一样。

但是我们可以在创建匿名结构体的时候起别名,这样就能通过别名正常使用结构体了:

typedef struct
{int a;float b;char c;
} st;

比方说上面这段代码就能继续通过st这个类型名继续进行创建变量等操作

结构体的自引用

结构体可以自引用,常用于链表(以后讲)

当然这不是说结构体的成员可以是该结构体本身

(如果这样就无限套娃了,大小无穷大)

结构体的自引用指的是结构体的成员变量可以是该结构体的指针类型

比方说:

struct chain
{int data;struct chain* next;
};

如果起别名和自引用同时进行的话,自引用的地方不能用别名

比方说这样是不行的:

typedef struct chain
{int data;st* next;
} st;

应该写成这样(这边顺便把指针起别名了):

typedef struct chain
{int data;struct chain* next;
} st;typedef st* pst;

结构体的内存对齐

我们现在研究一下结构体的内存大小

结构体类型占内存的大小是不是等于所有成员变量占内存的和呢?

我们写一段程序验证一下:

#include <stdio.h>struct S1
{char c1;int i;char c2;
};int main()
{printf("%d\n", sizeof(struct S1));return 0;
}

这段代码在Win11 VS2022 x64 Debug的环境下输出12

而我们知道如果单纯的成员变量大小相加,答案应该为6

所以,结构体内存究竟是如何排布的呢?

其实结构体在内存中的排布,遵循内存对齐规则

1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
        对齐数 = 编译器默认的对齐数该成员变量大小 的 较小值

        VS2022 的默认对齐数为8,Linux gcc没有默认对齐数

3. 结构体总大小为结构体成员中最大的对齐数的整数倍

4. 如果结构体嵌套了结构体,嵌套的结构体成员对齐到自己成员中最大对齐数的整数倍处,并且占据单独计算自身大小那么大的空间(因为内存对齐浪费的空间不会被下一个成员利用)。结构体总大小就是所有对齐数(包括嵌套结构体中的成员)中最大对齐数的整数倍

offsetof
//定义于头文件 <stddef.h> 
#define offsetof(type, member) /*implementation-defined*/ 

在stddef.h头文件中有一个宏offsetof,可以返回一个成员在结构体中的偏移量

其第一个参数是结构体类型名,第二个参数是成员变量名

其返回值可以用%zd、%zu接收

内存对齐练习
#include <stdio.h>struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};struct S3
{double d;char c;char i;
};struct S4
{char c1;struct S3 s3;char d;
};int main()
{printf("%d\n", sizeof(struct S1));printf("%d\n", sizeof(struct S2));printf("%d\n", sizeof(struct S3));printf("%d\n", sizeof(struct S4));return 0;
}

在Win11 VS2022 x64 Debug的环境下,4段输出为12、8、16、32

为什么要内存对齐

其实这是一种空间换时间的做法

平台原因

不是所有的硬件平台都能访问任意地址上的任意数据,某些平台只能在某些地址处(对齐的位置)取对应类型的数据,否则硬件异常。

性能原因

内存对齐的情况下访问速度一般会更快

访问未对齐内存的数据,处理器可能需要作两次内存访问(内存是一段一段访问的,数据不对齐可能存放在两个内存的访问段内),而对齐的内存访问仅需要一次访问

书写规范

所以为了节省空间,我们创建结构体应该尽量使较小的成员变量在前面,较大的放在后面

修改默认对齐数

我们可以使用预处理指令#pragma来修改编译器默认的对齐数

#include <stdio.h>#pragma pack(1)    //设置默认对⻬数为1struct S
{char c1;int i;char c2;
};#pragma pack()    //取消设置的对⻬数,还原为默认int main()
{printf("%d\n", sizeof(struct S));return 0;
}

上面这段代码的输出结果就为6

结构体传参

与其他数据类型类似,结构体传参也分为直接传参与传地址两种

#include <stdio.h>struct S
{int data[3];int num;
} s = { {1,2,3}, 1000 };void print1(struct S s)
{printf("%d\n", s.num);
}void print2(struct S* ps)
{printf("%d\n", ps->num);
}int main()
{print1(s); //传结构体print2(&s); //传地址return 0;
}

两种方式作用相同,但是我们优先使用传地址的方式

因为函数传参时需要拷贝实参作为形参压栈,如果传递结构体本身会占用较多的内存

位段(位域)

位段的概念

位段是一种特殊的结构体类型,其成员的内存宽度可以被我们规定

位段成员必须是int、signed、unsigned之间的一种(C99以前)

C99标准开始,位段成员也可以使用布尔类型

位段的声明

位段的声明与结构体类似,但是成员名(可以省略代表直接浪费一段空间)后有一个冒号和数字:

#include <stdio.h>struct A
{int _a : 2;signed _b : 5;unsigned _c : 10;int _d : 30;
};int main()
{printf("%d\n", sizeof(struct A));return 0;
}

这段程序在Win11 VS2022 x64 Debug的环境下的输出结果为8

为什么呢?这与位段的内存分配有关

位段的内存分配

位段声明中冒号后面的数字就代表了其被规定占据多少个比特位

而整个位段总大小是按4个字节(int类)或者1个字节(_Bool)逐步分配

上述代码中位段A,内存申请了一次4个字节(32位)。这32位填充了_a、_b、_c后只剩下15位了,发现不够继续填充_d,就再次申请了4个字节用来填充数据_d,所以总共占据了8个字节。

位段的特殊声明

相邻的几段如果类型占据的空间大小一致可以打包起来写在一起(通常可以),比方说:

struct B
{int _a : 2, _b : 5,  _c : 10;int _d : 30;
};    //宽8

规定空间可以省略,代表占据一整个类型的空间,比方说:

struct B
{int _a : 2, _b : 5,  _c : 10;int _d;
};    //宽8

这里_d就占据了整个4字节(32位)的空间

成员名可以省略用来占据一定不被使用的空间:

struct C
{unsigned _a : 2;signed _b : 5;int _c : 30, _d : 1, _e : 3;int _f : 3, :2, _g : 4;
};    //宽12

这里_f和_g直接有两个比特是被占位的

如果宽度规定为0(即零位域,必须未命名)代表直接开始下一个分配单元,这边剩下来的丢掉:

struct D
{unsigned _a : 2, :0;signed _b : 5, :0;int _c : 30, _d : 1, _e : 3;int _f : 3, : 2, _g : 4;
};    //宽16
位段的跨平台性

位段跨平台性很差,原因如下:

  • int位段被当做有符号还是无符号是不确定的
  • 机器的位数不一样导致类型宽度不一样
  • 位段在每个分配单元中数据从左往右填还是从右往左填不确定
  • 还有很多其他原因

所以,虽然位段很省空间,没事还是不要用位段

位段注意事项

位段不能取地址,不能有指针变量,会报错

因为位段的成员的起始位置可以不在整字节处,没有地址


这篇关于椋鸟C语言笔记#28:匿名结构体、结构体的内存对齐、结构体传参、位段的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/509802

相关文章

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.

Java内存分配与JVM参数详解(推荐)

《Java内存分配与JVM参数详解(推荐)》本文详解JVM内存结构与参数调整,涵盖堆分代、元空间、GC选择及优化策略,帮助开发者提升性能、避免内存泄漏,本文给大家介绍Java内存分配与JVM参数详解,... 目录引言JVM内存结构JVM参数概述堆内存分配年轻代与老年代调整堆内存大小调整年轻代与老年代比例元空

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

MySQL中的索引结构和分类实战案例详解

《MySQL中的索引结构和分类实战案例详解》本文详解MySQL索引结构与分类,涵盖B树、B+树、哈希及全文索引,分析其原理与优劣势,并结合实战案例探讨创建、管理及优化技巧,助力提升查询性能,感兴趣的朋... 目录一、索引概述1.1 索引的定义与作用1.2 索引的基本原理二、索引结构详解2.1 B树索引2.2

Go语言代码格式化的技巧分享

《Go语言代码格式化的技巧分享》在Go语言的开发过程中,代码格式化是一个看似细微却至关重要的环节,良好的代码格式化不仅能提升代码的可读性,还能促进团队协作,减少因代码风格差异引发的问题,Go在代码格式... 目录一、Go 语言代码格式化的重要性二、Go 语言代码格式化工具:gofmt 与 go fmt(一)

如何使用Maven创建web目录结构

《如何使用Maven创建web目录结构》:本文主要介绍如何使用Maven创建web目录结构的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录创建web工程第一步第二步第三步第四步第五步第六步第七步总结创建web工程第一步js通过Maven骨架创pytho

Python循环结构全面解析

《Python循环结构全面解析》循环中的代码会执行特定的次数,或者是执行到特定条件成立时结束循环,或者是针对某一集合中的所有项目都执行一次,这篇文章给大家介绍Python循环结构解析,感兴趣的朋友跟随... 目录for-in循环while循环循环控制语句break语句continue语句else子句嵌套的循