C语言-自定义类型

目录

前言

一、结构体

1.结构体类型的声明

2.结构体的自引用

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

4.结构体成员的访问

5.结构体的内存对齐

6.结构体传参

二、位段

1.什么是位段

2.位段的内存分配

3.位段跨平台存在的问题

4.位段的应用

三、枚举

1.枚举类型的定义

2.枚举的优点

四、C语言实现通讯录

五、联合(共用体)

1.联合类型的定义

2. 联合类型的特点

3.联合体大小的计算

总结 



前言

        在实际生活中,我们解决问题的对象往往不是单个类型的变量,例如描述一个学生,我们需要记录性别、年级、年龄、性别、学号...等等,这时候我们就需要自己定义需要用到的结构类型了,C语言将它们称为自定义类型(例如结构体、联合、枚举,我们已经学过的数组也属于自定义类型)。本文将会讲解自定义类型的相关知识。


一、结构体

1.结构体类型的声明

        结构体是一些数据的集合,这些数据称为成员变量,每个成员的类型可以相同也可以不同,也可以是基本数据类型或者又是一个构造类型。

(typedef )struct tag    //结构体名
{
    member_list;    //成员列表
}variable_list;    //结构体变量名

struct -- 创建结构体必要的关键字

下面我们用结构体描述一个学生:

typedef struct Stu
{
    char name[20];//姓名
    int age;//年龄
    char sex[5];//性别
    char id[20];//学号
}Stu;    //Stu1, Stu2;也可以创建多个相同类型的结构体

结构体的成员可以是标量、数组、指针……,甚至其他结构体;

下面这三种声明方法等效:

//
typedef struct Stu
{
    char name[20];
    int age;
    char sex[5];
    char id[20];
};

typedef struct Stu1;
typedef struct Stu2;

//
typedef struct Stu
{
    char name[20];
    int age;
    char sex[5];
    char id[20];
}Stu1, Stu2;

//更推荐上面两种声明方法
↓↓↓匿名结构体类型
typedef struct
{
    char name[20];
    int age;
    char sex[5];
    char id[20];
}Stu1, Stu2;

特殊的声明--在声明结构体的时候,可以不完全的声明

//结构体成员是其它结构体类型

struct A
{
	int i;
	char ch;
}A1;

struct B
{
	float f;
	struct A s;
}B1;

int main()
{
	A1 = { 97, 'a' };
	B1 = { 3.14, {98, 'b'} };
	return 0;
}

//匿名结构体类型

struct
{
	int i;
	char ch;
	float f;
}s1;

struct
{
	int i;
	char ch;
	float f;
}*ps;

typedef struct
{
	int i;
	char ch;
	float f;
}S;
//3.此处的S与上面的s1意义相同吗
int main()
{
	s1 = { 0, 'i', 1.00 };
    //1.是否可以再创建一个同样的匿名结构体类型的变量s2?
	struct s2 = { 1, 'a', 3.14 };//这条语句是错误的
    //2.是否可以进行如下操作
    ps = &s1;
    
	return 0;
}

1.不可以,匿名结构体连名字都没有,我们只能按照如下方式添加匿名结构体类型的变量
struct
{
	int i;
	char ch;
	float f;
}s1, s2;

2.不可以,编译器会报错:从“*”到“*”的类型不兼容
看来编译器根本没有把 s1 和 ps 看作同种类型的变量

3.不相同,s1表示的是变量名,而S则表示一种类型

我们可以粗略地理解为:匿名结构体是一次性用品

2.结构体的自引用

        如果有的同学已经在学校上过数据结构这门课了,一定已经对链表不陌生了

//1.这样可以吗?
struct Node
{
	int data;
	struct Node next;
};

//2.这样可以吗?
typedef struct 
{
	int data;
	Node* next;
}Node;

//3.这样可以吗?
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

//4.这样可以吗?
struct Node
{
	int data;
	struct Node* next;
};

//上述代码中只有3、4可以达到预期

int main()
{
	struct Node n1, n2;
	n1.next = &n2;
	return 0;
}

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

示例代码

struct Stu
{
	char name[20];
	int age;
	long id;
};

struct Stu s = { "zhangsan", 20 ,20230101};

————————————————————————————————————————————

struct Axis		//坐标
{
	int x;
	int y;
}a1;
struct Axis a2;
struct Axis a3 = { 2, 3 };

struct Node
{
	int data;
	struct Axis a4;
	struct Node* next;
}n1 = { 20,{4,5},NULL };

4.结构体成员的访问

' · ' 结构体变量访问成员:

结构体变量名.成员变量名

' -> ' 结构体指针访问指向变量的成员:

