首页 > 编程学习 > 赶紧进来看看---万字讲解结构体类型

赶紧进来看看---万字讲解结构体类型

发布时间:2022/10/1 14:15:07

本文详细介绍了C/C++中的自定义类型:结构体,主要包括结构体的声明,结构体的使用,结构体自引用,结构体变量定义和初始化,结构体类型名重定义,结构体成员访问,求结构体的大小(结构体内存对齐),结构体的传参…

详解C/C++自定义类型:结构体

  • 一.认识结构体
    • 1.结构体的声明
    • 2.结构体的使用
    • 3.结构体的特殊声明(匿名结构体)
    • 4.结构体自引用
    • 5.结构体变量的定义和初始化
      • ①.结构体全局变量的定义和初始化
      • ②.结构体局部变量的定义与初始化
      • ③.结构体嵌套定义和初始化
  • 二.结构体类型名重定义和结构体成员访问
    • 1.结构体类型名重定义
    • 2.结构体成员的访问
  • 三.结构体的内存对齐(求结构体类型大小)
    • 1.结构体内存对齐规则
    • 2.求结构体类型大小练习题1
      • ①.练习题1文字解析
      • ②.练习题1图解
    • 3.求嵌套结构体类型大小练习题2
      • ①练习题1文字解析
      • ②练习题2图解
    • 4,为什么会存在内存对齐?
    • 5.如何节省结构体类型空间
    • 6.修改默认对齐数
  • 四.结构体传参
  • 五.结构体类型总结

一.认识结构体

结构体是C/C++里的一种自定义类型, 结构描述的是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的也可以是相同的变量

1.结构体的声明

 struct tag  // struct 关键字 + tag 结构体标签(结构体名)
{
member-list1; //结构体的成员列表
....
}variable-list ; //声明结构体 后创建的变量

结构体类型关键字为struct 结构体的声明为struct 加结构体标签(结构体名) , 结构体标签是自己设置的用来标识声明的这个结构体类型 上面是struct tag 就是声明的结构体名为tag的结构体类型

后面一对{ }里面放一些成员变量 ,这些成员变量就是结构体类型的成员

{}后面的可以直接创建变量,这个变量是声明定义的struct tag结构体类型后直接创建的struct tag 结构体类型变量,这个变量是全局变量
可以不创建直接声明,但是{} 后面一定要加上 ; 分号

注意:结构体的声明要在函数外上方进行声明,在下面的函数内才能使用该结构体类型

2.结构体的使用

上面写到结构体的声明格式,但结构体具体如何使用的呢?
比如想描述一个学生的基本信息
一个学生基本信息可以有:名字,年龄,性别,学号
对应可以声明定义 ↓
char name[20] 20个字符元素的字符数组存放名字
int age 整形变量存放年龄,
char sex[5] 5个字符元素的字符数组存放性别信息,
char id[20] 20个字符元素的字符数组存放学号

而他们分布的变量名name age sex id就是表示的每个信息的标识(变量名取名要有意义)

有了这些描述学生的信息,我们表示这个学生时,每次都会用到这些信息,而学生信息一旦多起来,意味着又会多很多变量如:班级,成绩等等

对学生信息进行操作避免不了每次要用到这些信息,而变量又太多了,
每次使用函数调用去访问这个学生传的参又很多,就会浪费很多编码时间.并且每个变量都是独立存在的,不好集中管理
有什么方法能一次定义后集中描述一个学生而不用每次都使用每一个变量呢?
结构体就是为了将这些信息作为成员变量集合起来,用来集中描述一个复杂的对象↓

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

声明一个struct Stu 结构体类型 里面的成员变量为这些学生的基本信息.最后要加上分号!
此时该结构体类型就将这些描述学生信息的变量集中起来,用这个结构体类型创建的一个变量,这个结构体变量里面就涵盖着这些所有的学生信息,想要描述多个学生可以再创建一个变量或者创建一个结构体数组,
这样的方式就比一个个创建变量方便的多

3.结构体的特殊声明(匿名结构体)

在声明结构的时候,可以不完全的声明。表示匿名结构体,即没有写结构体标签

//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;

上面定义的两个结构体类型 都是struct,没有对应的结构体标签,表示是匿名结构体. 而匿名结构体因为没有结构体标签,每个struct 匿名结构体类型都是独立的,声明匿名结构体类型后,因为匿名结构体类型都是独立的.
它只能在声明定义的时候在后面同时创建对应的结构体变量,

思考://在上面代码的基础上,下面的代码合法吗?
p = &x;

注意:编译器会把上面的两个声明当成完全不同的两个类型。
所以是非法的.因为p是第二个匿名结构体类型创建的指针变量 ,而&x是第一个匿名结构体类型创建的变量的地址,
根据匿名结构体特性,每个struct是独立的,这两个变量虽然都是struct 类型创建的变量但是是不同成员变量,表示的匿名结构体,两种变量大小不同类型不同 不能将p里面的&x,属于非法访问

