结构体与共用体基础

结构体基础用法与共用体简述

  • 1.结构体的定义
  • 2.结构体声明及使用
  • 3.结构体成员初始化
  • 4.结构体占用空间探究
    • 4.1 结构体成员所在地址
    • 4.2 按地址值访问结构体内容
    • 4.3 内存对齐
  • 5.共用体
  • 6.总结

1.结构体的定义

之前的课程中,我们介绍了很多数据类型,如整形、浮点型、双精度实型等,不过想象另一种场景,我们申请了一个变量,希望它能够储存很多类型的数据,该怎么做呢?最简单的方法就是定义一个结构体:

#include<stdio.h>
#include<iostream>
struct Student
{int age;char name[32];char sex;
};

这样我们就有了一个自定义的Student结构,内含int类型的年龄、string类型的名字和char类型的性别。那么我们要如何声明和使用呢?

2.结构体声明及使用

结构体的声明方式有两种,分别为:
2.1 定义后使用结构体类型名进行声明
定义后的结构体可以当做一个自定义的类型名,声明的方法与其他类型一样:

int main()
{Student ZhangSan;
}

2.2 定义时直接声明
这种定义方式示例如下:

struct Student
{int age;char name[32];char sex;
}ZhangSan,*LiSi; // LiSi是定义的结构体指针

当然,如果使用这种方式进行定义时,后面我们不再需要申请Student类型的变量时,可以不定义结构体名,例如:

struct
{int age;char name[32];char sex;
}ZhangSan;

3.结构体成员初始化

在为结构体变量赋值时,我们需要首先要通过引用的方式找到需要被赋值的成员,通过点运算符实现。当然如果我们定义的是结构体指针变量,则需要用->进行引用:

#include <cstring> // 引用strcpy函数
int main()
{Student ZhangSan,*LiSi,lisi;ZhangSan.age=10;strcpy(ZhangSan.name,"ZhangSan");ZhangSan.sex='m';cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;LiSi=&lisi;LiSi->age=20;strcpy(LiSi->name,"LiSi");LiSi->sex='w';cout<<LiSi->age<<" "<<LiSi->name<<" "<<(*LiSi).sex<<endl; // 结构体指针引用结构体内容的两种方法
} 
// 输出为:10 ZhangSan m
//        20 LiSi w

strcpy(a,b)函数可以将b字符串赋值给a字符数组,如果a数组有原始内容则b字符串会对a字符数组进行覆盖,注意不要超界。
这里我再强调一下,如果仅仅定义一个结构体指针的话,是不可以引用的和赋值的,因为这个指针此时没有正确的指向(即没有初始化)。
除了上述赋值方法,我们还可以在声明时就进行赋值,方法如下:

int main()
{Student ZhangSan={10,"ZhangSan",'m'};
}

这种方法同样适用于另一种声明方式中:

struct Student
{int age;char name[32];char sex;
}ZhangSan={10,"ZhangSan",'m'};

4.结构体占用空间探究

下面我们来讨论结构体占用的空间有什么样的特点。在开始探究之前,先给大家做点小科普。首先我尝试用cout打印变量所占用的地址:

int main()
{int a=10;char sex='w';cout<<&a<<" "<<&sex<<endl;
}
// 输出为:0x61fe1c w

可以看出,虽然&a打印出了我们希望的16进制地址值,但是&sex还是打印出了w字符。 这是因为C++会将char*类型的地址作为C风格字符串处理。这意味着假如sex的所在地址是0x1000,cout<<&sex仍会尝试将0x1000处的字符及其后面的字符作为字符串打印出来,直到遇到空字符为止,这也就是输出仍为w的原因。想要看到sex所在地址,需要先将char地址类型转换成其他指针类型,如void指针类型:

int main()
{int a=10;char sex='w';cout<<&a<<" "<<static_cast<void*>(&sex)<<endl;
}

这样就可以看到我们想要的结果了。

4.1 结构体成员所在地址

我们先看以下代码:

struct Student
{int age;char name[32];char sex;
};
int main()
{Student ZhangSan={10,"ZhangSan",'m'},*p;p=&ZhangSan;cout<<&ZhangSan<<" "<<&(ZhangSan.age)<<" "<<&(ZhangSan.name)<<" "<<static_cast<void*>(&ZhangSan.sex)<<endl;cout<<sizeof(Student);
}
// 输出为:0x61fdf0 0x61fdf0 0x61fdf4 0x61fe14
//        40

