Cyuyan中的自定义类型——结构体

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

提示:这里可以添加本文要记录的大概内容:

在C语言中共有三种自定义类型——结构体、联合体、枚举。本文主要介绍第一种结构体,后面文章中会介绍联合体与枚举。本文主要围绕以下几个方面对结构体进行介绍——结构体的基础知识、结构体的内存对齐、结构体实现位段。结构体内存对齐,和实现位段是我们比较陌生的知识,需要努力掌握一下。


提示:以下是本篇文章正文内容,下面案例可供参考

一、结构体基础知识

结构是一些值的几何,这些值称为成员变量。值得一提的是结构的每个成员可以是不同类型的变量。
结构体的关键字为struct

(一)、结构体类型的声明、变量的创建与初始化

  • 我们以描述一个学生为例:
#include<stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//注意分号不能丢

我们列举了学生应该有的一些特征,利用不同的数据类型给定义变量值作为结构体的成员,将这些成员组合在一起就构成了一个描述学生的结构体。这就是它的声明.

  • 对于结构体的变量创建以及初始化,我们还是以描述一个学生为例:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//注意分号不能丢
int main()
{
struct Stu s={"张三"20,“男”,“20230818001}//这里我们就创建了结构体变量s,并对其进行了初始化的操作
}

同时我们也可以在声明部分直接定义结构体变量

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

这里我们直接在声明中大括号后面,分号之前直接定义了结构体变量s。

  • 结构体的特殊声明
    在声明结构体的时候,可以不完全声明,即将结构体的名字给抹掉。这样的话存在两个个弊端:
    第一个就是就是我们进行定义结构体变量的时候,如果没有对结构体进行typedef重命名的话,基本上只能在声明后面定义变量,且只能使用一次。
    第二个就是尽管两个结构体里面成员完全一致,但会将这两个结构的声明当成完全不同的类型。
//匿名结构体声明:
struct 
{
int a;
char b;
float c;
}x;//如果没有重定义的话只能在声明部分,定义变量
struct 
{
int a;
char b;
float c;
}a[20],*p;//如果没有重定义的话只能在声明部分,定义变量

根据第二个弊端可以得出:

p=&x;

这句代码是完全错误的,因为编译器会将上面两个匿名结构体声明当成两种不同的类型。

(二)、结构成员访问操作符

成员访问操作符一个是: . ;另外一个是:-> ;
这两个一个针对非指针结构体变量,一个针对指针型结构体变量。

#include<stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}a,*b;//注意分号不能丢
int main()
{
//我们要访问stu结构体中的成员两种方式:
//1.非指针型
a.age=20;
printf("%d\n",a.age);
//2.指针型
printf("%s",b->name);
}

在这里插入图片描述

(三)、结构体的自引用

  • 结构体的自引用就是结构体的成员中有类型为结构体本身。
  • 例如定义一个单链表的节点
struct Node
{
int data;
struct Node next;
};

其实上面的代码是有一些问题的,我们考虑一下啊,一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大,这样是不合理的,正确的引用应该用指针型:

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

这样的话指针在X86环境下只占4个字节,就可以避免了结构体变量大小为无穷大的现象。

  • 对于不完全声明的匿名结构体类型是不能进行自引用的(尽管进行了重定义)
typedef struct
{
int data;
Node* next;
}Node;

上面这段代码是错误的,因为自定义类型Node在后面声明的,不能提前使用。
故而定义结构体(自引用)不要使用匿名结构体。

(四)、结构体传参

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

上面两端代码打印的结果是相同的,分别是结构体的传值调用,以及传址调用,但是我们首先选择的是传址调用,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。而传址传的是指针,在X86环境下只有4个字节,比较小。故而结构体传参的时候,我们优先选择传结构体的地址。

二、结构体内存对齐!!

(一)、对齐规则

  • 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
    宏 offsetof(type,member)(头文件 stddef.h)是计算结构体成员相较于结构体变量起始位置的偏移量。
  • 其他成员要对齐到某个数字(对齐数)的整数倍的地址处
    在这里对齐数=编译器默认的一个对齐数 与该成员变量大小的较小值。
    VS中默认对齐数为8;Linux gcc中没有默认对齐数,对齐数就是成员自身的大小
  • 结构体总的大小为最大对齐数(结构体中每一个成员变量都有一个对齐数,所以对齐数中最大的)的整数倍
  • 对于镶嵌了结构体的情况,镶嵌的结构体成员对齐到自己成员中最大对齐数的整数倍处。故而结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍

(二)、分析结构体大小的详细过程

  • 练习1:
#include<stdio.h>
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd",sizeof(struct S2));

}

运行结果如下:
在这里插入图片描述
根据结构体对齐规则:对齐如下图:
在这里插入图片描述
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;然后第二个成员为n,它的内存大小为4个字节,VS默认对齐数为8,所以取较小值,故而它的对齐数为4,要从对齐数的整数倍开始,故而n变量的地址起始位置应该在偏移量为4的位置,顺至到偏移量为7的位置(共4个字节);最后一个成员为c2,它的内存大小为1个字节,VS默认对齐数为8,所以它的对齐数为1.故而偏移量8即为它的起始地址。结构体成员的最大对齐数为4,所以结构体变量的大小应该是最大对齐数的整数倍,即4的倍数,现在已经是9个字节了,所以还应该浪费3个字节,变成12个字节故而此结构体变量大小为12个字节。

  • 练习2:
#include<stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
int main()
{
printf("%zd",sizeof(struct S1));

}

运行结果如下:
在这里插入图片描述
根据结构体对齐规则:对齐如下图:
在这里插入图片描述
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;然后第二个成员为c2,它的内存大小为1个字节,VS默认对齐数为8,所以取较小值,故而它的对齐数为1,顺利的占据偏移量为1的地址处;最后第三个成员为int型,它的内存大小为4,VS默认对齐数为8,所以它的对齐数为4,要从对齐数的整数倍开始,故而n变量的地址起始位置应该在偏移量为4的位置,顺至到偏移量为7的位置(共4个字节)。它的所有成员的最大对齐数为4,根据对齐规则,此结构体变量的大小应该是最大对齐数的整数倍,所以占据8个字节。

  • 练习3:
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%zd",sizeof(struct S3));

}

运行结果如下:
在这里插入图片描述

根据结构体对齐规则:对齐如下图:
在这里插入图片描述
结构体第一个成员为d,它位于结构体变量起始位置偏移量为0的地址处,占用8个字节;第二个成员为c,它的内存大小为1个字节,VS默认对齐数为8,所以它的对齐数为1,故而它的起始位置是偏移量为8的位置,共占据一个字节;它的第三个成员为i,它的内存大小为4个字节,VS默认对齐数为8,所以它的对齐数为4,因为对齐数位置要是4的倍数,所以它的起始位置的偏移量为12,共占据4个字节,此时偏移量来到15。此结构体变量成员中最大对齐数为8,所以该结构体变量所占字节大小为8的倍数,此时正好为16个字节,正好满足为8的倍数,所以不用额外扩充,浪费字节。

  • 练习4:结构体嵌套问题
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd",sizeof(struct S4));

}

运行结果如下:
在这里插入图片描述
根据结构体对齐规则,对齐如下图:
在这里插入图片描述
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;紧接着,第二个成员为s3,它的内存大小为16(上面以求),VS默认对齐数为8,所以它的对齐数为8,由于它的起始 位置要是8的倍数,所以它的起始位置偏移量为8,共计16个字节,所以来到了偏移量为23的位置;该结构体的第三个成员为d,它的内存大小为8个字节,VS默认对齐数为8,所以它的对齐数为8,由于它的起始 位置要是8的倍数,所以它的起始位置偏移量为24,共计8个字节,来到了偏移量为31的位置。该结构体成员的最大对齐数为8,所以该结构体变量的大小为8的倍数,此时正好为32个字节,正好满足为8的倍数,所以不用额外扩充,浪费字节。

(三)、为什么存在内存对齐

  • 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。故而应该对齐指定位置。
  • 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐地内存,处理器需要作两次内存访问;而对齐地内存访问仅需要一次访问。假设一个处理器总是从内存中取4个字节,则地址必须是4的倍数。如果我们能保证所有的int类型的数据的地址都能对齐成4的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节的内存块中。
    以下面例子来解释:
    在这里插入图片描述
    我们一次要访问4个字节,如果没对齐,我们要完整的访问n变量需要两次访问,而对齐后我们只需要一次访问即可,这节省了时间。

