当前位置:网站首页>c语言进阶篇:自定义类型--结构体
c语言进阶篇:自定义类型--结构体
2022-08-04 18:36:00 【摸鱼王胖嘟嘟】
前言
大家好~我又来了!今天给大家带来的是自定义类型–结构体知识的总结。话不多说,让我们开始吧 ~
结构体的声明
️结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
结构体和数组很相似,都是集合。不过数组是一组相同类型的元素的集合,而结构体的每个成员可以是不同类型的。
️结构体的声明
struct:结构体关键字,它表明是个结构体,结构体的声明必须有struct。
结构体标签:它是你给你的结构体类型取的名字
struct 结构体标签:这表示一个结构体类型
成员列表:可以有多个成员,每个成员有各自的数据类型和成员变量名。
结构体变量列表:可以有多个变量,是使用结构体类型创建的变量,一般结构体变量列表省略不写,先先进行结构体声明,之后再创建结构体变量。
struct tag//tag是结构体的标签
{
member - list;//结构体的成员列表
}variable - list;//结构体的变量的列表
例如运用结构体描述一个学生:
struct Stu
{
//结构体成员
char name[20];//名字
int age;//年龄
char sex[10];//性别
int id[20];//学号
};//分号一定不能丢
以上代码是一个结构体类型,不占内存,相当于一个图纸作用。
️特殊的声明
在声明结构的时候,可以不完全的声明。
例如:
//匿名结构体--缺少结构体的标签
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
//a[20]表示可以存放20个这样的结构体变量
//*p表示结构体类型的指针变量
匿名结构体只能使用一次,因为无法使用来定义一个结构体变量。
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
因为结构体类型没有名字,而我们需要通过结构体类型来创建变量,不能通过结构体关键字struct来创建变量,所以之后这个结构体类型就不能再使用了。
p = &x;//error不合法的
编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。
#include <stdio.h>
//匿名结构体类型
struct
{
char name[20];
int age;
}x;//用匿名结构体类型创建了变量x
struct
{
char name[20];
int age;
}a[20],*p;//用匿名结构体类型创建了数组a,数组中可以放20个这种类型的元素
//用匿名结构体类型创建了指针变量p
//观察可以看到,这两个匿名结构体类型是相同的
int main()
{
//就像 int x;
// int* p = &x;这样
p = &x;//按道理这个代码也是可以的,但是因为是匿名结构体类型,结构体类型没有名字
//会报警告,编译器认为&x和p的类型是不一致的,编译器认为是两种不同的类型
//所以这样写是非法的。
return 0;
}
类型不兼容,认为是两个不同的结构体类型。
️结构体的自引用
结构体中包含一个同类型的结构体指针的成员,
通过结构体中的指针可以找到自己同类型的另一个数据。
数据结构是数据在内存中的存储结构,有线性,和树形(二叉树)。
其中线性又分为顺序表和链表。
其中顺序表是数据在内存中连续存放的,通过找到第一个数据就可以找到后面的数据。
但是链表的数据不是连续存放的,那么就需要通过上一个数据能找到下一个数据,那么这是后可以通过结构体的自引用
如何找到所有的数据呢?
假设红色方块为一个个结点,每个结点可以被拆成两部分,
每个结点中放数据和下一个结点的地址,最后一个结点中放数据和空指针NULL
如1结点中放数据1和2结点的地址,就能通过指针找到2结点接着找到2结点中的数据2;2结点中放的是数据2和3结点的地址,就能通过指针找到3结点接着找到3结点中的数据3;以此类推,这样最终就能找到所有数据。
但是,下列结构体的自引用是错误的:
#include<stdio.h>
struct Node
{
int data;
struct Node next;
};
int main()
{
printf("%d\n", sizeof(struct Node));//error
return 0;
}

