自定义类型:结构体,枚举,联合


前言

自定义类型有结构体、枚举、联合


一、结构体

为什么要存在结构体?

计算机是为了解决人的问题,人认识自然世界的方式是通过“属性”认识的。
任何事物抽象出来的属性集合,都可以用来表示一类事物,即类型。这个工作在C中,struct来完成。

1.1结构体的声明

1.1.1基础知识

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

1.1.2结构的声明

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

例如描述一个学生:

struct Stu
{
 char name[20];//名字
 int age;//年龄
 char sex[5];//性别
 char id[20]

1.1.3特殊的声明

在声明结构的时候,可以不完全声明。
匿名结构体类型
下面两个结构体在声明的时候省略了结构体标签(tag)

struct{
int a;
char b;
float c;
}x;

struct{
int a;
char b;
float c;
}a[20],*p;

那么,下面代码合法么?

int main(){
p=&x;
system("pause");
return 0;
}

错误写法。
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的(编译器会告警)。
只要是两个结构体类型,即便是内部元素完全一样,也是两种类型。

int main(){
p=x;
system("pause");
return 0;
}

正确写法。
结构体数组a[20]是数组,变量名就是首元素地址,也就是结构体地址,和结构体指针*p类型一致。

1.2结构体的自引用

在结构体内再定义一个结构体

struct 	Node{
 int data;
//struct Node next;//不行的,空间无法确定,无法开辟
 struct Node *next;//结构体的自引用,在32位下:指针大小为4字节。在64位下:指针大小为8字节。大小确定。
};
int main(){
 struct Node n;
 system("pause");
 return 0;
}

结构体的自引用只能通过指针完成!

注意:以下面为错误引用,因为是未定义前就是用Node

typedef struct{
 int data;
 Node *next;//相当于应用
}Node;//定义类型

正确写法为:

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

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

什么时候会使用结构体自引用?为什么?

struct person{
 char name[32];//某人的名字
 char sex;//某人的性别
 struct person *next;//认识的另一个人。事物之间是有联系的
}

因为变量之间要能够组织成某种关系

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

结构体内可以定义任何类型:变量,数组,指针,结构体等

struct Point{
 double x;
 double y;
 char name[32];
 struct Point *next;
 struct speed s;
};
struct speed{
 int sp;
 int dir;
};

结构体可以依次初始化

int mian(){
 struct Point pos={32.1, 45.6,"RM",NULL,{3.3,32}};
}

错误写法:结构体变量定义初始化不能分开

int mian(){
 struct Point pos
 pos={32.1, 45.6};
}

结构体访问方式一种是.操作符一种是->操作符

int mian(){
 struct Point pos={32.1, 45.6,"RM",NULL,{3.3,32}};
 printf("%s沿着方向,%d以百米速度,%fKM正在行驶,瞬间坐标[%f,%f]\n",pos.name,pos.s.dir,pos.s.sp,pos.x,pos.y);
 struct Point *p=&pos;
  printf("%s沿着方向,%d以百米速度,%fKM正在行驶,瞬间坐标[%f,%f]\n",pos.name,pos.s.dir,pos.s.sp,pos.x,pos.y);
}

1.4结构体内存对齐

请问一下两个结构体的大小分别是多少?

struct A{
 char a;
 char b;
 int c;
}

struct B{
 char a;
 int c;
 char b;
}

int main(){
  printf("%d\n",sizeof(struct A));
  printf("%d\n",sizeof(struct B));
}

结果为:
在这里插入图片描述
struct有“内存对齐”的现象。结构体的大小,不能简单地将内部成员大小相加,结构体的大小需要进行“内存对齐”操作。

1.为什么存在内存对齐?
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐;而对齐的内存访问仅需要依次访问。
总的来说:结构体的内存对齐是拿空间换时间的做法。(通过花费空间来减少访问内存次数从而提高效率的方式成为内存对齐)

2.为什么数组不谈而结构体会考虑存储对齐?
因为struct可能存在大量的不同类型的数据

3.如何对齐?

首先得掌握结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员(除了第一个)变量要对齐当前存入变量的起始偏移量能否整除变量自身大小)到某个数字(对齐数,自身大小)的整数倍(最小整数倍=起始偏移量/对齐数)的地址处。
    对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的最小整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
  5. 如果准备对齐,一定要让起始偏移量成为能够整除自身对齐数最小整数倍
  6. 第一个成员虽然不参与对齐,但是他要参与最大对齐数的比较。
  7. 第一个元素为何不考虑对齐?第一个元素处于0地址处,0除以任何数都能整除。

1.4.1内存对齐练习题

//练习1
struct S1
{
 char c1;
 int i;
 char c2;
};
printf("%d\n", sizeof(struct S1));

1+(3)+4+1=9 ,9/4扩充+3=12
在这里插入图片描述

//练习2
struct S2
{
 char c1;
 char c2;
 int i;
};
printf("%d\n", sizeof(struct S2));

1+1+(2)+4=8
在这里插入图片描述