4.结构体自引用

既然结构体里面可以放相同或者不同类型的多个成员变量
那结构体里面的成员变量可以是当前结构体类型创建的成员变量吗↓

//代码1
struct Node
{
int data;
struct Node next;
};
//可行否?

在这里插入图片描述

答案是:不行,编译器会报语法错误, 因为这个结构体类型里面的成员变量如果是自身结构体类型创建的变量,那这个变量的大小又是什么?
成员变量还没描述完就用自身结构体类型创建个变量是不行的
比如sizeof(struct Node),要算这个结构体类型的大小首先至少是由里面每个成员变量大小组成,int 是4字节但是下面一个又是struct Node 又要重新计算,这就形成了一个死循环…

那真正的自引用是什么样的?
真正自引用表示的是成员变量可以是自身结构体类型创建的结构体指针变量↓

//代码2
struct Node
{
int data;
struct Node* next;
};

在这里插入图片描述

这样设计结构体类型表示结构体自引用是正确的,因为结构体指针类型字节大小是根据计算机平台决定的,是一个指针类型,sizeof求字节大小时也不会出现死循环
用这个结构体类型创建的变量 x里面的结构体指针成员变量 可以存放&x即这个结构体类型变量或者可以存放这个结构体类型创建的另外的变量的地址,可以通过解引用访问到其他结构体变量
这也是数据结构里的链表基本形式…

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

下面介绍声明的结构体类型后,如何用这个结构体类型定义变量以及对变量初始化以及对结构体类型里面的每个成员变量进行访问

①.结构体全局变量的定义和初始化

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体全局变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { 1, 2 };   //定义结构体全局变量p3 并初始化

在声明这个结构体类型Point后可以直接创建变量,或者在整个声明语句后struct Point p2 直接用声明的类型创建变量 这些变量为全局变量是函数外定义的

而对每个结构体变量进行初始化需要用{},因为里面有一个或多个成语变量,表示成员列表,按顺序从第一个变量开始初始化 然后用,隔开后面的为第二个成员变量初始化,以此类推…

②.结构体局部变量的定义与初始化

对结构体局部变量的定义在声明结构体类型后在其下面的函数内部用其结构体类型定义的变量为局部变量

在这里插入图片描述

对局部结构体变量的初始化在{}成员列表里可以按顺序一一初始化,也可以用对应的变量然后访问其成员变量对其精准初始化(这种方法可以初始化后面的成员变量用按顺序一一初始化)
s里成员变量未完全初始化只初始化了第一个name数组成员,后面的成员变量默认里面的值都初始化为了0
结构体变量名可以和该结构体类型里的成员变量名相同,两个名字表达的意义不同,不会命名冲突,但最好不要这样使用

③.结构体嵌套定义和初始化

通过结构体自引用得到结构体里面的成员变量不能用自身结构体类型定义的变量,但能用自身结构体类型创建的指针变量
同时,结构体1里面的成员变量可以是其他结构体类型2创建的变量(但是结构体类型2必须得先在结构体1前声明,否则计算机识别不了该类型就无法定义) ,这种方法为结构体嵌套定义
同时对这个结构体变量初始化也初始化了里面另一个结构体变量为嵌套初始化

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体全局变量p2
//初始化:定义变量的同时赋初值。

struct Stu //类型声明
{
	char name[15];//名字
	int age; //年龄
	int s;
	int n;
}*ps;  //创建 结构体全局指针变量

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

嵌套定义

struct Node 结构体类型的成员变量里 包含了struct Point 结构体类型(该类型已在struct Node上声明)创建的成员变量p

嵌套初始化

用struct Node类型创建了变量n1 和n2, 对n1和n2结构体变量嵌套初始化{ }在第一个花括号成员变量列标里对成员变量一一初始化,当初始化到p这个struct Point 定义的变量时,因为其自身是一个结构体变量,对结构体变量初始化里面成员可能有多个需要再嵌套一个{}在里面此时对该变量里面的成员变量一一初始化,最后在为最后一个指针变量初始化为NULL,这就是结构体嵌套初始化…

二.结构体类型名重定义和结构体成员访问

结构体类型名重定义需要用到关键词typedef…
结构体成员访问有两个操作符: .和-> 在这篇博客里也介绍过->结构体成员访问操作符

1.结构体类型名重定义

根据上面的介绍,结构体类型声明需要struct加结构体标签,如果没有结构体标签是匿名结构体的话不能再单独使用这个类型创建变量,而用struct 加结构体标签创建变量每使用一次这个结构体类型,这个类型名又太长了,此时就可以用到typedef(类型名重定义)关键字,并且当你声明匿名结构体后将这个结构体重命名后,这个结构体类型具有了唯一标识,可以再次使用这个重命名后的标识符作为该匿名结构体类型创建变量

