自定义类型的介绍

本文详细介绍了C语言中的自定义类型,包括结构体的声明、自引用、变量定义与初始化、内存对齐规则,枚举的定义、优点和使用,以及联合的声明和特点。此外,还讨论了结构体在内存中的存储、位段的使用以及内存对齐的重要性。文章提供了示例代码帮助理解这些概念。
摘要由CSDN通过智能技术生成

自定义类型

本篇重点:
熟练掌握自定义类型

1.结构体

1.1结构体类型的声明

1.11结构体的基础知识

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

1.12结构体的声明
struct tag//结构体标签
{
 member-list;//成员列表
}variable-list;//变量列表  注意分号是不可缺的
struct stu
{
	char name[20];
	char sex[5];
	int age;
}s1,s2;
struct stu s3;//s1和s2和s3是全局结构体变量
int main()
{
	struct stu s4;//s4是局部结构体变量
	return 0;
}

如果觉得结构体的类型太麻烦,我们可以用typedef关键字,这个关键字是给类型重命名的。

typedef struct stu
{
	char name[20];
	char sex[5];
	int age;
}stu;//将结构体类型重命名为stu
//typedef struct stu Stu;//这种也行
stu s1;//创建全局结构体变量s1
int main()
{
	stu s2;//创建局部结构体变量s2
	return 0;
}
1.13特殊的声明

在声明结构的时候,可以不完全的声明(省去结构体标签)

struct
{
	char name[20];
	char sex[5];
	int age;
}s1,s2;//可以不写标签名,但一定要在创建完结构体后创建变量,后果是只能用一次

匿名结构体也可以用typedef关键字

typedef struct
{
	char name[20];
	char sex[5];
	int age;
}stu;
stu s1;
int main()
{
	stu s2;
	return 0;
}

那么问题来了,下面这段代码正确吗?

struct
{
	int a;
	char b;
}x;
struct
{
	int a;
	char b;
}*p;
int main()
{
	p = &x;
	return 0;
}

在这里插入图片描述

1.2结构体的自引用

这个知识会在数据结构中经常用到。自引用就是结构体内,存放能找到下一个相同的结构体的数据的地址

struct stu
{
	int data;
	struct stu* nextnode;//存放下一个节点的地址
};

我们也可进行重命名

typedef struct node
{
	int data;
	struct node* nextnode;
}Node;

能不能这样写?

typedef struct node
{
	int data;
	Node* nextnode;
}Node;

这样写是不行的,typedef关键字对类型重命名后,才能使用命名后的类型,这里的类型都没有创建好是不能使用Node的

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

struct stu
{
	char name[20];
	char sex[5];
	int age;
}s;//定义全局变量s
stu s1;//定义全局结构体变量s1
int main()
{
	stu s2;//定义局部结构体变量s2
	return 0;
}

那么如何进行初始化呢?

struct point
{
	int x;
	int y;
}p={10,20};
struct point p1 = { 3,4 };
int main()
{
	struct point p3 = { 1,2 };
	printf("%d %d\n", p.x, p.y);
	return 0;
}
struct point
{
	int x;
	int y;
};
struct s
{
	char a;
	struct point p;
	float c;
};
int main()
{
	struct s s1 = {'q',{3,4},3.14};//顺序进行初始化
	struct s s2 = { .p.x = 1,.p.y = 2,.c=3.14,.a = 'w' };//乱序进行初始化
	printf("%c %d %d %f\n", s1.a, s1.p.x, s1.p.y, s1.c);
	printf("%c %d %d %f\n", s2.a, s2.p.x, s2.p.y, s2.c);

	return 0;
}

1.4结构体内存对齐

如何计算一个结构体的大小

struct s1
{
	char a;
	char b;
	int i;
};
struct s2
{
	char a;
	int i;
	char b;
};
int main()
{
	printf("%d\n", sizeof(struct s1));
	printf("%d\n", sizeof(struct s2));
	return 0;
}

输出结果:
8
12

为什么会是这个结果呢?

这就涉及到了结构体在内存中的存储了

结构体的内存对齐规则:
1.结构体的第一个成员,对齐到结构体在内存中存放位置的0偏移处
2.从第二个成员开始,每一个成员都要对齐到(一个对齐数)的整数倍处
对齐数:结构体自身大小和默认对齐数的较小值
vs中的默认对齐数是8
Linux gcc没有默认对齐数,对齐数就是结构体成员变量的自身大小
3.结构体的大小必须是所有成员对齐数中最大的对齐数的整数倍
4.数组的对齐数为其单个元素的大小
5.如果结构体中嵌套了其他结构体成员,要将嵌套的结构体对齐到自己的成员中最大对齐数的整数倍处,结构体的总大小必须是最大对齐数的整数倍这里的最大对齐数包括嵌套结构体的最大对齐数。

偏移量:相对于其在内存中存储位置的偏移大小
如果不相信是否对齐好,可以用offsetof函数,需包含头文件#include<stddef.h>

#include<stddef.h>
struct stu
{
	int i;
	char a;
	double p;
};
int main()
{
	printf("%d\n",offsetof(struct stu, i));//参数:结构体类型,结构体成员名
	printf("%d\n", offsetof(struct stu, arr));
	printf("%d\n", offsetof(struct stu, p));
	return 0;
}

