C语言 结构体+枚举+联合+位段

在刚开始学习c语言时,我们就了解了整形、浮点型等常用的数据类型,把他们组合起来,就是我们今天学习的自定义类型。自定义类型包括结构体、枚举、联合。在编程的过程中,它们有不同的应用场景,下面我们一起来学习。

结构体

结构体的声明

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

结构体的声明

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

举例

描述一个学生:

struct student
{
	char name[20];//姓名
	short age;//年龄
	char sex[5];//性别
}s1,s2;//s1,s2是全局变量;分号不能丢

特殊的声明(匿名结构体)

在声明结构的时候,可以不完全的声明

举例

struct 
{
	char name[20]; 
	short age;
	char sex[5];
}s1;//全局变量

上面的结构在声明的时候省略掉了结构体标签(tag)。

匿名结构体易错点

struct 
{
	char name[20]; 
	short age;
	char sex[5];
}s1;//全局变量

struct student
{
	char name[20];
	short age;
	char sex[5];
}*p;

上面两个结构体定义的变量全都相同,那么我们可以写成p = &s1吗?
通过验证,这样写是不可以的,因为编译器会把上面的两个声明当成完全不同的两个类型,我们在写程序时要注意这一点。

结构体的自引用

思考一个问题:在结构中包含一个类型为该结构本身的成员是否可以呢?
我们先来看这段代码:

struct Node
 {    
    int data;    
    struct Node next; 
 }

在这个结构体中包含了类型为该结构体本身的变量,这个代码会进入死递归,因为没有结束的条件,struct Node next找不到出口。

正确的自引用方式可以用指针来构造:

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

自引用易错点

给匿名结构体重命名,在自引用时,不能使用它的重命名

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

这样写代码的方式是错误的,因为编译器到Node* next时,Node还没有创建,编译器不知道Node*是什么,就会出现错误,这也是需要注意的点。

解决方法

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

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

举例

struct Point
{
	int x;
	int y;
};

struct Node
{
	struct Point p;
	struct Node *next;
};

int main()
{
	struct Node s1 = { { 3, 5 }, NULL };
	printf("%d %d\n", s1.p.x, s1.p.y);

	struct Point a = { 2, 3 };
	printf("%d %d\n", a.x, a.y);
	return 0;
}

结构体内存对齐

计算下面结构体大小:

//练习一
struct s1
{
    char c1;
    int i;
    char c2;
};

先来介绍一个函数offsetof: 计算结构体变量相对首地址的偏移量

size_t offsetof(structName, memberName);
头文件 #include<stddef.h>

int main()
{
	printf("%d\n", offsetof(struct s1, c1));
	printf("%d\n", offsetof(struct s1, i));
	printf("%d\n", offsetof(struct s1, c2));
	return 0;
}

在这里插入图片描述
以编译结果来看,结构体里面的变量不是连续存储,不能直接用变量大小相加。实际上,计算结构体大小需要内存对齐。

结构体的对齐规则

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

以上面题为例:c1在偏移量为0的地址处,int i的大小为4字节,vs编译器默认值为8(选较小值),int i的对齐数为4,占偏移量为4~7的内存大小(对齐数的整数倍),char c2的大小为1字节,char c2的对齐数为1,占偏移量为8的内存大小,现在结构体总大小为9,最大对齐数为4,又因为结构体总大小为最大对齐数的整数倍,所以偏移量为10~11浪费,结构体总大小为12字节

例题

//练习二
struct S2 
{
    char c1;//成员大小1 默认对齐数8   对齐数1  0地址处
    char c2;//成员大小1 默认对齐数8   对齐数1  1地址处
    int i;//成员大小4  默认对齐数8  对齐数4   4-7地址处
    //总大小是8字节 是最大对齐数的整数倍
};

//练习三
struct S3 
{    
    double d;//成员大小8  默认对齐数8   对齐数8  0-7地址处
    char c;//成员大小1 默认对齐数8   对齐数1  8地址处
    int i;//成员大小4 默认对齐数8   对齐数4   12-15地址处
    //最大对齐数8 总大小为16
}; 

