结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)上

简介: 结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)

我们知道,C语言是允许我们自己来创造类型的,这些类型就叫做——自定义类型。

自定义类型又包括结构体类型,联合体类型还有枚举类型。

今天的文章,我们就着重讲解这其中的结构体类型。


结构体的声明


1.1结构的基础知识


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


1.2结构的声明


struct tag

{

member-list;

}variable-list;

我们以这种方式来描述一个结构体。下面是简单的示范,我们来描述一个学生:


struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20];//学号
}; //分号不能丢


定义局部变量和全局变量的关系:


#define _CRT_SECURE_NO_WARNINGS
struct Stu
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
}s1,s2,s3; //全局变量
int main()
{
  struct Stu s4;
  struct Stu s5;//局部变量
  return 0;
}


1.3 匿名结构体的情况


也可以省略不写结构体标签,不过这样会导致一个结果,结构体只能定义一次类型。


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct 
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
}s1; //全局变量
struct 
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
}*ps; //全局变量
int main()
{
  s1.age = 1;
  printf("%d", s1. age);
  return 0;
}


 fdf8b13461ab14ad898428ade131aa4d_6267d92ecf6c4d9998e384c45826ab65.png


在上述的代码中,体现为定义结构体变量s1之后,无法再次定义诸如s2,s3等结构体类型。


不过要是你本来就准备只用一次结构体的话,定义一个匿名结构体也不错就是了。


上面的两个结构在声明的时候省略掉了结构体标签, 那么问题来了?

//在上面代码的基础上,下面的代码合法吗?


ps=&s1;


bc7a15cd4b0953678d07a6ded3c6c166_7eb0cd46a7274ded95e6bba5041049f1.png


答案是否定的,及时两个结构体里面的元素都相同,编译器也会他们当成两个完全不同的类型,所以是非法的。


1.4结构的自引用


我们想要使用结构体实现类似于链表的功能。


e0599ff212de3d787db3516db381b6e1_bd73ea66322d426196343858af787669.png


在结构中包含一个类型为该结构本身的成员是否可以呢?

#include<stdio.h>
struct Node
{
  int data;
  struct Node n;
};
int main()
{
  return 0;
}

我们开动小脑筋,立马就发现了错误。


struct Node这个节点它所占用的空间有多大呢?


它不仅要存放一个整形,还要存放一个n。


这就无限循环下去了,struct Node里面还有一个struct Node。


大小是无法得出的,这是一个错误示范。


我们转变战略,用指针来实现。


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct Node
{
  int data;//4
  struct Node *next;//4/8
};
int main()
{
  struct Node n1;
  struct Node n2;
  n1.next = &n2;
  return 0;
}

创建两个节点n1,n2,把它们像链条一样串起来。


编译器没有报错,这样的写法是正确的,同时我们发现,struct Node的大小可以轻而易举地算出,我们得出一个结论:


不是在自己的类型里面包含一个自己类型的变量,而是在自己的类型里面包含一个自己类型的指针。这样的实现方式才是可行的。


1.5重命名匿名结构体的情况


下面的代码是否可行呢?


#include<stdio.h>
typedef struct 
{
  int data;
}S;
int main()
{
  return 0;
}

可行,不过S不再是匿名结构体的变量,而是变成了匿名结构体类型。


怎么用呢?这么用:


#include<stdio.h>
typedef struct 
{
  int data;
}S;
int main()
{
  S s;
  s.data = 1;
  printf("%d", s.data);
  return 0;
}

能用这种方式模拟实现上面的链表呢?


这样写行吗?


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

不行,在没有重命名出Node时就调用了Node。


在这种情况下,我们只能老老实实地写出类型名了!


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


1.6 结构体变量的定义和初始化


有了结构体类型,那如何定义变量,其实很简单。


int x;
 int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量
