首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >自定义类型:结构体、联合体、枚举

自定义类型:结构体、联合体、枚举

作者头像
用户11831438
发布2025-12-30 13:39:56
发布2025-12-30 13:39:56
910
举报

一、结构体

1.结构体类型的声明
1.1结构的声明
代码语言:javascript
复制
struct tag

{
    member-list;

}variable-list;

例如要描述一个学生:

代码语言:javascript
复制
struct stu
{
	char name[20];
	int age;
	char number[20];
};这里的分号不能丢
1.2 结构体变量的创建和初始化
代码语言:javascript
复制
struct stu
{
	char name[20];//姓名
	int age;//年龄
	char number[20];//学号
};
int main()
{
	struct stu s1={"zhangsan",20,"20203029292"};//按顺序初始化
	printf("%s\n", s1.name);
	printf("%d\n", s1.age);
	printf("%s\n", s1.number);
	printf("\n");
	//不按顺序初始化
	struct stu s2 = { .age = 18,.name = "lisi",.number = "32040320304" };
	printf("%s\n", s2.number);
	printf("%d\n", s2.age);
	printf("%s\n", s2.name);
	return 0;
1.3 结构体的特殊声明
代码语言:javascript
复制
struct
{
	char name[20];//姓名
	int age;//年龄
	char number[20];//学号
}s1,s2;

这个结构体在声明的时候省略了结构体标签(tag),这种写法只能像上面这种写法,其余写法均为错误,这种匿名结构体类型只能使用一次,后期不能使用这个类型在创建变量,只能再创建结构体的同时创建变量,以后不能再用了。

1.4 结构体的自引用
代码语言:javascript
复制
struct Node
{
	int data;
	struct Node* next;
};

我们前面学习了typedef重命名,是不是也可以将这个结构体重新命名一下?当然是可以了。

代码语言:javascript
复制
typedef struct Node
{
	int data;
	struct Node* next;
}Node;

通过上面的代码,我们就可以将这个结构体重新命名为Node,后面我们就可以直接使用这个新名字进行结构体的初始化,将struct Node重新命名为Node

代码语言:javascript
复制
typedef struct stu
{
	char name[20];//姓名
	int age;//年龄
	char number[20];//学号
}stu;
int main()
{
	stu s1={"zhangsan",20,"20203029292"};//按顺序初始化
	printf("%s\n", s1.name);
	printf("%d\n", s1.age);
	printf("%s\n", s1.number);
	printf("\n");
	//不按顺序初始化
	stu s2 = { .age = 18,.name = "lisi",.number = "32040320304" };
	printf("%s\n", s2.number);
	printf("%d\n", s2.age);
	printf("%s\n", s2.name);
	return 0;
}
2 结构体内存对齐

通过上面的学习,我们基本上掌握了结构体的基本使用。接下来让我们深入讨论一个问题:计算结构体的大小。

2.1 对齐规则(重要)

1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处(第一个成员总是放在偏移量为0的地址上)

2.从第二个成员变量开始,都要对齐到某个对齐数的整数倍的地址处 对齐数=编译器默认的一个对齐数与该成员变量大小的较小值。(vs中默认的值为8,Linux中gcc没有默认对齐数,对齐数就是成员自身的大小)

3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中的最大的)的整数倍

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

代码语言:javascript
复制
struct s1
{
	char c1;
	int i;
	char c2;
};
struct s2
{
	char c1;
	char c2;
	int i;
};
struct s3
{
	double d;
	char c;
	int i;
};
struct s4
{
	char c1;
	struct s3 s3;
	double d;
};
int main()
{
	printf("%d\n", sizeof(struct s1));
	printf("%d\n", sizeof(struct s2));
	printf("%d\n", sizeof(struct s3));
	printf("%d\n", sizeof(struct s4));
	return 0;
}

接下来,让我们来看看上面的结果分别是什么?

根据上面的规则,sizeof(struct s1)==12,sizeof(struct s2)==8,sizeof(struct s3)==16,sizeof(struct s4)==32