正确表示链表的结构体自引用:
struct Node
{
int data;
struct Node* next;
};
此时把结构体分为了数组域和指针域。
注意:
//代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码,可行否?//不可以的
//解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
Node n1;//结构体变量
struct Node n2;//结构体变量
typedef struct Node
{
int data;
struct Node* next;
}* linklist;
//相当于
typedf struct Node* linklist;
️结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单。
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {
x, y };
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {
"zhangsan", 20 };//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {
10, {
4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = {
20, {
5, 6}, NULL };//结构体嵌套初始化
结构体的使用:
#include<stdio.h>
struct Score
{
int n;
char ch;
};
struct Stu
{
char name[20];
int age;
struct Score s;
};
int main()
{
struct Stu s1 = {
"zhangsan",20,{
100,'q'} };
printf("%s %d %d %c\n", s1.name, s1.age, s1.s.n, s1.s.ch);
return 0;
}

️结构体内存对齐
偏移量:与起始位置中间相差的字节数就是偏移量。
如:内存中的第一个字节相对于起始位置的偏移量是0
对齐规则
1. 第一个成员在与结构体变量偏移量为0的位置。
2. 其他成员变量要对齐到偏移量为某个数字(对齐数)的整数倍的位置。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS中默认的值为8 其它没有默认对齐数的编译器,它的对齐数就是成员自身大小
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到偏移量为自己的最大对齐数的整数倍处,结构体的整体大小就是最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include<stdio.h>
struct S
{
char c1;
int i;
char c2;
};
int main()
{
struct S s;
printf("%d", sizeof(s));
return 0;
}


由上图可以看到,c1相对于起始位置的偏移量是0,i的偏移量是4,c2的偏移量是8
下面可以通过宏offsetof来验证上图是否正确,内存是否真的浪费了6个字节:
type:结构体类型,member:结构体成员名
offsetof的功能是:返回一个结构体成员(如:c1)在这个结构体类型(如:struct S1)创建的变量(s1)中的偏移量
头文件是 <stddef.h>
#include <stdio.h>
#include <stddef.h>
struct S
{
char c1;
int i;
char c2;
};
int main()
{
struct S s;
printf("%d\n", sizeof(struct S));//12
printf("%d\n", offsetof(struct S, c1));//0
//返回的是c1在S1中的偏移量
printf("%d\n", offsetof(struct S, i));//4
//返回的是i在S1中的偏移量
printf("%d\n", offsetof(struct S, c2));//8
//返回的是c2在S1中的偏移量
return 0;
}

经验证,c1在S中的偏移量是0,i在S中的偏移量是4,c2在S中的偏移量是8
所以,结构体在内存中确实是这么放的,会有一些内存是分配了但未使用的。
下面,我们再来看一个例子:
#include <stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
int main()
{
struct S1 s1;
printf("%d", sizeof(s1));
return 0;
}


结构体嵌套问题:
通过上图,我们已经知道,结构体struct S1的总大小是8字节,那么结构体struct S2中嵌套一个结构体类型的成员struct S1 s1时,结构体struct S2的总大小是多少呢?
#include <stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
struct S2
{
char c;
struct S1 s1;
double d;
};
int main()
{
struct S2 s2;
printf("%d", sizeof(s2));
return 0;
}


️为什么要存在内存对齐?
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
所以我们必须把某些数据放到我们能够访问的那些地址处,对齐到某些对齐边界上,这样我们才能访问到我们需要的数据。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说: 结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
对比发现,S与S1这两个类型明明是相同的,但是因为成员变量的顺序不同,造成了不同的内存情况,很明显,S2更节约内存空间
所以得出结论,在设计结构体的时候:可以尽量让占用空间小的成员尽量集中在一起。
️修改默认对齐数
#pragma:预处理指令,作用是设定编译器的状态或者指示编译器完成一些特定动作
我们可以使用#pragma pack来修改默认对齐数。
我们一般设置完默认对齐数后会加一条语句来取消默认对齐数的修改,还原为默认,让我们设置的默认对齐数只作用于一部分代码。
设置默认对齐数为1,那么对齐数就全是1了,最大对齐数也是1,偏移量只要是1的倍数就可以存放,那么内存中任意地方都可以存放,即不会有空间浪费,数据直接依次存放在内存中。结构体的总大小就是结构体成员的类型的总大小。
#include <stdio.h>
//默认对齐数为8
struct S1
{
int i;
double d;
};
#pragma pack(4)//设置默认对齐数为4
struct S2
{
int i;
double d;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S3
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));//16
printf("%d\n", sizeof(struct S2));//12
printf("%d\n", sizeof(struct S3));//6
return 0;
}

️结构体传参
#include <stdio.h>
struct S
{
int data[1000];
int num;
};
//结构体传参
void print1(struct S s)
{
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ",s.data[i]);
}
printf("%d\n", s.num);
}
//结构体地址传参
void print2(const struct S* ps)//指针ps接收的是&s,为了防止ps误操作改变了s,
{
//可以在指针类型前面加上const,保护指针指向的值。
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", ps->data[i]);
}
printf("%d\n", ps->num);
}
int main()
{
struct S s = {
{
1,2,3,4}, 1000 };
print1(s); //传结构体
print2(&s); //传地址
return 0;
}

比较而言,print2更好
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
边栏推荐
- 基于激励的需求响应计划下弹性微电网的短期可靠性和经济性评估(Matlab代码实现)
- 情绪的波动起伏
- dotnet core 输出调试信息到 DebugView 软件
- ptables基本语法使用规则
- 基于 eBPF 的 Kubernetes 可观测实践
- Flask framework implementations registered encryption, a Flask enterprise class learning 】 【
- npm配置国内镜像(淘宝镜像)
- unity中实现ue眼球的渲染
- 路由懒加载
- 2019 Haidian District Youth Programming Challenge Activity Elementary Group Rematch Test Questions Detailed Answers
猜你喜欢

链表的经典入门LeetCode题目

The upgrade of capacity helps the flow of computing power, the acceleration moment of China's digital economy

【STM32】入门(五):串口TTL、RS232、RS485

解决错误:The package-lock.json file was created with an old version of npm

Win10只读文件夹怎么删除

MySQL安装教程(详细)

PHP代码审计10—命令执行漏洞

LVS负载均衡群集之原理叙述

ptables基本语法使用规则

After EasyCVR is locally connected to the national standard device to map the public network, the local device cannot play and cascade the solution
随机推荐
Iptables防火墙基础知识介绍
Matlab drawing 1
不论你是大众,科班和非科班,我这边整理很久,总结出的学习路线,还不快卷起来
mq消息积压怎么对应
猜数字游戏
【web自动化测试】playwright安装失败怎么办
解决错误:The package-lock.json file was created with an old version of npm
Win10只读文件夹怎么删除
袋鼠云思枢:数驹DTengine,助力企业构建高效的流批一体数据湖计算平台
测试工程师如何突破职业瓶颈?
buuctf(探险1)
The Industrial Metaverse Brings Changes to Industry
图解LeetCode——899. 有序队列(难度:困难)
敏捷开发项目管理的一些心得
BigDecimal 使用注意!!“别踩坑”
After EasyCVR is locally connected to the national standard device to map the public network, the local device cannot play and cascade the solution
GBase8s存储过程
通配符SSL证书不支持多域名吗?
面试官:MVCC是如何实现的?
EuROC dataset format and related codes