匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参

简介: 匿名结构体类型、结构体的自引用、结构体的内存对齐以及结构体传参

🚀前言

在C语言中有着各种数据类型,这些类型有配划分为内置类型自定义类型两大类(如下图)。铁子们,今天阿辉要分享的就是自定义类型中的结构体联合体和枚举将在下篇文章分享,至于数组阿辉之前的文章数组篇中已经详细讲到,铁子们感兴趣的话可以点击跳转😘,不多bb直接开始我们今天的学习👊

🚀结构体

铁子们是否有这样的疑问——C语言为什么要引入结构体这一自定义类型?

别急,听阿辉一一道来👇

其实结构体数组有一点类似,数组是存储同一种数据类型的集合,而结构体是存储不同类型的集合,比如当你想描述一个学生时,你得有姓名、年龄、学号等等一系列特征

可是我们发现这不是某一种单一的数据类型能够描述的,这时引入结构体这一自定义类型是非常有必要的

对于结构体有何用想必铁子们有了初步的认识,咱们接着往下看👇

✈️结构体类型的声明

声明结构体的语法结构:

struct tag 
{
  member_list; 成员列表,
}variable_list;  变量列表,在结构体声明时就创建的变量
struct tag 这个整体属于类型名,和int,char等等类型名一样

我们来创建一个描述学生的结构体类型:

struct stu
{
  char name[20];名字
  int age;年龄
  int id;学号
  char sex[5];性别
};//注意这里的分号不能丢了

结构体类型的声明同样分为全局声明和局部声明,结构体全局声明以及声明时创建的变量作用域都是整个程序,而结构体的局部声明以及声明时创建的变量的作用域在该大括号内部{}

✈️结构体变量的创建与初始化

结构体变量有两种创建方式,一种在结构体类型声明时就创建,与结构体类型声明具有相同的作用域;另一种在结构体类型声明后创建,作用域与结构体类型声明无关,咱直接上代码👇

struct stu
{
  char name[20];
  int age;
}s1;//s1属于全局变量
int main()
{
  struct stu s2;//s2局部变量,作用域在main函数内
  return 0;
}

结构体变量初始化:

#include <stdio.h>
struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
};
int main()
{
 //按照结构体成员的顺序初始化
 struct Stu s1 = { "张三", 20, "男", "20230818001" };
 
 //按照指定的顺序初始化
 struct Stu s2 = { .age = 18, .name = "lisi",
  .id = "20230818002", 
 .sex = "⼥};
 return 0;
}

✈️结构体类型的特殊声明

铁子们,结构体还有一种特殊的声明,这种声明把结构体的标签tag给干掉了,这种特殊声明的结构体被称为匿名结构体类型

我们来上一组例子:

struct
{
  int a;
  int b;
}a;
struct
{
  int a;
  int b;
}*p;
int main()
{
  p = &a;
  return 0;
}

上⾯的两个结构在声明的时候省略掉了结构体标签tag

那么问题来了?

p = &a这样写是否合法?

对于匿名结构体类型,上述两个结构体类型看似一样,实则不同,匿名结构体的变量只能在声明时创建且只能有一个变量,上述编译器会把匿名结构体指针变量p与&a当作两个不同的类型

✈️结构体的自引用

结构体的自引用本质是结构体的递归定义,但是这会存在很大问题,如下面这个代码

struct node
{
  int data;
  struct node next;
}

在编译期间,编译器需要知道结构体变量大小为结构体变量分配空间,但是上述这个结构体我们仔细想一下会发现这个结构体无限递归,根本无法确定其大小,为解决上述问题,我们可以通过指针来间接引用结构体,如下:

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

上述就是正确的结构体自引用,通过结构体自引用我们可以创建具有互相关联关系的数据结构,如链表、树等,在数据结构中结构体尤为重要。

✈️结构体的内存对齐

有了上面对于结构体的理解,铁子们对结构体的基本使用应该不成问题了,接下来咱们来研究一个深入的问题——结构体类型的大小

有的老铁可能会说:不是很简单吗❓直接把所有变量所占字节空间大小全都加起来就完事了

但是真有这么简单吗?我们接着看👇

其实结构体存在内存对齐这一规则:

  • 第一个成员在与结构体变量偏移量为0的地址处
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值
    VS中默认的值为8,Linux中gcc没有默认对齐数对齐数就是该成员大小
  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

在VS中char的对齐数就是1int对齐数就是4double对齐数就是8

我们来看看一个是否如此

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

我们可以看到对于两个char和一个int应该是6个字节,但是通过sizeof却打印出了12

这里我用用图为铁子们解释:

上图一个方块代表一个字节,对于第一个成员c1就存在偏移量为0的地址处,而对于第二个成员i它是int类型对齐数为4,要存在为4的倍数的偏移量处也就是上图位置出,第三个成员c2char类型对齐数为1存在i成员后面,现在整个大小只有9个字节并非最大对齐数4的整数倍所以还要补3个字节分配给它,上图中蓝色方块代表浪费的内存

知道了结构体内存对齐的计算之后,问题又来了:为什么存在内存对齐❓

有两个原因:

平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

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

在设计结构体的时候,我们既要满足对齐,又要节省空间,这该如何做呢?

在我们声明结构体时尽量让占用空间小的成员集中在一起,如:

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

struct s2就要比struct s1的空间小4个字节

🚁修改默认对齐数

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

例子:

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

输出为6当默认对齐数为1时也就不存在对齐了,直接把所有变量所占字节空间大小全都加起来就完事了

✈️结构体传参

与其他类型变量传参一样,同样可以传址和传值

#include <stdio.h>
struct S
{
  int data[1000];
  int num;
};
struct S s = { {1,2,3,4}, 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;
}

上述代码都可以帮我们打印,但是传址调用更好

原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降

SO结构体在传参时要传递结构体地址


感谢老铁能看到这,到这里结构体的分享就到此为止了,如果觉得阿辉写得不错的话,记得给个赞呗,你们的支持是我创作的最大动力🌹

相关文章
|
1月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
117 13
|
1月前
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
60 11
|
1月前
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
42 3
|
1月前
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
30 3
|
1月前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
1月前
|
存储 缓存 算法
结构体和类在内存管理方面有哪些具体差异?
【10月更文挑战第30天】结构体和类在内存管理方面的差异决定了它们在不同的应用场景下各有优劣。在实际编程中,需要根据具体的需求和性能要求来合理选择使用结构体还是类。
|
3月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
4月前
|
存储 Java 程序员
Java中对象几种类型的内存分配(JVM对象储存机制)
Java中对象几种类型的内存分配(JVM对象储存机制)
96 5
Java中对象几种类型的内存分配(JVM对象储存机制)
|
3月前
|
编译器 Linux API
基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存
基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存
48 0
|
4月前
|
Python
Python变量的作用域_参数类型_传递过程内存分析
理解Python中的变量作用域、参数类型和参数传递过程,对于编写高效和健壮的代码至关重要。正确的应用这些概念,有助于避免程序中的错误和内存泄漏。通过实践和经验积累,可以更好地理解Python的内存模型,并编写出更优质的代码。
41 2