在这里插入图片描述

typedef可以放在struct前 要重命名的内容放在最后,表示 在对结构体类型声明完后,将这个结构体类型重命名

在这里插入图片描述

直接使用匿名结构体类型在main函数里定义和初始化变量是不行的,匿名结构体类型可以创建好多,同样的名字编译器无法识别是哪个匿名结构体, 要使用匿名结构体类型需要在前面将其typedef类型名重定义 使其有一个自己的名字

在这里插入图片描述

typedef 类型重命名后不能再直接在后面定义全局变量 需要单独sizeof()使用重命名的类型再定义变量

在这里插入图片描述

对结构体类型重命名需要注意是在结构体类型声明后 成员变量都确定后再进行重命名的,在成员变量里使用自身重命名后的类型名是不允许的,因为当时还没进行重命名,计算机无法识别S这个是什么类型…

2.结构体成员的访问

结构体成员访问操作符: .

因为结构体变量里有多个成员变量,当拿到结构体变量时需要访问里面每个成语变量如:打印每个成员变量的值时,不能直接操作结构体变量,而要用结构体成员访问操作符. 访问结构体变量里某个成员变量才能打印里面的值

结构体成员访问操作符:->

当我们拿到的是结构体指针变量p时,要访问里面的结构体指针指向的结构体变量某个里面的成员变量y时,可以使用(*p).y先对指针解引用访问结构体变量再用.操作符访问里面的成员变量y,也可以用->操作符
p->y 表示访问结构体指针p指向的结构体变量里的y成员变量,二者是等价的,但是p->y操作更简单,第一种需要考虑优先级使用括号

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体全局变量p2
//初始化:定义变量的同时赋初值。

struct Stu //类型声明
{
	char name[15];//名字
	int age; //年龄
	int s;
	int n;
}*ps;  //创建 结构体全局指针变量

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

int main()
{
	struct Point p3 = { p3.y=1,p3.x=2 };   //定义结构体局部变量p3 并指定成员变量 初始化

	struct Stu s = { "zhangsan", };//定义结构体 局部变量 并初始化  未完全初始化为初始化部分值为0
	ps = &s;
	printf("%d ", s.age);
	printf("%d ", s.s);  //  访问结构体变量s里的成员变量s的值
	printf("%d ", s.n); 
	printf("%d ", n2.p.y);  //访问n2结构体变量里的p结构体成员变量 再访问p结构体变量里的 y成员变量 
	printf("%s ", ps->name);  // 访问结构体指针 指向里面的name成员变量 
	
	return 0;
}

这串代码最后运行结果是什么?

在这里插入图片描述

三.结构体的内存对齐(求结构体类型大小)

我们已经掌握了结构体的基本使用了。现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct S1
{
	char c1; 
	int a;
	char c2;
};
int main()
{
	printf("%d", sizeof(struct S1)); //最后结果是什么?
	return 0;
}

在没学过结构体内存对齐时,看这个结构体的大小,首先考虑的应该就是将所有成员变量的大小加起来,总和即为这个结构体类型大小sizeof(c1+a+c2)结果为6

在这里插入图片描述

但是运行结果却是12!!
这就是结构体内存对齐造成的结果.

1.结构体内存对齐规则

1. 第一个成员在与结构体变量偏移量为0的地址处。
偏移量:把存储单元 的 实际地址 与其所在段的段地址之间的距离称为段内偏移 即以起始地址为标准,偏移量就是当前的地址相对于起始地址移动了多少个字节位置…(偏移量0即为起始地址,因为起始地址相对于起始地址偏移0个字节)
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8
3. 结构体总大小为成员变量里最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

2.求结构体类型大小练习题1

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

①.练习题1文字解析

c1第一个结构体成员变量对齐到偏移量0地址处,c1自身大小是1字节默认对齐数8字节 成员对齐数为1字节,即占偏移量0处的一个内存单元空间

a是第二个结构体成员变量,自身大小4字节,默认对齐数8字节 ,成员对齐数为4字节,对齐到4的整数倍的偏移量处,此时偏移量0地址处的内存单元存放了c1从偏移量1地址开始与c1对齐,而1不是4个整数倍,会对齐到偏移量4处开始往后4个字节的空间即偏移量4.5.6.7处的内存单元为a的空间,而偏移量1,2,3处的内存空间因为需要对齐被浪费

c2是第三个结构体成员变量,自身大小1字节,默认对齐数8字节,成员对齐数1字节对齐到1的整数倍的偏移量处,此时可用的是从偏移量8开始,而8是1的整数倍,此时对其到偏移量8,即偏移量8开始往后一个内存单元为c2的空间,