//练习3
struct S3
{
 double d;
 char c;
 int i;
};
printf("%d\n", sizeof(struct S3));

8+1+(3)+4=16,16%8=0不用扩充
在这里插入图片描述

//练习4-结构体嵌套问题
struct S4
{
 char c1;
 struct S3 s3;
 double d;
};
printf("%d\n", sizeof(struct S4));

如果结构体包含了结构体,内部结构体的对齐数就是自身的最大对齐数
所以struct S3 s3大小是16内部最大对齐数是8
1+(7)+16+8=32,32%8=0不用扩充
在这里插入图片描述

//练习5-结构体嵌套问题
struct S5
{
 char c1;
 struct S3 s3;
 double d;
 char *p;
 short *p[3];
 char x[7];
 char *y[3];
};
printf("%d\n", sizeof(struct S5));

如果结构体包含了数组,第一个元素对齐了,其他元素都对齐了
1+(7)+16+8+4+12+7+(1)+12=68,68%8=4快充+4=72
在这里插入图片描述

struct S6
{
 char a;
 double b;
 double *c;
 short d[2];
 struct S5 e;
 struct S5 *f;
 struct S4 g[3];
 char h;
 double *i;
 struct S2 *j[2];
};
printf("%d\n", sizeof(struct S6));

在这里插入图片描述

1.5修改默认对齐数

之前我们见过了 #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));//结果为1+(3)+4+1=9,9扩充+3=12
    printf("%d\n", sizeof(struct S2));//结果为1+4+1=6
    return 0;
}

在这里插入图片描述

杂注参数应为2^N,否则会有警告
在这里插入图片描述
结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

百度笔试题:

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
考察: offsetof 宏的实现

