C语言自定义类型详解 —— 结构体、枚举、联合体

1. 结构体

1.1 结构体的基本概念

  • 我们学过一种集合——数组,但是数组仅仅是同种类型元素的集合。结构体也是一类集合,但是它的元素类型可以不一样,我们把这些元素叫做结构体成员变量。我们通常把结构体类型称为自定义类型

1.2 结构体的声明

例如我们描述一个人:

struct Person //这里的 Person 是结构体标签
{
	char name[20];//姓名
	char sex[10];//性别
	int age;//年龄
}person;//结构体变量,在 main 函数外定义的是全局变量

这种做法是完全声明的结构体。

1.3 结构体的特殊声明

 例如我们这样写,这是一种不完全声明的写法:

//匿名结构体类型
struct //省略结构体标签
{
	char a;
	int b;
	float c;
}s1;//结构体声明时顺带定义结构体变量 s1

匿名结构体的变量只能定义一次,并且只能是在结构体声明的时候定义。

如果我们非要定义第二次变量,或者是不在结构体声明时定义变量,那么编译器就会报错。

#include <stdio.h>
//匿名结构体类型
struct //省略结构体标签
{
	char a;
	int b;
	float c;
}s1;//结构体声明时顺带定义结构体变量 s1

int main()
{

	struct s2;//看似是定义第二次变量
	//实际上使用不了
	s2.a = 'b';
	s2.b = 2;
	s2.c = 2.0;
	return 0;
}

介绍了匿名结构体,我们来看一下下面这段代码:

struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}a[20], * p;

p = &x;//这条语句合法吗?

 我们主观上会认为这两个结构体是一样的,但是在编译器看来,两个结构体就是两个不同的自定义类型,即使他们的成员变量都一样。所以变量 x 的地址不能存放在另一种类型的结构体指针变量 p 当中。

1.4 结构体的自引用

我们来判断一下这种写法是否合法:

struct Node
{
	int data;
	struct Node next;
};

 很明显是不合法的,就比如要使用 sizeof 计算结构体的大小,那么这个大小能计算的出来吗?这就类似陷入了死递归。

改进的方法:

#include <stdio.h>
int main()
{

	struct Node
	{
		int data;
		struct Node* next;
	};
	printf("%d", sizeof(struct Node));

	return 0;
}

这种写法就是链表的写法,一个结点包含数据域和指针域,这个大小是可算的,因为指针的大小很明确。

至于大小为什么是 16 而不是 8 或 12 ,我们在后面的计算结构体大小中会详细解答。

我们再来结合 typedef 关键字来分析一段代码是否正确:

int main()
{
	typedef struct Node
	{
		int data;
		 Node* next;
	}Node;

	return 0;
}

这个问题就类似于是先有鸡还是先有蛋了。这里要说的是,成员变量的声明是先于类型重命名的。也就是说,我们使用类型重定义后的类型名来声明指针,那么在编译器看来是非法的。

 解决方案:

int main()
{
	typedef struct Node
	{
		int data;
		struct Node* next;//使用类型重命名之前的类型名称
	}Node;

	return 0;
}

结构体声明中不能引用自己,但是可以引用其他的结构体:

struct Person
{
	char name[20];
	char sex[10];
	int age;
};
struct Count
{
	struct Person data[100];//struct Person 类型的数组
	int count;
};
int main()
{
	return 0;
}

1.5 结构体变量的定义和初始化 

我们先谈谈定义。

struct Point
{
	int a;
	int b;//注意结构体成员变量是不需要初始化的
}p1;//这里是一个全局结构体变量
struct Point p2;//这里也是一个全局结构体变量
int main()
{
	struct Point p3;//这里是一个局部结构体变量
	return 0;
}

定义就非常简单了,只需要一个类型+变量名即可。

接下来我们看初始化。什么是初始化呢?初始化就是在定义变量的时候顺带赋值。 

struct Person
{
	char name[20];
	char sex[10];
	int age;
};
struct Person p1 = { "龙兆万","男",20 };
int main()
{
	struct Person p2 = { "龙亿万","男",21 };
	return 0;
}

