自定义类型之结构体的基础和进阶(有关位段、结构体自引用、嵌套、内存对齐、修改对齐数、结构体的传参、和offsetof宏的使用)

简介: 一、结构体基础知识二、结构体的进阶(有关结构体的自引用,嵌套,内存对齐和内存设计)(一、)首先是结构体的嵌套(二、)结构体的自引用(三、)结构体的内存对齐(如何计算结构体的所占内存大小)(四、)如何修改默认对齐数三、offsetof的意思四、结构体的传参五、位段的使用和注意总结:

一、结构体基础知识

1.虽然今天弄了一个目录,但是开头必须还是我的大学生活,今天是军训第3天,双腿酸痛,军训还是非常磨砺人的,每天5点50起床(搞得我现在眼睛都要睁不开了),所以为了早睡,现在步入正题

2.首先我们讲一下如何声明结构体,如何初始化结构体和什么是结构体变量的成员变量,以一个代码为例:

#include<stdio.h>
struct people     //这个就是声明结构体的类型(特别要注意这个是一个类型(类似于int类型的数据),所以在使用时就应该把整个结构体类型用上)
{
  //下面这些就是我的结构体变量的成员变量;
  char name[20];
  char tele[12];
  int age;
  double num;
}s1;//这个是一个全局变量,是在我struct 结构体类型中创建的一个全局变量(且这个位置要注意什么是全局变量什么是局部变量(在main函数内部的就是局部变量,在main函数外的就是全局变量,所以此时这个s1就是一个全局变量))
所以此时我就可以拿这个结构体类型来创建一个结构体变量

创建一个结构体变量,代码如下:

int main()
{
    struct people s1;//这个就是利用结构体类型创建一个结构体变量
  struct people s1 = { "校园网","10086",18,3.14};//这个就是结构体类型的初始化,要用大括号进行
  printf("%s %s %d %lf", s1.name, s1.tele, s1.age, s1.num);//这个就是结构体成员的访问,用操作符 . 
  return 0;
}

二、结构体的进阶(有关结构体的自引用,嵌套,内存对齐和内存设计)

(一、)首先是结构体的嵌套

1.现在我们讲一下结构体的进阶,比如结构体中含有结构体的使用和关系,首先看一个结构体中含有结构体的代码:


#include<stdio.h>
struct people//切记这个东西是结构体类型,不是变量,不能乱用,想要用的话,就一定要创建一个变量出来 例:后面的 p 就是我可以用的一个结构体变量
{
  char name[20];
  char ch;
  int age;
};
struct S//切记这个东西是结构体类型,不是变量,不能乱用,想要用的话,就一定要创建一个变量出来 例:下面的 s 就是我的可以用的一个结构体变量
{
  char sex[10];
  double d;
  struct people p;
};
int main()
{
  struct S s = { "男",3.14,{"我是小白",'W',18 } };//这个还是那个道理(对结构体的初始化,但是因为我要初始化两个结构体,所以在 {} 中,我还要再写一个 {} ,这样我才可以把两个结构体都给初始化)
  printf("%s %lf %s %c %d", s.sex, s.d, s.p.name, s.p.ch, s.p.age);//这边的对结构体成员变量的访问和上述说的是一个道理的,不过只是二重访问,因为是结构体中包含结构体,所以我要二重访问
  return 0;
}
以上这个就是结构体中含有结构体的代码

(二、)结构体的自引用

2.第二点进阶,我们来讲一下什么是结构体的自引用,看代码如下:

struct Node
{
  int data;
  struct Node* next;//这个与数据结构有一点关系,意思就是struct Node这个数据结构类型的位置的指针,此时有了这个指针我就可以找到下一个数据位置,这个就叫链表,(并且这个就是结构体的自引用(结构体的自引用:就是找一个与这个结构体类型相同的结构体,我用一个指针变量去指向这个结构体的数据))
};
int main()
{
  struct Node n ={0};//在main函数内部,我只能定义局部变量,想要定义全局变量,我就一定要在main函数外部定义,例:s1;
  return 0;
}
这个可以附上一个与数据结构有关的图(所以这个结构体的自引用就是与数据结构有一定的关系)

就是当我将我的数据(例:1 2 3 4 5)放在我的内存中时