struct Point
{
p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu        //类型声明
{
 char name[15];//名字
 int age;      //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
 int data;
 struct Point p;
 struct Node* next; 
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化


1.7 结构体内存对齐


我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

这就到了本文的重中之重: 结构体内存对齐。

计算以下的结构体大小。


#include<stdio.h>
int main()
{
  struct S1
  {
  char c1;
  int i;
  char c2;
  };
  printf("%d\n", sizeof(struct S1));
  //练习2
  struct S2
  {
  char c1;
  char c2;
  int i;
  };
  printf("%d\n", sizeof(struct S2));
  //练习3
  struct S3
  {
  double d;
  char c;
  int i;
  };
  printf("%d\n", sizeof(struct S3));
  //练习4-结构体嵌套问题
  struct S4
  {
  char c1;
  struct S3 s3;
  double d;
  };
  printf("%d\n", sizeof(struct S4));
}


运行结果如下:


54bfce3920cbe8a7a0566fefbfe68ec1_b71ee125c7af463cba81c51df6602813.png


是不是跟想的完全不一样?


没错,结构体的大小并不是成员大小的简单相加,而是有自己的一套规则的。


结构体的第一个成员永远是放在零偏移处。

从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。

这个对齐数是成员自身大小和默认对齐数的较小值。

VS中默认的值为8

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

如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。如果不够,则浪费空间来对齐。

我们以s1为例子来试验一下上述规则,如图所示。


6759d447408f3b661e5ff1046cd1000a_76300cec43324e498c77a7d83e9c1817.png


因为从第二个成员开始,以后每个对齐成员都要对齐到某个对齐数的整数倍处。


所以1,2,3三个字节被浪费,int类型的存储从4开始到7,char类型存到8处。


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


S1中最大对齐数为4,结构体总大小要为最大对齐数(每个成员变量都有一个对齐数)的整数倍。而现在大小为9,为了让其变为4的倍数,结构体S1的总大小变为12。


再看S4的情况:


a4a33511660de9b00277936593ee224d_ab127bf17e8b4ce8ab26e32f39faea09.png


白色为浪费部分,黄色为char,绿色是double,粉色是int。


1.8为什么存在内存对齐?


1.不同硬件平台不一定支持访问任意内存地址数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。使用内存对齐可以保证每次访问都从块内存地址头部开始存取


2.提高cpu内存访问速度,内存是分块的,如两字节一块,四字节一块,考虑这种情况:一个四字节变量存在一个四字节地址的后三位和下一个四字节地址的前一位,这样cpu从内存中取数据便需要访问两个内存并将他们组合起来,降低cpu性能


用内存对齐达到了用空间换时间的效果


1.9我们可以耍些小聪明达到节省空间的效果。


让占用空间小的成员尽量集中在一起。

//例如:
struct S1
{
 char c1;
 int i;
 char c2;
};
struct S2
{
 char c1;
 char c2;
 int i;
};

S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。


相关文章
|
6月前
|
存储 缓存 安全
C++数组全解析:从基础知识到高级应用,领略数组的魅力与技巧
C++数组全解析:从基础知识到高级应用,领略数组的魅力与技巧
134 1
|
2月前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
329 3
|
3月前
|
图形学 人工智能 C#
从零起步,到亲手实现:一步步教你用Unity引擎搭建出令人惊叹的3D游戏世界,绝不错过的初学者友好型超详细指南 ——兼探索游戏设计奥秘与实践编程技巧的完美结合之旅
【8月更文挑战第31天】本文介绍如何使用Unity引擎从零开始创建简单的3D游戏世界,涵盖游戏对象创建、物理模拟、用户输入处理及动画效果。Unity是一款强大的跨平台游戏开发工具,支持多种编程语言,具有直观编辑器和丰富文档。文章指导读者创建新项目、添加立方体对象、编写移动脚本,并引入基础动画,帮助初学者快速掌握Unity开发核心概念,迈出游戏制作的第一步。
162 1
|
3月前
|
存储 编译器 数据处理
【编程秘籍】解锁C语言数组的奥秘:从零开始,深入浅出,带你领略数组的魅力与实战技巧!
【8月更文挑战第22天】数组是C语言中存储同类型元素的基本结构。本文从定义出发,详述数组声明、初始化与访问。示例展示如何声明如`int numbers[5];`的数组,并通过下标访问元素。初始化可在声明时进行,如`int numbers[] = {1,2,3,4,5};`,编译器自动计算大小。初始化时未指定的元素默认为0。通过循环可遍历数组,数组名视为指向首元素的指针,方便传递给函数。多维数组表示矩阵,如`int matrix[3][4];`。动态数组利用`malloc()`分配内存,需用`free()`释放以避免内存泄漏。掌握这些技巧是高效数据处理的基础。
70 2
|
5月前
|
存储 Java 数据处理
启航Java编程:基础三部曲-第二部
启航Java编程:基础三部曲-第二部 Java语法全接触:变量、数据类型与运算符详解
72 1
|
6月前
|
编译器 API Windows
windows编程基础
windows编程基础
41 0
|
6月前
第七章:函数实训
第七章:函数实训
39 0
|
编译器 程序员 C语言
游戏编程之二 windows编程基础
游戏编程之二 windows编程基础
77 0
结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)下
结构体全解,适合初学者的一条龙深度讲解(附手绘图详解)
63 0
|
Java C#
【c#入门杂选】-带你熟知c#基础知识点《思维导图》
【c#入门杂选】-带你熟知c#基础知识点《思维导图》
173 0