自定义类型:结构体(自引用、内存对齐、位段(位域))

2023-12-03 09:04

本文主要是介绍自定义类型:结构体(自引用、内存对齐、位段(位域)),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

一. 结构体类型的声明和定义

1.1结构体相关概念

1.11结构的声明

1.12成员列表

1.2定义结构体类型变量的方法

1.21先声明结构体类型再定义变量名

​​​​1.22在声明类型的同时定义变量

1.23直接定义结构类型变量

二、结构体变量的创建、初始化​和访问

2.1结构体成员的直接访问​

2.2结构体成员的间接访问​

2.3匿名的结构体类型

     

三、结构的自引用

 自引用的使用案例:

四、计算结构体的大小(结构体内存对齐)

4.1计算结构体大小示例:

4.2偏移量计算的示例:

4.3嵌套结构体的计算:

4.4为什么存在内存对齐?​

4.5修改默认对齐数​

五、结构体传参

六、结构体位段(位域)实现

6.1位段(位域)的介绍

6.2内存分配

6.3位段的跨平台问题​

6.4位段的应用​

6.5位段使用的注意事项​


一. 结构体类型的声明和定义

在实际问题时,有时候我们需要其中的几种一起来修饰某个变量,例如一个学生的信息就需要成绩(整型),姓名(字符串),年龄(整型)等等,这些数据类型都不同但是他们又是表示一个整体,要存在联系,那么我们就需要一个新的数据类型,结构体。

(数组是一组相同类型的元素集合)

1.1结构体相关概念

1.11结构的声明

结构体由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据是互相联系的。

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

声明一个结构休类型的一般形式如下:

struct 结构体名
{成员列表};

1.12成员列表

成员列表称为域表,第一个成员也称为结构体中的一个域。成员名定名规则写变量名同。

成员列表:

类型名 成员名;

1.2定义结构体类型变量的方法

1.21先声明结构体类型再定义变量名

struct student{  
    成员表列
}student1, student2 //结构体变量名

​​​​1.22在声明类型的同时定义变量

struct 结构体名
{
    成员表列
}变量名表列;

1.23直接定义结构类型变量

struct
{
    成员表列
}变量名表列;

二、结构体变量的创建、初始化​和访问

2.1结构体成员的直接访问​


结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数
使用方式:结构体变量.成员名

int main()
{
 //按照结构体成员的顺序初始化​
 struct Stu s = { "张三", 20, "男", "20230818001" };
 printf("name: %s\n", s.name);
 printf("age : %d\n", s.age);
 printf("sex : %s\n", s.sex);
 printf("id : %s\n", s.id);
 
 //按照指定的顺序初始化​
 struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女")
 printf("name: %s\n", s2.name);
 printf("age : %d\n", s2.age);
 printf("sex : %s\n", s2.sex);
 printf("id : %s\n", s2.id);
 return 0;
}

2.2结构体成员的间接访问​

有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。
使用方式:结构体指针->成员名

struct Point
{
    int x;
    int y;
};
int main()
{
    struct Point p = { 3, 4 };
    struct Point* ptr = &p;
    ptr->x = 1;
    ptr->y = 2;
    printf("x = %d y = %d\n", ptr->x, ptr->y);
    return 0;
}

2.3匿名的结构体类型

匿名结构体类型,也称为未命名结构体,是指在定义结构体成员时省略了结构体的名字,直接定义其成员。由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。匿名结构体类型的作用域仅限于包含它的联合体,它不能在其他地方被引用。

//匿名结构体类型
//只能使用一次
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}a[20], * p;

问:在上面代码的基础上,下面的代码合法吗?​

编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。

   .

     

三、结构的自引用

注:结构体自引用方式里面必须包含同类型的结构体指针

在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表的节点:

如果这样编写

struct Node
{
 int data;
 struct Node next;
};

sizeof(struct Node) 是多少?

仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大
小就会无穷的大,是不合理的。

正确的自引用方式:

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

 是否可以使用匿名结构体呢?

