【C语言】【数据结构】自定义类型:结构体

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 这是一篇对结构体的详细介绍,这篇文章对结构体声明、结构体的自引用、结构体的初始化、结构体的内存分布和对齐规则、库函数offsetof、以及进行内存对齐的原因、如何修改默认对齐数、结构体传参进行介绍和说明。

结构体的声明

 

结构体的基础

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

在一个变量中,要存放性别、年龄、成绩、地址多种类型的数据时,C语言允许用户自己建立由不同类型数据组成的组合型的数据结构,它称为结构体。

   

结构的声明

结构体是怎么声明的呢?

struct tag
{
    member_list;
}variable_list;  //分号不能丢
struct Student
{
  //学生的相关信息
  char name[20];
  int age;
}s1,s2;
  • tag,Student是结构体名
  • member_list是成员表列
  • struct是声明结构体类型是必须使用的关键字,不能省略
  • s1,s2变量就是学生变量。
  • { }后面要记得把“ ;”带上

struct tag就是一个结构体类型,我们可以根据自己的需要建立结构体类型,struct Teacher,struct Student等结构体类型,各自包含不同的成员。

如果将s1,s2放在main函数的外面,那么s1,s2就是全局变量。

struct Student
{
  //学生的相关信息
  char name[20];
  int age;
}s1,s2;
int main()
{
    return 0;
}

匿名结构体类型

结构体在声明的时候省略了结构体标签(tag),没有名字的结构体类型只能使用一次,被称为匿名结构体类型

由于没有名字,编译器会把下面的两个代码当成完全不同的两个类型。

所以,p = &x.

会因为类型不同报错。

struct
{
  char name[20];
  int age;
}s1;
struct
{
  char name[20];
  int age;
}a[20],*p;

结构体的自引用

结构体的自引用用到数据结构中的链表。

数据结构中有顺序表、链表的概念,

顺序表

数据在内存中是顺序排放的,可以逐个根据地址找到下一个数据。

链表

数据在内存中的存放是没有规律的但是存放数据,会分为两个部分,

一个部分叫数据域,存放有效数据,

另一个部分叫指针域,用来存放下一个数据的地址,可以通过地址直接找到下一个数据。

我们通过链表就可以实现结构体的自引用。

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

typedef作用于结构体的问题

下面在结构体自引用使用的改成中,夹杂了typedef对匿名结构体类型重命名,看看下面的代码,有没有问题?

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

答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。

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

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

struct Point是结构体类型,它相当于一个模型,是没有占据具体空间的,

当我们建立结构体变量p1,它相当于具体的房屋,在内存中储存数据。

struct Point
{
  int x;
  int y;
}p1 = { 2,3 };

多个元素的初始化要用大括号{ }

在结构体中,如果存在多个元素的变量,我们初始化时要使用大括号。

像数组一样,arr[] = { 0, 1, 2, 3, 4 };

  • 打印结构体,s1是struct Stu的变量,name是s1的成员变量,用s1.name表示s1结构体的name变量
  • s是struct Stu中的成员变量,用s1.s.n表示在结构体struct score的成员变量n。
struct score
{
  int n;
  char ch;
};
struct Stu
{
  char name[20];
  int age;
  struct score s;
};
int main()
{
  struct Stu s1 = { "zhangsan",20,{100,'q' } };
  printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);
  return 0;
}

结构体的内存对齐

如何计算结构体的大小?

结构体的内存分布是怎样的?

1.对齐规则

首先掌握结构体的对齐规则

1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处

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

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

- VS 中默认的值为 8

- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3. 结构体总⼤⼩最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

只是文字的说明,免不了晦涩难懂,接下来用例子来给大家讲解

1.例子

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

解析:

右边表示的是偏移量,

1.第一个成员char c1要对齐到和结构体变量起始位置偏移量为0的地址处,占一个字节

2.其他成员要对齐到对齐数的整数倍的地址处

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

