【C语言】自定义类型详解(结构体+枚举+联合)

作者简介:大家好我是狂暴于涛侠本侠
🦸个人主页:狂暴于涛侠
这一次给大家带来c语言关于结构体的讲解

⛽1. 结构体

🌊1. 1结构体类型的声明

🚢结构体的声明

结构的基础知识:
数组是一组同类型元素的集合。
结构体也是一些值的结合,结构的每个成员可以是不同类型。

//结构体写法
struct Book
{
	char name[20];
	int price;
	char id[12];
}b4,b5,b6;//b4,b5,b6是全局的 不放直接加;号也是可以的

int main()
{
	//b1,b2,b3是局部变量
	struct Book b1;
	struct Book b2;
	struct Book b3;
	return 0;
}

🚢特殊的声明

匿名结构体只能用开始创建的变量,因为接下来无法创建了

//匿名结构体类型  下面的两个结构在声明的时候省略掉了结构体标签。
struct            //省略结构体标签。
{
	char c;
	int i;
	char ch;
	double d;
} s;

struct          //省略结构体标签。
{
	char c;
	int i;
	char ch;
	double d;
}* ps;  //意思是创建了一个匿名结构体的指针,用这个指针创建了一个ps变量  *是跟着结构体的

int main()
{
	ps = &s;//err //这样写法报错,虽然说两个结构体大小都一样,成员也都一样
	              //但是对于编译器来说这就是两个不同的结构体
	              //会把它当做两个完全不同的类型
	return 0;
}

🌊1.2结构的自引用

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

struct A
{
	int i;
	char c;
};

struct B
{
	char c;
	struct A sa;
	double d;
};   //完全可以



struct N
{
	int d;
	struct N n; 
	//这种可以么?
	//很显然是不可以的这种循环无限
};
//那是不是调用自己不可以了?
//并不是,结构体引用自己变量不可以但是引用自己的指针确可以
struct M
{
	int d;
	struct M* m; 
};
struct //这样写法同样错 不知道结构体标签 struct M* m; 怎么执行
{
	int d;
	struct M* m; 
};

//那这种呢?
typedef struct //我直接重命名结构体为Node行么?  
               //不行,程序是一步一步进行的,当进行到	struct Node* next;
               //并不知道Node是什么,根本没有到命名完成的步骤
{
	int data;
	 Node* next;
}Node;       //命名完成

可以这么写
typedef struct Node  
{
	int data;
	struct Node* next;
}Node;


这是一种链表数据结构,每个结构体分两个内存其中一个位数据域,另一个为指针域
数据域储存着数据,指针域存着下一个位置的指针

在这里插入图片描述


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

#include<stdio.h>
struct S
{
	char c;
	int i;
}s1, s2;

struct B
{
	double d;
	struct S s;
	char c;
};
int main()
{
	struct S s3 = {'x', 20};
	struct S s4 = {'a', 10} ,s5 = { 'v', 15 } ;

	struct B sb = { 3.14, {'w', 100},'q' };
	//. 
	//->
	printf("%lf %c %d %c\n", sb.d, sb.s.c, sb.s.i, sb.c);

	return 0;
}

🌊1.4结构体内存对齐

struct S
{
	int i;//4
	char c;//1
};

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

//结构体内存对齐

int main()
{
	struct S s = {0};
	printf("%d\n", sizeof(s));//8
//为什么是8和12
	struct S2 s2 = { 0 };
	printf("%d\n", sizeof(s2));//12

	return 0;
}

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

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

在这里插入图片描述
在这里插入图片描述

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

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

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。

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

🚢修改默认对齐数

#pragma pack(2)//以下以2为默认对其数
struct S
{
	char c1;//0
	int i;//
	char c2;//
};
#pragma pack()//以下默认对齐数恢复正常

#pragma pack(1)//以下以1为默认对其数
struct A
{
	char c1;//1 1 1
	int i;//4 1 1
	char c2;//1 1 1
};
#pragma pack()//以下默认对齐数恢复正常

int main()
{
	printf("%d\n", sizeof(struct S));//8
	printf("%d\n", sizeof(struct A));//6
	return 0;
}

在这里插入图片描述
结论:

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

🚢小练习

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

在这里插入图片描述

//关于offsetof
#include <stddef.h>//需要引头文件
#include <stdio.h>
struct S
{
	char c1;
	int i;
	char c2;
};

int main()
{   // 返回的偏移量
	printf("%d\n", offsetof(struct S, c1));//0
	printf("%d\n", offsetof(struct S, i));//4
	printf("%d\n", offsetof(struct S, c2));//8

	return 0;
}

🌊1.5结构体传参

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); //传地址  传递地址就4个字节,效率更快而且可以修改原变量
 return 0;
}

结论:使用址传递


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

🚢什么是位段

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

比如:

//性别 - 3
//00 男
//01 女
//10 保密
//11
//
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

int main()
{
	printf("%d\n", sizeof(struct A));//输出8  为什么呢?
	return 0;
}