结构体指针变量->成员变量名

5.结构体的内存对齐

        我们在使用结构体的时候经常会考虑一个问题:结构体的大小

 

        结构体在内存中存储时遵循以下规则:

1.结构体的第一个成员永远放在0偏移处

2.从第二个成员开始,以后的每个成员都要对齐到某个对齐数的整数倍,,这个对齐数是成员自身大小和默认对齐数的较小值(备注:博主的演示环境为VS2022,该环境下默认对齐数为8,gcc环境下没有默认对齐数,对齐数就是成员自身的大小)

3.当所有结构体成员全部存放进去后,结构体的总大小必须是所有成员对齐数中最大对齐数的整数倍,如果不满足条件,则浪费内存对齐

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

        下图中每个格子表示一个字节的内存大小,其中橙色表示int类型占用的内存空间,蓝色表示char类型占用的内存空间,绿色部分表示为了对齐而浪费的空间(注意,虽然这部分的内存是因为char类型浪费的,但是它无权访问这部分的内存空间)

        看了这么多,可能有点同学会问,为什么存在内存对齐呢?

1.平台原因(便于移植):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常

2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因是为了访问未对齐的内存空间,编译器需要作两次内存访问,而访问对齐的内存空间,仅需作一次内存访问

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

  在对内存对齐有了一定的了解之后,我们可以在满足对齐条件的情况下,尽量地减少浪费的空间:S1与S2的成员相同,但是开辟的内存空间却不同!!!

        其实我们根据需求还可以修改默认对齐数

6.结构体传参

哪种传参方式更好?<Print1/Print2>

struct Book
{
	long no;
	char name[20];
	float price;
}book = {100111,"C program", 37.5};

void Print1(struct Book b)
{
	printf("%.2f\n", b.price);
}

void Print2(const struct Book* pb)
{
	printf("%.2f\n", pb->price);
}

int main()
{
	Print1(book);//传值调用
	Print2(&book);//传址调用
	return 0;
}

址传递Print2更好,函数在传参的时候需要开辟一段临时空间存放参数,如果结构体很大,那么就会消耗较多的空间,从而导致性能下降


二、位段

1.什么是位段

        你一定知道什么是段位,那位段又是什么呢?

位段的声明和结构体是类似的,但是也有两个不同的地方

1.位段的成员通常是 unsigned int、int、 signed int等整型家族的成员类型

2.位段的成员名后面有一个冒号和一个数字

        其中位段的位指的是二进制位,A就是一个位段类型,冒号后面的数字表示为前面的变量开辟的内存空间的大小(单位是bit)

2.位段的内存分配

1.位段的成员通常是 unsigned int、int、 signed int、char等整型家族的成员类型

2.位段的空间上是按照需要以4个字节(int)或一个字节(char)的方式来开辟的

3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应避免使用位段

        我们不知道具体的内存分配方式,也不知道是从高地址处开始存放还是从低地址处开始存放的

3.位段跨平台存在的问题

1.int位段被当作有符号数还是无符号数是不确定的

2.位段中的最大位数目不能确定

3.位段中的成员在内存中从左向右分配还是从右向左分配尚未定义

4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段的剩余的位时,是舍弃剩余的空间还是利用剩余的空间,这是不确定的

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

4.位段的应用

 小声:图片网上找的,比较模糊,将就一下吧~


三、枚举

        顾名思义,枚举就是把所有可能的情况一一列举,例如:一周有包括星期一,星期二,星期三,星期四,星期五,星期六,星期天共七天

enum Sex//性别
{
	MALE,
    FEMALE,
	SECRET
};

enum Week//星期
{
	MON,
	TUES,
	WED,
	THUR,
	FRI,
	SAT,
	SUN
};

1.枚举类型的定义

默认第一个枚举常量为0,向后依次递增1,当然也可以自己赋初值

2.枚举的优点

我们可以用#define定义常量,那为什么还需要枚举呢?

1.增加代码的可读性和可维护性

