自定义类型

本文详细介绍了C/C++中的自定义类型,包括枚举类型、结构体类型、共用体以及位域。枚举类型用于限制变量的赋值范围,提供有意义的标识符。结构体用于组合不同类型的变量,形成一个整体,强调逻辑上的定义。共用体允许多个变量共享同一块内存,而位域则允许在结构体中指定变量的位宽,提高内存利用率。文章通过实例探讨了这些自定义类型的使用、存储方式和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

自定义类型

除了之前提到的整数、浮点数、数组等内置数据类型,C/C++还提供了一些自定义类型。所谓自定义类型就是允许在编程时定义新的数据类型,内置类型可以直接使用,比如可以直接定义int类型的变量,但自定义类型需要先定义类型再使用该类型。所以,自定义类型的使用分为两步:
1.定义该类型
2.使用该类型定义变量
内置类型最重要的就是数据宽度,而自定义类型更侧重于逻辑上的定义,类型本身反映了数据的逻辑特征而不是存储特征。C/C++允许的自定义类型有多种。

一、枚举类型

枚举类型只能定义枚举变量,枚举变量希望定义一种赋值被限制为有限个确定整数的变量。例如,想要定义一个变量来存储周一到周天的某一天,一种方法是定义一个普通的整数型变量,给变量赋值0-6。但这种方法可能被赋值其他类型的数值如浮点数,而且0-6这六个整数代表的意义并不那么明显。
枚举变量则可以解决这个问题,例如:

enum DAY{SUN=0, MON=1, TUE=2, WED=3, THU=4, FRI=5, SAT=6};
enum Day day = MON; // day == 1
day = SAT; // day == 6

此时枚举变量相比于普通的整数型变量有两个优点:
1.希望将变量可赋值的范围限制在给定的有限个整数中(通常称这些整数为枚举数)
2.用有意义的标志代表这些用于赋值的整数
首先定义枚举类型,再使用这种自定义类型定义枚举变量。这里提到,只是“希望”将可赋值的范围限制到给出的有限个整数中,但是实际上完全可以为一个枚举变量赋此范围之外的其他值,例如:

enum DAY {a = 10,b = 20}; // 1.定义枚举类型
enum DAY day = a; // 2.定义枚举变量
day = b;
day = 30; // 可以赋值枚举数之外的整数
day = 'a'; // ,day == 97,可以赋值枚举数之外的其他被识别为整数的字面量
day = 123.45; // day == 123,可以赋值非整数类型进行隐式类型转换

在定义枚举类型时,各个枚举数可以不赋值,例如:

enum DAY {a = 10,b = 20,c = 30,d = 40};
enum DAY {a = 10,b,c,d};	// 10,11,12,13
enum DAY {a,b = 20,c = 5,d}; // 0,20,5,6
enum DAY {a,b,c,d}; // 0,1,2,3

如果第一个枚举数不赋值,则默认为0,其后不赋值则默认增加1。遇到中间赋值的则重新计算。即除对于不赋值的枚举数,默认比他前一个枚举数增加1。
定义枚举类型是,对枚举数的赋值必须是整数类型或是识别为整数类型的字面量,例如:

enum DAY {a = 10,b = 123.45,c = 'a',d = "abc"};
// b和d的赋值出错(这里"abc"也不能得到字符串的起始地址)

可以在定义枚举类型的同时直接定义枚举变量(其他自定义类型也有这个特性):

enum DAY {a = 10,b = 20,c = 30,d = 40} day,dday;	// enum DAY类型变量

也可以直接定义有某种结构但无名的枚举类型变量(其他自定义类型也有这个特性):

enum {a = 10,b = 20,c = 30,d = 40} day,dday; //enum <unnamed> 类型变量

显然这种枚举变量只能在第一次时定义,之后不能直接使用这个枚举类型定义新的变量。
枚举数的标识符不能是已经定义的变量名,也不能在枚举类型定义之后再使用枚举数标识符定义其他变量,例如:

int a = 1;
enum DAY { a = 10, b = 20, c = 30, d = 40 }; // a重定义
int b = 2; // b重定义

一个枚举变量如果不赋初始值,则不会给出默认值,和一个不赋初始值的普通变量一样。
枚举变量本身的类型是定义它的枚举的类型,但可以将一个枚举变量当作整数参与运算或赋值给其他变量,例如:

enum DAY { a = 10, b = 20, c = 30, d = 40 };
enum DAY day = a;
int m = a + 1; // m == 11 

还有一点要特别注意,枚举数可以不必赋值给枚举变量而完全当作一个int字面量的标识符使用:

enum DAY { a = 10, b = 20, c = 30, d = 40 };
int x = a + b + c + d;

但是,这里的枚举数只能是int类型而不会随着数值变化,因此如果对枚举数赋值超过int宽度的整数则会被截断。同时枚举变量本身也只能是4字节,即和int类型一致,例如:

enum DAY { a = 1000000000000, b = 20, c = 30, d = 40 };
enum DAY day = 1000000000000;
int x = sizeof(a);	// 4字节,int类型,而不是long long类型
int xx = sizeof(day); // 4字节 enum DAY类型

再者,枚举数本身并不是变量,在定义枚举类型初始化后就不能在对其赋值,既然不是变量也不能对其取地址:

enum DAY { a = 10, b = 20, c = 30, d = 40 };
a = 60; // 错误,不是可修改的左值
int x = &a; // 错误,不是左值或函数指示符

在实际使用枚举类型时,建议按照枚举类型设计的初衷:
只对枚举变量赋枚举数、枚举数标识设置为有意义的标识符。

二、结构体类型

结构体类型是一种非常重要的自定义类型,也是之后C++面向对象的前身。C语言和C++中的结构体有所不同,C++将结构体类型的功能进行了扩展,这里先讨论C语言中的结构体,C++中的结构体在C++面向对象的部分进行补充。
前面提到的数组,定义一个数组实际上是定义了多个变量(数组的元素),这些变量属于这个数组类型的变量(以数组名代表数组整体)。像数组这样一种类型和内置的整数、浮点数类型是不同的,数组一旦定义其实是包含了多个某种类型的变量,但这些变量的类型都是相同的,例如:

int arr[3];
// 实际相当于定义了arr[0]、arr[1]、arr[2]这三个int类型变量,这些变量的类型由定义数组时的类型“int [3]”确定

有时候我们也需要一次性定义若干个变量,但不限于同一类型的变量,同时这些变量也能有一个所属的“整体”概念,就像数组名那样。这面临几个问题:
1.如何指定要创建几个、什么类型的变量?(数组在定义时确定元素类型和个数)
2.这些变量如何存储?(数组是连续存储的)
3.如何找到这些变量?(数组通过数组名和索引的方式找到变量,本质是由它们的连续存储决定的)
例如,现在要定义三个变量存储一个人的年龄和体重:

int age = 18;
float weight = 63.5;

虽然这样直接定义变量可以实现,但不能表明它们是属于同一个人的数据。当要存储多个人的数据时缺点更明显,因为不能再定义同名的变量,要存储的人越多,这种直接定义变量的方式就越蹩脚:

int age1 = 19;
float weight1 = 62.2;
int age2 = 20;
float weight3 = 66.5;
// ...

这种方式很糟糕,正常的方式是每个人有自己的年龄和体重(应该都用age、weight表示)。
但是C语言并没有一种内置的类型满足以上的需求(也不应该提供,因为要定义什么类型的多个变量不应该是固定的,应该根据需求自己定义),结构体类型就是这样一种自定义类型,允许同时定义多个不同类型的变量,而且有一个所属的整体。
和枚举等自定义类型一样,首先要先定义处这种类型,再用此类型定义变量:

	struct Person
	{
		int age;
		float weight;
	};	// 先定义这种类型
	struct Person Jam; // 再用类型定义这种类型的变量
	// 看似定义了一个Person类型变量,实际定义了两个变量:age和weight