🚢位段的内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct A
{
	//因为是int所以开始开辟4个字节 - 32bit
	int _a : 2;  a只用了2个bit                                        //_a 成员占2个bit位
	int _b : 5;  b只用了5个bit                                        //_b 成员占5个bit位
	int _c : 10; c只用了10个bit                                         //_c 成员占10个bit位
	//还剩与15bit 但是d一次就要30个位置 
	//直接新建4个字节 - 32bit    不用上面的剩余的15个字节因为是vs编译器,别的编译器不一定
	int _d : 30;                                          //_d 成员占30个bit位
	//int _e : 34;这种写法不允许因为int最大32个字节
};
//16位 - int - 2byte - 16 bit
//32位 - int - 4byte - 32 bit

int main()
{
	printf("%d\n", sizeof(struct A));//8    4个字节32个bit位

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

在这里插入图片描述

🚢位段的跨平台问题

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

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

🚢位段的应用

在这里插入图片描述
压缩网络发送信息,减少字节,加快网速


⛽2.枚举

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

比如我们现实生活中:
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举

🌊2.1枚举类型的定义

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,
 FEMALE,
 SECRET
}enum Color//颜色
{
 RED,
 GREEN,
 BLUE
};

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。
{}中的内容是枚举类型的可能取值,也叫 枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。
例如:

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

🌊2.2枚举的优点

我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
1. 增加代码的可读性和可维护性
2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
3. 防止了命名污染(封装)
4. 便于调试
5. 使用方便,一次可以定义多个常量


🌊2.3枚举的使用

//枚举类型就是一种类型  枚举变量
//整形就是整形


void menu()
{
	printf("*****************************\n");
	printf("****  1. add    2. sub  *****\n");
	printf("****  3. mul    4. div  *****\n");
	printf("****  0. exit          *****\n");
	printf("*****************************\n");
}

enum Option
{
	EXIT,//0
	ADD,//1
	SUB,//2
	MUL,//3
	DIV//4
};

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			break;
		case SUB:
			break;
		case MUL:
			break;
		case DIV:
			break;
		case EXIT:
			break;
		default:
			break;
		}
	} while (input);


	return 0;
}

⛽3.联合体(共用体)

🌊3.1联合类型的定义

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


🌊3.2联合的特点

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

🚢联合小练习

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

//方法一
int check_sys()
{
	int a = 1;
	if ((*(char*)&a) == 1)
	{
		return 1;//小端
	}
	else
	{
		return 0;//大端
	}
}
//方法二
int check_sys()
{
	union U
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
	//返回1 就是小端
	//返回0 就是大端
}

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}

在这里插入图片描述


🌊3.3联合大小的计算

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

union Un
{
	char a[5];//1    5
	int i;//4     4的整数倍比5大取8
};
union Un1
{
	short s[5];// 2 10
	int a;//4     4的整数倍比10大取12
};

int main()
{
	union Un u;
	union Un1 u1;
	printf("%d\n", sizeof(u));//8
	printf("%d\n", sizeof(u1));//12

	return 0;
}

⛽4.练习实现通讯录

创建三个文件
test.c文件


#define _CRT_SECURE_NO_WARNINGS 1


#include "contact.h"

void menu()
{
	printf("********************************\n");
	printf("******  1. add    2. del  ******\n");
	printf("******  3. search 4. modify*****\n");
	printf("******  5. sort   6. print *****\n");
	printf("******  0. exit            *****\n");
	printf("********************************\n");
}

enum Option
{
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT
};

int main()
{
	int input = 0;
	//创建通讯录
	Contact con;//通讯录
	//初始化通讯录
	//给data申请一块连续的空间再堆区上
	//sz=0
	//capacity 初始化为当前的最大的容量
	InitContact(&con);

	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			//增加人的信息
			AddContact(&con);
			break;
		case DEL:
			//删除
			DelContact(&con);
			break;
		case SEARCH:
			SearchContact(&con);
			break;
		case MODIFY:
			ModifyContact(&con);
			break;
		case SORT:
			//自己完善
			break;
		case PRINT:
			PrintContact(&con);
			break;
		case EXIT:		
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);

	return 0;
}


contact.h文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_NAME 20
#define MAX_SEX 10
#define MAX_TELE 12
#define MAX_ADDR 30


#define MAX 1000

//类型的定义
typedef struct PeoInfo
{
	char name[MAX_NAME];
	char sex[MAX_SEX];
	int age;
	char tele[MAX_TELE];
	char addr[MAX_ADDR];
}PeoInfo;

//通讯录-静态版本
typedef struct Contact
{
	PeoInfo data[MAX];//存放添加进来的人的信息
	int sz;//记录的是当前通讯录中有效信息的个数  默认从0开始
}Contact;


//初始化通讯录
void InitContact(Contact* pc);


//增加联系人
void AddContact(Contact* pc);
//打印联系人信息
void PrintContact(const Contact* pc);

//删除联系人的信息
void DelContact(Contact* pc);

//查找指定联系人
void SearchContact(Contact* pc);

//修改指定联系人
void ModifyContact(Contact* pc);

contact.c文件