2.枚举有类型检查(只能用枚举常量给枚举变量赋值),更加严谨(而#define没有类型检查)

3.防止命名污染,把枚举常量封装起来

4.便于调试,使用也很方便,可以定义多个常量


四、C语言实现通讯录

contact.h

#pragma once

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

#define MAXSIZE 1000
#define MAX_NAME 20
#define MAX_SEX 5
#define MAX_TELE 15
#define MAX_ADDR 30

typedef struct PeoInfo
{
	char name[MAX_NAME];
	int age;
	char sex[MAX_SEX];
	char tele[MAX_TELE];
	char addr[MAX_ADDR];
}PeoInfo;

typedef struct Contact
{
	PeoInfo data[MAXSIZE];//存放数据
	int sz;//记录通讯录中的有效信息个数
}Contact, * pContact;

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

//增加指定联系人
void AddContact(Contact* pc);

//显示联系人信息
void ShowContact(const Contact* pc);

//删除指定联系人
void DelContact(pContact pc);

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

//修改通讯录
void ModifyContact(Contact* pc);

//排序通讯录元素
void SortContact(Contact* pc);

//清空所有联系人
void CleanContact(Contact* pc);
contact.c

#include "contact.h"

void InitContact(Contact* pc)
{
	pc->sz = 0;
	memset(pc->data, 0, sizeof(pc->data));
}

void AddContact(Contact* pc)
{
	if (pc->sz == MAXSIZE)
	{
		printf("通讯录已满,无法增加\n");
		return;
	}
	printf("请输入名字:>");
	scanf("%s", pc->data[pc->sz].name);
	printf("请输入年龄:>");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("请输入性别:>");
	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 ShowContact(const Contact* pc)
{
	int i = 0;
	//姓名      年龄      性别     电话      地址
	//zhangsan 20        男      123456    北京
	//
	//打印标题
	printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	//打印数据
	for (i = 0; i < pc->sz; i++)
	{
		printf("%-10s %-4d %-5s %-12s %-30s\n",
			pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
}

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

void DelContact(pContact pc)
{
	char name[MAX_NAME] = { 0 };
	if (pc->sz == 0)
	{
		printf("通讯录为空,无法删除\n");
		return;
	}
	//删除
	//1. 找到要删除的人 - 位置(下标)
	printf("输入要删除人的名字:>");
	scanf("%s", name);
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要删除的人不存在\n");
		return;
	}
	int i = 0;
	//2. 删除 - 删除pos位置上的数据
	for (i = pos; i < pc->sz - 1; i++)
	{
		pc->data[i] = pc->data[i + 1];
	}
	pc->sz--;
	printf("删除成功\n");
}

void SearchContact(const Contact* pc)
{
	char name[MAX_NAME] = { 0 };
	printf("请输入要查找人的名字:>");
	scanf("%s", name);
	//查找
	int pos = FindByName(pc, name);
	if (pos == -1)
	{
		printf("要查找的人不存在\n");
		return;
	}
	//打印
	printf("%-10s %-4s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	//打印数据
	printf("%-10s %-4d %-5s %-12s %-30s\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;
	}
	//修改
	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");
}

//按照名字来排序
int cmp_by_name(const void* e1, const void* e2)
{
	return strcmp(((PeoInfo*)e1)->name, ((PeoInfo*)e2)->name);
}

void SortContact(Contact* pc)
{
	qsort(pc->data, pc->sz, sizeof(PeoInfo), cmp_by_name);
	printf("排序成功\n");
}

void CleanContact(Contact* pc)
{
	while (pc->sz--)
	{
		*(pc->data[pc->sz].name) = 0;
		pc->data[pc->sz].age = 0;
		*(pc->data[pc->sz].sex) = 0;
		*(pc->data[pc->sz].tele) = 0;
		*(pc->data[pc->sz].addr) = 0;
	}
}
test.c

#include "contact.h"

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

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

int main()
{
	int input = 0;
	//构造一个通讯录
	Contact con;
	//初始化通讯录
	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 SHOW:
			ShowContact(&con);
			break;
		case SORT:
			SortContact(&con);
			break;
		case EXIT:
			printf("退出通讯录\n");
			break;
		case CLEAN:
			CleanContact(&con);
			//ShowContact(pc);
			break;
		default:
			printf("选择错误\n");
			break;
		}
	} while (input);
	return 0;
}


五、联合(共用体)

1.联合类型的定义

        联合也是一种特殊的自定义类型,需要用到关键字--union,这种类型定义的变量包含一系列的成员,这些成员共用同一块空间(所以联合也叫做共用体)

         

2. 联合类型的特点

        联合体的成员是共用同一块内存空间的,因此联合变量的大小至少是最大成员的大小(因为联合变量至少得有能力保存最大的那个成员),并且同时最后只使用一个联合成员(因为联合成员共用内存,所以修改某个成员会影响到其它成员)

        大小端的判断:

 

3.联合体大小的计算

1.联合变量的大小至少是最大成员的大小

2.当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

 


总结 

        如果我们能够根据具体问题的需求创建合适的自定义类型的变量,就可以让数据操作起来更加方便(例如:数据结构),把自定义类型学好对我们学习数据结构也有一定程度上的帮助。

        感谢阅读,欢迎读者提出自己的建议、批评指正本文中的任何错误~

  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

φ冰霰ξ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值