此时所有成员变量都已对齐分配好空间,使用了偏移量0到8共9个字节空间,而结构体总大小是成员中最大对齐数倍数,最大对齐数是4字节
9不是4的倍数会补齐到12字节,此时12才是结构体总大小,为了最后对齐偏移量9.10.11三个内存单元也被浪费掉最后结构体总大小为12字节浪费了6个字节空间.

②.练习题1图解

在这里插入图片描述

3.求嵌套结构体类型大小练习题2

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

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

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

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

在这里插入图片描述

①练习题1文字解析

S2第一个变量d 对齐数为8从偏移量0处开始后面8个字节为d的空间,第二个变量c对齐数为1从偏移量8开始一个字节的空间为c的空间,第三个变量i对齐数是4,从偏移量12开始往后4个字节空间是i的空间,0-15偏移量地址处内存单元共用16个内存单元是最大对齐数8的倍数,最后结构体S2的大小为16

S3第一个变量c1对齐数为1从偏移量0开始后1个字节为c1的空间,第二个变量为s2是结构体类型,它的对齐数是S2结构体里最大成员对齐数即8,即从偏移量8处开始往后16个内存单元此时偏移量到23
第三个变量d对齐数为8字节从偏移量24开始往后8个字节空间
全部分配好后0-31偏移量地址处内存单元共用32个内存单元,是最大对齐数8的倍数,最后结构体S3的大小为32

②练习题2图解

在这里插入图片描述

4,为什么会存在内存对齐?

在上面做题发现,内存对齐可能都会浪费空间,那为什么要这样做呢?

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

总体来说:结构体的内存对齐就是用空间换取时间

5.如何节省结构体类型空间

那在设计结构体的时候,我们既要满足对齐,又要节省空间,
那就要让占用空间小的成员尽量集中在一起。

在这里插入图片描述

可以看到S1和S2两个结构体类型里的成员变量类型是一样的,但是因为位置的不同使得结构体类型的大小也有不同,在考虑内存对齐情况下,成员变量类型较小的集中放在一起,就能有效避免因为对齐数对齐偏移量而造成浪费空间的情况,这样既能最小节省空间又能节省时间

6.修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数

#pragma pack(n)为设置默认对齐数为n
在设置默认对齐数后,要取消默认对齐数在下面加上#pragma pack()

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

上面代码经过修改默认对齐数后,即可能改变每个成员的对齐数,会影响内存分配时对齐到偏移量的倍数

在这里插入图片描述

S1设置默认对齐数为8和vs编译器默认对齐数一样,最后得的结果是12
而S2设置默认对齐数为1,成员对齐数为成员自身大小和默认对齐数中最小的那个即每个成员对齐数都变为了1,而每个偏移量都是1的倍数,c1为偏移量0的内存单元,而i对齐数为1对齐到1的倍数从偏移量1开始往后4个字节即1-3偏移量处4个内存单元为i的空间而偏移量5处的内存空间为c2,0-5共六个字节空间,是最大对齐数倍数 所以S1大小为6

四.结构体传参

结构体里可能有多个成员变量,那结构体变量作为函数参数应该如何传参呢?
思考下面代码1和2两个哪个传参方式更合适呢?

struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体  1
print2(&s); //传地址  2
return 0;
}

代码1将s这个struct S结构体类型的变量作为实参传参调用print1函数实现打印s结构体变量里的num成员
代码2将s的这个structS结构体类型的变量地址作为实参传参调用printf函数实现解引用打印s结构体变量里的num成员

在之前函数章节说过,函数传参传的是一份临时拷贝,如果传的是s变量即传的是里面值的一份临时拷贝,而对应的print1用一个相同类型的结构体变量接受这份临时拷贝,对临时拷贝进行操作打印出num的值,这种操作能够满足需求,但是求这个s变量大小得到这个结构体类型大小为4004字节,说明对应的printf函数要接受这份值需要再额外创建40004字节的结构体变量接受
而代码2传的是该结构体变量的指针,对应的print2函数创建结构体类型指针变量接受这个指针,
而指针大小是计算机运算平台决定,32位是4字节,64位是8字节,最大也就8字节,通过解引用也能满足需求

综上所述,函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降,
结论:
结构体传参的时候,要传结构体的地址。

五.结构体类型总结

本文详细介绍了自定义类型–结构体(包括结构体声明,使用,定义变量和初始化,结构体类型重命名,结构体成员的访问方法,如何求结构体的类型大小(结构体的内存对齐)以及结构体如何传参)
在下一篇博客会用到结构体类型实现一个小程序:学生信息管理系统

在这里插入图片描述

写文不易,给个一键三连支持下叭~

Copyright © 2010-2022 mfbz.cn 版权所有 |关于我们| 联系方式|豫ICP备15888888号