可以看出,对结构体变量取地址时拿到的是该变量的首地址,这一点与数组相同,而且看起来这几个16进制数字很像连续地址,也就是说结构体的存储方式也是连续存储。不过可能有小伙伴发出疑问了,Student所占用的空间应该是37字节(int为4字节,char为1字节),可为什么打印出的是40字节呢?再看另一个例子,我们把字符数组换成字符串(string类型,所占空间为32字节)再来打印看看:

// 只将结构体内容换掉,其他不变即可
struct Student
{int age;string name;char sex;
};
// 输出为:0x61fdd0 0x61fdd0 0x61fdd8 0x61fdf8
//        48

这下就更奇怪了,int类型明明是占了4字节,为什么age和name之间差了8个字节呢?32位数组和string类型所占的空间应该是一样的,为什么结构体所占空间却变化了呢?

4.2 按地址值访问结构体内容

带着这个疑问,我们来访问结构体所占地址的存储内容分别是什么。C++为我们提供了一种根据地址值访问空间的方法,我们可以利用这种方法来查看下各空间存储的具体内容,应用方法和注意事项我会写在代码的注释里:

struct Student
{int age;string name;char sex;
};
int main()
{Student ZhangSan={10,"ZhangSan",'m'};/*C++中,指针类型的变量均占8字节,因此我使用char指针接收地址值,因为char类型变量占1个字节,某些情况下更易于处理*/int* ptr = reinterpret_cast<int*>(&ZhangSan);// 由于已知ZhangSan的首个元素已知为int型,//这里可以直接申请一个int指针,然后将ZhangSan的起始地址转换成int指针进行记录cout<<*ptr<<endl; // 将会输出10long long address; // C++中指针类型值占8字节,长长整型才装得下并保证内容不丢失 address=reinterpret_cast<unsigned long long>(&ZhangSan); // 将ZhangSan的地址值转换成长长整型,以数字形式记录在address中,之后就可以进行整数加法了/*在C++中,如果你想在同一个作用域内重新声明一个变量,你需要使用花括号来创建一个新的作用域。这样就可以安全地重新声明变量。这里不理解也可以申请不重名的变量,不影响效果*/{char* ptr=reinterpret_cast<char*>(address+4); // 将address值加4再转换成char*并查看存储内容cout<<*ptr<<endl; // 将只输出换行,有兴趣的小伙伴可以尝试用不同的指针取这个地址的内容}{char *ptr=reinterpret_cast<char*>(address+8); // address值+8并记录成char*类型cout<<*ptr<<endl; // 将没有实际输出内容,只输出换行cout<<*(reinterpret_cast<string*>(ptr))<<endl; // 将输出ZhangSan}
}

以4.1节中最后一段代码输出为例,0x61fdd0地址以后的四位才有int类型数据,而0x61fdd4到0x61dd8是四个空字节。为什么会是这样的情况呢?

4.3 内存对齐

解释上述问题就要了解计算机的硬件结构了。本文以64位计算机为例,计算机的ram上有8个chip,CPU读取数据最快速的方法就是从8个chip的相同物理位置并行读出内容。假如我们想要的结构体含有两个整型数据,而存储方式是这样存储的:在这里插入图片描述
图中,每个方块代表一个chip的对应存储位置,四个方块代表一个int类型。含有两个int元素的结构体内容占用8个字节,这样紧凑的排列下CPU就可以一次读取全部的结构体内容。但如果我要的结构体要存放一个short和两个int类型的数据呢?紧密型的存储就成了这样:
在这里插入图片描述

这种情况下,CPU想要读取到结构体的所有内容就必须读取两次RAM了,而且大家应该也注意到了,读取c的时候需要先读前两个地址内容,在读后两个地址内容,然后再拼接到一起才可以,这样很影响读取效率。那么怎么存比较方便读取呢?计算机会选取结构体中所占空间最大的类型作为参考标准,如果这个类型所占字节数小于8,会以这个类所占字节数为标准大小(本例中为4),对其他所占字节小于该类型的其他类型进行“扩容”,令这些类型所占空间为标准大小,也就是这样存储:
在这里插入图片描述
这样一来,虽然CPU仍需读取两次才能得到完整的结构体内容,但不再需要将得到的地址重新拼接整理,也就达到了节约时间的目的。当然,这个“扩容”并不是将short类型所占的字节强行扩大为4,而是在short内容后补充两个空内容字节进行占位。如果结构体存储内容中占用空间最大的类型大于8,那么标准大小则默认为8。这就是鼎鼎大名的内存对齐,典型的用空间换时间的方法。
讲到这里,细心的小伙伴可以观察一下C++中是否存在占用空间小于8且不是8的整数因数的类型。