赋值的方法与数组是一样的,只需要注意顺序、成员变量的类型即可。

1.6 结构体变量的数据输出

我们给结构体的变量存放了一些数据,现在我们想要通过 printf 函数来打印这些数据,应该如何操作呢?

#include <stdio.h>
struct Person
{
	char name[20];
	char sex[10];
	int age;
};
struct Person p1 = { "龙兆万","男",20 };
int main()
{
	struct Person p2 = { "龙亿万","男",21 };
	printf("%s %s %d\n", p1.name, p1.sex, p1.age);
	printf("%s %s %d\n", p2.name, p2.sex, p2.age);

	return 0;
}

 可以看到一个全新的字符 '.' 。这就跟数组的原理是一样的,想要输出数组的某个元素,只需要引用这个元素的下标即可。只不过在结构体中,需要结构体变量.成员变量。 

1.7 结构体的内存对齐 

内存对齐决定了结构体类型占用内存多大的空间。就好比有这段代码:

#include <stdio.h>
struct S1
{
	char c1;
	int i;
	char c2;
}s1;
int main()
{
	printf("%d\n", sizeof(s1));
	return 0;
}

 这似乎是有一些违背常理的,因为结构体包含两个 char 类型,一个 int 类型,应该是 6 字节才对啊?为什么会是 12 个字节呢?这就涉及到结构体内存对齐了。

我们来了解一下结构体内存对齐规则:

  • 第一个成员在结构体变量(内存)偏移量为 0 的地址处。
  • 其他成员要对齐到某个数字(对齐数)的整数倍的地址处。

                对齐数 = 编译器默认对齐数 与 成员变量大小 的较小值

                VS 编译器中的默认对齐数为 8 

  •         结构体的总大小为最大对齐数(每个成员都有一个对齐数)的整数倍。
  • 如果嵌套了结构体,那么嵌套的结构体的对齐数是自己的最大对齐数,并且结构体的大小为最大对齐数(包括嵌套结构体的对齐数)的数整数倍。

就例如我们现在分析这个例题为什么是 12 :

我们再看一个例题,也是求结构体的大小:

#include <stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

 

接下来我们来学会计算结构体嵌套的问题:

#include <stdio.h>
struct S2
{
	char c1;
	char c2;
	int i;
};
struct S3
{
	int i;
	struct S2 s2;
	char c3;
};
int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

 我们上面已经计算了 struct S2 的大小为 8 。

 

  

至于为什么存在结构体内存对齐,大部分的参考资料给出两个原因:

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

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

不过我们需要注意,并不是所有的开发环境都有默认对齐数的。 例如在 gcc 平台下就没有默认对齐数这一说法。

1.8 修改默认对齐数

我们可以使用 #pragma 这个预处理指令修改默认对齐数:

#include <stdio.h>

#pragma pack(1)//设编译器默认对齐数为 1
struct S2
{
	char c1;
	char c2;
	int i;
};
#pragma pack()//恢复编译器默认对齐数

int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

那么这时候就如我们还不知道结构体内存对齐时所料,大小就为两个 char 类型大小 + 一个 int 类型大小。

1.9 结构体传参

这就跟我们普通数据传参一样的道理。如果我们想要在某个函数内改变外部变量的内容,就必须要传地址(使用指针)。 

比如说,我们的结构体已经初始化好了,只需要封装一个打印函数打印结构体的内容,不涉及到改变内容,那我们可以不使用地址传参:

#include <stdio.h>

typedef struct Person
{
	char name[20];
	char sex[10];
	int age;
}Person;
void print(Person person)
{
	printf("%s %s %d\n", person.name, person.sex, person.age);
}
int main()
{
	Person person = { "张三","男",20 };//结构体已经初始化
	print(person);
	return 0;
}

现在我们想封装一个函数来专门初始化结构体的内容,那么这时候就需要传址调用:

#include <stdio.h>
#include <string.h>
typedef struct Person
{
	char name[20];
	char sex[10];
	int age;
}Person;