看几个题目:

struct s1
{
	char a;//对齐0偏移处
	char b;//自身大小:1 默认对齐数:8  对齐数:1
	int i;//自身大小: 4 默认对齐数:8  对齐数:4
};

在这里插入图片描述

struct s2
{
	char a;//对齐0偏移处
	int i;//自身大小:4 默认对齐数:8 对齐数:4
	char b;//自身大小:1 默认对齐数:8 对齐数:1
};

在这里插入图片描述

struct S3
{
 double d;//对齐到0偏移处
 char c;//自身大小:1 默认对齐数:8 对齐数:1
 int i;//自身大小:4 默认对齐数:8 对齐数:4
};

在这里插入图片描述

struct S3
{
 double d;//自身大小:8 默认对齐数:8 对齐数:8
 char c;//自身大小:1 默认对齐数:8 对齐数:1
 int i;//自身大小:4 默认对齐数:8 对齐数:4
};
struct S4
{
 char c1;//对齐到0偏移处
 struct S3 s3;//最大对齐数:8
 double d;//自身大小:8 默认对齐数:8 对齐数:8
};

过程:0 8 ~ 23 24 ~ 31所以总共是32,而32恰好也是最大对齐数的整数倍

struct stu
{
	int i;
	char arr[10];
	double p;
};

过程:0 ~ 3 4 ~ 13 16 ~ 23所以结构体大小为24,注意数组的对齐数为其一个元素的对齐数。

为什么要内存对齐呢?
资料说法:

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

总体来讲:
结构体的内存对齐是拿空间来换取时间的做法,我们写程序时,尽量将占取空间小的集中在一起

如何修改默认对齐数?

#include <stdio.h>
#pragma pack(4)//设置默认对齐数为4
struct S1
{
 char c1;
 int i;
 char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

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

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); //传地址
 return 0;
}

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

利用结构体模拟静态通讯录:
contact.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
void initcontact(contact* pc)
{
	assert(pc);
	pc->sz = 0;
	memset(pc->data, 0, sizeof(pc->data));
}
void addcontact(contact* pc)
{
	assert(pc);
	if (pc->sz >= 100)
	{
		printf("添加失败\n");
		return;
	}
	printf("请输入姓名:");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入性别:");
	scanf("%s", pc->data[pc->sz].sex);
	printf("请输入地址:");
	scanf("%s", pc->data[pc->sz].addr);
	printf("请输入号码:");
	scanf("%s", pc->data[pc->sz].tele);
	printf("请输入年龄:");
	scanf("%s", &pc->data[pc->sz].age);
	pc->sz++;
	printf("添加成功\n");
}
int findname(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 delecontact(contact* pc)
{
	assert(pc);
	if (pc->sz == 0)
	{
		printf("删除失败\n");
		return;
	}
	char name[20];
	printf("请输入要删除的人的名字\n");
	scanf("%s",name);
	int ret = findname(pc,name);
	if (ret == -1)
	{
		printf("找不到\n");
		return ;
	}

	//将后面的每一位都覆盖掉前面的一个元素
	for(int i=ret;i<pc->sz-1;i++)
	pc->data[i] = pc->data[i + 1];
	pc->sz--;
	printf("删除成功\n");
}

void showcontact(contact* pc)
{
	int i = 0;
	printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名","性别","地址","号码","年龄");
	for (i = 0; i < pc->sz; i++)
	{
		printf("%-10s%-10s%-10s%-10s%-10d\n", pc->data[i].name, pc->data[i].sex, pc->data[i].addr, pc->data[i].tele, pc->data[i].age);
	}
}
void searchcontact(const contact* pc)
{
	assert(pc);
	char name[100] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	int pos = findname(pc, name);
	if (-1 == pos)
	{
		printf("要查找的人不存在\n");
		return;
	}
	//打印信息
	printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名", "性别", "地址", "号码", "年龄");
	printf("%-10s%-10s%-10s%-10s%-10d\n", pc->data[pos].name, pc->data[pos].sex, pc->data[pos].addr, pc->data[pos].tele, pc->data[pos].age);
}
void modifycontact(contact* pc)
{
	assert(pc);
	char name[100] = { 0 };
	printf("请输入要修改人的名字:>");
	scanf("%s", name);
	int pos = findname(pc, name);
	if (-1 == pos)
	{
		printf("要修改的人不存在\n");
		return;
	}
	//也可用switch语句+枚举类型
	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].addr);
	printf("请输入电话:>");
	scanf("%s", pc->data[pos].tele);

	printf("修改完成\n");
}
void sortcontact(contact* pc)
{
	int i = 0;
	for (i = 0; i < pc->sz - 1; i++)
	{
		int j = 0;
		for (j = 0; j < pc->sz - 1 - i; j++)
		{
			if (strcmp(pc->data[j].name, pc->data[j + 1].name) > 0)
			{
				peoinfo tmp = pc->data[j];
				pc->data[j] = pc->data[j + 1];
				pc->data[j + 1] = tmp;
			}
		}
	}
}

