- 本篇会加入个人的所谓‘鱼式疯言’
- ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言,而是理解过并总结出来通俗易懂的大白话,我会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的,可能说的不是那么严谨.但小编初心是能让更多人能接受我们这个概念。
前言
欢迎来得小编的新的一篇文章,在这篇文章中我们主要讲解如下:
💖💖💖
- 结构体变量的创建和初始化
- 结构成员访问操作符
- 结构体内存对⻬
- 结构体实现位段💕💕💕
一.结构体变量的创建和初始化
1.结构体基本形式
struct tag
{
member-list;
}variable-list;
<1>.关键字:struct
<2>.结构体标签:tag,就是起标志性作用的命名
<3>.结构体成员名:member-list,可以是各种数据类型的声明,char ,doule add(), int *,short[]…都有可以作为我们结构体成员。
<4>.变量名:variable-list,变量名可在结构体声明时创建 也可以在main函数内创建。
2.结构体创建示范
struct Stu1
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
char id[20];//学号
};
struct Stu1
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
char id[20];//学号
}S1,S2;
鱼式疯言
在我们结构体创建的同时,我们也声明了 变量名,那么我们就可以把它认为是 全局变量,
因为宝子们想啊,它是在 main 外部的.
main 内部的,我们不妨把它看作是局部变量
注意注意一点的是,结构体声明在最后的花括号后一定要打上我们的分号 “;” 结尾。
3.结构体变量的初始化
<1>.全局初始化
#include <stdio.h>
struct Stu1
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
char id[20];//学号
}S1 = { "zhoushauibi",12,"nv","36517519" };
int main()
{
printf("name: %s\n", S1.name);
printf("age : %d\n", S1.age);
printf("sex : %s\n", S1.sex);
printf("id : %s\n", S1.id);
return 0;
}
<2>.局部初始化
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
return 0;
}
看到这里,小爱同学有疑问啦,如果我不想要默认初始化,按照自己的顺序来初始化呢 !!!💕💕💕
请宝子们往下看小编的操作哦
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[10];//性别
char id[20];//学号
};
int main()
{
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "nv" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
就问帅不帅啦啦啦😁😁😁😁😁😁😁
4.结构体的特殊声明
在声明结构的时候,可以不完全的声明。
et:
// 匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
//在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
鱼式疯言
警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是⾮法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使⽤⼀次。
二.结构成员访问操作符
1.实例说明
#include<stdio.h>
struct stu
{
char name[20];
int age;
int num;
};
void print1(struct stu x)
{
printf("传值调用:name->%s age->%d num->%d\n", x.name, x.age, x.num);
}
void print2(struct stu *p)
{
printf("传址调用:name->%s age->%d num->%d\n", p->name, p->age, p->num);
}
int main()
{
struct stu a = { "zhoushuaibi",19,848621 };
//传值调用
print1(a);
//传址调用
print2(&a);
return 0;
2,解释
😲😲😲在上面小编利用了函数的 传值调用 和 传址调用
发现输出内容是一样的 ! ! !
本次小编最想说明的还是 传址调用 ,相信细心的小伙伴已经观察到了我利用了 “->” 这个符号,哈哈哈哈😁😁😁
其他它就是今天我们要讲解的 结构体成员访问操作符 ,
这里我们主要说明 传址调用 , 上面我们先创建了指针变量,我们都知道 指针 的作用 就是 指向地址 的 变量,
那么我们就利用指针来 -> 来指向我们想要的成员对象,来达到调用的效果.
鱼式疯言
看到上面这两种方法,友友们认为是传值好,还是传址好呢?
我认为嘛,如果我们从空间内存来看:
1.如果是传值的话 我们需要把整个结构体传过去,就需要开辟整个结构体大小的内存空间.
那如果结构体成员很多呢,有可能导致内存空间开辟过大就会出现问题.
2.如果是传址的话 我们只需要把首空间地址传过去就可以啦 !
综上所述: 我们因为考虑到内存空间的合理利用,我们还是优选传地址调用.
三 .结构体内存对齐
首先我们让宝子们看个东西:
1.栗子:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
#include<stdio.h>
int main()
{
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S2));
return 0;
}
是不是感到很疑惑,这是为啥,这是为啥呢???
答案说简单不简单,说难不难,主要我们要理解这个结构体啊他是如何存储的,这个是最关键的!!!
2.结构体内存对齐
结构体的⼤⼩
<1>引入
我们已经掌握了结构体的基本使⽤了
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
这也是⼀个特别热⻔的考点: 结构体内存对⻬
<2> 对⻬规则
⾸先得掌握结构体的对⻬规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
对⻬数 = 编译器默认的⼀个对⻬数 与 该成员变量⼤⼩的较⼩值。
- VS 中默认的值为 8
- Linux中 gcc 没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩
- 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的
整数倍。 - 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。
3.作图说明
4.具体步骤
<1>. 我们先标好偏移量
<2>. c1 的字节数与默认对齐数进行比较从而得到我们的较小值 1 ,直接填充在我们的偏移量为 0 的格上
<3>. 找准 对齐数 的倍数
<4>.结构体最终得到的内存是 最大偏移量 的整数倍
5.为什么存在内存对⻬
⼤部分的参考资料都是这样说的:
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要
作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地
址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以
⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两
个8字节内存块中。
鱼式疯言
显而易见,我们的内存分配中,有多好空出来的内存,为什么要这样做呢?🤔🤔🤔
总体来说:结构体的内存对⻬是 拿空间 来 换取时间 的做法.
比如用 int 来读取时,必然是4个字节4个字节读,如果黏在一起,不能完整的读取,就会多读几遍导致 时间的消耗 。
6.结构体嵌套的特殊情况
struct S3
{
double d;
char c;
int i;
};
struct S2
{
char c1;
struct S3 s3;
double d;
};
#include<stdio.h>
int main()
{
printf("%zd\n", sizeof(struct S2));
return 0;
}
见下图💕💕💕
嵌套的结构体的内存是
结构体成员(包括本身的结构体成员和嵌套的结构体)中最大整数倍,千万不然认为是嵌套结构体的对齐数的整数的倍。
7.修改默认对齐数
#pragma 这个预处理指令
可以改变编译器的默认对⻬数
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
int main()
{
//输出的结果是什么?
printf("%zd\n", sizeof(struct S));
return 0;
}
#pragma pack () 这个预处理可以设置默认对齐数
() 内可以放设定对齐个数,如果为空即可还原对齐数.
#include <stdio.h>
#pragma pack(1)
//设置默认对⻬数为1
#pragma pack()
//取消设置的对⻬数,还原为默认
struct S
{
char c1;
int i;
char c2;
};
int main()
{
//输出的结果是什么?
printf("%zd\n", sizeof(struct S));
return 0;
}
鱼式疯言
在这里我们就利用设置默认对齐数有效的进行认为的结构体内存分配啦 ! ! !
四.结构体实现位段
结构体讲完就得讲讲结构体实现 位段 的能⼒。
1. 什么是位段
位段的声明和结构是类似的,有两个不同:
位段的成员必须是 int、unsigned int 或 signed int ,在 C99 中位段成员的类型也可以
选择其他类型。
2.位段的形式
位段的成员名后边有⼀个 冒号 和⼀个 数字
3.实例
#include<stdio.h>
//⼀个例⼦
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
printf("%zd", sizeof(struct S));
return 0;
}
在这里小伙伴们是不是也疑问万分呢?
按道理来说整个结构体内存大小应为 4 个字节,可突然怎么就变成了 3 个呢 ???😲😲😲
见下图分解:
鱼式疯言
位段,位段…
说白了就是按 二进制 中的比特位来人为控制 比特的长度 分配内存
上面是以 VS2022 为例的,它的分配规则是一旦该单位字节不够分配 ***比
特的长度***
就会浪费剩余的 比特的长度 重新开辟 单位字节 存放 比特的长度
但是呢?
VS 2022 是这样,但是在不同的编译环境是不一样的存储方式,也有可能不浪费剩余的 比特长度, 继续存放的.
所以就会出现以下问题 ! ! ! 💕💕💕
4.位段的跨平台问题
- int 位段被当成有符号数还是⽆符号数是不确定的。
- 位段中最⼤位的数⽬不能确定。(16位机器最⼤16,32位机器最⼤32,写成27,在16位机器会
出问题。 - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当⼀个结构包含两个位段,第⼆个位段成员⽐较⼤,⽆法容纳于第⼀个位段剩余的位时,是舍弃
剩余的位还是利⽤,这是不确定的。
总结:
跟结构相⽐,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段使用的注意事项
错误示范
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
return 0;
}
鱼式疯言
这里是绝对不允许取地址的 ! ! !
因为啊,我们的 & 取出的是以字节为单位的内存空间,但这里的位段是以比特为单位的字节空间.
正确示范
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
//正确的⽰范
int a = 0;
scanf("%d", &a);
sa._b = a;
return 0;
}
总结
💖💖💖最后小编再梳理一下本篇文章的主要内容:
1.结构体是怎么创建的 ,并 全局初始化 或 局部初始化 的,但也要注意有个 匿名声明 的玩意.
2.结构体访问操作符"->"的实际运用及其特点.
3.结构体是依据 内存对齐 来存储的,和实际所 内存对齐 存在的意义
4.位段的 概念理解 ,以及它的 实际的用法 及 特点 与 不足.
💖💖💖本次博文就到这里了,感觉各位小伙伴的赏脸品读小编写的拙作哦,
如果觉得小编写的还不错的咱可支持三关下,不妥当的咱评论区指正,希望我的文章能给各位家人们带来哪怕一点点的收获就是小编创作的最大动力💖💖💖