void Init(Person* per)
{
	strcpy(per->name, "张三");//per->name 相当于 (*per).name ,找到的是数组的地址,
	strcpy(per->sex, "男");//如果 per->name = "张三"; 这样赋值的话是相当于在修改数组的地址
	per->age = 20;
	printf("%s %s %d\n", per->name, per->sex, per->age);
}
int main()
{
	Person person;
	Init(&person);
	return 0;
}

2. 位段

  • 结构体是有能力实现位段的。位段的成员必须是整形家族。

2.1 位段的声明

我们先看看位段是如何声明的:

struct A
{
	char a : 2;
	char b : 3;
	char c : 4;
};
int main()
{
	return 0;
}

2.2 位段的内存分配

我们应该如何计算位段的大小呢?

#include <stdio.h>
struct A
{
	char a : 2;
	char b : 3;
	char c : 4;
};
int main()
{
	printf("%d", sizeof(struct A));
	return 0;
}

首先,位段,就是控制位。就比如上面这段代码,我们定义了一个 char 类型变量 a 。这个 a 本来是有 8 个比特位的。但是我使用位段使 a 的比特位只有 2 个了,b 修改成只有 3 个了,c 修改成只有 4 个了。

在定义 a 时就会开辟一个字节的空间大小,即 8 个比特位。但是 a 现在只有 2 个比特位,放进这一个字节当中还剩 6 个比特位,b 只有 3 个比特位还可以往里放,这时空间大小还剩 3 个比特位,这就不够 c 放了,因为 c 有 4 个比特位,那么此时又会单独开辟一块空间一字节的空间大小。所以位段 A 的大小是 2 字节。

了解了如何计算大小,那么我们来研究位段如何存储数据:

#include <stdio.h>
#include <string.h>
struct A
{
	char a : 2;
	char b : 3;
	char c : 4;
};
int main()
{
	char arr[2];
	struct A* p = (struct A*)arr;
	memset(arr, 0, 2);
	p->a = 2;
	p->b = 3;
	p->c = 4;
	printf("%02x %02x\n", arr[0], arr[1]);
	return 0;
}

分析一下这个程序:我们定义了一个位段指针 p 指向了 arr 强转为位段 struct A* 之后的空间,也就是说数组 arr 不能用 char 类型的方式查看了,而是要用 struct A 的位段形式查看。我们往 arr 数组里存放 2、3、4 这个几个数字。2 的二进制为 10,可以存放至 a 的两个比特位当中,3 的二进制为 11,可以存放至 b 的三个比特位当中,4 的二进制为 110,可以存放至 c 的四个比特位当中。

这里在提一嘴,如果我们定义的数据的二进制位超过了我们定义的位段,那么就会发生截断。这与 整形数据放在字符类型空间里的道理一样。 

3. 枚举

3.1 枚举的基本概念

  • 顾名思义枚举就是列举,把可能的取值列举出来。 

例如在我们的生活中,周一到周日是有限的七天,可以一一列举。

3.2 枚举的定义

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
int main()
{
	return 0;
}

以上就是枚举的定义。我们要补充的是,枚举的默认值从 0 开始,每往下走递增 1 。就好比上面这段代码,Mon 的值默认为 0 ,Tues 的值从 0 递增 1,为 1,Wed 为 2,Thur 为 3……

当然我们可以自定义,不从 0 开始:

enum Color
{
	Red,
	Yellow,
	Green = 80,
	Brown,
	Black = 90,
	White
};
int main()
{
	return 0;
}

Red 的值为 0 ,Yellow 为 1,但是 Green 我们给它赋了 80 ,那么 Brown 就应该为 81,同理 White 为 91 。

3.3 枚举的使用 

enum Color
{
	Red,
	Yellow,
	Green = 80,
	Brown,
	Black = 90,
	White
};
enum Color co1 = Red;
int main()
{
	enum Color co2 = Brown;
	enum Color co3 = 66;//此种写法是不推荐的,因为存在类型差异
	return 0;
}

我们只需要注意,尽量把枚举成员赋给枚举变量即可,避免类型差异。