在形式上和自定义类型的枚举是一致的:

	enum DAY { a = 10, b = 20, c = 30, d = 40 };	// 先定义这种类型
	enum DAY day; // 再用类型定义这种类型的变量

实际上和内置的数组类型更相似:

	//内置定义好了int [N]数组类型
	int arr[3]; //定义这种类型的变量arr(实际上数组名不是变量,这里只整个数组本身)
	//看似定义的是一个数组变量arr实际上是定义了3个int变量arr[0]、arr[1]、arr[2]

像数组一样,我们使用的是数组元素来存取数据而不是整个数组本身。这里,我们也更关心结构体变量中的两个子变量(称为该结构体变量的成员变量),即如何使用这些成员变量。
参考数组元素的使用方式,这里显然不能使用索引,因为结构体的成员变量之间并不存在“第几个”的关系,另外由于成员变量之间的类型可能不同所以即使是连续存储也不容易确定每个成员变量的内存地址(实际上并不一定是连续存储)。
前面提到,结构体一方面是为了解决要定义同名变量的问题,例如:

struct Person Jam;
struct Person Tom;

这里不同结构体变量都会有自己的成员变量age、weight,为了解决重名问题,只需要区分是哪个结构体变量的成员即可:

Jam.age = 19;
Jam.weight = 63.5;
Tom.age = 20;
Tom.weight = 65.6;

这样使用"."运算符就可以找到属于某个结构体变量的成员变量,本质上和数组用数组名+索引的形式是相同的。这里每个结构体变量的同名成员变量并不是指同一个变量,即使它们是由同一个结构体类型定义的。成员变量的实际变量名并不是单纯的成员变量的标识符,而是结合它所属的结构体变量生成了新的变量名,“结构体变量.成员变量”是其完整的变量名,例如:

age = 19;
weight = 63.5;
// 这里并不明确属于哪个结构体变量的成员变量,所以报错
// 例如我们对[2]赋值是错误的,它并不明确是对哪个数组的第3个元素赋值

C/C++允许像枚举那样在定义类型的同时定义该类型的结构体变量:

	struct Person
	{
		int age;
		float weight;
	}Jam,Tom,Students[3];	// 也可以定义元素是该结构体变量的数组

也允许像数组那样在定义结构体变量时对其中的成员变量初始化:

struct Person Jam = {19,65.3}; // 或者先定义结构体变量再分别对成员变量初始化
int arr[3] = {1,2,3};

这里要特别注意区分数据类型和该类型定义的变量:
上面的代码中:

	struct Person
	{
		int age;
		float weight;
	}; // 定义数据类型
	struct Person Jam; //用该类型定义变量

变量是可以被赋值或访问的,但数据类型本身不行:

	struct Person
	{
		int age = 19;
		float weight = 66;
		// 直接对类型定义中的变量赋值是错误的,类型本身的定义并不变量,该初始数据没有所属结构体变量
	};
	// 这里和“int = 10”一样错误,这并不会给用此定义的结构体变量提供默认值,就像用此int定义的变量a不会被赋予初始值10一样。

同时,可以像枚举类型那样定义无名的结构体同时定义该种类型的结构体变量:

	struct 
	{
		int age
	}s; // s的类型为struct <unnamed>

结构体变量的成员变量的存储方式

像数组的元素那样(连续存储、各自所占的空间大小相等),我们要探究结构体变量的成员变量时如何存储的。
首先探究在一段代码中连续定义的几个变量是不是连续存储:

	int a = 1;  // 起始地址0x0031F758
	int b = 2;  // 起始地址0x0031F74C
	char c = 3; // 起始地址0x0031f743
	int d = 4;  // 起始地址0x0031F734

