C中自定义类型——结构体

news2024/11/25 20:43:36

一.前言

在C语言中,不仅有int、char、short、long等内置类型,C语言还有一种特殊的类型——自定义类型。该类型可以由使用者自己定义,可以解决一些复杂的个体。

二.结构体

2.1结构体的声明

我们在利用结构体的时候一般是用于描述一些有多种因素的对象。比如我们要描述一个学生,那么学生就有他的姓名,性别,年龄,如果我们使用内置类型的话,非常麻烦,我们就可以定义一个结构体类型用来描述一个学生:

//定义了一个学生类型
struct student
{
	char name[15];//学生姓名
	char sex[5];//性别
	int age;//年龄
};

 我们创建了类型之后还得依靠此类型创建变量。

2.2结构体变量的创建和初始化

我们要时刻记住,我们创建的是一个类型,而不是一个变量,我们要根据此类型来创建我们需要的变量。 下来,我们依据上面创建的学生类型来创建变量并进行初始化:

#include <stdio.h>

//定义了一个学生类型
struct student
{
	char name[15];//学生姓名
	char sex[5];//性别
	int age;//年龄
};

int main()
{
	//在创建变量的同时进行初始化
	//该初始化必须得按照结构体内部成员的顺序进行初始化
	struct student stu1 = { "zhangsan","nan",18 };

	printf("%s\n", stu1.name);
	printf("%s\n", stu1.sex);
	printf("%d\n", stu1.age);

	//利用访问操作符可实现自定义顺序进行初始化
	struct student stu2 = { .age = 21,.name = "lisi",.sex = "nv" };

	printf("%s\n", stu2.name);
	printf("%s\n", stu2.sex);
	printf("%d\n", stu2.age);

	return 0;
}

我们除了在主函数内创建结构体变量外,还可以在定义结构体的同时进行创建变量:

#include <stdio.h>

struct student
{
	char name[15];
	char sex[5];
	int age;
}stu1;

int main()
{
	struct student stu2 = { 0 };

	return 0;
}

stu1和stu2都是结构体类型变量,区别是stu1是全局变量存放在静态区,而stu2是局部变量存放在栈上。

2.3结构体访问操作符

结构体访问操作符有.(句点操作符)和->(箭头操作符)。句点操作符用于结构体变量的访问,而箭头操作符用于结构体指针来访问结构体变量。该操作符我在之前已经介绍过了,大家可以看我之前的博客。结构体中的访问运算符-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/xsc2004zyj/article/details/136722599?spm=1001.2014.3001.5502

 2.4结构体的特殊声明

结构体除了正常的声明外,还有一种特殊的声明方式,叫做匿名结构体。

匿名结构体就是在声明结构体的时候不给结构体标识符,而直接创建一个变量。

//匿名结构体,该结构体没有标识符
struct
{
	char name[15];
	char sex[5];
	int age;
}stu;

而对于匿名结构体类型来说,该结构体基本上只能使用一次,因为该结构体没有标识符,后续没法再对此来创建变量。

下面提出一个问题:

#include <stdio.h>


struct
{
	int a;
	char b;
	float c;
}x;

struct
{
	int a;
	char b;
	float c;
}*p;

int main()
{
	p = &x;
	return 0;
}

主函数中的p = &x;是否合法?

答案是:非法,因为以上两个结构体的成员变量完全一样,并且都使用匿名的方式,所以编译器会把两个声明当成两个完全不同的类型,所以是非法的。

2.5结构体的自引用 

如果我们在描述某个对象的时候,需要用到结构体的自引用,我们应该如何写?

struct stu
{
	int age;
	struct stu s;
};

上面代码正确么?如果正确,那么sizeof(struct stu)的大小是多少呢?上面的代码其实是错误的,如果这样进行自引用,那么一个结构体变量的空间将会无穷大,因为一个结构体里面永远还有一个结构体。

正确的应该是存放一个该结构体的指针,因为一个指针的大小不是四个字节,就是八个字节。而且该指针也指向了一个该类型的变量,所以也实现了结构体的自引用。

struct stu
{
	int age;
	struct stu *s;
};

2.6利用typedef重命名结构体类型

我们在创建结构体变量的时候每次都要写出struct tag x;这样写起来非常麻烦,并且有人可能粗心大意而忘记struct。所以,我们在声明结构体的时候,可以利用typedef关键字给该结构体重新起个名字,用该名字来创建变量。

#include <stdio.h>

typedef struct student
{
	char name[14];
	int age;
}stu;

int main()
{
	stu s;
	struct student s2;
	return 0;
}