2.2 为什么存在内存对齐

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

2. 性能原因: 数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要 作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地 址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以 ⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两 个8字节内存块中。

总体来说:结构体的内存对⻬是拿空间来换取时间的做法。

那在设计结构体的时候,我们既要满足对齐,又要节省空间: 让占用空间小的成员尽量集中在一起

2.3 修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对齐数。

代码语言:javascript
复制
#include <stdio.h>
#pragma pack(1)//设置的一般为2的次方数
//设置默认对⻬数为1
struct S
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//
//取消设置的对⻬数,还原为默认
int main()
{
	//输出的结果是什么
	printf("%d\n", sizeof(struct S));
	return 0;
}

通过上面代码中的#pragma pack( 1 )就可以改变编译器的默认对齐数,将默认对齐数8改为1,#pragma pack()是取消设置的对齐数,还原为默认。如果需要计算结构体的大小,可以根据规则计算出大小。

3 结构体传参
代码语言:javascript
复制
struct S
{
	int data[1000];
	int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
	printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
	printf("%d\n", ps->num);

}
int main()
{
	print1(s);  //传结构体
	print2(&s); //传地址
	return 0;
}

在上面两种结构体传参的方式中,推荐传址调用,这样可以减少空间的使用

4 结构体实现位段
4.1 什么是位段

位段的声明和结构体是类似的,有两个不同:

  1. 位段的成员必须是int,unsigned int或者signed int,在c99中位段成员的类型可以选择其他整型家族类型,比如:char。
  2. 位段的成员名后边有一个冒号和一个数字。(数字表示这个成员要占用的比特位的数量)

比如:

代码语言:javascript
复制
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

A就是一个位段类型,冒号后面的数字分别表示a占2个比特位,b占5个比特位,c占10个比特位,d占30个比特位。

前面我们学习了如何计算结构体的大小,那位段A所占内存的大小是多少?

代码语言:javascript
复制
printf("%d\n",sizeof(struct A));

位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟。

在开辟空间的时候,会出现下面的几个问题:

1.一个字节(整型)的内存中,到底是从左向右使用,还是从右向左使用不确定

2.剩余的空间不能满足下一个成员的时候,是否浪费不确定。(vs编译器认为空间需要被浪费)

我们先假设从右向左使用,通过编译器我们可以得出sizeof(struct A)==8,这是什么原因呢?

我们知道a占2个比特位,b占5个比特位,c占10个比特位,d占30个比特位,加起来一共是47个比特位,6个字节是48个比特位,很明显是可以存下47个比特位的,但编译器给我们的结果却是8,这就说明存在空间浪费,剩余的空间不能满足下一个成员的时候,vs编译器认为这些空间需要被浪费,那这样的话,a,b,c一共是17个比特位就需要申请4个字节,d占30个比特位需要再申请4个字节的空间,这样就达到了8个字节的空间。

4.2 位段的内存空间分配

  1. 位段的空间成员可以是int ,unsigned int ,signed int或者是char 等类型
  2. 位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
代码语言:javascript
复制
struct s
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct s s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;
	return 0;
}

10的二进制是00001010;12的二进制是00001100;3的二进制是00000011;4的二进制是00000100。

我们分别将上面以16进制的形式打印地址:从右向左0x62 03 04 ,从左向右0x58 18 40

通过vs编译器,我们很明显看出vs是从从右向左的形式使用的。

4.3 位段的跨平台问题

  • 1. int 位段被当成有符号数还是无符号数是不确定的。
  • 2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  • 3. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
  • 4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

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

4.4 位段使用的注意事项

通过上面的学习,我们知道位段的几个成员共用一个字节,内存中每个字节会分配一个地址,但是一个字节内部的比特位是没有地址的。共用一个字节的位段成员并不是每个成员都有地址。所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。

代码语言:javascript
复制
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};
int main()
{
	struct A s = { 0 };
	//scanf("%d\n", &s.a);这是错误的

	//正确写法
	int n = 0;
	scanf("%d\n", &n);
	s.a = n;
	return 0;
}