总体来说:结构体的内存对齐实质上是拿空间来换取时间的做法。

而我们肯定会想,设计结构体的时候,如何做到又能对齐,又能节省空间。我们的做法是应该让占用空间小的成员尽量集中在一起!!

#include<stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd\n",sizeof(struct S1));
printf("%zd\n",sizeof(struct S2));
}

运行结果如下:
在这里插入图片描述
S1和S2结构体的成员一样,但是S1中让占用空间小的成员c1,c2集中在一起,那么它占用的内存就小

(四)、默认对齐数的可修改性

  • 我们用#pragma 这个预处理指令, 可以改变编译器的默认对齐数
#pragma pack(1)
struct S
{
	char c1;
	int n;
	char c2;

};

int main()
{

	printf("%zd\n", sizeof(struct S));


}

运行结果如下:
此时VS的默认对齐数为1.
在这里插入图片描述

三、结构体实现位段!

(一)、位段的声明

  • 位段的声明与结构体的声明类似,但需要注意以下几点:
    1.位段的成员必须是 int、unsigned int、signed int、c类型,但在C99中也可以选择其他类型;
    2.位段的成员名后边有一个冒号和一个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};

在这里A就是一个位段类型,这里里面成员_a冒号后的数字代表所给变量定义所占的二进制位数。
那么这个结构体的大小会是多少呢,要解决这个问题,我们得先了解位段的内存分配规则

(二)、位段的内存分配

  • 位段成员可以是int、unsigned int、signed int或者是char等类型

  • 位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。

  • 位段涉及很多不确定因素,位段是不跨平台的,要注意可移植的程序应该避免使用位段

  • 分析下面位段的大小:

#include<stdio.h>
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;

};
struct S s={0};
int main()
{
printf("%zd",sizeof(struct S));
}

运行结果如下:
在这里插入图片描述
咱们假设给定空间后,在空间内部都是从右向左使用;当剩下空间不足以放下一个成员的时候,空间就浪费掉;且位段的哦那关键是按照需要以1个字节的方式来开辟。
在这里插入图片描述
这样,在开辟第一个字节时候,a从右向左存储三个bit位,紧接着b存储4个比特位,这个字节就浪费了一个bit位,紧接着开辟下一个字节,这是c从右向左占据5个比特位,剩下3个bit位不足以存放d变量,所以又重新开辟一个字节。故而这个位段的大小为3个字节。

(三)、位段的跨平台问题

  • int 位段被当作有符号数还是无符号数是不确定的。
  • 位段中最大位的数目不能确定。(例如16位机器最大为16,而32位机器最大是32。故而写成27,在16位机器中会出错)
  • 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
  • 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时候,是舍弃剩余的位还是将剩余位利用,这个是不确定哒。

总而言之:与结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是位段存在跨平台问题。

(四)、位段的应用

  • 在网络协议中,IP数据报的格式,很多属性只需要几个bit位就可以描述,在这里我们可以使用位段,来达到理想的效果,充分节省空间,这样下来,在网络传输的时候,数据报的大小也会较小一些,这对网络的畅通是有帮助的。
    在这里插入图片描述

(五)、位段的使用注意事项

  • 位段的几个成员共用一个字节,这样有些成员的起始位置并不是某个字节的起始位置,故而这些位置处是没有地址的。因为在内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。故而我们在对没有地址的位段成员不能使用取地址操作符(&),所以在用scanf函数的时候,我们应该采用间接赋值。
#include<stdio.h>
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};
int main()
{
	struct A sa = { 0 };
	int d = 0;
	scanf("%d", &d);
	sa._d = d;
	printf("%d", sa._d);
}

为了避免位段中的成员可能不能进行取地址操作,我们引入中间变量d,来对位段成员间接赋值。

总结

本文介绍了C语言自定义类型中的第一种结构体的相关知识,着重介绍了结构体内存对齐以及位段的相关知识,如有错误,请批评指正。

  • 40
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值