3.4 枚举的优点

我们本可以使用 #define 来定义常量,但为什么要使用枚举?

  • 增加代码的可读性和可维护性
  • 和 #define 定义的标识符比较,枚举有类型检查,更加严谨
  • 防止命名污染
  • 便于调试
  • 使用方便,一次可以定义多个常量

4. 联合(共用体)

4.1 联合的基本概念

  • 联合体也是一种特殊的自定义类型。
  • 这种类型定义的变量也包含一系列成员,特征是这些成员共用一块空间

4.2 联合类型的声明

union Un
{
	char i;
	int c;
};
int main()
{
	return 0;
}

这个就是最基本的联合体声明。

4.3 联合的特点

联合的成员是共用同一块内存空间的,联合的大小至少是最大成员的大小。 

为了验证联合是共用同一块内存空间的,我们可以写这样一个程序:

#include <stdio.h>
union Un
{
	char i;
	int c;
}Un;
int main()
{
	printf("%p\n%p\n", &(Un.i), &(Un.c));
	return 0;
}

可以看到,两个不一样的变量但是地址却一样,这就说明了联合的成员是共用同一块内存空间的。 

那怎么计算联合的大小呢?非常简单:

#include <stdio.h>
union Un
{
	char i;
	int c;
}Un;
int main()
{
	printf("%d", sizeof(Un));
	return 0;
}

成员里面谁的类型最大?int ,有 4 个字节,所以联合的大小为 4 。

 但是,我们同样不能忽略对齐数。

#include <stdio.h>
union Un
{
	char a[5];
	int c;
}Un;
int main()
{
	printf("%d", sizeof(Un));
	return 0;
}

像这个程序,成员最大的是 char a[5]; ,5 个字节,但是 5 个字节显然不合理。所以联合也需要对齐最大对齐数,很明显,最大对齐数为 4 ,所以联合的大小为 8 。

  • 34
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
C语言中,结构体是一种自定义数据类型,可以将不同类型的变量组合在一起,形成一个新的数据类型结构体定义的基本形式如下: ```c struct 结构体名 { 数据类型 成员名1; 数据类型 成员名2; // ... }; ``` 其中,结构体名是用户自定义的名称,成员名是结构体中每个成员的名称,数据类型可以是任意C语言的数据类型,包括基本数据类型自定义数据类型结构体变量的定义方式如下: ```c struct 结构体名 变量名; ``` 读取结构体中的成员变量可以通过“.”运算符来实现,例如: ```c #include <stdio.h> struct Person { char name[20]; int age; }; int main() { struct Person p; printf("请输入姓名:"); scanf("%s", p.name); printf("请输入年龄:"); scanf("%d", &p.age); printf("姓名:%s,年龄:%d\n", p.name, p.age); return 0; } ``` 枚举是一种特殊的数据类型,用于定义一组常量。枚举的定义方式如下: ```c enum 枚举名 { 常量名1, 常量名2, // ... }; ``` 其中,枚举名是用户自定义的名称,常量名是枚举中每个常量的名称。枚举常量的值默认是从0开始自动递增的,也可以手动指定值。例如: ```c #include <stdio.h> enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; int main() { enum Weekday today = Tuesday; printf("今天是星期%d\n", today + 1); return 0; } ``` 联合是一种特殊的数据类型,它的成员变量共享同一块内存空间。联合的定义方式如下: ```c union 联合名 { 数据类型 成员名1; 数据类型 成员名2; // ... }; ``` 其中,联合名是用户自定义的名称,成员名是联合中每个成员的名称,数据类型可以是任意C语言的数据类型,但所有成员的大小不能超过联合的大小。例如: ```c #include <stdio.h> union Number { int i; float f; }; int main() { union Number n; n.i = 123; printf("int: %d, float: %.2f\n", n.i, n.f); n.f = 3.14; printf("int: %d, float: %.2f\n", n.i, n.f); return 0; } ``` 以上就是C语言自定义数据类型中的结构体枚举、联合的基本用法和注意事项。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小龙向钱进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值