在这里插入图片描述
可见,即使是连续定义的变量也不是按照各自所占空间大小连续存储的(但也是按顺序存储,并没有打乱相对顺序),这是编译器优化导致的,提高数据访问速度。

	struct MyStruct
	{
		int a;
		int b;
		char c;
		int d;
	};
	struct MyStruct s = { 1,2,3,4 };
	int ss = &s;	//0x003dfed4
	int sa = &s.a;	//0x003dfed4
	int sb = &s.b;	//0x003dfed8
	int sc = &s.c;	//0x003dfedc
	int sd = &s.d;	//0x003dfee0

在这里插入图片描述
可见,结构体变量的起始地址和第一个成员变量的起始地址是相同的(这一点和数组相同),各个成员变量的起始地址和定义该结构体类型时的相对位置一致(但并不一定连续)。实际上,结构体变量中的各个成员变量之间相对连续存储但遵循字节对齐原则。一个结构体变量所占的总的空间大小不一定是所有成员变量所占字节数之和。

字节对齐

在vs中可以选择结构体成员变量的对齐方式(默认是8字节):
在这里插入图片描述
结构体变量所占的空间并不一定是所有成员变量所占空间之和,因为各个成员变量要根据字节对齐方式确定起始地址,而并非连续存储。例如:

	struct Flags
	{
		char c1;
		char c2;
		int a;
		
	} flags;
	int num = sizeof(flags); //8
	int add_c1 = &flags.c1; // 4061796
	int add_c2 = &flags.c2; // 4061797
	int add_a = &flags.a;	// 4061800

整个结构体变量所占的空间为8字节,并非1+1+4=6字节,其内存分布如下:
在这里插入图片描述
每个成员变量的起始地址都必须保证是其类型所占字节的整数倍,c1、c2都是1字节,所以任意一个内存单元都可以直接存储,a占4字节,所以顺次找到第一个地址为4的整数倍的单元4061800作为起始最大的成员变量为int类型4字节,没有超过字节对齐方式8,整个结构体变量所占的空间必须是最大成员a的4字节的整数倍,此时结构体变量所占的空间为8字节,满足要求,因此不必在a之后再进行填充。
但是对于如下的例子:

 struct Flags
	{
		char c1;
		int a;
		char c2;
		
	} flags;
	int num = sizeof(flags); // 12
	int add_c1 = &flags.c1; // 3209644
	int add_a = &flags.a; // 3209648
	int add_c2 = &flags.c2; // 3209652

仅仅是调换了成员变量的顺序,结构体变量所占的空间大小也变了:
在这里插入图片描述
在c2之后又扩充了3字节,因为如果不扩充,结构体变量的大小为9字节,并不是4字节的整数倍。
简单总结,结构体成员变量之间的存储并非连续存储,而是要遵循字节对齐规则:
如果所占空间最大的成员变量不超过编译器指定的字节数,则每个成员变量的起始地址必须是其所占字节的整数倍,如果不是则顺次找到下一个符合要求的地址,并且最终结构体变量所占的空间也应该是最大成员所占字节的整数倍。
对于编译器的其他字节对齐方式,只需要加以验证即可。VS编译器默认的8字节已经达到了64位机器中的基本数据类型所占空间的最大值。

空结构体和结构体的嵌套定义

C语言不允许定义不含任何成员变量的结构体类型,但C++是允许的,因为C++对结构体进行了一些扩展,o在C++的结构体部分进行说明。
定义结构体类型时,结构体的成员变量类型可以是任意的,可以是整型、数组等内置类型,也可以是已经定义的另一个结构体类型的变量,例如:

	struct B {
		int age;
	};

	struct A {
		struct B b;
	};

也可以在定义结构体类型时将一个本结构体的变量作为成员变量:

	struct MyStruct
	{
		struct MyStruct *ms;
		// struct MyStruct ms;是错误的
	}s;

这里只能用本结构体的指针变量作为成员变量,因为在定义结构体类型时就需要确定各个成员所占的空间大小,而这是本结构体尚未定义结束,无法确定所占空间大小,而无论何种类型的指针变量所占的空间大小都是确定的。
有时候可能会需要两个结构体在定义时需要对方的结构体变量作为成员,例如:

	struct A {
		struct B b;
	};
	struct B {
		struct A a;
	};