#include "contact.h"
#define _CRT_SECURE_NO_WARNINGS 1
//静态版本的
void InitContact(Contact* pc)
{
	pc->sz = 0;
	//memset();  - 内存设置
	memset(pc->data, 0, sizeof(pc->data)); //pc->data 是数组名计算的是整个数组
}



// 静态版本的-增加联系人
void AddContact(Contact* pc)
{
	if (pc->sz == MAX)
	{
		printf("通讯录已满,无法添加\n");
		return;
	}
	//增加一个人的信息
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->sz].name); //将pc放到data数组下标为sz的位置处.name处
	printf("请输入年龄:>");
	scanf("%d", &(pc->data[pc->sz].age));//除了年龄都是数组,数组就是首元素地址!所以除了年龄不用取地址 
	printf("请输入性别:>");              //age是变量 创建的时候没有让age是数组
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入电话:>");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入地址:>");
	scanf("%s", pc->data[pc->sz].addr);

	pc->sz++;
	printf("增加成功\n");
}


void PrintContact(const Contact* pc)
{
	int i = 0;
	//打印标题  %s-20\t -代表左对齐  20代表20个字符位置\t不足用空格代替
	printf("%-20s\t%-5s\t%-5s\t%-12s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
	//打印数据
	for (i = 0; i < pc->sz; i++)
	{
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-20s\n",
			pc->data[i].name,
			pc->data[i].age,
			pc->data[i].sex,
			pc->data[i].tele,
			pc->data[i].addr);
	}
}

static int FindByName(Contact* pc, char name[])
{
	int i = 0;
	for (i = 0; i < pc->sz; i++)
	{
		if (strcmp(pc->data[i].name, name) == 0)
		{
			return i;
		}
	}

	return -1;//找不到
}


void DelContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };

	if (pc->sz == 0)
	{
		printf("通讯录为空,无需删除\n");
		return;
	}
	printf("请输入要删除人的名字:>");
	scanf("%s", name);

	//1. 查找要删除的人
	//有/没有
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
	//2. 删除
	int i = 0;
	for (i = pos; i < pc->sz - 1; i++)
	{
		pc->data[i] = pc->data[i + 1];
	}

	pc->sz--;
	printf("删除成功\n");
}


void SearchContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);

	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	else
	{
		printf("%-20s\t%-5s\t%-5s\t%-12s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
		//打印数据
		printf("%-20s\t%-5d\t%-5s\t%-12s\t%-20s\n",
			pc->data[pos].name,
			pc->data[pos].age,
			pc->data[pos].sex,
			pc->data[pos].tele,
			pc->data[pos].addr);
	}
}


void ModifyContact(Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	printf("请输入要修改人的名字:>");
	scanf("%s", name);

	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要修改的人不存在\n");
		return;
	}
	else
	{
		printf("请输入名字:>");
		scanf("%s", pc->data[pos].name);
		printf("请输入年龄:>");
		scanf("%d", &(pc->data[pos].age));
		printf("请输入性别:>");
		scanf("%s", pc->data[pos].sex);
		printf("请输入电话:>");
		scanf("%s", pc->data[pos].tele);
		printf("请输入地址:>");
		scanf("%s", pc->data[pos].addr);

		printf("修改成功\n");
	}
}

测试题:

你能算出下面代码的运行结果么?

#define A 2
#define B 3

#define MAX_SIZE A+B
struct _Record_Struct
{
	unsigned char Env_Alarm_ID : 4;
	unsigned char Para1 : 2;
	unsigned char state;
	unsigned char avail : 1;
}*Env_Alarm_Record;
int main()
{
	int sz = (sizeof(struct _Record_Struct) * MAX_SIZE);
	printf("%d\n", sz);

	return  0;
}

参考:


struct _Record_Struct
{
	unsigned char Env_Alarm_ID : 4;//1 - 8bit  用了4个bit位
	unsigned char Para1 : 2;//用了剩余4个中的两个bit位
	unsigned char state;//1 开辟了一个新的字节
	unsigned char avail : 1;//1 再次开辟新字节8个bit用了1个
}*Env_Alarm_Record;
代码运行结果为9

你能算出下面代码的运行结果么?

int main()
{
    unsigned char puc[4];
    struct tagPIM
    {
        unsigned char ucPim1;
        unsigned char ucData0 : 1;
        unsigned char ucData1 : 2;
        unsigned char ucData2 : 3;
    }*pstPimData;
    pstPimData = (struct tagPIM*)puc;
    memset(puc, 0, 4);
    pstPimData->ucPim1 = 2;
    pstPimData->ucData0 = 3;
    pstPimData->ucData1 = 4;
    pstPimData->ucData2 = 5;
    printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]);
    return 0;
}

解释:
在这里插入图片描述


你能算出下面代码的运行结果么?(小端存储)

int main()
{
    union
    {
        short k;
        char i[2];
    }*s, a;
    s = &a;
    s->i[0] = 0x39;
    s->i[1] = 0x38;
    printf("%x\n", a.k);

    return 0;
}

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狂暴于涛侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值