前言
请先看我的另一篇文章: 《随笔——自定义类型:结构体》
本文章建立在该文章的基础之上。
在C语言中,除了结构体,还有两种自定义类型,它们就是联合体和枚举体。
联合体
联合体和结构体非常相似:
结构体的关键字是struct,联合体的关键字是union
结构体由一个或者多个类型不限的成员构成,联合体也是如此
结构体的成员可以通过’.‘或’->'来访问,联合体也是这样
结构体的内存开辟要以内存对齐为前提,联合体还是这样
联合体类型声明
联合体的类型声明格式如下:
union 联合体名
{
数据类型 成员名1;
数据类型 成员名2;
...
};
联合体变量创建格式
联合体变量的创建格式如下:
union 联合体名 变量名;
联合体成员访问
#include<stdio.h>
union test
{
char _c;
int _i;
};
int main()
{
union test t = { 0 };
//直接访问
t._i = 314;
printf("%d\n", t._i);
//间接访问
(&t)->_c = 'h';
printf("%c\n", (&t)->_c);
return 0;
}
联合体内存开辟
尽管联合体和结构体的内存开辟都建立在内存对齐的基础上,但具体的开辟方式还存在差异:
结构体中的成员内存空间相互独立,互不干扰;联合体中的成员内存空间存在重叠,相互联系,若其中一个成员数值被修改,其它成员数值也会改变。
还是上面的例子,若修改代码顺序,结果就会不同:
#include<stdio.h>
union test
{
char _c;
int _i;
};
int main()
{
union test t = { 0 };
t._i = 314;
(&t)->_c = 'h';
printf("%d\n", t._i);
printf("%c\n", (&t)->_c);
return 0;
}
随笔——自定义类型:联合和枚举——示例一
联合体内存开辟的步骤十分简单:
- 找出联合体中内存占用最大的成员,把它对齐到联合体的0偏移量地址处
- 找出联合体成员中最大的对齐数,把联合体的大小补成这个最大对齐数的整数倍
还是以上面的代码为示例:
内存开辟好了,该怎么存数呢?
所有数据都从低地址处开始存。
为了方便描述,换个量赋值:
#include<stdio.h>
union test
{
char _c;
int _i;
};
int main()
{
union test t = { 0 };
t._i = 0x44332211;
(&t)->_c = 0x00;
return 0;
}
上面是没有补位的例子,下面是有补位的例子:
union test
{
char _c[5];
int _i;
};
相同成员的结构体和联合体比较:
联合体的应用
由于联合体成员间互相影响的特点,联合体仅适用于只使用其中一个成员的场景。
比如,我们要搞⼀个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯子、衬衫。
每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。
图书:书名、作者、页数
杯子:设计
衬衫:设计、可选颜色、可选尺寸
如果之前没学过联合体,可能就只能这样写:
struct gift_list
{
//公共属性
int stock_number;//库存量
double price; //定价
int item_type;//商品类型
//特殊属性
char title[20];//书名
char author[20];//作者
int num_pages;//⻚数
char design[30];//设计
int colors;//颜⾊
int sizes;//尺⼨
};
如果用这个礼品清单去描述礼品图书的话,那么结构体中的设计,可选颜色,可选尺寸就被浪费了;
如果用这个礼品清单去描述礼品杯子的话,那么结构体中的书名,作者,页数,可选颜色,可选尺寸就浪费了;
如果用这个礼品清单去描述衬衫的话,那么结构体中的书名,作者,页数就被浪费了;
我们知道,结构体本身就有些浪费空间,结果,上面的结构体中有些成员还有可能不用,不更浪费空间了吗?
学了联合体之后,我们就可以把公共属性单独写出来,剩余属于各种商品本身的属性使用联合体表示,这样就可以
减少所需的内存空间,⼀定程度上节省了内存。
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;
};
比如把书名改为为"联合体设计":
#include<stdio.h>
#include<string.h>
int main()
{
struct gift_list gl_book;
char title[20] = { "联合体设计" };
size_t n = sizeof(title);
strncpy(&(gl_book.item.book._title), title, n + 1);
printf("%s\n", gl_book.item.book._title);
return 0;
}
其实这个程序我一开始是用:
int main()
{
struct gift_list gl_book;
gl_book.item.book._title = "联合体设计";
return 0;
}
的形式写的,后来发现报错了,然后才意识到,在创建变量gl_book的时候编译器已经把它初始化了,所以要用strncpy拷贝过去。
另一个应用:
还记得我们在《随笔——数据在内存中的存储》写过一个判断某个机器字节序的程序吗?它是用取地址,强制类型转换实现的:
#include<stdio.h>
int main()
{
int i = 1;
char j = (*(char*)&i);
if (j)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
借助于联合体成员间相互联系,数据都从低地址出存起的特点,如今我们可以用联合体实现相同的功能:
#include<stdio.h>
int check_sys(void)
{
union test
{
char c;
int i;
};
union test t = { 0 };
t.i = 1;
if (t.c)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int ret = check_sys();
if (ret)
{
printf("小端");
}
else
{
printf("大端");
}
return 0;
}
枚举体
枚举体顾名思义,就是一一列举
比如在日常生活中
一周的星期一到星期日是有限的7天,可以一一列举
性别有:男、女、保密,也可以一一列举
月份有12个月,也可以一一列举
三原色,也是可以一一列举
这些数据就可以用枚举表示:
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜⾊
{
RED,
GREEN,
BLUE
};
其中的成员被称为枚举常量
从中我们可以发现,枚举的关键字是enum,成员后面是逗号而非分号,最后一个成员后面没有标点
以我的个人理解来说,枚举有些像错误码,其中的枚举常量背后都有一个序列号,序列号默认从0开始,这个序列号被称为枚举值:
#include<stdio.h>
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
枚举值在枚举声明的时候可以更改,更改过后,后面的枚举值会在此基础上依次加一:
#include<stdio.h>
enum Color//颜⾊
{
RED = 2,
GREEN,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
#include<stdio.h>
enum Color//颜⾊
{
RED,
GREEN = 5,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
不过我只说过枚举常量在声明时可以改变枚举值,声明过后,由于它是常量,就不能改了。
枚举类型在内存中通常存储为整数,毕竟枚举值就是整数。
注意:枚举常量和枚举值是两个不同的概念,比如对于上面的
enum Color//颜⾊
{
RED,
GREEN,
BLUE
};
其中,枚举常量是RED,GREEN,BLUE;枚举值是0,1,2。
经常有人拿#define和枚举比较:
enum Color//颜⾊
{
RED,//0
GREEN,//1
BLUE//2
};
#define RED 0
#define GREEN 1
#define BLUE 2
似乎功能上是一样的,那它们有什么区别呢?
枚举的优点:
- 增加代码可读性;
比如我们之前写过的计算器程序,大致结构是这样的:
void menu()
{
printf("*************************");
printf("**** 1.add 2.sub ****");
printf("**** 3.mul 4.div ****");
printf("***** 0.exit ******");
printf("*************************");
}
int main()
{
int input = 0;
printf("请选择:");
scanf("%d",&input);
switch (input)
{
case 1://略
break;
case 2://略
break;
case 3://略
break;
case 4://略
break;
default:
break;
}
return 0;
}
有了枚举就可以这样写了:
void menu()
{
printf("*************************");
printf("**** 1.add 2.sub ****");
printf("**** 3.mul 4.div ****");
printf("***** 0.exit ******");
printf("*************************");
}
enum option
{
exit,
add,
sub,
mul,
div
};
int main()
{
int input;
printf("请选择:");
scanf_s("%d", &input);
switch (input)
{
case add://略
break;
case sub://略
break;
case mul://略
break;
case div://略
break;
default:
break;
}
return 0;
}
- 枚举有类型检查,相比#define定义的标识符,更加严谨安全;
之前我们在 《随笔——自定义类型:结构体》说过,#define相当于内容替换,它会把遇到的所有符合的标识符替换,枚举则有一个类型检查的过程,对于语法更严谨的C++来说,这种类型检查更为明显;
在C中,对于枚举,可以这样赋值:
#include<stdio.h>
enum Color//颜⾊
{
RED,//0
GREEN,//1
BLUE//2
};
int main()
{
enum Color color;
color = RED;
return 0;
}
也可以这样赋值:
#include<stdio.h>
enum Color//颜⾊
{
RED,//0
GREEN,//1
BLUE//2
};
int main()
{
enum Color color;
color = 0;
return 0;
}
此时还看不出#define与枚举间的区别,这两种方法#define也可以用;
换成C++就不一样了,此时#define两种方法还是都可以用,但枚举就只能用第一种方法了:
VS的错误提示:
错误(活动) E0513 不能将 "int" 类型的值分配到 "Color" 类型的实体
0是int,无法赋给enum Color的变量
不过这到底是优点还是缺点,严格来说,要看开发环境,比如对于嵌入式来说,这个就是优点,单片机就那么大,多个类型检查就多点负担。所以此时#define比枚举更好
- 与#define相比,枚举便于调试;#define在预处理阶段就会被删除替换
如果你这样写:
#define RED 0
#define GREEN 1
#define BLUE 2
int main()
{
enum Color color;
color = RED;
return 0;
}
预处理阶段后,代码实际就变成这样了:
int main()
{
enum Color color;
color = 0;
return 0;
}
这会导致肉眼所见的代码和实际调试或者运行的代码不一样,非常容易误导人。枚举就不会被替换,肉眼代码和实际代码是一致的。
- 枚举常量是有作用域的,如果声明在函数里,作用域就是那个函数,如果声明在函数外,作用域就是那个文件;#define没有作用域,只要标识符对得上,就统统替换,它的使用范围无法被限制和控制,是不可控的,谁知道#define会不会把不该替换的替换了。
- 枚举可以一次定义多个常量,#define要一个一个定义。