// 这是错误的语法,在定义A时B尚未定义

这个错误是无法解决的,因为在定义结构体A时需要先对成员变量“占位”,此时B所占的空间大小尚不确定。即使是暂时保留,当遇到定义B时同样需要先确定A所占的空间大小来“占位”,这样双方就僵持下来。
如果在双方的结构体中使用的是对方结构体类型的指针变量,则这种相互引用的问题可以通过前置声明的方式解决(有些编译器不需要前置声明就认为这种语法是正确的),因为无论是何种指针类型的指针,所占的空间都是确定、相等的,所以不需要先定义好引用的结构体类型就能完成“占位”。

三、共用体

共用体和结构体类似,也是一种自定义类型。共用体也允许同时定义多个不同类型的变量,但这只是在形式定义多个成员变量,实际上使用的是同一起始地址。
结构体中形式上定义的多个成员变量共用同一个地址,也就是说使用其中一个成员变量可能会覆盖之前使用的变量的值,例如:

union MyUnion
	{
		char c;
		int i;
		float f;
		long long ll;
	};
	union MyUnion mu;
	mu.c = 1;
	mu.i = 2;
	// 执行之后,mu.c的值也为2

共用体的本质是允许多个变量操作同一个内存(这一点类似于指针),所以共用体的成员变量任意时刻都对应着同一起始地址的那个二进制串:

int ac = &mu.c;
int ai = &mu.i;
int af = &mu.f;
int all = &mu.ll;
// 各个成员变量的起始地址是相同的,0x002cfa40
// 以此为起始地址的8字节空间在ai = 2的赋值之后其中的二进制串变成02 0x 02 00 00 00 cc cc cc cc,所以此时mu.c的值为一字节0x02,mu.i的值为4字节0x02000000,mu.f也为4字节0x02000000,直接将此二进制串解释为float类型而不是浮点数2.0,同理mu.ll值为8字节0x02000000cccccccc

共用体在某种程度上和通过指针操作变量的内存空间相似,例如上面的代码不是将2变成浮点数存储到mu.f中而是直接解释当前的二进制串,如果将2赋值给mu.f则:

mu.f = 2; // 2转换为浮点数二进制串0x00 00 00 40
// 此时,mu.c取一字节0x00,mu.i取4字节0x00 00 00 40对应十进制值1073741824而不是2.0

因为多个成员变量共用一个起始地址,所以共用体变量所占的内存空间只需要是宽度最大的变量的空间,例如上面的代码,结构体变量mu所占空间为long long 类型成员变量所占的空间8字节。
除成员变量的内存分布不同之外,共用体变量的一些性质和结构体变量完全相同。
例如,共用体变量的起始地址和每一个成员变量的起始地址都是相同的。
也可以以类似形式在定义结构体变量时对成员变量初始化:

	union MyUnion
	{
		char c;
		int i;
		float f;
		long long ll;
	};
	union MyUnion mu = {1};
	// 只能给出一个初始值,将这个初始值按识别类型转化为二进制串存储到这个共用的内存空间

使用共用体时最好按建议的那样使用哪个成员变量就提前为其赋值一次,不要将一个成员变量的二进制串解释为另一个成员变量的二进制串,否则可能会出现逻辑错误,例如上面的代码:

mu.i = 2; // 此时8字节内存空间的内容为0x02000000cccccccc

如果直接使用mu.ll的值则不会是2而是将0x02000000cccccccc解释为整数类型,因为mu.i只修改了4字节的内容,另外4字节的内容则是未初始化的.按照逻辑,int 和 long long 类型都应该是2,但实际上这和不同整数之间的赋值不同,很容易混淆。

四、位域