11.jpeg


此时的数据在内存中是随机分布的,所以如果想要重新按照顺序把它们找出来,我们就要用一个叫(链表的东西),让我的数据此时不仅有数据域,还有一个指针域(只有有了指针,我才有可能按照顺序找到它们),所以如下图:

image.jpeg


所以这个就是结构体的自引用的作用,我可以用自己结构体中创建的指针去指向我自己结构体中的数据,这样我就可以很好的找回数据;(且这边再一次起强调,指针的大小只跟电脑是32位的还是64位的有关,如果是32位,则指针的内存大小就只能是4,是64位内存大小就只能是8)

(三、)结构体的内存对齐(如何计算结构体的所占内存大小)

一、如果我们想要计算一个结构体所占内存空间的大小,首先我们就要掌握结构体的对齐规则

1.第一个成员与结构体变量的偏移量为0的地址处;

2.其它成员变量对齐某个数字(对齐数)的整数倍地址;

3.并且结构体的总大小为最大对齐数的整数倍(并且每一个变量都有一个对齐数),在vs编译器下,对齐数默认是8;并且对齐数指的是:一个对齐数与该成员大小的较小值

4.当结构体在嵌套使用时,嵌套的结构体对齐到自己的最大对齐数的整数倍的地址处,结构体的总大小就是所有最大对齐数的整数倍(含嵌套结构体中的最大对齐数);

看完这4点,人都傻了,看不懂什么意思,所以现在进行总结:

1.首先举一个例子:有char a; 按照上述对齐的的概念,我就还可以知道此时char a 的对齐数是 (1)

所以同理 int name;此时的对齐数是(4)

2.所以按照上述所说此时的 int name;只有在内存中遇到4的倍数的时候,此时才可以进行存放我的地址(如果地址是1 2 3 的时候就不能存放,所以此时这个1 2 3 地址处的空间就会被略过,就是里什么都不能放,也就是内存的浪费),如果想char a 这种对齐数为(1)则在内存中的任意位置都可以进行存放

3.所以得出结论:默认成员的大小其实就是我的对齐数(我的对齐数的整数倍就是我这个变量能够存放地址的位置空间)

4.所以下面让我们用一个题目,来进一步认识什么是内存对齐,代码如下:

#include<stdio.h>
struct S1
{
  char ch1;
  int a;
  char ch2;
};
struct S2
{
  char ch1;
  char ch2;
  int a;
};
int main()
{
  struct S1 s1 = { 0 };
  printf("%d\n", sizeof(s1));
  struct S2 s2 = { 0 };
  printf("%d", sizeof(s2));
  return 0;
}

输出结构如图:

13.png



这里应该大家就非常不理解,为什么是这个答案了,所以这里就用图的方式给大家讲一下(具体原理就是上面所说的内存对齐),如图:

image.jpeg

所以上图就是为什么struct S1的答案是12的原因,并且struct S2原理同上,这个就是对齐规则的使用和结构体占内存大小的计算(就是使用了内存对齐的方式进行数据的储存)

二、我们讲一讲为什么要进行内存的对齐

1.就是平台的原因:并不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取得某些特定类型的数据

2.性能的原因:数据结构(尤其是栈),应该尽可能地在边界上对齐,原因在于,为了访问未对齐的内存,处理器要进行两次访问才可以读取那个内存的数据,而对齐了的内存就只要访问一次,具体原因如图所示:


image.jpeg


这个就是为什么不进行内存对齐我的处理器需要访问两次内存的原因;所以假如我进行了内存对齐把char和int中间多余的3个字节的空间省略,实现了内存的对齐,那么此时我的处理器就只需要访问一次内存了(这样就可以节省我处理器的时间,使处理器运行更加的快)

3.总结就是:拿空间换时间

(四、)如何修改默认对齐数

1.如代码所示:

#pragma pack(1)//设置默认对齐数为1
#pragma pack(4)//设置默认对齐数是4
struct S 
{
    char ch;
    int i;
};
#pragma pack()//取消默认对齐数的设置

三、offsetof的意思

1.首先offsetof是一个宏的定义,意思就是什么什么的偏移量,使用方法如代码和图所示:

#include<stddef.h>
#include<stdio.h>
struct S1
{
  char ch1;
  int a;
  char ch2;
};
struct S2
{
  char ch1;
  char ch2;
  int a;
};
int main()
{
  printf("%d\n",offsetof(struct S1,a));
  printf("%d\n",offsetof(struct S2,a));
  return 0;
}

答案如下图:

16.png


使用的规则如图:


17.png

四、结构体的传参

1.看到传参两个字,我们首先应该想到的就是(传值和传址两种类型),如下代码,我们一起看一下结构体是如何进行传参的

#include<stdio.h>
struct S
{
  int a;
  char ch;
  double b;
};
void Init1(struct S tmp)//因为此时要改变外部的数据,所以这里用传值不好
{
  tmp.a = 100;
  tmp.ch = 'W';
  tmp.b = 3.14;
}
void Init2(struct S* ps)
{
  ps->a = 100;
  ps->b = 3.14;
  ps->ch = 'W';
}
Print1(struct S tmp)//因为此时只是进行打印,所以可以用传值的方法
{
  printf("%d %c %lf\n",tmp.a,tmp.ch,tmp.b);
}
Print2(struct S* ps)//这边因为是用传地址的方法,所以可以写成Print2(const struct S tmp);这样就可以防止我外部的数据被改变,更加安全
{
  printf("%d %c %lf\n", ps->a, (*ps).ch, ps->b);//ps->a, (*ps).ch这两种写法是一样的道理
}
int main()
{
  struct S s = { 0 };
  Init1(s);//这个就是结构体的传值调用
  Init2(&s);//这个就是结构体的传址调用
  Print1(s);
  Print2(&s);
  return 0;
}

这个代码就可以清晰的看出我的结构体是如何进行传参的,和如何进行传参后的使用


五、位段的使用和注意

1.首先位段就是一个二进制位是一个比特位,且位段的成员必须是int、unsigned int、signed int

2.位段的成员后有一个冒号和数字(且注意这个数字是不能超过32的)

3.位段的使用就是为了节省我的空间

4.例:代码所示


#include<stdio.h>
struct A
{
  int _a : 2;//写成int a : 2;也可以,一个意思
  int _b : 5;//这个数字代表的就是比特位
  int _c : 10;
  int _d : 30;
};
int main()
{
    struct S s={0};
    printf("%d\n",sizeof(A));
    return 0;
}

18.png

5.注意点就是在于这个比特位到底是怎样计算的和位段是如何进行空间的节省的,原因就是:当我的位段前面的类型是int类型时,那么内存每次就会开辟32和比特位,如果是char类型是内存每次就开辟8个比特位,具体原理如图:

image.jpeg

所以这个就是位段的使用

总结:

结构体和数据的使用息息相关,内存的理解至关重要!

相关文章
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
1675 13
|
存储 编译器 数据处理
C 语言结构体与位域:高效数据组织与内存优化
C语言中的结构体与位域是实现高效数据组织和内存优化的重要工具。结构体允许将不同类型的数据组合成一个整体,而位域则进一步允许对结构体成员的位进行精细控制,以节省内存空间。两者结合使用,可在嵌入式系统等资源受限环境中发挥巨大作用。
530 12
|
编译器 Go
探索 Go 语言中的内存对齐:为什么结构体大小会有所不同?
在 Go 语言中,内存对齐是优化内存访问速度的重要概念。通过调整数据在内存中的位置,编译器确保不同类型的数据能够高效访问。本文通过示例代码展示了两个结构体 `A` 和 `B`,尽管字段相同但排列不同,导致内存占用分别为 40 字节和 48 字节。通过分析内存布局,解释了内存对齐的原因,并提供了优化结构体字段顺序的方法,以减少内存填充,提高性能。
200 3
|
存储 Java 程序员
结构体和类的内存管理方式在不同编程语言中的表现有何异同?
不同编程语言中结构体和类的内存管理方式既有相似之处,又有各自的特点。了解这些异同点有助于开发者在不同的编程语言中更有效地使用结构体和类来进行编程,合理地管理内存,提高程序的性能和可靠性。
322 3
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
9月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
2897 0
|
9月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
961 1
|
9月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
1149 0
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
1054 0