自定义类型:结构体

2023-11-04

C语言之自定义类型——结构体


前言

   本文重点讲解结构体的知识点。结构体、枚举和联合属于自定义类型。与自定义类型相对应的是内置类型,比如int、short、char等都属于内置类型


1 结构体

   我们描述一个事物通常需要描述其不同的特性,比如描述人,需要知道每个人的姓名、年龄和性别等。如果这些值能够存储在一起,访问起来会简单一些。在C中,使用结构体可以把不同类型的值存储在一起1。结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.1 结构体声明

1.1.1 一般格式

   结构体类型的声明一般格式:

struct tag { 
    member-list
    member-list 
    member-list  
    ...
};

tag 是结构体标签。
member-list 是成员变量的定义,比如 int i; 或者 float f,或者其他有效的变量定义。

   结构体的声明就是创造一个结构体类型,用这个类型可以描述真实世界事物。例如我们想描述一个学生,一般可以用以下格式进行声明一个结构体类型,然后用这个类型描述学生。

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

其中Stu是一个结构体类型,name、age、sex、id都是结构体类型的成员。我们利用这个类型可以定义结构体变量,这一点下文会讲。

1.1.2 特殊结构体声明

   匿名结构体类型的声明格式:

struct { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;

可以发现没有匿名结构体标签,variable-list 是结构体变量。下面例程声明两个匿名结构体类型,进行详细说明。

struct
{
	int a;
	char b;
	float c; 
}x;
struct
{
	int a;
	char b;
	float c; 
 }a[20], *p;

  以上程序在声明结构的时候省略了结构体标签(tag),这种不完全的声明,称为匿名结构体类型(anonymous struct),虽然两个匿名结构体类型的成员变量相同,但属于两种不同的结构体类型。由于无法利用匿名结构体类型定义结构体变量,所以必须在声明的时候定义结构体变量,即variable-list不可以省略。

  匿名结构体与共用体(union)联合使用时,可以像使用结构体成员一样直接使用其中联合体的成员2

1.2 结构的自引用

  结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等3,例程如下所示。

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

重定义类型名的一个注意点:
  利用typedef可以重新定义类型的名字,这样可以使用类型定义变量时可以很方便,但是在结构体的自引用中需要注意,否则容易出错。
错误例程:

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

  结构体内部不能使用重新定义的结构体名字,所以上面的程序会出错。下面对以上程序进行修改。

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

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

  前面我们讲了如何声明结构体类型,那么如何定义结构体变量呢?定义有两种方式,见例程。

struct Point
{
 int x;
 int y; }p1; //第一种:声明类型的同时定义变量p1
 
struct Point p2; //第二种:定义结构体变量p2

  结构体初始化有两种方式,见例程。

struct Point
{
	int x;
	int y;
}p1 = { 1,2 }; //第一种:结构体嵌套初始化

struct Point p2 = { 3, 4 };//第二种:定义结构体变量p2的同时赋初值。

1.4 结构体内存对齐

  通过以上内容,我们已经掌握了结构体的简单使用,下面来点难的。结构体内存对齐是指结构体在内存中存储时会进行对其,以方便处理器进行访问。如果我们想计算结构体的大小,首先您必须掌握结构体的对齐规则。

1.4.1 结构体对齐规则

  1. 第一个成员变量的偏移量为0,即第一个成员变量和结构体变量的地址相同。
  2. 其他成员变量的偏移量等于某个数字(对齐数)的最小正整数倍。
  3. 结构体总大小为最大对齐数的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

  先对以上提到的两个概念进行解释:
偏移量:成员变量结构体变量相差的字节数。
对齐数 = 编译器默认对齐数 与 该成员大小中的较小值。(Visual Studio 编译器的默认对齐数为8(字节))

1.4.2 计算结构体的大小

  掌握了以上规则,就可以计算结构体的大小,下面举例说明。

练习1

struct S1
{
	char c1;
	int  d1;
	char c2;
};

分析:c1是第一个成员变量,故偏移量为0,c1的存储起始地址与结构体变量S1的地址相同。c1的大小为1字节,故c1的存储位置如图1所示。(注意:变量的起始地址就是变量的地址)

在这里插入图片描述

图1 成员变量c1的存储状况

  d1的大小为4,编译器默认的对齐数为8,故可得d1的对齐数取两者之中的最小值4。d1的偏移量为对齐数4的最小正整数倍,这个最小正整数只能是1,因此偏移量为1*4 = 4。d1的存储地址就是偏移量为4的位置。d1的大小为4字节,故d1的存储位置如图2所示。
在这里插入图片描述

图2 成员变量c1、d1的存储状况