以上图为例,含有两个int和一个short类型的结构体所占空间应该为12字节,我们用代码验证一下:

struct test
{int a;short b;int c;
};
int main()
{cout<<sizeof(test)<<endl;
}
// 输出为:12

看起来我们的猜想是正确的。那么回到刚才的问题:

struct Student
{int age;string name;char sex;
};

这样定义的结构体所占空间是多少呢?由于string所占空间为32,因此内存对齐的标准大小为8,int类元素后会补4个字节,char类后会补7个字节。因此这个Student类型所占空间为8+32+8=48。如果用长度为32的字符数组声明name,尽管字符数组所占的空间也为32,但是内存对齐并不会考虑数组,只会对比各个类型所占的空间大小,因此此时的Student结构体内存对齐的标准大小仍为4,也就是

struct Student
{int age;char name[32];char sex;
};

这个Student结构体所占空间为40(4+32+4)的原因了。
当然,C++也为我们设计了自行修改内存对齐方式的方法:

#pragma pack(1) // 设置此后结构体内容内存对齐的标准大小为1
struct Student
{int age;char name[32];char sex;
};
#pragma pack() // 此后恢复默认内存对齐方式

这样设置的Student结构体内容就会“紧密”排列,它的大小就是37字节了。实操中,我们可以用这种方法改变结构体内存对齐方式,但最好能平衡内存消耗和时间消耗。

5.共用体

共用体在C++中应用不多,他与结构体在写法上有些类似,我们用union定义一个共用体如:

union test
{int a;double b;char c;
};

声明和使用方式与结构体完全一样,这里不再赘述。与结构体不同的是,共用体所占空间取决于共用体中所占空间最大的类型,上例中double所占空间最大,因此test共用体所占空间为8。另外,如果共用体中有数组,如将上例中的char c换成char c[32],该共用体所占空间就变为32这一点也是与结构体明显不同。
我们看以下例子:

union Student
{int age;char name[32]; // 共用体不能使用string类char sex;
};
int main()
{Student ZhangSan;ZhangSan.age=10;strcpy(ZhangSan.name,"ZhangSan");ZhangSan.sex='m';cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;strcpy(ZhangSan.name,"ZhangSan");ZhangSan.sex='m';ZhangSan.age=10;cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;ZhangSan.sex='m';ZhangSan.age=10;strcpy(ZhangSan.name,"ZhangSan");cout<<ZhangSan.age<<" "<<ZhangSan.name<<" "<<ZhangSan.sex<<endl;
}
// 输出为:1851877485 mhangSan m
//        10
//
//
//        1851877466 ZhangSan Z

可以看到,只有最后赋值的变量才可以正确的存入内容。
共用体的特点,也是与结构体最大的区别总结如下:
1.在每一瞬间只能存有一个正确的内容,不可以同时存放几种不同类型的数据
2.在为共用体的另一类型变量赋值时,之前赋值的变量将失去作用
3.公用体所有成员的地址和公用体本身的地址都是相同的
4.不可以在定义共用体时对其进行初始化,不能使用共用体变量名作为函数的参数。
共用体的应用不多,所以我们不需要太深的理解。以上区别如果不能理解,记住也是没有问题的。

6.总结

本节我们讨论了结构体和公用体的基本用法,以及结构体的内存对其原则,下一节开始我们将讨论结构体的更高级用法。

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

测试用例级别该如何定义 ? 在工作中该如何应用它 ? 把握好这5个场景即可。

1.级别的作用 在编写测试用例的过程中&#xff0c;用例的级别经常是一个不可缺少的字段 &#xff0c;本篇幅就来聊下这个字段 &#xff0c;首先从它的作用是什么呢 &#xff1f;我觉得主要有两点 &#xff0c;分别是 &#xff1a; 用于测试用例不同套件的选取 &#xff0c;即用…

MMCLMC公差计算.exe

一、概要 软件及完整代码请戳这里&#xff1a;MMC&LMC公差计算软件及代码 图1 软件操作界面 本软件功能主要是根据实际应用选择MMR或者LMR原则&#xff0c;输入基本尺寸、形位公差尺寸和实际测量尺寸&#xff0c;即可计算出对应的公差值。以孔的MMR为例见如图2、3&#xf…