VS中的默认对齐数是8.

 int i的大小是4个字节,对齐数就是4。int i 的地址要对齐到为偏移量整数倍的地址,也就是4的整数倍,偏移量为4的地址。int i 是4个字节,那占据的地址偏移量为4~7

char c2 的大小是1个字节,对齐数是1。1可以为任意偏移量的整数倍。所以char c2的地址的偏移量就是8.

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

成员变量有char c1,int i ,char c2。它们的对齐数分别是1,4,1。因此最大对齐数为4。

结构体总大小为最大对齐数的整数倍,现在偏移量是0~8,一共是9个字节,要凑成4的整数倍,就是12个字节,在浪费3个字节就可以了,地址偏移量9~11一共是3个字节。

这个结构体的内存就储存在偏移量为0~11的空间。

2.例子

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

解析: 

右边表示的是偏移量,

1.第一个成员char c1要对齐到和结构体变量起始位置偏移量为0的地址处,占一个字节

2.其他成员要对齐到对齐数的整数倍的地址处

对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。

VS中的默认对齐数是8.

char c1  的大小是1个字节,对齐数就是1。char c1的地址要对齐到为偏移量整数倍的地址,也就是1的整数倍,偏移量为1的地址。

int i 的大小是4个字节,对齐数是4。int i 的地址就要移到偏移量为4的倍数的地址。所以int i 的地址的偏移量就是4.int i 是4个字节,那占据的地址偏移量为4~7

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

成员变量有char c1,int i ,char c2。它们的对齐数分别是1,4,1。因此最大对齐数为4。

结构体总大小为最大对齐数的整数倍,现在偏移量是0~7,刚好是8个字节,是4的倍数。

这个结构体的内存就储存在偏移量为0~7的空间。


3.例子

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

解析: 

1.第一个成员要对齐到结构体变量起始位置偏移量为0的地址处,double d占8个字节,所以占据的内存空间是偏移量为0~7的地址

2.其他成员要对齐到对齐数的整数倍的地址处

char c的大小是1个字节,任意偏移量都可以为1的整数倍,所以char c的地址是下一位,偏移量为8的地址。

int i 的大小是4个字节,要对齐到偏移量为4的倍数的地址,也就是偏移量为12,int i 占据的内存空间为偏移量为12~15的地址。

3.结构体的大小为最大对齐数的整数倍。

最大对齐数是double的对齐数,也就是8。现在的结构体占16个字节(偏移量为0~15),刚好是8的倍数。


4.例子

这个例子包括了嵌套结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

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

解析:

1.第一个成员要对齐到结构体变量起始位置偏移量为0的地址处,char c1占1个字节,占据偏移量为0的空间。

2.嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

接下来是struct s3,要对齐自己成员的最大对齐数,double d的对齐数为8个字节,对齐到偏移量为8的地址,

3.其他成员要对齐到对齐数的整数倍的地址处,嵌套的结构体成员也是这样,double d占据8个字节,占据偏移量为8~15的地址。

char c对齐偏移量16,占据一个字节。

int i 的对齐数为4,对齐偏移量为20,占据4个字节,就是偏移量为20~23的空间。

struct S3整理完,继续到struct S4,轮到double d

double d的对齐数为8,对齐偏移量24,占据8个字节,占据空间偏移量为24~31。

4.结构体的大小为最大对齐数的整数倍。

当前空间一共是32个字节(0~31),结构体struct S4,struct S3中的成员的最大对齐数是8。因此结构体的大小要是最大对齐数的整数倍。32刚好是8的整数倍。

offsetof

返回成员的偏移量 ,头文件<stddef.h>

offsetof (type,member)

offsetof的使用

type是类型,

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


为什么要存在内存对齐


1. 平台原因 (移植原因):

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

2. 性能原因:

数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。

总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

以32为机器为例,32位机器一次可以访问32位比特位的数据,

如果没有对齐规则,就像左边,机器要访问两次才可以得到 int i 的值,