  成员变量c2的大小为1,编译器默认的对齐数为8,故可得c2的对齐数取两者之中的最小值1。c2的偏移量为对齐数1的最小正整数倍,由图2可以看出,结构体变量S1内存已经存到第八个字节,故可得对齐数最小正整数倍为8,故偏移量为8*1 = 8。所以c2的存储地址就是偏移量为8的位置。c2的大小为1字节,故c2的存储位置如图3所示。
在这里插入图片描述

图3 成员变量c1、d1、c2的存储状况

  经过以上分析,我们知道了每个成员变量的存储状况,那么结构体的总大小为多少呢?其实可以根据规则的第三条进行计算。由以上可知c1的对齐数为0,d1的对齐数为4,c2的对齐数为1,所以成员变量的最大对齐数为4,又当前结构体内存已经存到了第9个字节,但9除以4的商不是整数,9/4 = 2.25,2.25向无穷大取整,可得到3,所以可以得到结构体的大小为3*4 = 12,即结构体变量S1占12个字节。下面通过程序验证一下。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S1
{
	char c1;
	int  d1;
	char c2;
};
int main()
{
	printf("S1成员c1偏移量:%d\n", offsetof(struct S1, c1));
	printf("S1成员d1偏移量:%d\n", offsetof(struct S1, d1));
	printf("S1成员c2偏移量:%d\n", offsetof(struct S1, c2));
	printf("S1结构体的大小为:%d\n", sizeof(struct S1));
	return 0;
}

运行结果:
在这里插入图片描述

图4 计算结构体大小及成员变量的偏移量

  其中宏offsetof可以计算成员变量的偏移量,其存在的头文件为<stddef.h>,可以看到图4运行的结果与我们上面分析的一样。下面再来个程序,进行分析练习。

练习2

struct S2
{
	char c1;
	char c2;
	int d1;
};

  这一次简化分析,c1的偏移量为0,占用S2的第1个字节,c2偏移量为1,占用S2的第2个字节,d1的偏移量为4,占用S2的第5~8字节,由于最大的对齐数为4,S2的内存已经存储到第8个字节,并且8/4 = 2,2是个整数,所以可以得出S2的大小为8个字节,十分细心的人可能已经发现,上文的结构体变量S1和本例程的S2的成员变量相同,但是两者占用的内存却不同这说明,我们在声明结构体的时候,调整成员变量的位置,可以节省内存,即如果我们做到让占用空间小的成员尽量集中在一起,就可以节省空间。S2的存储位置如图5所示。
在这里插入图片描述

图5 S2的存储状况 程序验证:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S2
{
	char c1;
	char c2;
	int  d1;
};
int main()
{
	printf("S2成员c1偏移量:%d\n", offsetof(struct S2, c1));
	printf("S2成员c2偏移量:%d\n", offsetof(struct S2, c2));
	printf("S2成员d1偏移量:%d\n", offsetof(struct S2, d1));
	printf("S2结构体的大小为:%d\n", sizeof(struct S2));
	return 0;
}

运行结果:
在这里插入图片描述
  为了熟悉,再来个练习
练习3

struct S3
{
	double d;
	char c;
	int i;
};

分析:

表1 结构体变量S3存储状况

在这里插入图片描述
  上面提供了一个表格,利用这个表格可以简化我们的分析,希望诸位能学会。

  下面分析一个嵌套结构体的情况,嵌套结构体就选择上文分析的S3,我们已经知道S3的大小为16字节。那么请看练习4,试着分析结构体变量S4的大小。

练习4-结构体嵌套问题

struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

分析:

表2 结构体变量S4存储状况

在这里插入图片描述
  可以看到结构体S3作为S4的成员,S3的最大对齐数变成了8,这一点需要特别注意。我们可以简化表2分析,如表3所示。

表3 结构体变量S4存储状况(简化版)

在这里插入图片描述
程序验证S4

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	printf("S4成员c1偏移量:%d\n", offsetof(struct S4, c1));
	printf("S4成员s3偏移量:%d\n", offsetof(struct S4, s3));
	printf("S4成员d偏移量:%d\n", offsetof(struct S4, d));
	printf("S4结构体的大小为:%d\n", sizeof(struct S4));
	return 0;
}

运行结果:
在这里插入图片描述
  上文提到Visual Studio 编译器的默认对齐数为8字节,其实利用#pragma 这个预处理指令可以修改默认对齐数,下面对这一点进行介绍。

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stddef.h>
//visual studio 2019的默认对齐数为8
struct s1
{
	char c1;
	double d;
	int i;
};