此时,在声明时分号前面就不是创建变量了,还是该结构体的一个新名字——stu。在创建变量的时候,stu和struct student是一个意思。

 我们也可以利用typedef来解决匿名结构体的问题,我们只需要给匿名结构体重新起一个名字,利用该名字创建变量就行了。这时,该匿名结构体与正常声明的结构体没有区别。

#include <stdio.h>

typedef struct
{
	char a;
	int b;
}X;

int main()
{
	X x;
	X s;
	return 0;
}

这样就解决了匿名结构体只能使用一次的局限。我们可以利用重命名的结构体进行创建变量,初始化等操作

 三.结构体内存对齐

我们已经基本了解了结构体的内容,下来我们来讨论结构体中的热门话题:结构体的大小。这也是最近热门的考点:结构体内存对齐。

3.1对齐规则

在理解内存对齐之前,我们得先了解结构体内存对齐的规则。

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小间的较小值。VS上的默认对齐数 = 8。Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体的总大小等于最大对齐数(结构体的每一个成员变量都有对齐数,所有对齐数中最大的一个)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

 下来我来一一介绍结构体的对齐规则。我们通过一个练习来引出:

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));

	return 0;
}

我们利用该结构体来进行解说,判断该结构体的大小。有的人会想,该结构体的成员两个char,一个int,那就占6个字节,是这样么?

我们看到,结果是12个字节,与我们的猜测不符。这就是因为结构体内部存在内存对齐规则。

在上图,我借助练习题详细介绍了结构体对齐规则如何理解,以及内存是如何进行对齐的。大家先仔细理解上图,后进行下面几个结构体大小的练习。

3.1.1练习1

#include <stdio.h>

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

int main()
{
	printf("%zd\n", sizeof(struct S2));
	return 0;
}

 

3.1.2练习

#include <stdio.h>

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

int main()
{
	printf("%zd\n", sizeof(struct S3));
	return 0;
}

 

3.1.3练习3

#include <stdio.h>
//结构体嵌套

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

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%zd\n", sizeof(struct S4));
	return 0;
}

3.2为什么存在内存对齐

大部分的参考资料是这样说的:

3.2.1平台原因(移植原因):

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

3.2.2性能原因:

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

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

 3.2.3那么如何尽可能节省空间的浪费呢?

我们来分析一下,这两个结构体:

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

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

S1和S2的成员类型都一样,只是存储的顺序不同,那这两个结构体的大小是否一样大呢?

我们看到S1和S2的成员虽然相同,但是两者的大小却不同,我们来分析一下。

 那么,我们在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:

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

 3.2.4修改默认对齐数

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

//利用#pragma修改默认对齐数

#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;
}

我们在上面已经分析该结构体的大小为12个字节(在VS默认8为对齐数的情况下),而我们利用#pragma已经将编译器的默认对齐数修改为1,结果还是12个字节么?

 我们分析后得出,修改默认对齐数后该结构体的大小变成了6字节。

四.结构体传参

我们创建好结构体变量后,可能需要把该结构体变量的某个成员变量传给某个函数。而我们到现在由两种传参的方式:值传递地址传递。那我们应该选择哪一种方式对结构体变量进行传参呢?

//结构体传参

#include <stdio.h>

struct S
{
	char c1;
	char c2;
	int i;
};

//值传递
void test1(struct S s)
{
	printf("%c\n", s.c1);
	printf("%c\n", s.c2);
	printf("%d\n", s.i);
}

//地址传递
void test2(struct S* ps)
{
	printf("%c\n", ps->c1);
	printf("%c\n", ps->c2);
	printf("%d\n", ps->i);
}

int main()
{
	struct S s = { 'a','b',10 };
	test1(s);
	test2(&s);
	return 0;
}

我们看到,无论是值传递,还是地址传递,都可以达到我们的目的。那我们到底应该选那种方式呢?

我们首先要知道,值传递中的形参是实参的一份临时拷贝,会在栈上存储其拷贝内容,也就意味着会消耗内存,那如果该结构体的大小非常大的话,我们在栈上就会消耗很多内存。

无论是何种传参方式,都会进行压栈操作,而值传递在压栈过程中机会在时间和空间上有大量的系统开销。而地址传递的话,指针的大小不是8个字节就是4个字节,在压栈过程中不会有太多空间和时间上的开销。 

所以,在结构体传参的时候,要传结构体的地址

 五.结构体实现位段

利用结构体实现位段。位段这个概念大家可能没听过,但段位大家肯定不陌生。位段是一种特殊的结构体类型,位段必须得依靠结构体来实现。

5.1什么是位段?

位段的声明和结构体是类似的,但有两个不同:

  1. 位段的成员必须是int 、unsigned int、或者signed int,在C99中位段成员的类型也可以选择其他类型。
  2. 位段的成员后面有一个冒号和一个数字。

 比如:

struct S
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _e : 30;
};

 通常在创建位段式结构体的时候习惯在每个变量前面加上下划线,以此来说明这是位段式结构体。

我们看到结构体和位段式结构体的区别就在于位段的每个成员后面多了一个冒号和一个数字,这是什么意思呢?

这每个成员后面的数字表示该成员占的bit位,_a占2个bit位,_b占5个bit位,_c占10个bit位。为什么要这样设置变量呢?

我们想,如果_a里面只会存1,2,3这三个数,1二进制就是01,2二进制就是10,3二进制就是11,所以存这三个数用两个bit位就够了。以此类推,_b就是存只需5个bit位就能表示的数,_c就是只存10个bit位就能表示的数。这样就可以大大减少空间的浪费。那位段式结构体是怎样储存数据的呢?是如何达到节省空间的?下面我们来了解位段的内存对齐规则。

5.2位段的内存对齐

位段的成员最好都是同一种类型的,位段会根据成员类型来开辟空间,比如成员是int型,就一次开辟4个字节,如果是char型,就一次开辟一个字节。下面我们举一个例子。

 那该位段的大小是不是跟我的结论一样呢?

 我们看到,该位段的大小与我们的推测相同。我们再来看一下该位段是如果写入数据的。

 我们分析完之后,在VS上调试一下,发现s的内存如下,和我们分析的一样,占三个字节,存储的是0a 0c 05。

 5.3位段的跨平台问题

  1. int位段是被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目是不能确定的。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。(在VS上默认从右向左分配)
  4. 当⼀个结构包含两个位段,第⼆个位段成员比较大时,无法容纳于第⼀个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。(VS上默认舍弃)

 总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

 5.4位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小⼀些,对网络的畅通是有帮助的。

 5.5位段使用的注意事项

位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

 所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在⼀个变量中,然后赋值给位段的成员。

#include <stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//这是错误的 

	//正确的⽰范 
	int b = 0;
	scanf("%d", &b);
	sa._b = b;
	return 0;
}

完! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1587947.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Nodejs + express连接数据库mongoose

文章目录 先创建一个js文档安装 MongoDB 驱动程序&#xff1a;引入 MongoDB 模块&#xff1a;设置数据库连接&#xff1a;新建一个表试试执行数据库操作&#xff1a;关闭数据库连接&#xff1a; 前面需要准备的内容可看前面的文章&#xff1a; Express框架搭建项目 node.js 简单…

Kivy 学习2

from kivy.app import App from kivy.uix.button import Button from kivy.uix.floatlayout import FloatLayout from kivy.graphics import Rectangle, Colorclass FloatLayoutApp(App):def build(self):def update_rect(layout, *args):设置背景尺寸&#xff0c;可忽略layout…

java实现TCP交互

服务器端 import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.PriorityQueue; import java.util.Scanner;public class TCP_Serv…

2023年MathorCup数学建模D题航空安全风险分析和飞行技术评估问题解题全过程文档加程序

2023年第十三届MathorCup高校数学建模挑战赛 D题 航空安全风险分析和飞行技术评估问题 原题再现 飞行安全是民航运输业赖以生存和发展的基础。随着我国民航业的快速发展&#xff0c;针对飞行安全问题的研究显得越来越重要。2022 年 3 月 21 日&#xff0c;“3.21”空难的发生…

Python —— 简述

Houdini Python | 笔记合集 - 知乎 Houdini内置三大语言&#xff1a; 表达式&#xff0c;主要用于节点参数控制&#xff0c;可实现跨模块控制&#xff1b;vex&#xff0c;速度最快&#xff08;比表达式和Python快一个数量级&#xff09;&#xff0c;非常适合密集型计算环境&…

uni-app实现分页--(2)分页加载,首页下拉触底加载更多

业务逻辑如下&#xff1a; api函数升级 定义分页参数类型 组件调用api传参

Pytest精通指南(02)对比Unittest的差异

文章目录 前言用例编写规则不同用例前置与后置条件不同断言功能不同测试报告失败重跑机制参数化用例分类执行Unittest 前后置示例Pytest 前后置示例总结 前言 在Python中&#xff0c;unittest和pytest是两个主流的测试框架&#xff1b; 它们都旨在支持自动化测试、使用断言验证…

AI的尽头真的是能源吗?

引言 近日&#xff0c;英伟达黄仁勋、OpenAI奥特曼等科技界大佬也表达了AI被能源制约的焦虑。 黄仁勋在一次公开演讲中指出&#xff0c;AI未来发展与光伏和储能紧密相连。他强调&#xff0c;不应仅仅关注计算力&#xff0c;而是需要更全面地考虑能源消耗问题。黄仁勋表示&…

PostgreSQL入门到实战-第十七弹

