04.自定义类型:结构体

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1 结构体的声明

1.1 结构的基础知识

结构是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.2 结构的声明 

struct tag
{
member-list;//成员列表
}variable-list;//变量列表

EG:

描述一位学生 

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

1.3 特殊的声明

在声明结构的时候可以不完全的声明。
比如

/*struct Stu
{
	char name[20];
	int age;
} s1,s2;*///全局变量,s1和s2是两个结构体变量



typedef struct Stu//将复杂类型简单化
{
	char name[20];
	int age;
}Stu;//相当于将struct stu进行了重命名主函数运用的时候直接用stu即可。

struct
{
	char name[20];
	int age;
}s1;//匿名结构类型直接创建名字

struct
{
	int a;
	char c;
	double d;
}* p;//匿名结构类型的指针创建p

int main()
{
	struct Stu s3, s4;//局部变量
	Stu s5, s6;

	return 0;
}
//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;//x是全局变量int main创建的是局部变量
struct
{
	int a;
	char b;
	float c;
}a[20], * p;

上面的两个结构在声明的时候省略掉了结构体标签tag。
那么问题来了

//在上面代码的基础上下面的代码合法吗
p = &x;

警告
原因编译器会把上面的两个声明当成完全不同的两个类型。所以是非法的。

1.4 结构的自引用

//链表

//数据结构->数据在内存中存储的结构

//顺序数据结构顺序表

//链表✳

//放入同类型的下一个类型的指针结构体自引用的方式 

//二叉树 

在结构中包含一个类型为该结构本身的成员是否可以呢

//代码1
struct Node
{
int data;
struct Node next;
};
//可行否
如果可以那sizeof(struct Node)是多少

正确的自引用方式

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

用法

typedef struct Node
{
	int data;
	struct Node* next;
}Node;

int main()
{
	struct Node n1;
	struct Node n2;
	n1.next = &n2;

	return 0;
}

 注意

/代码3
typedef struct
{
int data;
Node* next;
}Node;
//这样写代码可行否
//解决方案
typedef struct Node
{
int data;
struct Node* next;
}Node;

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

有了结构体类型那如何定义变量其实很简单

struct Point
{
	int x;
	int y;
}p1 = {10, 20};

struct Point p2 = {0,0};

struct S
{
	int num;
	char ch;
	struct Point p;
	float d;
};

int main()
{
	struct Point p3 = {1,2};
	struct S s = { 100, 'w', {2,5}, 3.14f};
	struct S s2 = {.d=1.2f, .p.x=3,.p.y=5, .ch = 'q', .num=200};
	printf("%d %c %d %d %f\n", s.num, s.ch, s.p.x, s.p.y, s.d);//乱序初始化
	printf("%d %c %d %d %f\n", s2.num, s2.ch, s2.p.x, s2.p.y, s2.d);

	return 0;
}

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};//结构体嵌套初始化

1.6 结构体内存对齐

探讨计算结构体的大小

热门考点结构体内存对齐

#include <stdio.h>
#include <stddef.h>


struct S1
{
	char c1;//
	int i;//
	char c2;//
};

struct S2
{
	char c1;//1
	char c2;//1
	int i;//4
};

//offsetof

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

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

int main()
{
	printf("%d\n", sizeof(struct S4));

	//printf("%d\n", sizeof(struct S3));

	//printf("%d\n", sizeof(struct S1));//12// 论证方法宏offsetof
	//printf("%d\n", sizeof(struct S2));//8
	//printf("%d\n", offsetof(struct S1, c1));
	//printf("%d\n", offsetof(struct S1, i));
	//printf("%d\n", offsetof(struct S1, c2));

	return 0;
}

//S1和S2里面只是改变了顺序为什么内存大小不一样 

1.结构体的第一个成员对齐到结构体在内存中存放位置的0偏移处

2. 从第二个成员开始每个成员都要对齐到(一个对齐数) 的整数倍处对齐数: 结构体成员自身大小和默认对齐数的较小值

VS :默认对齐数数8
Linux gcc:没有默认对齐数对齐数就是结构体成员的自身大小
3.结构体的总大小必须是所有成员的对文数中最大对齐数的整数倍

4.如果结构体中嵌套了结构体成员要将嵌套的结构体成员对齐到自己的成员中最大对齐数的整数停处。结构体的总大小必须是最大对齐数的整数倍这里的最大对齐数是: 包含嵌套结构体成员中的对齐数的所有对齐数中的最大值。

struct S1
{
	char c1;//最大对齐数1
	int i;//最大对齐数4/8中最小的值故为4
	char c2;//最大对齐数1/4故为4
};
//现在用了9必须为最大对齐数4的倍数又浪费了3个空间故为12

struct S2
{
	char c1;//1
	char c2;//1
	int i;//4
};

s2用到的空间更小是因为它将小类型的放在一起了 

我们在设计时为了使得空间占用小需要将小类型的放在一起。

//去较小值为对齐数 

struct S3
{
	double d;//占用8个字节 
	char c;//对齐到整数倍的对齐数8和char字节的最小值
	int i;//48最小为4倍数为1212131415
};

占用8个字节 

存在内存对齐的原因

1. 平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特
定类型的数据否则抛出硬件异常。
2. 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访问。
总体来说
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候我们既要满足对齐又要节省空间如何做到
让占用空间小的成员尽量集中在一起。

//例如
struct S1
{
	char c1;
	int i;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int i;
};

S1和S2类型的成员一模一样但是S1和S2所占空间的大小有了一些区别。

 1.7 修改默认对齐数

//VS环境下默认对齐数是8

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

#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;
}

//默认对齐数为1就相当于没有对齐了 

结论
结构在对齐方式不合适的时候我么可以自己更改默认对齐数

1.8 结构体传参

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); //传结构体
print2(&s); //传地址
return 0;
}

print2优于print 1

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

结论
结构体传参的时候要传结构体的地址。

2. 位段

结构体讲完就得讲讲结构体实现位段的能力。

2.1 什么是位段

位段的声明和结构是类似的有两个不同
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字

EG:

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};//节省空间
//作用于结构体类似

 32:5

2 (30)

5 (25)

10 (15)//有很多的不确定因素

32:

30

A就是一个位段类型。
那位段A的大小是多少
printf("%d\n", sizeof(struct A));

2.2 位段的内存分配

%2.7.023315

//位段是为了节省空间的不存在对其

1. 位段的成员可以是 int unsigned int signed int 或者是 char 属于整形家族类型
2. 位段的空间上是按照需要以4个字节 int 或者1个字节 char 的方式来开辟的。
3. 位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段。

//一个例子
struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的


2.3 位段的跨平台问题

总结
 1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机
器会出问题。
3. 位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。
4. 当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。

总结
跟结构相比位段可以达到同样的效果但是可以很好的节省空间但是有跨平台的问题存在。


 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6