#pragma pack(4)//设置默认对齐数为4
struct s2
{
	char c1;
	double d;
	int i;
};
#pragma pack()//取消设置对齐数,对齐数被还原为默认值
int main()
{
	printf("s1成员c1偏移量:%d\n", offsetof(struct s1, c1));
	printf("s1成员c2偏移量:%d\n", offsetof(struct s1, d));
	printf("s1成员i偏移量:%d\n", offsetof(struct s1, i));
	printf("s1结构体的大小为:%d\n", sizeof(struct s1));
	printf("s2成员c1偏移量:%d\n", offsetof(struct s2, c1));
	printf("s2成员c2偏移量:%d\n", offsetof(struct s2, d));
	printf("s2成员i偏移量:%d\n", offsetof(struct s2, i));
	printf("s2结构体的大小为:%d\n", sizeof(struct s2));
	return 0;
}

运行结果:
在这里插入图片描述

1.5 内存对齐的原因

  上文讲述了内存对齐的一点知识,那么为什么要内存对齐呢?主要有以下两点。

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总结

  本文讲述了结构体的声明两种方式,结构体定义和初始化的方式,介绍了匿名结构体,并给出了结构体的自引用方法。结构体的内存对齐是本文的重点内容,掌握结构体内存对齐的规则是计算结构体大小的基础。以上内容如有错误,还请各位看官多多批评。


  1. C和指针——【美】Kenneth A.Reek. ↩︎

  2. 例解 C 语言中的匿名联合体和匿名结构体. ↩︎

  3. C 结构体. ↩︎

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

自定义类型:结构体 的相关文章

  • eclipse 配置 C++

    前言 最近有项目需要c 但是c 自从离校那时就没碰过了 所以要重新学习下 因为曾经为了做自己的博客网站 学了java 下载了eclipse 也是在eclipse上写的博客网站的 所以对eclipse还是相对熟悉的 而且平时写代码都是用vim
  • android手势识别opencv,较为成熟的安卓项目--人面识别,手势识别向

    一 人脸识别 1 目标检测 目标追踪 人脸检测 人脸识别 效果 2 Android下使用OpenCV实现人脸检测 效果 3 人脸标识 效果 4 人脸检测 github https github com VernonVan Face 效果 主
  • Jmeter 中随机函数__Random 的使用

    前段时间 在做接口测试时 经常遇到接口参数需要输入不同的内容或者手机号码等 不允许输入重复的参数内容 比如不同的手机号码 那此时可以通过Random 随机函数来解决此问题 以前的文章有介绍过使用time函数来实现 详见 http blog
  • RuntimeError: Error(s) in loading state_dict for Net(让人心累的错误)

    RuntimeError Error s in loading state dict for Net size mismatch for classifier 4 weight xxxxxx 后面一堆错误 这个是model py 千万千万别
  • 【DL】第 6 章:语言建模

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore

随机推荐

  • 小程序跳转:云开发之h5跳小程序

    目录 前言 前提条件 注意 实现步骤 更多前端知识 前言 此方案是我在实际开发中的全部过程 因为我也是第一次做小程序的云开发 一开始根据这个文档就遇到了一些坑 所以在这里我做了更详细的步骤分解 非个人主体并且已认证的 微信认证 小程序 使用
  • tictoc例子理解10-12

    tictoc10 12 tictoc 10 几个模块连接 发送消息直到模块3收到消息 tictoc 11 新增信道定义 tictoc 12 双向连接信息简化定义 tictoc 10 几个模块连接 发送消息直到模块3收到消息 让我们用几个 n
  • 【机器学习】 Matlab 实现多种分类器(感知机、KNN、Logistic、最大熵、决策树、朴素贝叶斯)的二分类

    写在之前 这是本人的统计学习方法作业之一 老师要求一定要用Matlab编程 本人在此之前未曾大量使用Matlab 因此某些算法可能因为不知道函数或者包而走了弯路 代码高亮查了一下 没找到Matlab的所以用了C的 部分算法参考了某些算法的p
  • github中fork分支和pullrequest的最佳实践

    github中fork分支和pullrequest的最佳实践 github中fork分支和pullrequest的最佳实践 最近在参与一个国外的github开源项目 遇到自己fork了源库 一段时间之后 源库已经更新了一些内容 这样 自己f
  • 【uni-app】使用uni-app实现简单的登录注册功能

    文章目录 前言 一 页面布局 二 注册页面 1 注册接口使用 2 注册成功提示 3 注册成功页面跳转 4 完整代码 三 登录页面 1 登录接口使用 2 本地存储使用 3 完整代码 总结 前言 大家好 今天和大家分享一下如何在uni app中
  • Vue3(快速上手)

    Vue2 与 Vue3 的区别 数据双向数据绑定 Vue2 0 数据绑定 是通过 Object defineProperty 来劫持对象属性的 geter 和 seter 操作 当数据发生改变发出通知 Object defineProper
  • 简单工厂模式、工厂方法模式和抽象工厂模式之间的异同

    注 纯属个人理解 如有错误请大家指正 相同之处 AbstractProduct ap Factroy createClass 1 都是利用工厂类 工厂子类 来创建对应的类对abastractProduct进行实例化操作 不同之处 简单工厂模
  • 循环-13. 求特殊方程的正整数解(15)

    本题要求对任意给定的正整数N 求方程X2 Y2 N的全部正整数解 输入格式 输入在一行中给出正整数N lt 10000 输出格式 输出方程X2 Y2 N的全部正整数解 其中X lt Y 每组解占1行 两数字间以1空格分隔 按X的递增顺序输出
  • Comparable接口对list的多条件排序

    版权声明 本文为博主原创文章 未经博主允许不得转载 https blog csdn net xiaoyanghapi article details 52496325 普通的类要实现排序 必须实现Comparable接口 并重写Compar
  • python生成exe文件运行闪退解决方法

    python生成exe文件运行闪退解决方法 使用pyinstaller生成 exe文件 pyinstaller F filename py 用python写了一个程序 在python下运行是正常的 但是生成exe文件后运行闪退 我当时怀疑是
  • 一文讲透缓存方案及常见问题——进阶篇

    前文有提到 缓存其中一种实施方式是利用硬件的读取速度的差异来做缓存加速 但是更高速的存储介质往往受限于成本 价格贵 或硬件限制 CPU缓存物理大小 其容量相较硬盘要小很多 再加上根据二八原则 热点数据可认为只占20 因此无论是处于实用还是成
  • 第九课:面向对象-类和对象

    第九课 面向对象 类和对象 一 面向对象简介 1 数据存储方面 1 变量 只能存储一个 如果内容多了 存储起来需要定义n个变量 麻烦 2 数组 可以存储一组相同数据类型的数据 3 数据类型相同 如果用数组存储一个人的信息 比较丰富的信息的话
  • 计算机系统基础课程实验课bomb--phase_4

    首先还是栈指针减24 应该也是为了存储多个值 然后和phase 3一样 把输入的值放在了 rcx和 rdx 输入格式也和phase 3一样 如图 然后将返回值与2作比较 如果不等于2就爆炸 所以只能输入两个数字 然后将第一个输入值d1与14
  • Layout布局中Button被拉伸解决方法

    问题描述 从一个项目中复制Layout布局到新的项目后 UI界面的Button总是被拉伸变大 网上找了许多资料 对Button属性各种设置却也未果 甚是郁闷 效果图如下 复制到另一项目后 gt 复制的布局代码
  • java中四种引用类型

    在JDK 1 2以前的版本中 若一个对象不被任何变量引用 那么程序就无法再使用这个对象 也就是说 只有对象处于可触及 reachable 状态 程序才能使用它 从JDK 1 2版本开始 把对象的引用分为4种级别 从而使程序能更加灵活地控制对
  • Java连接MySQL、Oracle加载多个不同版本的驱动

    Java连接MySQL Oracle加载多个不同版本的驱动 连接MySQL以下步骤 加载驱动 通过DriverManager getConnection String url String user String password 方法尝试
  • 解决git 出现 Your account has been blocked问题

    场景 前同事离职 所用电脑上的git账户被屏蔽 如何更换为自己的账户继续使用该本地库 解决 重新本地生成SSH密钥 将ssh rsa贴入自己服务器账户下的SSH Keys中即可
  • 大型网站后台架构的Web Server与缓存

    1 1 Web server Web server 用来解析HTTP协议 当web服务器接收到一个HTTP请求时 会返回一个HTTP响应 例如送回一个HTML页面 为了处理一个请求 web服务器可 以响应一个静态页面或者图片 进行页面跳转
  • 三万字机器学习项目整理(基础到进阶)

    如果你是学生 计算机领域的工作者 我强烈建议你学习 掌握机器学习 我不敢说它是最简单的 机器学习的确很简单 但是掌握机器学习一定是性价比最高的 本文用浅显易懂的语言精准概括了机器学习的相关知识 内容全面 总结到位 剖析了机器学习的what
  • 自定义类型:结构体

    C语言之自定义类型 结构体 文章目录 C语言之自定义类型 结构体 前言 1 结构体 1 1 结构体声明 1 1 1 一般格式 1 1 2 特殊结构体声明 1 2 结构的自引用 1 3 结构体变量的定义和初始化 1 4 结构体内存对齐 1 4