您现在的位置是:首页 >技术交流 >自定义类型:结构体网站首页技术交流

自定义类型:结构体

azaz_plus 2023-06-18 04:00:02
简介自定义类型:结构体

ok,兄弟们,今天来写关于自定义类型的博客,先来看结构体。

结构体

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

1.结构体类型的声明

struct tag
{
 member-list;
}variable-list;

以上就是结构体声明的格式。比如说我们定义一个学生变量可以这样来。

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

 当然,结构体中也存在一些特殊的声明。

比如说匿名类型

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

可以看到,上面两个结构体并没有名字,因此被称为匿名结构体。那么问题就来了,我们可以很明白的看到上面的两个结构体里面所包含的变量是相同的,那么,这两个结构体是否相同呢?其实啊,编译器会将这两个结构体看成是两个完全不相同的类型,所以他们并不是相同的。

2.结构的自引用

顾名思义,结构体的自引用就是结构体自己引用自己。

先来看一个自引用的栗子

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

好像这就完成了自引用,但是,这样写会不会出现问题呢?

是的,这样写的话,这个结构体的大小是未知的。所以我们需要一个换一种写法

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

 下面再来看一种写法是否可行

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

很明显,这是不行的,因为Node的调用在Node之前。

所以我们可以采取typedef来改一下

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

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

ok,下面我们来学习一下关于结构体变量的定义和初始化

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};//结构体嵌套初始化
//或者是使用 . ->来单个赋值,这里就不在展示了。

4.结构体内存对齐

下面呢,我们来学一下关于结构体大小的求法,不妨先来看一个栗子

struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d
", sizeof(struct S1));

从结果来看,很明显,结构体的大小就肯定不是简单的各变量求和了

这就不得不提到结构体的内存对齐了,通常来讲,内存对齐的规则如下

1. 第一个成员在与结构体变量偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值
VS 中默认的值为 8
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面,我将通过画图的方式来详细的讲解内存对齐

从0开始存储c1,然后下一个i是Int类型,大小是4(小于vs的最大对齐数),但是存储完c1之后的下一个偏移量是1,不是4的倍数,因此要浪费一些空间来找到int的偏移量,然后从偏移量为4开始存储,存四个字节之后再继续,存完i之后的偏移量为8,是char的整数倍,因此在下一个存储8 ,然后所有的对齐数最大的是4,因此对吼的总大小应该是4的整数倍,所以答案是12。

到这里肯定就有人会有疑问了,哎呀,这个结构体这样存,那浪费的空间不来大了啊。其实,结构体这样存储是有道理的,我们不妨来看一下

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

所以,总的来说

结构体的内存对齐是拿空间来换取时间的做法。  

所以啊,我们在设计结构体变量的时候,要尽可能的减少他的大小。

那就是让占用空间小的成员尽量集中在一起。  

比如

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

其实在C语言提供了一种修改默认对齐数的方法,那就是使用#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
", sizeof(struct S1));
    printf("%d
", sizeof(struct S2));
    return 0;
}

 

 其实上述的代码就是将S1的对齐数改成了8,将S2的对齐数改成了1,因此答案会出现12和6

5.结构体传参

我们还是直接来看代码吧

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

在上面的代码中,我们首先的print2函数,因为

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

6.结构体实现位段(位段的填充&可移植性)

首先我们闲来了解一下位段的概念

1. 位段的成员必须是 int unsigned int signed int ,或者是char,unsigned char
2. 位段的成员名后边有一个冒号和一个数字。

我们还是用一个栗子来看一下位段的定义

struct A
{
 int _a:2;
 int _b:5;
 int _c:10;
 int _d:30;
};

 在上面学习结构大小的计算的时候我们了解到,结构体有时候会存在很多的内存空间浪费,所以,我们给出了位段这个概念,目的就是为了减少内存浪费,比如上述代码的 int _a:2的意思就是给_a分类两个比特的空间。也就是说我们可以根据要存储的数据的大小不同,来灵活的改变变量的大小来起到节约空间的作用。

不妨将两个代码做个比较看一下

struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
}A;
struct B
{
	int a;
	int b;
	int c;
	int d;
}B;
int main()
{
	printf("A=%d B=%d
", sizeof(A), sizeof(B));
	return 0;
}

很明显,在使用位段之后的结构体大小明显的减少了。

下面,我们将主要来研究一下关于位段的大小计算。

1. 位段的成员可以是 整形家族类型。
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;

 因为位段的限制,所以变量能存储的比特位数也就被限制了。

关于位段的跨平台问题。

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

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

关于位段的一些应用,这个暂作了解,详情以后再谈

好的,在最后呢,给大家展示一些关于结构体和位段的练习,并且我会在之后的博客中进行讲解。

//练习1
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d
", sizeof(struct S2));
//练习2
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d
", sizeof(struct S3));
//练习3-结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d
", sizeof(struct S4));

 练习4

struct _Record_Struct
{
  unsigned char Env_Alarm_ID : 4;
  unsigned char Para1 : 2;
  unsigned char state;
  unsigned char avail : 1;
}*Env_Alarm_Record;
struct _Record_Struct *pointer = (struct _Record_Struct*)
malloc(sizeof(struct _Record_Struct) * MAX_SIZE);

 当A=2, B=3时,pointer分配几个字节的空间?

ok,今天的博客就到这里了,我们下期债见。

答案:1. 8   2. 16   3. 32   4. 9

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。