结构体
- 结构体的认知
- 结构体的声明
- 一般声明
- 特殊声明
- 匿名结构体类型
- 结构体自引用
- 结构体变量的定义与初始化
- 结构体变量的定义
- 结构体变量的初始化
- 结构体传参
- 结构体内存对齐
- 位段
- 位段声明
- 位段的内存分配
- 位段跨平台问题:
结构体是由我们自己创造的一种类型,使得C语言有能力描述复杂类型。比如学生包含了:名字、年龄、性别、学号…
结构体的认知
结构是一些值的集合,这些值叫做成员变量。每个成员可以是不同类型的变量。
————-----------结构体与数组的区分:
数组:是一组相同类型的元素的集合。
结构体:是一些值的集合,但这些值可以是不同类型:可以是标量、数组、指针、甚至是其他结构体。
结构体的声明
一般声明
假如这里要描述一个学生:
//描述一个学生
struct Stu //Stu是结构体变量名,可以自定义(根据实际需求)
{
//里面是成员列表
char name[20];//学生名字
int age; //学生年龄
char id[10]; //学生学号
//....
}s1,s2,s3; //这里是结构体变量,是全局变量,
//(创建结构体类型时顺带创建3个结构体全局变量)
//这后面的分号不能丢
int main()
{
struct Stu s4, s5; //创建结构体变量,这些是局部变量
return 0;
}
特殊声明
就是再结构体声明的时候,可以不完全的声明。
匿名结构体类型
struct //没有结构体变量名
{
int a;
char b;
double c;
}s1; //只能在这里创建结构体变量名,
//且只能使用一次
结构体自引用
定义:结构体要能够找到和他同类型的下一个结构体;
- 结构体中可以包含另一个结构体变量
struct book1
{
char name[20];
int age;
int mony;
};
struct book
{
char name[20];
int mony;
struct book1 s1;
};
但是结构体内不能有自身结构体变量,但是可以用指针指向一个本类型结构体来实现结构体自引用
struct Note
{
int date; //数据域
struct Note* next;//指针域
};
关于typedef(定义类型)定义定义匿名结构体类型 :
typedef struct
{
int data;//数据域
struct Node* next;//指针域
} Node;
void main() {
Node n;
}
结构体变量的定义与初始化
结构体变量的定义
// struct book 是定义的数据类型的名字,它向编译系统声明这是一个“结构体类型”
struct book
{
int mony;
char name[20];
}s1,s2;//全局变量(创建结构体类型时顺带创建2个结构体全局变量)
//最后的分号千万不能省略
int main()
{
struct book s3, s4;//局部变量
return 0;
}
结构体变量的初始化
struct book
{
int mony;
char name[20];
}s1={30,"xiyouji"}, s2 = {20,"hongloumeng"};//s1,s2也是结构体变量,其是全局变量,
//并对其全部初始化,其实也可不初始化
int main()
{
struct book s3 = { 15,"三国演义" }, s4 = {25,"水浒传"};
//创建结构体对象并完成初始化
//其中初始化一个汉字占两个字节
return 0;
}
结构体嵌套初始化:
struct history
{
int age;
int id;
};
struct book
{
char name[20];
int mony;
struct history s1;
};
int main()
{
struct book s1 = { "abcdef",20,{1000,2023}};//初始化完成
return 0;
}
匿名结构体类型初始化
struct {
char name[20];
int price;
char id[12];
}s = { "git",7,"123" };
注意:
- 结构体的声明和初始化也可以在函数里面进行
- 结构体定义完就必须对其进行初始化
- 结构体也可以通过操作符"."的形式来进行里面值的操作
结构体传参
struct BOOK
{
char name[20];
int money;
};
//传值调用
void test1(struct BOOK s)
{
printf("%s %d", s.name, s.money);
}
//传址调用
void test2(struct BOOK* ps)
{
printf("%s %d", ps->name, ps->money);
}
int main()
{
struct BOOK s1 = { "shuihuzhuan",20 };
test1(s1);//传结构体,不会改变结构体变量
test2(&s1);//传结构体地址,可以改变结构体变量
return 0;
}
注意:
结构体传参尽可能传地址,因为传参数需要压栈,如果传递一个结构体对象时,结构体过大,参数压栈的系统开稍过大,所以会导致性能下降;
传址调用时,可以用上const;结构体传参中const的作用:使原结构体只可以进行读取操作不可以进行更改操作,以防止误操作。
结构体内存对齐
现在我们已经了解结构体的基本使用了,接下来我们探讨一个深入的问题:
如何计算结构体的大小?
这里就涉及到结构体内存对齐。
要了解这些,首先给大家介绍一个宏( offsetof() ),其可以计算结构体成员相对于结构体起始位置的偏移量。
size_t offsetof( structName, memberName );
1. 第一个参数是结构体名称
2.第二个参数是结构体成员
输入两个参数,就会返回结构体成员相对于结构体起始位置的偏移量。
以这个代码来探究:
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
int i;
char c1;
char c2;
};
int main()
{
printf("%d\n", offsetof(s1, c1));
printf("%d\n", offsetof(s1, i));
printf("%d\n", offsetof(s1, c2));
return 0;
}
当知道了偏移量,咱们就可以来分析结构体储存在内存中的方式;
结构体存储在栈区
这样的话应该是9个字节,但结果是12字节,这是为什么?
上面出现的问题,说明结构体成员不是按照顺序在内存中连续存放的,而是有一定规律的。
结构体内存对齐的规则:
1. 结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置。
2.从第二个成员开始,往后的每个成员都要对齐到某个数(对齐数)的整数倍的地址处,
对齐数:编译器默认的一个对齐数与该成员大小值的较小值。
VS编译器上对齐数默认是:8
gcc:没有默认对齐数,对齐数就是该结构体成员的大小。
3.结构体的总大小必须是最大对齐数的整数倍。
最大对齐数:所有对齐数的最大值
4.如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍出,结构体的整数大小,就是所以最大对齐数(包含了嵌套结构体的对齐数)的整数倍。
那怎么运用这个规则呢?
看图说话:
特殊一点 : 如果成员变量是一个数组。
举个例子:int arr [3];则把其看成三个整形变量依次存放就好。
为什么要内存对齐?
1.平台原因:
- 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
- 数据结构应该尽可能的在自然边界上对齐。原因在于:为了访问未对齐的内存,处理器需要两次内存访问;而对齐的内存访问仅只需要一次访问。
3.如图解释:
总结:结构体内存对齐是拿空间换时间的做法
所以在设计结构体时我们既要满足对齐,又要节省空间:
1.合理安排好结构体成员空间
2.也可以修改默认对齐数
修改默认对齐数:
#include <stdio.h>
//修改默认对齐数为1
#pragma pack(1)
struct Hand
{
char c;
int size;
char q;
};
#pragma pack()//取消设置的默认对齐,还原默认
int main() {
printf("%d\n", sizeof(struct Hand));//默认对齐数8时——12,默认对齐数1时——6
return 0;
}
位段
相比结构体更能节省空间,但是有跨平台问题;
为什么存在位段:因为如果一个字节里有32bit,当你只用到了2个bit,其他的空间就都浪费了,而位段就可以选择你的每一个成员占多大的空间,所以可以更好的节约空间。
位段声明
其声明和结构体声明是类似的,有两点不同:
1.位段成员可以是 int 、unsigned int 、signed int 或者是 char (整形家族)类型。
__
2.位段的成员名后边有一个冒号和一个数字。
__
举例:
** S 就是一个位段类型**
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
位段的内存分配
就拿上述的位段类型,来探究;
1. 位段的成员可以是 int 、unsigned int、signed int 或者是 char (整形家族)类型
2. 位段的空间上是按照需要以4个字节(int)或1个字节(char )的方式来开辟的
3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
4.首先每一个成员后面的数字都是位,是二进制位
struct S
{
//因为先是char类型,先开辟一个字节--8个bit位
char a : 3;//a成员占3个bit
char b : 4;//b成员占4个bit
char c : 5;//这时用了7个bit,还剩1个,因为下面还是char类型,不够就再开辟一个字节
//c成员在新开辟的字节占5个bit
char d : 4;//这时还剩3个bit,char类型,开辟一个字节,d成员占4个bit
//因此S位段类型所占3个字节
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 20;
s.c = 3;
s.d = 4;
printf("%d ", sizeof S);//3
return 0;
}
在VS编译器下一个字节内部的数据,先使用低比特位的地址,再使用高比特位的地址(在内存中分配从右往左使用)
位段跨平台问题:
1. int 位段类型,被当做有符号位还是无符号位,是不确定的;
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
总结:和结构相比,位段可达到同样的效果,可以很好的节省空间,但是有跨平台的问题。
—连肝6个小时真不是盖的呀 ^ _ ^ 最后希望兄弟姐妹们小小支持一下呗,咱下次肝到爆。