C/C++中虽然提供了位运算的功能但未提供基本的位类型,基本数据类型所占的空间大小都是以字节为单位。但在一些小内存的硬件上进行操作时还经常要用到变量来记录某一位的状态是0还是1,例如某个寄存器的各个位分别对应不同的功能,我们要用一个结构体变量表示该寄存器,定义若干成员变量表示该寄存器的各个位状态。通常我们可以用int或更小的short整数类型成员变量保存0或1,比如:

	struct Reg
	{
		char b1;
		char b2;
		char b3;
	}reg;
	int num = sizeof(reg); // 3字节

仅仅保存3个位的状态就需要3个字节,空间利用率很低,特别对于一些内存很小的硬件来说是非常浪费的。
位域和结构体的定义形式类似但它允许我们突破基本数据类型的固定数据宽度,为一个变量指定要使用的存储宽度为多少位,例如:

	struct Reg
	{
		char b1 : 1;
		char b2 : 1;
		char b3 : 1;
	}reg;
	int num = sizeof(reg); // 1字节

这里三个成员变量都指明仅各自使e用1位存储,整个结构体变量的所占的空间仅仅是一个字节(这里仍把变量reg称为结构体,因为它的类型仍是struct Reg,位域实际上并不是一种新的类型)。
同时我们看到,虽然为三个成员变量各自指定使用1位,但整个结构体变量所占的空间并非3位而是1字节。将每个成员变量指定使用的尾数改变:

	struct Reg
	{
		char b1 : 6;
		char b2 : 1;
		char b3 : 1;
	}reg;
	int num = sizeof(reg); // 1字节
	struct Reg
	{
		char b1 : 6;
		char b2 : 1;
		char b3 : 2;
	}reg;
	int num = sizeof(reg); // 2字节

整使用位域时整个结构体变量所占的空间至少是第一个成员变量所占空间大小,如果所有指定的位数之和不超过第一个成员变量的位数则结构体变量所占的大小为第一个成员变量的空间否则继续扩展到下一个成员变量的空间。这个规则只在所有成员变量的数据宽度相同时有效。
如果将成员变量修改为不同类型即成员变量的数据宽度不同时,则更能体现位域所使用的内存空间并不会随着指定的位数而做相应的调整,始终保持非位域的结构体的内存分布:

	struct Flags
	{
		char c1;
		int a;
		char c2;
		
	} flags;
	int num = sizeof(flags); // 12
	int add_c1 = &flags.c1; // 3209644
	int add_a = &flags.a; // 3209648
	int add_c2 = &flags.c2; // 3209652
	// 12字节

在这里插入图片描述
为每个成员变量指定使用的位数:

struct Flags
	{
		char c1 : 1 ;
		int a : 1;
		char c2 : 1;
		
	} flags;
	int num = sizeof(flags);  // 12字节

无论如何指定成员变量所用的位数都不会改变结构体变量所占的空间大小,也就是说位域只在成员变量都是相同宽度时才能根据指定的位数节省一定的空间。
可以只对其中的某些成员变量指定所使用的位数,如果不指定则默认是该成员变量所占字节所对应的位数,例如int为4字节即默认为使用32位。对一个成员变量指定使用位数的范围为从1位到其字节数所对应的位数,如果指定的位数在此范围外则会报错。
如果成员变量指定了位数,则该成员变量成为位域(但类型仍是变量原来的类型),不能对其使用取地址和sizof运算。
如果像一个成为位域的成员变量赋值,则其只存储指定的位数,高位的二进制串被舍弃,例如:

struct Reg
	{
		char b1 : 3;
		char b2 : 2;
		char b3 : 1;
	}reg;
	int num = sizeof(reg); // 1字节
	reg.b1 = 7; // 0b111被存入reg,b1,按char有符号数为-1
	reg.b2 = 5; // 0b101中的低位0b01被存入reg.b2,按char有符号数为1
	reg.b3 = 5; // 0b101中的低位0b1被存入reg.b2,按char有符号数为-1
	//此处符合二进制串解释为有符号数时的补码转换规则
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值