struct A {
 char a;
 char b;
 double c;
}
int main(){
 printf("%d\n",&(((struct A *)0)->c);//求偏移量。相当于将结构体地址指向0号地址,然后再指向C得到偏移量
   printf("%d\n",(int&(a.c)-(int)&(((struct A *)0)->c));//通过C的地址减去c相对于起始地址的偏移量,从而能够得到a的地址。与&a的值相等
   printf("%d\n"(struct A*(int&(a.c)-(int)&(((struct A *)0)->c)))->a);//通过C的地址访问a的值 
}

在这里插入图片描述

1.6结构体传参

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.7结构体实现位段

1.7.1位段

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

比如:

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

A就是一个位段类型。

那位段A的大小是多少?

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

结构体在存储时是内存对齐的存法,用空间换时间
而位段采用类似“压缩式”存储。

在这里插入图片描述

一、struct A中_a是int型占32个比特位,但是它不需要占32个,它只想占用开辟的多个比特位中的2个,所以从最低的位置分配2个比特位名为_a。
二、因为_b只需要5个比特位,在之前开辟的空间中由于_a只占了2个还剩余30个比特位足够b放入,因此b在a后面紧接着占用5个比特位。
三、因为_c占10比特位,所以依然可以紧挨着b占位,剩余20比特位
四、因为_d需要30比特位而剩余比特位不足,因此开辟新的32个比特位存放_d
五、因此一共开辟了8字节的空间大小
在这里插入图片描述

1.7.2位段的内存分配

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

举一个例子并思考几个问题:
空间是如何开辟的?
在一个字节里8bit里面有没有大小端?
在一个字节里面,那些比特位位属于高比特位?

struct S
{
 char a:3;//cahr类型开辟8个字节,a在此占3个剩余5个
 char b:4;//b占4个可以放在a后面,剩余1个
 char c:5;//c占5个不够放b后面,重新开辟8个字节,剩余3个
 char d:4;//d占4个不够放c后面,重新开辟8个字节
};//所以我们知到sizeof(struct S)为3
struct S s = {0};
s.a = 10;//a的位段只有3个比特位而10转换成十六进制是1010会发生截断,只存储010
s.b = 12;//b的段位有4个比特位,所以存入1100
s.c = 3;//c占5个比特位,存入00011
s.d = 4;//d占4个比特位,存入0110
//综上所述:0110(6) 0010(2)第一个字节 0000(0) 0011(3)第二个字节 0000(0) 0100(4)第三个字节 

在这里插入图片描述

回答上述问题:
空间是如何开辟的?在位段里如果超出比特位将段位截取
在一个字节里8bit里面有没有大小端?没有!!!
在一个字节里面,那些比特位属于高比特位吗?不能确定!(但90%以上是从右向左的)

1.7.3位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机
    器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是
    舍弃剩余的位还是利用,这是不确定的。
    总结:
    跟结构体相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

1.7.4位段的应用

位段通常用在网络协议定制时使用(类比日常生活中寄快递,写信这一类协议。),比如下图:
在这里插入图片描述
从图中我们可以得到下列信息

struct IP{
  unsigned int version : 4;
  unsigned int head_len : 4;
  unsigned int tos :8;
  unsigned int total_len : 16;
  ...
};

定义位段,就是定义一个新的类型,因此我们可以定义IP

int main(){
  struct IP ip={4,60,0,80};
}

二、枚举

枚举顾名思义就是一一列举。
把可能的取值一一列举。

比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这里就可以使用枚举了。

枚举的本质:一堆常量的集合
枚举相比于宏,这些常量之间多少有一些联系

1.类型定义

enum dir{
  UP,
  DOWN,
  LEFT,
  RIGHT,
  LEFT_UP
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
}enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

以上定义的 enum dir, enum Sex , enum Color 都是枚举类型。 { }中的内容是枚举类型的可能取值,也叫 枚举常量 。这些常量是不可被赋值的。

定义一个枚举类型变量

enum dir d;

初始化d,理论上是只能使用{ }内的常量

enum dir d=UP;
printf("%d\n",sizeof(d));

sizeof(d)输出结果为4,所以说枚举本质其实就是整数,且里面的常量也是整数

因此以下写法也能够编译通过

enum dir d=7;

执行下列代码:

enum dir{
  UP,
  DOWN,
  LEFT,
  RIGHT,
  LEFT_UP
};
int main(){
 printf("%d\n",UP);
 printf("%d\n",DOWN);
 printf("%d\n",LEFT);
 printf("%d\n",RIGHT);
 return 0;
}

得到以下结果
在这里插入图片描述

枚举常量是默认从0开始,依次递增加1连续的整数。
使用枚举本质是使用一堆常量。

【为什么不直接使用整数?因为这些枚举常量具有自描述性,可以做到见名知意,是一种良好的编程方案。但不推荐如此使用,因为主要是给用户和程序员看的】

这些可能取值都是有值的,当然在定义的时候也可以赋初值,但他依旧连续增1。 例如

enum dir{
	UP = -1,
	DOWN,
	LEFT,
	RIGHT = 10,
	LEFT_UP,
	RIGHT_DOWN
};
int main()
{
	printf("%d\n", UP);
	printf("%d\n", DOWN);
	printf("%d\n", LEFT);
	printf("%d\n", RIGHT);
	printf("%d\n", LEFT_UP);
	printf("%d\n", RIGHT_DOWN);
 return 0;
}

在这里插入图片描述
所以说枚举可以划分区间,可以设定初始值

2.枚举的优点

为什么使用枚举?
我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。(宏一般用于类型替换,不会进行类型检查,枚举更严谨一些)
  3. 防止了命名污染(封装)(就是将枚举命名出来的放在花括号里,不会在其他地方乱出现)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

4.枚举的使用

enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr=5;  //ok?当然可以

三、联合(共用体)

1.定义

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

//联合类型的声明
union Un
{
 char c;
 int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));

union Un是一个全新的类型,可以用来定义变量。所以所有内部成员共享同一份空间,当某段时间c用时是c的,当某段时间i用时是i的(类比图书馆坐座位,某段时间是你的某段时间是别人的)

那么c和i类型不一致,该如何开辟空间?
1.不会多开辟空间,开辟空间大小以最大元素所需空间为准。所以现在开辟空间大小应该是4。
2.意义:通过联合体可以快速对某段空间进行特定划分
在这里插入图片描述

既然c和i是共享空间的,那么c和i哪一个可以被称为un的第一个元素呢?
答:联合体内的任意一个元素都可以称为联合体内的第一个元素,都是从起始地址放射状向上读取对应类型字节个数(c语言中任意元素的地址都是其开辟地址中地址值最小的那一个)

printf("%p\n",&un);
printf("%p\n",&(un.c));
printf("%p\n",&(un.i));

在这里插入图片描述

2.特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。

面试题:
判断当前计算机的大小端存储

union Un
{
	char c;
	int i;
};
int main()
{
//un.c是1则是小端反知un.c是大端
	union Un un;
	un.i = 1;
	if(un.c == 1){
		printf("小端\n");
	}
	else{
		printf("大端\n");
	}
	return 0;
}	

在这里插入图片描述
在这里插入图片描述
利用联合将数据拆分

union un{
	unsigned int x;
	unsigned char a[4];
};
int main()
{
	union un o;
	o.x = 0xaabbccdd;//dd处于低权值位
	for (int i = 0; i < 4; i++){
		printf("0x%x\n", o.a[i]);
	}
}

在这里插入图片描述
可以应用到存储IP地址及转换:
IP地址比如192.168.3.254
每个数字范围在0 ~ 255,也就是0.0.0.0~255.255.255.255
所以存储一个IP地址分成0000 0000 . 0000 0000. 0000 0000. 0000 0000
可以将一个IP地址快速转成上述字符,反过来也可以

位段比较适合通信

2.联合大小的计算

联合需不需要内存对齐?

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

因此,联合需要内存对齐

union un {
  int a;//对齐数是4
  char b[9];//对齐数是1
};//本来开辟9个字节,但是最大对齐数是4,能够整除4的最小整数倍数是12
int main(){
  printf("%d\n",sizeof(union on));
}

联合体的总大小也要对齐,联合体的最终总大小必须能够整除联合体内的最大对齐数
在这里插入图片描述

练习

通讯录程序


总结

以上为个人学习总结,如有问题欢迎大家讨论交流~

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值