contact.h

#pragma once
#include<stdio.h>
#include<string.h>
#include<assert.h>
typedef struct peoinfo
{
	char name[20];//可将这里的数字改为标识符常量
	char sex[5];
	char addr[20];
	char tele[12];
	int age;
}peoinfo;
typedef struct contact
{
	peoinfo data[100];//存放人的信息的
	int sz;///用来记录已存放人的个数
}contact;
void initcontact(contact* pc);
void addcontact(contact* pc);
void delecontact(contact* pc);
void showcontact(contact* pc);
void searchcontact(const contact* pc);
void modifycontact(contact* pc);
void sortcontact(contact* pc);

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
void menu()
{
	printf("***********************\n");
	printf("******1.add   *********\n");
	printf("******2.dele  *********\n");
	printf("******3.search*********\n");
	printf("******4.modify*********\n");
	printf("******5.show  *********\n");
	printf("******6.sort  *********\n");
	printf("******0.exit  *********\n");
	printf("***********************\n");
}
void test()
{
	int input = 0;
	//创建通讯录
	contact con;
	//初始化通讯录
	initcontact(&con);
	do
	{
		menu();
		printf("请选择功能:");
		scanf("%d", &input);
		switch (input)//这里的数字也可用枚举类型
		{
		case 1:
			//录入
			addcontact(&con);
			break;
		case 2:
			//删除指定人的信息
			delecontact(&con);
			break;
		case 3:
			//查找指定人信息
			searchcontact(&con);
			break;
		case 4:
			//修改指定人信息
			modifycontact(&con);
			break;
		case 5:
			//展示信息
			showcontact(&con);
			break;
		case 6:
			//按姓名进行排序
			sortcontact(&con);
			break;
		case 0:
			printf("退出通讯录\n");
			break;
		default :
			printf("输入错误,请重新选择\n");
			break;
		}
	} while (input);

}
int main()
{
	test();
	return 0;
}

1.6结构体实现位段

什么是位段?位段的位是二进制位(内存不对齐)
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字(不能超过本身所占字节大小)。

#include<stdio.h>
struct A
{
	int _a : 2;//2个二进制位
	int _b : 5;//5个二进制位
	int _c : 10;//10个二进制位
	int _d : 30;//30个二进制位
};
//有的数据不需要32位即4个字节就能表达它的内容,就是它不需要太多空间就能表达它的意思
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 S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct S s = { 0 };
	s.a = 10;//二进制1010,但是只存3位即010
	s.b = 12;//二进制1100
	s.c = 3;//二进制00011
	s.d = 4;//二进制0100
	printf("%d", sizeof(s));//在vs上是3byte
	return 0;
}

分析:
在这里插入图片描述

在这里插入图片描述
位段跨平台问题:

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

位段的应用:
在这里插入图片描述
在网络中,有的数据只需要4位啊,8位等,如果直接用int会浪费很多空间,导致传输速率慢。

2.枚举

枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:一周的星期一到星期日是有限的7天,可以一一列举。

2.1枚举的定义

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

enum Day//星期
{
 Mon,//默认为0
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Sex//性别
{
 MALE,//默认为0
 FEMALE,
 SECRET
}enum Color//颜色
{
 RED,//默认为0
 GREEN,
 BLUE
};
enum Color//颜色,也可赋初始值
{
 RED=1,
 GREEN=2,
 BLUE=4
};
//以上都是可以typede的
enum color
{
	red,
	green=2,
	blue//下面的数据就会自增1
};
int main()
{
	enum color a = red;
	printf("%d\n", a);
	printf("%d\n", green);
	printf("%d\n", blue);
	return 0;
}

在这里插入图片描述

2.2枚举的优点

枚举的优点:

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

2.3枚举的使用

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

3.联合(共用体)

3.1联合类型的定义

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

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

3.2联合的特点

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

union Un
{
 int i;
 char c;
};
union Un un;
//sizeof(un)=4
// 下面输出的结果是一样
printf("%d\n",&un);
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
union Un
{
	int i;
	char c;
};
union Un un;
int main()
{
	un.i = 0x11223344;
	printf("%x\n", un.i);//11223344
	printf("%x\n", un.c);//44
	//所以一般情况下不能同时使用
	return 0;
}

在这里插入图片描述

3.3联合大小的计算

条件:

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

union Un1
{
 char c[5];//大小5,对齐数1
 int i;//大小4,对齐数4
 //联合体的大小至少是5,最大对齐数是4,那么8就最大对齐数的整数倍,联合体大小就是8byte
};
union Un2
{
 short c[7];//大小是14 对齐数2
 int i;//大小是4 对齐数是4
 //联合体大小至少是14,最大对齐数为16,所以联合体为16byte
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16

判断大小端

```c
int main()
{
	int a = 1;//01 00 00 00小端
	if (*(char*)&a == 1)
	{
		printf("小端\n");
	}
	else
	{
		printf("大端\n");
	}

	return 0;
}

也可用联合

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

在这里插入图片描述

总结:

以上就是篇的所有内容了,感谢阅读,如果喜欢本篇不妨点个赞,我们下期见,拜!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值