在结构体自引用使用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看
下面的代码,可行吗?
typedef struct
{
    int data;
    Node* next;
}Node;
答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的

解决方案如下:定义结构体不要使用匿名结构体了

   
typedef struct Node
{
    int data;
    struct Node* next;
}Node;

 自引用的使用案例:

链表:

在链表中,每个节点都包含数据和指向下一个节点的指针,这个指针就是自引用,它指向下一个相同类型的节点。这种结构可以使得链表在内存中灵活存储,并且不要求数据元素连续存放,从而大大提高存储器的使用效率。

四、计算结构体的大小(结构体内存对齐)

对齐规则​:

  • 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

  • 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。

  • 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的 整数倍。

  • 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

问:为什么结构体中的成员相同但占用的空间不同?

答:因为要对齐。

4.1计算结构体大小示例:

 

因为其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,所以有浪费的内存空间。

                                       对齐是从对齐数的倍数地址开始

在上述结构体S1和S2中,c1和c2都为char类型,占一个字节,对齐数为1。

i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。

所以此处S1、S2大小分别为8、12个字节。 

struct S1
{char c1;char c2;int i;
};struct S2
{char c1;int i;char c2;
};int main()
{struct S1 s1 = { 0 };//8struct S2 s2 = { 0 };//12printf("%d\n", sizeof(struct S1));//8printf("%d\n", sizeof(struct S2));//12return 0;
}

此处也有一个偏移量的概念,即相对于起始位置偏移的数值。

4.2偏移量计算的示例:

struct Example {  char a;      // 1字节  int b;       // 4字节  short c;     // 2字节  double d;    // 8字节  
};

4.3嵌套结构体的计算:

计算时把嵌套中的结构体作为一个数据类型去计算,最大对齐数也要与其比较,故有时候会出现最大对齐数为3,5的情况(结构体中包含一个char数组,数组大小为奇数)

在上述结构体S3和S4中,c和c1都为char类型,占一个字节,对齐数为1。

i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。

d为双精度浮点型,占8个字节,对齐数为8个字节,从8的倍数的地址开始对齐。

S3为结构体,占16个字节,但最大对齐数为8个字节,所以从8的倍数开始对齐。

所以S3占16个字符,S4占32个字符。

   

 .

4.4为什么存在内存对齐?​

结构体的内存对齐是拿空间来换取时间的做法

1. 平台原因 (移植原因):​
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。​

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

4.5修改默认对齐数​

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。

#pragma pack(1)//设置默认对齐数为1​
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认​
int main()
{//输出的结果是什么?​printf("%d\n", sizeof(struct S));return 0;
}

五、结构体传参

  • 值传递: 在值传递中,结构体的副本被传递给函数。这意味着函数内部对结构体所做的任何修改都不会影响到原始的结构体。这种传递方式适用于小型结构体,因为结构体的副本需要占用额外的内存空间。

  • 指针传递: 在指针传递中,结构体的地址被传递给函数,函数内部通过使用指针来访问和修改结构体的内容。这种方式可以避免结构体的副本创建,因此对于大型结构体更为高效。同时,函数内部对结构体的修改会影响到原始的结构体。

// 定义一个结构体 S,包含一个整数数组 data 和一个整数 num  
struct S  
{  int data[1000]; // data 是一个可以存储1000个整数的数组  int num;       // num 是一个整数  
};  // 通过值传递的方式打印结构体的内容  
void printf1(struct S t) // 通过值传递接收一个 S 类型的结构体  
{  printf("%d %d\n", t.data[0], t.num); // 打印结构体的 data 数组的第一个元素和 num 的值  
}  // 通过指针传递的方式打印结构体的内容  
void printf2(struct S* ps) // 通过指针传递接收一个 S 类型的结构体的指针  
{  printf("%d %d\n", ps->data[0], ps->num); // 使用指针访问并打印结构体的 data 数组的第一个元素和 num 的值  
}  int main() // 主函数  
{  // 初始化一个 S 类型的结构体 s,并为其 data 数组和 num 赋值 struct S s = { {1,2,3,4,5},100 }; // data 数组初始化为 {1,2,3,4,5},num 初始化为 100  printf1(s);  // 通过值传递调用 printf1 函数,打印 s 的内容  printf2(&s); // 通过指针传递调用 printf2 函数,打印 s 的内容(取 s 的地址传递给函数)  return 0; // 主函数返回 0,表示程序正常结束  
}