二、联合体

1 联合体类型的声明

像结构体一样,联合体也是由一个或者多个成员构成,这些成员可以是不同类型。联合体的关键字是union。但是编译器只为最大的成员分配足够的内存空间。联合体的特点是所有成员共用一块内存空间。所以联合体也叫:共用体。

给联合体其中一个成员赋值,其他成员的值也会跟着变化。

//联合体的声明 union tag { //成员 }:

代码语言:javascript
复制
#include<stdio.h>
//联合体的声明
union s
{
	char c;
	int i;
};
int main()
{ 
	//联合变量的定义
	union s un = { 0 };
	//计算联合体变量的大小
	printf("%d\n", sizeof(un));
	return 0;
}
2 联合体的特点

联合体的成员是共用一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员)

代码语言:javascript
复制
//代码1
union s
{
	char c;
	int i;
};
int main()
{ 
	union s un = { 0 };
	printf("%p\n", &un);
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));
	return 0;
}
代码语言:javascript
复制
//代码2
union s
{
	char c;
	int i;
};
int main()
{
	union s un = { 0 };
	un.i = 0x11223344;
	un.c = 0x55;
	return 0;
}

代码1输出的结果是001AF85C,001AF85C,001AF85C ;代码2输出的结果是11223344。我们发现代码1输出的三个结果是一模一样的,代码2的输出中,我们发现将i的第4个字节的内容由44改成了55

通过仔细分析,我们画出un的内存分布图

通过上面的学习,我们可以得出联合体的使用情况:用 i 的时候不用 c,用c 的时候不用 i ,换句话来说就是 i 和 c 不能同时使用(联合体的成员不同时使用)

3 联合体大小的计算

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

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

代码语言:javascript
复制
union s 
{
	int i;
	char c[5];
};
int main()
{
	printf("%d\n", sizeof(union s));
	return 0;
}

在计算数组的对齐数的时候,要按一个元素的大小来算对齐数,我们知道这个联合体的最大成员的大小是5,但5不是最大对齐数4的整数倍,所以我们需要对齐到8,由此可以得出这个联合体的大小是8。

由于联合体只为最大成员分配足够的内存,所以使用联合体是可以节省空间的,举例:

比如,我们要搞⼀个活动,要上线一个礼品兑换单,礼品兑换单中有三种商品:图书、杯⼦、衬衫。 每⼀种商品都有:库存量、价格、商品类型和商品类型相关的其他信息。

图书:书名、作者、页数 杯⼦:设计 衬衫:设计、可选颜色、可选尺寸

库存量、价格和商品类型是共同拥有的,书名、作者、页数是图书单独拥有的,设计是杯子单独拥有的;设计、可选颜色,可选尺寸是衬衫单独拥有的。当我们需要图书的时候,就不需要杯子和衬衫所独有的信息;当我们需要杯子的时候,就不需要图书和衬衫所独有的信息;当我们需要衬衫的时候,就不需要杯子和图书所独有的信息。这不就是一个联合体的使用情况嘛!

话不多说,看代码:

代码语言:javascript
复制
struct gift_list
{
	int stock_number;
	double price;
	int type;
	union
	{
		struct a
		{
			char name[20];
			char author[20];
			int num_of_page;
		}book;
		struct b
		{
			char design[20];
		}mug;
		struct c
		{
			char design[20];
			char colors;
			int size;
		}skirt;
	}item;
}list;
4 联合体的练习

判断大小端字节序

代码语言:javascript
复制
int check()
{
	union un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}
int main()
{
	int ret = check();
	if (ret == 1)
	{
		printf("小段\n");
	}
	else
		printf("大端\n");
	return 0;
}

上面代码中的 u.c 是拿出第一个字节,1在内存中的地址是以16进制形式打印,1的地址是00 00 00 01,1如果以小端字节序存储的话,那就是01 00 00 00;大端字节序存储就是00 00 00 01,然后我们进行判断是否等于1,如果等于1(16进制的01也是1),那就是小端字节序存储,如果等于0,那就是大端字节序存储。

