C++ 炼气期之结构体
1. 前言
随着计算机
向着不同领域的延伸,数据
的概念已经不仅局限于数值型数据,计算机需要处理大量的非数值、且复杂的类型数据。
为了能抽象地描述这些非数值、复杂类型的数据,C++
引入了复合数据类型
的概念。
C++
数据类型分基本(原生)数据类型
和复合数据类型
,结构体
就是一种复合数据类型。可认为复合数据类型
是通过组合基本数据类型得到的一种新类型
,新类型
用来描述问题域中的特定数据
。
本文所用到的成员
一词指的是组成复合数据类型中的某一个子类型。
2. 结构体
现有一个开发学生管理系统
的需求,系统需要一个学生信息
管理模块,包含添加
、删除
、更新……
学生信息功能。解决这个问题之前,则需要考虑如何存储学生的个人信息以及一个学校的所有学生信息。
学生的个人信息包含学生的年龄
、性别
、成绩……
如果仅存储一个学生信息,这个问题很好解决,定义 3
个变量即可。
如果需要存储全校学生信息,可以考虑使用数组
,因受限于数组只能存储同类型数据的特点。为了完成这个需求,则需要 3
个数组,一个用来存储年龄
、一个用来存储性别
一个用来存储成绩
。显然,在编码时,需要随时随地同步 3
个数组,稍有不慎,便会出现错误。
此时,可能会有一个想法,能不能创建一个学生类型
,然后存储在数组中,数组中不再存储基本数据类型,而是一种新的学生类型
,如同二维数组一样,一维数组中存储一维数组,且不是一件很开心的事情 。
于是诞生出了一种设计理念:复合基本类型,设计出一种新的数据类型。
复合的方式有很多种,结构体
仅是其中之一。
2.1 结构体语法
//学生结构体:复合了 3 种基本数据类型
struct Student{
//学生年龄
int age;
//学生性别
char sex;
//学生成绩
float score;
};
结构体
是一种数据类型,使用语法和基本类型一样。
数据类型名 变量名;
一旦在语法上承认了这种数据类型
,和其它类型的区别就在于编译器为之所分配的内存大小。
结构体
和数组
类似。创建数组
和结构体
时,都是开辟了一个连续区域, 这个连续区域是多个变量的集合。数组
这个连续区域只能保存类型相同的数据,结构体
这个连续区域则可以存储不同类型的数据。
也就是说,在定义结构体之后,C++
运行时系统为之分配的是一个连续区域。那么这个区域有多大?是不是由组成此结构体的子数据类型的大小之和?
下面来求证一下。
首先使用c++
的sizeof
函数计算一下结构体的大小:
int main(int argc, char** argv) {
//创建结构体类型变量
Student stu;
//计算结构体的大小
int size= sizeof(stu);
cout<<size<<endl;
return 0;
}
输出结果:12
。也就是说在使用此结构体时,运行时系统分配的空间是12
。
Student
结构体由一个int
、一个char
、一个float
复合而成。理论上所需要的存储空间大小应该是4+1+4=9
。
int
是 4 字节
char
是1
字节
float
是4
字节
通过对比,可以推翻前面的结论:运行时系统为结构体
所分配的内存空间大小
并不一定是构建这个结构体的所有子数据类型的大小之和。
原因何在?
这是因为内存对齐
的缘故,内存对齐并不是本文的主题。这里只粗略说一下,运行时为结构体分配内存时,并不是我们所想象地简单地按顺序依次分配,实际上是为了提高内存的访问速度,以首地址为起始点,后续的内存空间地址尽可能是首地址的倍数。
如上图所示,在为char
类型的变量分配空间时,为了保证访问float
时的地址能被 4
整除,会为 char
类型变量填充 3
个空字节,导致结构体最后被分配到的空间是 12
。
如下结构体类型:
struct Student {
double age;
char sex;
double score;
};
在内存中占用 24
个字节,原由和上述是一样的。
对结构体有了一个大致了解后,再看一下使用结构体的 2
种语法结构:
- 静态声明。
- 动态声明。
2
种语法结构的区别在于数据存储的位置有差异性。当然,从字面意思而言,动态创建更有灵活性,事实也是如此。
2.2 静态声明
静态声明的特点:数据存储在栈中,变量中保存的是结构体本身。
如下代码:
#include <iostream>
using namespace std;
//学生结构体
struct Student {
//年龄
double age;
//性别
char sex;
//成绩
double score;
};
int main(int argc, char** argv) {
//静态声明
Student stu;
return 0;
}
和使用其它的变量一样,声明后需要给结构体初始化数据,常用初始化方式有 3
种:
- 使用
{}
进行初始化。优点是,一次到位,简洁明了。
Student stu={12,'M',99.5};
//可以省略 =
Student stu1 {12,'M',89};
//可以使用空 {} 为每一个分量设置一个默认值
Student stu2 {}
使用
.
运算符访问结构体的各个分量,对结构体进行初始化和使用。数组是同类型变量的集合,数组会为每一个存储单元指定一个唯一编号 。结构中的类型差异性很大,编号策略并不合适。但
.
运算符本质和编号是一样,都是通过移动指针来寻找变量。
Student stu;
//初始化
stu.age=12;
stu.score=98.8;
stu.sex='M';
给结构体赋值后,方能使用结构体中保存的数据,可以使用.运算符
使用结构体中的每一个分量。
cout<<"年龄:"<<stu.age<<endl;
cout<<"成绩:"<<stu.score<<endl;
cout<<"性别:"<<stu.sex<<endl;
- 使用另一个结构体中的数据。静态声明的结构体之间,采用的是
值复制
策略,即把一个结构体中的值赋值给另一个结构体。
//原结构体
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
//通过静态创建的结构体之间可以直接赋值
Student stu1=stu;
cout<<"年龄:"<<stu1.age<<endl;
cout<<"成绩:"<<stu1.score<<endl;
cout<<"性别:"<<stu1.sex<<endl;
//输出结果
//年龄:12
//成绩:98.8
//性别:M
这里做一个测试,如果更改第一个结构体中某个分量的值,是否会影响第二个结构体中同名分量的值。
Student stu1=stu;
//修改 stu 结构体中的年龄信息
stu.age=15;
//输出 stu1 中的数据
cout<<"年龄:"<<stu1.age<<endl;
cout<<"成绩:"<<stu1.score<<endl;
cout<<"性别:"<<stu1.sex<<endl;![3.png](https://s2.51cto.com/images/20220822/1661143541316303.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
输出结果:
答案是不会,因为 2
个结构体有各自独立的内存区域,一旦完成最初的赋值之后,2
者之间就没有多大联系了。如下图,修改 stu
的数据,不可能影响到 stu1
的数据。
2.3 动态声明
动态创建的结构体的特点:数据存储在堆中,结构体变量存储的是结构体在内存中的地址。如下语句:
Student * stu_=new Student();
new
运算符会在堆中为结构体开辟一个内存区域,并且返回此内存区域的首地址,然后保存在 stu_
指针变量中。所以 stu_
变量存储的是指针类型数据,可以随时更改所指向的结构体实体。
- 初始化结构体:动态声明的结构体可以使用
->
运算符(指针引用运算符)为结构体中的每一个分量赋值,也可以使用.
运算符访问结构体中的分量。
//初始化结构体
stu_->age=13;
stu_->sex='W';
stu_->score=99.7;
//使用结构体中的数据
cout<<"年龄:"<<stu_->age<<endl;
cout<<"成绩:"<<stu_->score<<endl;
cout<<"性别:"<<stu_->sex<<endl;
//也可以使用 . 运算符访问动态结构体中的数据。
cout<<"年龄:"<<(* stu_).age<<endl;
cout<<"成绩:"<<(* stu_).score<<endl;
cout<<"性别:"<<(* stu_).sex<<endl;
- 使用另一个静态结构体中的数据。
因为动态声明的结构体变量保存的是地址,需要使用 &
取地址运算符,才能把静态结构体的地址赋值给动态声明的结构体类型变量。
//静态声明结构体
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
//把静态结构体的地址赋值给结构体指针变量
Student * stu_=&stu;
cout<<"年龄:"<<stu_->age<<endl;
cout<<"性别:"<<stu_->sex<<endl;
cout<<"成绩:"<<stu_->score<<endl;
如果修改静态结构体中分量的值,动态引用会不会受影响?如下测试一下,便可知答案是会
。
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
Student * stu_=&stu;
cout<<"年龄:"<<stu_->age<<endl;
cout<<"性别:"<<stu_->sex<<endl;
cout<<"成绩:"<<stu_->score<<endl;
//修改静态结构体中的年龄
stu.age=15;
cout<<"修改之后……"<<endl;
cout<<"年龄:"<<stu_->age<<endl;
cout<<"性别:"<<stu_->sex<<endl;
cout<<"成绩:"<<stu_->score<<endl;
输出结果:
为什么?
其实 stu
是才是真正的结构体实体,存储了结构体的所有分量数据。而stu_
是指针实体,存储的是真正结构体所在的地址。也就是使用 stu
和stu_
访问的是同一个结构体内存空间
。
结构体实体只有一个,结构体变量名和结构体指针只是
2
种不同的访问入口。
- 使用另一个动态声明的结构体中的数据。因为动态声明结构体的变量都是指针类型,直接赋值即可。
Student * stu_=new Student();
stu_->age=12;
stu_->sex='M';
stu_->score=78.9;
cout<<"年龄:"<<stu_->age<<endl;
cout<<"性别:"<<stu_->sex<<endl;
cout<<"成绩:"<<stu_->score<<endl;
//直接赋值
Student * stu_1=stu_;
cout<<"年龄:"<<stu_1->age<<endl;
cout<<"性别:"<<stu_1->sex<<endl;
cout<<"成绩:"<<stu_1->score<<endl;
输出结果:
此种方案和上面的引用静态结构体的方案本质是一样的,真正的结构体实体只有一个,但有 2
个结构体指针变量引用此结构体。无论使用那一个结构体指针变量修改结构体,都是可以的。
3. 结构体和函数
结构体
可以作为函数的参数类型,也可以作为函数的返回类型。
作为函数的参数:
#include <iostream>
using namespace std;
//结构体
struct Student {
double age;
char sex;
double score;
};
void updateStudent(Student stu){
stu.age=15;
stu.sex='W';
stu.score=100;
}
int main(int argc, char** argv) {
//结构体
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
//调用函数修改
updateStudent(stu);
//输出
cout<<stu.age<<endl;
cout<<stu.sex<<endl;
cout<<stu.score<<endl;
return 0;
}
输出结果:
如上代码,试图通过调用函数修改原结构体中的数据信息,结论是修改不了的。main
函数中调用updateStudent
函数时,是把主函数中结构体中的值复制给updateStudent
函数的结构体参数。默认情况下,以结构体作参数,采用的是值传递。
只有当形式参数的类型是指针或引用时,才可以影响主函数中的结构体中的数据。
//结构体指针作为参数
void updateStudent(Student *stu){
stu->age=15;
stu->sex='W';
stu->score=100;
}
//结构体引用
void updateStudent_(Student & stu){
stu.age=15;
stu.sex='W';
stu.score=100;
}
int main(int argc, char** argv) {
//结构体
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
//调用 updateStudent_(stu) 能达到相同效果
updateStudent(&stu);
//输出
cout<<stu.age<<endl;
cout<<stu.sex<<endl;
cout<<stu.score<<endl;
return 0;
}
结构体作为函数的返回值。
- 返回静态结构体,如下代码,本质是把
createStudent
函数中创建的结构中的数据复制给主函数中名为stu
的结构体。函数调用完毕后,createStudent
函数中的结构体所使用的内存空间会被自动回收。
Student createStudent() {
//结构体
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return stu;
}
int main(int argc, char** argv) {
Student stu=createStudent();
cout<<stu.age<<endl;
cout<<stu.score<<endl;
cout<<stu.sex<<endl;
return 0;
}
返回结构体指针。
注意,返回结构体指针时,不能是指向局部变量的指针。
Student stu;
Student * createStudent() {
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return &stu;
}
int main(int argc, char** argv) {
Student *stu=createStudent();
cout<<stu->age<<endl;
cout<<stu->score<<endl;
cout<<stu->sex<<endl;
return 0;
}
- 返回结构体引用,不能返回局部变量的引用。因为局部变量在函数调用结束后就会被回收,返回的引用就没有任何意义可言。
Student stu;
Student & createStudent() {
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return stu;
}
int main(int argc, char** argv) {
Student stu=createStudent();
cout<<stu.age<<endl;
cout<<stu.score<<endl;
cout<<stu.sex<<endl;
return 0;
}
4. 再论结构体
一旦确定一种数据类型后,同时也确定了在此数据类型上所能做的操作。结构体类型是由开发者遵循语法规则自定义的一种新数据类型,对于这种数据类型的内置操作也只能由开发者自己决定。
结构体中除了可以指定复合了那几种子数据类型,还可以提供相应的函数。
#include <iostream>
using namespace std;
//结构体
struct Student {
double age;
char sex;
double score;
//初始化函数,此函数没有返回类型说明
Student(double age, char sex,double score) {
this->age=age;
this->sex=sex;
this->score=score;
}
//自我显示函数
void showSelf() {
cout<<"年龄:"<<this->age<<endl;
cout<<"性别:"<<this->sex<<endl;
cout<<"成绩:"<<this->score<<endl;
}
//其它函数……
};
int main(int argc, char** argv) {
//调用初始化函数
Student stu(12,'M',87.9);
stu.showSelf();
Student stu_(14,'W',80.1);
stu_.showSelf();
return 0;
}
上述代码中出现了一个this
关键字,此关键字的含义是什么?
this
是结构体函数的隐式变量,用来存储已经分配了内存空间的结构体实体。因为无论创建多少个结构体,结构体中的函数代码都只有一份,保存在代码区。当某个结构体需要调用函数时,则需要把自己的地址传递给这个函数,以便让此函数知道处理的数据源头。
如上图示,如果 this
中保存的是 stu
的地址,,函数会处理 stu
的数据。如果this
中保存的是 stu_
的地址,函数会处理 stu_
的数据。
5. 总结
结构体是C++
中最基础的知识,只有熟练掌握后,才能构建出宠大的程序体系。
C++ 炼气期之结构体的更多相关文章
- C++_系列自学课程_第_12_课_结构体
#include <iostream> #include <string> using namespace std; struct CDAccount { double bal ...
- c语言结构体指针初始化
今天来讨论一下C中的内存管理. 记得上周在饭桌上和同事讨论C语言的崛起时,讲到了内存管理方面 我说所有指针使用前都必须初始化,结构体中的成员指针也是一样 有人反驳说,不是吧,以前做二叉树算法时,他的左 ...
- (转) C/C++中结构体(struct)知识点强化
本文转载于 http://pcedu.pconline.com.cn/empolder/gj/c/0503/567942_all.html#content_page_1 所有程序经过本人验证,部分程序 ...
- 结构体的sizeof
首先有几条规则: 1. 结构体的成员相对于结构体的偏移量,是该成员所包含的最大简单类型(指占用内存数)的整数倍(如果该成员本身又是一个结构体,就要递归查找其简单类型,简单类型就是char short ...
- C++结构体中sizeof(1)
sizeof sizeof操作符的作用是返回一个对象或类型名的长度,长度的单位是字节. 返回值的类型是标准库命名为size_t的类型,size_t类型定义在cstddef头文件中,该头文件是C标准库的 ...
- ANSI C中取得结构体字段偏移量的常用方法
来自http://blog.chinaunix.net/u2/62910/showart_492571.html 假设在ANSI C程序中定义了一个名为MyStruct的结构类型,其中有一个名为MyF ...
- C语言结构体指针初始化(转)
reference: https://www.cnblogs.com/losesea/archive/2012/11/15/2772526.html 今天来讨论一下C中的内存管理. 记得上周在饭桌上和 ...
- C语言(C99标准)在结构体的初始化上与C++的区别
C++中由于有构造函数的概念,所以很多时候初始化工作能够很方便地进行,而且由于C++标准库中有很多实用类(往往是类模板),现代C++能十分容易地编写. 比如现在要构造一个类Object,包含两个字段, ...
- 怎样求结构体成员的偏移地址 || 结构体的 sizeof 总结
C 语言中同意将值为 0 的变量强制转换成任一类型的指针,转换结果是一个NULL指针. (type*)0 // 一个 type 类型的NULL指针 用这个指针訪问结构体内的成员是非法的,可是 & ...
随机推荐
- EasyExcel-合并单元格
pom版本 <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</ ...
- 【抬杠C#】如何实现接口的base调用
背景 在三年前发布的C#8.0中有一项重要的改进叫做接口默认实现,从此以后,接口中定义的方法可以包含方法体了,即默认实现.不过对于接口的默认实现,其实现类或者子接口在重写这个方法的时候不能对其进行ba ...
- SpringBoot整合RabbitMQ实战附加死信交换机
前言 使用springboot,实现以下功能,有两个队列1.2,往里面发送消息,如果处理失败发生异常,可以重试3次,重试3次均失败,那么就将消息发送到死信队列进行统一处理,例如记录数据库.报警等 环境 ...
- VmWare安装Centos8注意事项
VmWare安装Centos8注意事项 1.需选择稍后安装操作系统 2.选择操作系统版本 3.修改虚拟机配置 4.配置完成点击开启虚拟机(注意要将鼠标放在屏幕中央,点击一下后才能使用上下键进行选择) ...
- Node.js精进(5)——HTTP
HTTP(HyperText Transfer Protocol)即超文本传输协议,是一种获取网络资源(例如图像.HTML文档)的应用层协议,它是互联网数据通信的基础,由请求和响应构成. 在 Node ...
- Spring jdbctemplate和事务管理器 全注解配置 不使用xml
/** * spring的配置类,相当于bean.xml */@Configuration//@Configuration标注在类上,相当于把该类作为spring的xml配置文件中的<beans ...
- UiPath数据抓取Data Scraping的介绍和使用
一.数据抓取(Data Scraping)的介绍 使用截据抓取使您可以将浏览器,应用程序或文档中的结构化数据提取到数据库,.csv文件甚至Excel电子表格中. 二.Data Scraping在UiP ...
- 数仓的字符截取三胞胎:substrb、substr、substring
摘要:下面就来给大家介绍这三个函数在字符截取时的一些用法与区别. 本文分享自华为云社区<GaussDB(DWS)中的字符截取三胞胎>,作者:我站在北方的天空下 . 在GaussDB(DWS ...
- Mybatis中@select注解联合查询
前言 在项目中经常会使用到一些简单的联合查询获取对应的数据信息,我们常规都是会根据对应的mapper接口写对应的mapper.xml的来通过对应的业务方法来调用获取,针对这一点本人感觉有点繁琐,就对@ ...
- 程序分析与优化 - 9 附录 XLA的缓冲区指派
本章是系列文章的案例学习,不属于正篇,主要介绍了TensorFlow引入的XLA的优化算法.XLA也有很多局限性,XLA更多的是进行合并,但有时候如果参数特别多的场景下,也需要进行分割.XLA没有数据 ...