问:上面的 print1 和 print2 函数哪个好些?
答:首选print2函数。​

原因:

  • 函数传参的时候,参数是需要压栈,会有时间和空间上的损耗。

  • 如果传递一个结构体对象的时候,结构体过大,参数压栈的的损耗比较大,所以可能导致性能的下降。

六、结构体位段(位域)实现

6.1位段(位域)的介绍

(有些资料里称为“位段”,也有的称为“位域”)

C 语言的位域(bit-field)是一种特殊的结构体成员,允许我们按位对成员进行定义,指定其占用的位数。

位段的声明和结构是类似的,有两个不同:

  • 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。

  • 位段的成员名后边有一个冒号和一个数字。

以下是VS 中的实例:

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;// 2+5+10+30 = 47bit
};
//变量名
//1.字母,数字,下划线
//2.不能是数字开头int main()
{printf("%d\n", sizeof(struct A));//8 - 64bit
}

此处为什么struct A是64bit而不是48bit呢?

这就涉及到位段的内存分配了!

6.2内存分配

  • 位段的成员可以是 int unsigned int signed int 或者是 char 等类型。

  • 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

以下是VS 中的实例:

struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
}s;int main()
{struct S s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%d\n", sizeof(s)); //3
}

    int _a : 2;    //8bit
    int _b : 5;    //8bit
    int _c : 10;  //8bit
    int _d : 30;  //8bit

所以对于6.1中的实例,占用内存上为8bit+8bit+16bit+32bit = 64bit。

6.3位段的跨平台问题​

  • int 位段被当成有符号数还是无符号数是不确定的。

  • 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

  • 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  • 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

  • 总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在

6.4位段的应用​

下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。

6.5位段使用的注意事项​

位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。​
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入
放在一个变量中,然后赋值给位段的成员。

struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{struct A sa = {0};/scanf("%d", &sa._b);//这是错误的​//正确的示范​int b = 0;scanf("%d", &b);sa._b = b;return 0;
}

今天就先到这里了!!!

看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!

你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。

这篇关于自定义类型:结构体(自引用、内存对齐、位段(位域))的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

usaco 1.3 Mixing Milk (结构体排序 qsort) and hdu 2020(sort)

到了这题学会了结构体排序 于是回去修改了 1.2 milking cows 的算法~ 结构体排序核心: 1.结构体定义 struct Milk{int price;int milks;}milk[5000]; 2.自定义的比较函数,若返回值为正,qsort 函数判定a>b ;为负,a<b;为0,a==b; int milkcmp(const void *va,c

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

两个月冲刺软考——访问位与修改位的题型(淘汰哪一页);内聚的类型;关于码制的知识点;地址映射的相关内容

1.访问位与修改位的题型(淘汰哪一页) 访问位:为1时表示在内存期间被访问过,为0时表示未被访问;修改位:为1时表示该页面自从被装入内存后被修改过,为0时表示未修改过。 置换页面时,最先置换访问位和修改位为00的,其次是01(没被访问但被修改过)的,之后是10(被访问了但没被修改过),最后是11。 2.内聚的类型 功能内聚:完成一个单一功能,各个部分协同工作,缺一不可。 顺序内聚:

Mysql BLOB类型介绍

BLOB类型的字段用于存储二进制数据 在MySQL中,BLOB类型,包括:TinyBlob、Blob、MediumBlob、LongBlob,这几个类型之间的唯一区别是在存储的大小不同。 TinyBlob 最大 255 Blob 最大 65K MediumBlob 最大 16M LongBlob 最大 4G