三、枚举类型

1 枚举的声明

枚举顾名思义就是一一列举,把所有可能的取值一一列举出来。枚举的关键词是enum

代码语言:javascript
复制
enum Days//星期
{
	Mon,
	Tus,
	Wed,
	Thur,
	Fir,
	Sat,
	Sun
};

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

以上定义的enum Days,enum Sex都是枚举类型。{ }中的内容是枚举类型可能的取值,也叫作枚举常量。另外这些取值都是有值的,因为他们叫做常量,默认是从0开始,依次递增1,这时就有小伙伴提出疑问了,难道这些值都是不能改的吗?当然不是,我们可以在声明枚举类型的时候赋一个初始值,如果没有赋值,会依次递增1。比如:

代码语言:javascript
复制
enum Sex//性别
{
	MALE=2,
	FEMALE=5,
	SECRET=7
};

那我们该如何使用枚举类型创建变量呢?接下来,让我们使用枚举类型来创建变量:

枚举类型 变量=初值(初值是大括号内的可能内容),比如: enum Sex a=MALE;

代码语言:javascript
复制
#include<stdio.h>
enum sex
{
	MALE,
	FEMALE,
	SACRET
};
int main()
{
	//使用枚举类型来创建变量
	enum sex s = MALE;//使用枚举创建变量的时候,
	                 //初值是enum sex大括号内的可能内容
	if (s == MALE)
		printf("男\n");
	else if (s == FEMALE)
		printf("女\n");
	else
		printf("保密\n");
	return 0;
}
2 枚举类型的优点

  • 1. 增加代码的可读性和可维护性
  • 2. 和#define定义的标识符比较枚举有类型检查,更加严谨,#define定义的标识符没有类型。
  • 3. 便于调试,预处理阶段会删除 #define 定义的符号
  • 4. 使用方便,一次可以定义多个常量
  • 5. 枚举常量是遵循作⽤域规则的,枚举声明在函数内,只能在函数内使⽤
3 枚举类型的使用
代码语言:javascript
复制
void menu()
{
	printf("********************\n");
	printf("****1.add 2.sub ****\n");
	printf("****3.mul 4.div ****\n");
	printf("**** 0.exit     ****\n");
}
enum option
{
	exit,
	add,
	sub,
	mul,
	div
};
int Add(int x,int y)
{
	return x + y;
}
int Sub(int x,int y)
{
	return x - y;
}
int Mul(int x, int y)
{
	return x * y;
}
int Div(int x, int y)
{
	return x / y;
}
int main()
{
	int input = 0;
	int x = 0, y = 0;
	do
	{
		menu();
		printf("请选择>>");
		scanf("%d", &input);
		printf("请输入两个数>>");
		scanf("%d %d", &x, &y);
		switch (input)
		{
		case add:
			printf("%d\n", Add(x, y));
			break;
		case sub:
			printf("%d\n", Sub(x, y));
			break;
		case mul:
			printf("%d\n", Mul(x, y));
			break;
		case div:
			printf("%d\n", Div(x, y));
			break;
		case exit:
			break;
		default:
			break;
		}
	} while (input);
	return 0;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、结构体
    • 1.结构体类型的声明
      • 1.1结构的声明
      • 1.2 结构体变量的创建和初始化
      • 1.3 结构体的特殊声明
      • 1.4 结构体的自引用
    • 2 结构体内存对齐
      • 2.1 对齐规则(重要)
      • 2.2 为什么存在内存对齐
      • 2.3 修改默认对齐数
    • 3 结构体传参
    • 4 结构体实现位段
      • 4.1 什么是位段
      • 4.2 位段的内存空间分配
      • 4.3 位段的跨平台问题
      • 4.4 位段使用的注意事项
  • 二、联合体
    • 1 联合体类型的声明
    • 2 联合体的特点
    • 3 联合体大小的计算
    • 4 联合体的练习
  • 三、枚举类型
    • 1 枚举的声明
    • 2 枚举类型的优点
    • 3 枚举类型的使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档