C语言 — 自定义类型(结构体,联合体,枚举)
目录
- 1.结构体
- 1.1 结构体类型的声明
- 1.2 结构体变量的创建和初始化
- 1.3 结构体成员的使用
- 1.4 结构体的大小
- 1.4.1 计算以下结构体的大小
- 1.4.2 计算以下结构体的大小
- 1.4.3 结构体中嵌套结构体的大小计算
- 1.5 位段
- 1.5.1 使用位段计算结构体大小
- 1.5.2 位段的缺陷
- 2.联合体
- 2.1 联合体的声明
- 2.2 联合体的使用
- 2.3 联合体的大小
- 2.4 联合体的应用
- 2.5 通过联合体判断当前环境的大小端
- 3. 枚举
- 3.1 枚举的概念及使用
- 3.2 枚举的应用
1.结构体
1.1 结构体类型的声明
struct Name//结构体类型
{member - list//成员列表}variable - list;//变量列表
1.2 结构体变量的创建和初始化
#include<stdio.h>//创建一个学生类型的结构体
struct Stu
{char name[20];//名字int age;//年龄float score[3];//成绩
}s1 = { 0,0,{0} };//创建一个学生变量s1,并初始化为0//全局变量
struct Stu s2;//创建一个学生变量S2;
struct Stu s3 = { "lihua",18,{90.0f,85.5f,99.0f} };
//创建变量S3的同时初始化变量(给变量赋值)int main()
{//创建s4变量并赋值struct Stu s4 = { "wangwu",20,{100.0f,88.0f,80.0f} };return 0;
}
结构体成员的创建可以是在结构体声明的同时成员列表中创建并赋值;在结构体声明后,创建全局变量后赋值;在函数中创建结构体变量后赋值。
1.3 结构体成员的使用
//创建一个人结构体
#include<stdio.h>struct Peo
{char name[20];//名字int age;//年龄char po[20];//籍贯char tel[12];//联系方式}p1 = {"wangan",22,"hubei","xxxxxx"};//创建结构体变量p1void print1_mem(struct Peo p1)//传值调用的输出函数
{printf("p1的成员列表:\n");//结构体变量访问成员变量使用 .(点操作符)//按照格式输出,每输出一个成员换行printf(" name = %s\n age = %d\n po = %s\n tel = %s\n",p1.name, p1.age, p1.po, p1.tel);
}void print2_mem(struct Peo* p2)//传址调用的输出函数
{printf("p2的成员列表:\n");//通过结构体变量地址访问结构体成员,使用->(箭头操作符)printf(" name = %s\n age = %d\n po = %s\n tel = %s",p2->name, p2->age, p2->po, p2->tel);
}
int main()
{//创建一个结构体变量struct Peo p2 = {"lili", 29,"beijing","*******"};//创建结构体变量p2//创建输出函数print1_mem(p1);//传值调用printf("\n");//换行print2_mem(&p2);//传址调用return 0;
}
在结构体的传参中,一般使用的是传址调用,因为如果采用传值调用,函数调用时会开辟函数栈帧,为函数的参数开辟临时的空间,当结构体成员比较多时,会占用较大的内存空间,程序的运行会消耗更多的时间,消耗较大的性能,因此在结构体的传参中一般使用结构体指针传参,占用的空间是4个字节,开辟的函数栈帧内存空间较小,可以有效提高程序性能。
1.4 结构体的大小
结构体的大小由以下几个规则:
1.结构体的首个元素存放在结构体的0偏移处;
2.从第二个结构体成员起,存放的位置是默认对齐数与成员大小的较小值的整数倍的偏移量位置;
3.结构体的最终大小是每一个成员大小与默认对齐数比较后的较小值中的最大值的整数倍;
4.如果在结构体中嵌套结构体,嵌套的结构体的大小按照该结构体成员中与默认对齐数比较后的较小值中,选择最大值作为该成员比较后的对齐数,最后按照前3条规则即使出结构体大小。
1.4.1 计算以下结构体的大小
#include<stdio.h>
struct S
{char a;char b;int c;
};
int main()
{printf("%zu", sizeof(struct S));return 0;
}
第一个元素是char类型,大小1个字节,存放于0偏移量的位置第二个元素需要存放的位置是自身大小1于默认对齐数的比较后的较小值整数倍的偏移量位置
,在VS2022中的默认对齐数是8,与1比较1是较小值,存放于1的整数倍偏移量处,刚好存放
偏移量为1的位置;第三个元素的大小是4个字节,于默认对齐数8比较,4是较小值,因此需要存放在4的整数倍
偏移量位置,前面已经占用0和1偏移量位置,偏移量为2和3不是4的整数倍,因此第3个元素
需要存放在偏移量为4的位置,占用4,5,6,7偏移量位置;三个元素对比后的较小值是1,1,4,三个数的最大值是4,最后结构体的大小是4的整数倍,
此时结构体占用的位置是0 - 7偏移量的位置,是8个字节,刚好是4的倍数,所以结构体最终大小是8个字节。
1.4.2 计算以下结构体的大小
#include<stdio.h>
struct S
{char a;int b;char c;
};
int main()
{printf("%zu", sizeof(struct S));return 0;
}
第一个元素的大小是char类型,大小1个字节,存放于0偏移处;第二元素是int类型,大小4个字节,于默认对齐数8相比,较小值是4,存放于4的整数倍
偏移量位置,即4偏移量位置,占用4-7偏移量位置;第三个元素是char类型,大小是1,与默认对齐数相比较,较小值是1,存放于1的整数倍位置,
可以存放于8偏移量位置;经过比较后成员的对齐数的1,4,1,最大值是4,结构体的最后大小是4的整数倍,此时占用
0-8偏移量位置,共9个字节,需要补3个字节补齐4的倍数,浪费9-11偏移量位置。
1.4.3 结构体中嵌套结构体的大小计算
#include<stdio.h>struct S
{int ranking;float score;double deviation;
};
struct T
{char name[16];struct S;
};int main()
{printf("%zu", sizeof(struct T));return 0;
}
struct S第一个成员是int类型占用4个字节,0-3偏移量位置;第二个成员是float类型,大小4个字节,与最大对齐数比较的较小值是4,存放在对齐数的
整数倍位置,从偏移量4位置起,占用4-7偏移量位置;第三个成员是double类型,大小与最大对齐数一样,从偏移量8的位置起存放,占用8-15偏移量
位置;比较后的对齐是4,4,8,最大值是8,结构体此时大小是0-15,占用16个字节,为8的倍数,最终
结构体大小是16.struct T第一个成员的大小是16,占用0-15偏移量位置;第二个成员是结构体struct S,其成员对比后的最大对齐数是8,与默认对齐数一致,从16偏移量
位置起存放,占用16-31偏移量位置;对比后成员的对齐是1,8,最大值是8,结构体此时占用0-31偏移量位置,大小是32个字节,是8的
倍数,最终结构体大小是32.
1.5 位段
位段可以指定结构体成员占用的bit位的大小,使用是在结构体后加上 ‘:’和指定的bit位,如下:
struct A
{char a: 2;char b: 4;char c;
}
1.5.1 使用位段计算结构体大小
#include<stdio.h>struct D
{char a : 1;char b : 4;int c;
};
int main()
{printf("%zu", sizeof(struct D));return 0;
}
位段的使用会根据成员的大小先开辟一个成员大小的空间,当该成员不够存放其它成员时,再开辟新的空间;以上结构体中,会先开辟char类型的大小的空间,共8个bit位,第一个成员占用1bit位,第二个成员占用4bit位,可以使用第一次开辟的空间,第三个成员是int类型,需要4个字节,搜首次开辟的内存空间不够使用,会重新一个4个字节的空间存放第三个成员,因为结构体大小需要对齐,最终结构体大小是8个字节。
以下程序运行的结果是什么?
#include<stdio.h>
#include<string.h>
struct INT
{int a : 2;int b : 3;int c : 4;int d : 5;
}*pi,s;//创建结构体指针int main()
{pi = &s;memset(pi,0,4);pi->a = 1;pi->b = 2;pi->c = 3;pi->d = 4;char* ptr = (char*)pi;printf("%02x %02x %02x %02x", ptr[0],ptr[1], ptr[2], ptr[3]);return 0;
}
结构体的第一个成员是int a,根据类型会先开辟4个字节的空间,位段是2,占用两个bit位;
第二个成员的位段是3,占用3个bit位;第三个成员占用4个bit位;第四个成员占用5个bit位;
结构体成员共使用14个bit位,不需要再开辟空间,结构体的大小是4字节;使用memset函数将结构体的内容全部设置为0;使用pi指针初始化结构体成员,根据位段的bit截断赋值,a的值是01,b的值是010,c的值是0011
,d的值是00100;将pi指针强制类型转换后,每次访问一个char类型大小的内容,输出内容是69 08 00 00(小端存储)
1.5.2 位段的缺陷
1.位段在使用上并未规定是从低地址处开始存放还是从高地址处开始存放;
2.当开辟的空间足结构体的下一个成员存储时,剩下的空间是否得舍弃,开辟新的空间;
3.VS上默认是从低地址处开始存放,当开辟的空间不足结构体下一成员存储时会重新开辟新的空间,
而不同编译器的处理方式不同,可能导致跨平台性问题,需要不同的硬件设计处理,以此需要跨平台
的程序应该尽量减少位段的使用。
2.联合体
2.1 联合体的声明
联合体的声明
union name//联合体类型名
{member - list;//成员列表}variable -list;//变量列表
联合体的声明与结构体类似,由类型名,成员列表,变量列表组成;
2.2 联合体的使用
union un
{//成员列表int a;char c;
}un1;//创建un1变量union un un2 = {0};//创建un2变量,并初始化int main()
{union un un3 = { 0 };//创建并初始化//赋值,通过.操作符访问成员并初始化un3.a = 1;un3.c = 'a';return 0;
}
2.3 联合体的大小
联合体与结构体的区别在于,联合体的成员是共用一块内存空间,联合体的大小至少是联合体成员中占用内存空间最大的成员的大小。
#include<stdio.h>
union un
{char c;//大小是1int a;//大小是4
}un1;//创建un1变量int main()
{un1.a = 0x44332211;un1.c = 0x00;printf("%zu", sizeof(un1));return 0;
}
按F10启动调试,打开内存窗口输入&un1,观察内存空间以及赋值前后的变化:当执行un1.a的语句时,执行的内存空间被初始化为 11 22 33 44(小端存储),同时可以观察到此时un1.c成员的值是11,说明结构体的成员un1.a与un1.c是占用同一内存空间的。
按F10执行un1.c的赋值语句,可以观察到首字节位置被初始化为00,再次说明联合体成员是占用同一内存空间。
按F10执行输出语句,输出结构体的大小4,因为占用同一内存空间,开辟的大小是4字节,成员的最大对齐数是4,此时大小为4的整数倍,最终输出4.
2.4 联合体的应用
日常生活中经常有促销活动,买东西送礼品,礼品可能包含书籍,衣服,杯子等,每一种礼品都有库存,定价,类型以及各自的特点,基于以上内容可以使用联合体与结构体定义一个礼品清单结构体;
struct gift_list
{int stock_number;//库存量double price; //定价int item_type;//商品类型//使用联合体可以节省内存空间union //匿名联合体,只能使用一次{struct{char title[20];//书名char author[20];//作者int num_pages;//页数}book;//书struct{char design[30];//设计}mug;//杯子struct{char design[30];//设计int colors;//颜?int sizes;//尺?}shirt;//衬衫};}item;//变量
2.5 通过联合体判断当前环境的大小端
#include<stdio.h>
union sys_check
{int i;char c;
}un;
int main()
{un.i = 1;//小端 01 00 00 00 大端 00 00 00 01//un.c 小端:01即1,大端00即0if (un.c == 1)printf("小端\n");elseprintf("大端\n");return 0;
}
3. 枚举
3.1 枚举的概念及使用
枚举(enum) 是将可能性列举,例如每一个星期都有7天,交通信号灯是红蓝绿,性别有男和女等,枚举的使用是列举的每一种可能后面加上逗号,最后一种可能不需要加上,如下:
enum Week//星期
{day1 = 1,//默认是0,可以赋值day2,//2day3,//3day4,//4day5,//5day6,//6day7//7
};
enum Sex//性别
{man,//0woman,//1
};
enum TL//交通灯
{red,//0green,//1yellow//2
};
3.2 枚举的应用
//通过枚举替换
#include<stdio.h>
#pragma warning(disable:4996)enum calculator
{EXIT,//0ADD,//1SUB,//2MUL,//3DIV//4
};
//功能选择
void menu()
{printf("************************\n");printf("**** 1.Add 2.Sub *****\n");printf("**** 3.Mul 4,Div *****\n");printf("**** 0.Exit *****\n");printf("************************\n");}//函数定义:加减乘除
double Add(double x, double y)
{return x + y;
}
double Sub(double x, double y)
{return x - y;
}
double Mul(double x, double y)
{return x * y;
}
double Div(double x, double y)
{return x + y;
}int main()
{int input;do {menu();printf("请选择:");scanf("%d", &input);double x, y;printf("请输入两个操作数:");scanf("%lf%lf", &x, &y);switch (input){case ADD://ADD是枚举中的1printf("%.2lf\n",Add(x, y));break;case SUB://SUB是枚举中的2printf("%.2lf\n", Sub(x, y));break;case MUL://MUL是枚举中的3printf("%.2lf\n", Mul(x, y));break;case DIV://DIV是枚举中的4printf("%.2lf\n", Div(x, y));break;case EXIT://WXIT是枚举中的0printf("退出中...");break;}} while (input);return 0;
}
枚举替换的好处主要包括以下几个方面:
可读性提升:枚举通过定义具体的值来替代布尔值或整数常量,使得代码更加直观易懂。
可拓展性增强:枚举可以轻松添加新的成员,而不需要修改使用这些枚举的代码。这与使用整数常量或布尔值相比,扩展性更强,维护成本更低。
安全性提高:使用枚举可以避免无效值的输入,因为枚举成员是预先定义好的,编译器会在编译时检查非法值,从而减少运行时错误。
性能优化:例如,EnumSet比传统的位标识更高效,因为它内部使用位数组存储,性能更好。这对于处理大量开关状态非常有用