椋鸟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

相关文章

R语言中的正则表达式深度解析

《R语言中的正则表达式深度解析》正则表达式即使用一个字符串来描述、匹配一系列某个语法规则的字符串,通过特定的字母、数字及特殊符号的灵活组合即可完成对任意字符串的匹配,:本文主要介绍R语言中正则表达... 目录前言一、正则表达式的基本概念二、正则表达式的特殊符号三、R语言中正则表达式的应用实例实例一:查找匹配

Go语言结构体标签(Tag)的使用小结

《Go语言结构体标签(Tag)的使用小结》结构体标签Tag是Go语言中附加在结构体字段后的元数据字符串,用于提供额外的属性信息,这些信息可以通过反射在运行时读取和解析,下面就来详细的介绍一下Tag的使... 目录什么是结构体标签?基本语法常见的标签用途1.jsON 序列化/反序列化(最常用)2.数据库操作(

MySQL快速复制一张表的四种核心方法(包括表结构和数据)

《MySQL快速复制一张表的四种核心方法(包括表结构和数据)》本文详细介绍了四种复制MySQL表(结构+数据)的方法,并对每种方法进行了对比分析,适用于不同场景和数据量的复制需求,特别是针对超大表(1... 目录一、mysql 复制表(结构+数据)的 4 种核心方法(面试结构化回答)方法 1:CREATE

C语言逗号运算符和逗号表达式的使用小结

《C语言逗号运算符和逗号表达式的使用小结》本文详细介绍了C语言中的逗号运算符和逗号表达式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习... 在C语言中逗号“,”也是一种运算符,称为逗号运算符。 其功能是把两个表达式连接其一般形式为:表达

Go语言实现桥接模式

《Go语言实现桥接模式》桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,使它们可以独立地变化,本文就来介绍一下了Go语言实现桥接模式,感兴趣的可以了解一下... 目录简介核心概念为什么使用桥接模式?应用场景案例分析步骤一:定义实现接口步骤二:创建具体实现类步骤三:定义抽象类步骤四:创建扩展抽象类步

GO语言实现串口简单通讯

《GO语言实现串口简单通讯》本文分享了使用Go语言进行串口通讯的实践过程,详细介绍了串口配置、数据发送与接收的代码实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 目录背景串口通讯代码代码块分解解析完整代码运行结果背景最近再学习 go 语言,在某宝用5块钱买了个

Java JAR 启动内存参数配置指南(从基础设置到性能优化)

《JavaJAR启动内存参数配置指南(从基础设置到性能优化)》在启动Java可执行JAR文件时,合理配置JVM内存参数是保障应用稳定性和性能的关键,本文将系统讲解如何通过命令行参数、环境变量等方式... 目录一、核心内存参数详解1.1 堆内存配置1.2 元空间配置(MetASPace)1.3 线程栈配置1.

GO语言zap日志库理解和使用方法示例

《GO语言zap日志库理解和使用方法示例》Zap是一个高性能、结构化日志库,专为Go语言设计,它由Uber开源,并且在Go社区中非常受欢迎,:本文主要介绍GO语言zap日志库理解和使用方法的相关资... 目录1. zap日志库介绍2.安装zap库3.配置日志记录器3.1 Logger3.2 Sugared

Go语言中如何进行数据库查询操作

《Go语言中如何进行数据库查询操作》在Go语言中,与数据库交互通常通过使用数据库驱动来实现,Go语言支持多种数据库,如MySQL、PostgreSQL、SQLite等,每种数据库都有其对应的官方或第三... 查询函数QueryRow和Query详细对比特性QueryRowQuery返回值数量1个:*sql

GO语言中gox交叉编译的实现

《GO语言中gox交叉编译的实现》本文主要介绍了GO语言中gox交叉编译的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录一、安装二、使用三、遇到的问题1、开启CGO2、修改环境变量最近在工作中使用GO语言进行编码开发,因