目录
前言
在实际生活中,我们解决问题的对象往往不是单个类型的变量,例如描述一个学生,我们需要记录性别、年级、年龄、性别、学号...等等,这时候我们就需要自己定义需要用到的结构类型了,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.当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
总结
如果我们能够根据具体问题的需求创建合适的自定义类型的变量,就可以让数据操作起来更加方便(例如:数据结构),把自定义类型学好对我们学习数据结构也有一定程度上的帮助。
感谢阅读,欢迎读者提出自己的建议、批评指正本文中的任何错误~