有对齐规则,就像右边,想要访问 i ,只需要访问一次就足够了。

对齐规则的思想:把数据放在机器可以一次访问得到数据的空间内,使访问更具效率。  


修改默认对齐数

当结构体的对齐方式不适合时,我们也可以修改默认对齐数。

  • 在括号填写数字,对默认对齐数进行修改。
  • 如果()内没有数字,则时将默认对齐数恢复到默认值。
#pragma pack()

下面的struct S原本是占据12个字节的空间,对默认对齐数进行修改后,只占据6个字节的空间。

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


结构体传参

  • 传值调用,将数据通过参数传过去,然后函数print会创立独立的空间,对传过来的数据进行存储
  • 传址调用,将数据的地址传过去,函数通过指向数据的地址对数据进行使用,不需要再建立空间对数据进行存放
#include<stdio.h>
struct S
{
  int data[1000];
  int num;
};
void print1(struct S ss)
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", ss.data[i]);
  }
  printf("%d\n", ss.num);
}
void print2(struct S* ps)
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", ps->data[i]);
  }
  printf("%d\n", ps->num);
}
int main()
{
  struct S s = { {1,2,3},100 };
  print1(s);
  print2(&s);
  return 0;
}

上面的传值调用print1 和 传址调用print2 函数那哪个更好?


答案是:⾸选print2函数。

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。


目录
相关文章
|
1月前
|
存储 算法 C语言
"揭秘C语言中的王者之树——红黑树:一场数据结构与算法的华丽舞蹈,让你的程序效率飙升,直击性能巅峰!"
【8月更文挑战第20天】红黑树是自平衡二叉查找树,通过旋转和重着色保持平衡,确保高效执行插入、删除和查找操作,时间复杂度为O(log n)。本文介绍红黑树的基本属性、存储结构及其C语言实现。红黑树遵循五项基本规则以保持平衡状态。在C语言中,节点包含数据、颜色、父节点和子节点指针。文章提供了一个示例代码框架,用于创建节点、插入节点并执行必要的修复操作以维护红黑树的特性。
49 1
|
9天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
9天前
|
C语言
数据结构基础详解(C语言):图的基本概念_无向图_有向图_子图_生成树_生成森林_完全图
本文介绍了图的基本概念,包括图的定义、无向图与有向图、简单图与多重图等,并解释了顶点度、路径、连通性等相关术语。此外还讨论了子图、生成树、带权图及几种特殊形态的图,如完全图和树等。通过这些概念,读者可以更好地理解图论的基础知识。
|
11天前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
11天前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
|
11天前
|
存储 C语言 C++
数据结构基础详解(C语言) 顺序表:顺序表静态分配和动态分配增删改查基本操作的基本介绍及c语言代码实现
本文介绍了顺序表的定义及其在C/C++中的实现方法。顺序表通过连续存储空间实现线性表,使逻辑上相邻的元素在物理位置上也相邻。文章详细描述了静态分配与动态分配两种方式下的顺序表定义、初始化、插入、删除、查找等基本操作,并提供了具体代码示例。静态分配方式下顺序表的长度固定,而动态分配则可根据需求调整大小。此外,还总结了顺序表的优点,如随机访问效率高、存储密度大,以及缺点,如扩展不便和插入删除操作成本高等特点。
|
11天前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
11天前
|
存储 C语言
C语言程序设计核心详解 第九章 结构体与链表概要详解
本文档详细介绍了C语言中的结构体与链表。首先,讲解了结构体的定义、初始化及使用方法,并演示了如何通过不同方式定义结构体变量。接着,介绍了指向结构体的指针及其应用,包括结构体变量和结构体数组的指针操作。随后,概述了链表的概念与定义,解释了链表的基本操作如动态分配、插入和删除。最后,简述了共用体类型及其变量定义与引用方法。通过本文档,读者可以全面了解结构体与链表的基础知识及实际应用技巧。
|
11天前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。