//练习4-结构体嵌套问题
struct S4 
{    
    char c1;//成员大小1 默认对齐数8   对齐数1  0地址处    
    struct S3 s3;//成员大小16  自己最大对齐数8   8-23地址处    
    double d; //成员大小8 默认对齐数8   对齐数8  24-31地址处
    //总大小为32  是最大对齐数的整数倍
};

为什么存在内存对齐?

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

总体来说

  • 结构体的内存对齐是拿空间来换取时间的做法。
  • 在设计结构体的时候,既要满足对齐,又要节省空间,如何做到:让占用空间小的成员尽量集中在一起。

例如:上面的s1和s2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。

修改默认对齐数

使用#pragma预处理指令,可以改变默认对齐数

举例

#pragma pack(1)//设置默认对齐数为1 
struct S1 
{    
      char c1;    
      int i;    
      char c2; 
}; 
#pragma pack()//取消设置的默认对齐数,还原为默认
//修改的默认对齐数,只能是2^n(n=0,1,2,3……)

结构体传参

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

上面的 print1 和 print2 函数哪个好些?

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

总结: 结构体传参的时候,要传结构体的地址。

位段

位段的声明和结构是类似的,有两个不同:

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

//冒号后面的数字,指的是这个变量需要几个比特位来存储
struct A 
{    
     int _a:2;//4字节   32位  用2位   剩30位    
     int _b:5;//用5位   剩25位    
     int _c:10;//用10位    剩15位  
     int _d:30;//不够用,重新开4字节
     //共用8字节
     //对于vs编译器,当比特位不够用时,需要重新开辟 
}

位段的内存分配

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

位段的跨平台问题

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

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

枚举:把可能的取值一一列举

定义:

enum Day//星期 
{    Mon,    
     Tues,    
     Wed,    
     Thur,   
     Fri,    
     Sat,    
     Sun 
}; 
enum Sex//性别 
{
     MALE,    
     FEMALE,    
     SECRET 
}; 

以上定义的 enum Day , enum Sex 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫枚举常量 。

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:

enum Color//颜色 
{   
     RED=1,    
     GREEN=2,    
     BLUE=4 
};

枚举的优点

  • 增加代码的可读性和可维护性
  • 和#define定义的标识符比较枚举有类型检查,更加严谨。
  • 防止了命名污染(封装)
  • 便于调试
  • 使用方便,一次可以定义多个常量

枚举的使用

enum Color
{
	RED,
	GREEN,
	BLUE
};

int main()
{
	enum Color c = RED;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。 
	return 0;
}

联合(共用体)

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。

//联合类型的声明 
union Un 
{    
     char c;    
     int i; 
};

//联合变量的定义
 union Un un; 

联合大小的计算

  • 联合的大小至少是最大成员的大小。
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
//例一
union Un
{
	char c[5];//最少为5字节  要对齐到最大对齐数的整数倍
	int i;//最大对齐数为4   总大小为8字节
};

//例二
union U
{
	short s[7];//最少为14字节
	int i;//最大对齐数4,总大小16
};

面试题

判断当前计算机的大小端存储

  • 大端字节序存储:把一个数的低位字节序的内容放在高地址处,把高位字节序内容存放在低地址处
  • 小端字节序存储:把一个数的低位字节序的内容放在低地址处,把高位字节序内容存放在高地址处

int a=0x11223344;
将a以大端字节序存储和小端字节序存储分别存储
在这里插入图片描述

思路:利用共用体的知识解此题 。因为共用体的特点是成员共用同一块空间 ,构造有int型和char类型成员变量的共用体,因为用同一块空间,所以访问char,就是访问int类型的第一个字节,再进行判断

union un
{
	int i;
	char c;
};

int is_check()
{
	union un u;
	u.i = 1;//00 00 00 01  判断大小端只需要判断第一个字节处是1还是0
	return u.c;
}

int main()
{
	int ret=is_check();
	if (ret == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}

总结:
我们要学会并掌握结构体内存对齐,因为在笔试题中这也是一个考点,我们要重视起来。在使用枚举的时候,尽可能列举出枚举变量的所有可能,这样在后面写代码的过程中,会方便一些。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值