PostgreSQL入门到实战 PostgreSQL表联接(一)官网地址PostgreSQL概述PostgreSQL中Join命令理论更新计划 PostgreSQL表联接(一) 各种PostgreSQL联接&#xff0c;包括内部联接、左侧联接、右侧联接和完全外部联接 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列内容不…

亚马逊的核心壁垒:物流

物流为美国电商市场渗透及格局的核心影响因素&#xff0c;也是亚马逊的核心壁垒所在。 从行业规模来看&#xff0c;美国电商渗透率低于中国&#xff0c;主要由于 两国地理及人口密度差异导致美国物流履约难度更大&#xff0c;此外美国更发达的实 体零售业和更为严苛的电商政策…

[Linux][环境变量][进程地址空间]详细解读

目录 1.环境变量1.基本概念2.常见环境变量3.查看环境变量的方法4.测试PATH5.测试HOME6.和环境变量相关的命令7.环境变量的组织方式8.通过代码如何获取环境变量9.通过系统调用获取或设置环境变量10.环境变量通常是具有全局属性 2.进程地址空间0.这里的地址空间&#xff0c;是物理…

【感谢】心怀感恩,共赴知识之旅——致每一位陪伴我突破百万总访问量的您

小伙伴朋友们&#xff1a; 此刻&#xff0c;我怀着无比激动与深深感激的心情&#xff0c;写下这篇特别的博文。今天&#xff0c;我的CSDN总访问量成功突破了百万大关&#xff0c;这不仅是一个数字的跨越&#xff0c;更是你们对我的支持、信任与鼓励的有力见证。在此&#xff0…

CNN家族的族谱!

没有过时的技术&#xff0c;只是看什么样的应用场景&#xff0c;某些场景下&#xff0c;老技术也能焕发光彩&#xff01; 发展历程 CNN思想起源——喵星人的视觉系统 20世纪60年代左右&#xff0c;加拿大神经科学家David H. Hubel和Torsten Wisesel发现了猫的视觉中枢里存在感…

SAP_ABAP_MM_PO审批_队列实践SMQ1

SAP ABAP 顾问&#xff08;开发工程师&#xff09;能力模型-CSDN博客文章浏览阅读1k次。目标&#xff1a;基于对SAP abap 顾问能力模型的梳理&#xff0c;给一年左右经验的abaper 快速成长为三年经验提供超级燃料&#xff01;https://blog.csdn.net/java_zhong1990/article/det…

Python - 深度学习系列32 - glm2接口部署实践

说明 前阵子&#xff0c;已经对glm2的接口部署做了镜像化。本次的目的是&#xff1a; 1 测试在隔了一阵子&#xff08;忘记&#xff09;的情况下&#xff0c;快速部署时是否有障碍&#xff0c;是不是足够方便2 在算网机环境下&#xff0c;能否快速的实现部署。仅考虑文件方式…

策略模式【行为模式C++】

1.概述 策略模式是一种行为设计模式&#xff0c; 它能让你定义一系列算法&#xff0c; 并将每种算法分别放入独立的类中&#xff0c; 以使算法的对象能够相互替换。 策略模式通常应用于需要多种算法进行操作的场景&#xff0c;如排序、搜索、数据压缩等。在这些情况下&#x…

D-LinkNAS 远程命令执行漏洞(CVE-2024-3273)RCE漏

声明&#xff1a; 本文仅用于技术交流&#xff0c;请勿用于非法用途 由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;文章作者不为此承担任何责任。 简介 D-LinkNAS是由D-Link公司制造的网络附加存储设备。…

有趣的css - 动态雷达扫描

大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是使用 css 实现一个动态的雷达扫描&#xff0c;快学起来吧&#xff01; 《有趣的css》系列最新实例通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码css 部分代码…

产品推荐 | 瑞苏盈科基于立体帧捕捉和视频处理应用的火星Mars EB1开发板

01 产品概述 火星Mars EB1底板是为火星Mars系列FPGA和SoC核心板设计的通用底板&#xff0c;非常适用于立体帧捕捉和视频处理应用&#xff0c;可以为构建基于FPGA的定制化硬件系统提供一个良好的基础和开端。 02 核心亮点 ■ 与所有火星Mars系列FPGA和SoC核心板兼容 ■ 适用…

2024mathorcup数学建模C题思路分析-物流网络分拣中心货量预测及人员排班

# 1 赛题 C 题 物流网络分拣中心货量预测及人员排班 电商物流网络在订单履约中由多个环节组成&#xff0c;图 ’ 是一个简化的物流 网络示意图。其中&#xff0c;分拣中心作为网络的中间环节&#xff0c;需要将包裹按照不同 流向进行分拣并发往下一个场地&#xff0c;最终使包裹…