Java - JDBC

Java - JDBC 文章目录 Java - JDBC引言JDBC1 什么是JDBC2 MySQL数据库驱动3 JDBC开发步骤4 具体介绍 引言 思考: 当下我们如何操作数据库&#xff1f; 使用客户端工具访问数据库&#xff0c;手工建立连接&#xff0c;输入用户名和密码登录。编写SQL语句&#xff0c;点击执行…

【论文阅读|半监督小苹果检测方法S3AD】

论文题目 &#xff1a; : Semi-supervised Small Apple Detection in Orchard Environments 项目链接&#xff1a;https://www.inf.uni-hamburg.de/en/inst/ab/cv/people/wilms/mad.html 摘要&#xff08;Abstract&#xff09; 农作物检测是自动估产或水果采摘等精准农业应用不…

GitCode|部分项目开源代码

1.EasyKeyboard 基于MFC的简单软键盘&#xff0c;使用vs2017开发 PangCoder / EasyKeyboard GitCode基于Windows平台的软键盘&#xff0c;使用VS2017开发&#xff0c;使用MFC框架https://gitcode.net/qq_36251561/easykeyboard 2.EncoderSimulator 基于WPF应用的编码器模拟工…

Vue2 通过.sync修饰符实现数据双向绑定

App.vue <template><div class"app"><buttonv-on:clickisShowtrue>退出按钮</button><BaseDialog:visible.syncisShow></BaseDialog></div> </template><script> import BaseDialog from "./components…

开源:基于Vue3.3 + TS + Vant4 + Vite5 + Pinia + ViewPort适配..搭建的H5移动端开发模板

vue3.3-Mobile-template 基于Vue3.3 TS Vant4 Vite5 Pinia ViewPort适配 Sass Axios封装 vconsole调试工具&#xff0c;搭建的H5移动端开发模板&#xff0c;开箱即用的。 环境要求&#xff1a; Node:16.20.1 pnpm:8.14.0 必须装上安装pnpm&#xff0c;没装的看这篇…

Blender 与 3ds Max | 面对面的直接较量(2024)

Blender和3ds Max&#xff0c;哪个动画软件更好&#xff1f;作为一个从事动画领域十年的专业人士&#xff0c;Mark McPherson提供了八条最新建议&#xff0c;帮助你了解哪个软件更适合满足你的3D动画需求。 1.建模 获胜者&#xff1a;3ds Max。3ds Max的建模机制已经被证明是…

FairGuard游戏加固入选《CCSIP 2023中国网络安全行业全景册(第六版)》

2024年1月24日&#xff0c; FreeBuf咨询正式发布《CCSIP 2023中国网络安全行业全景册(第六版)》。本次发布的全景图&#xff0c;共计展示20个一级分类、108个细分安全领域&#xff0c;旨在为广大企业提供网络安全产品选型参考&#xff0c;帮助企业了解中国网络安全技术与市场的…

《幻兽帕鲁》1月29日游戏服务器推荐!腾讯云降低规格再次降价!

腾讯29日刷新规格&#xff0c;从14M降低到12M&#xff0c;硬盘和流量都有降低&#xff0c;但价格打下来了&#xff01;价格从66元/月降低到32元/月&#xff0c;277元/3个月降低到96元/3个月&#xff01; 三大厂商4核16G的云服务器价格对齐&#xff0c;不过具体参数略有不同 阿里…

iOS 自动打包如何配置配置打包证书和profile provision文件

Jenkins 打包相关问题记录 打包失败截图&#xff1a; 1、证书找不到 NO certificate matching ‘ ‘ for ‘ ’ code singing is required …. D791BAD1-390A-4587-A35C-A743A3D88D52.png 由于更新过证书配置&#xff0c;导致新证书没有导入到Jenkins中。 配置步骤&#xf…

idea docker 内网应用实践

文章目录 前言一、服务器端1.1 离线安装docker1.2 开启docker远程访问1.3 制作对应jdk镜像1.3.1 下载jdk171.3.2 Dockerfile 制作jdk17镜像1.3.3 镜像导出1.3.4 服务器引入镜像 二、Idea 配置2.1 Dockerfile2.2 pom 引入docker插件2.3 idea docker插件配置2.4 打包镜像上传2.5 …