超全超详细的C语言结构体、位段、枚举、联合体详解

自定义类型

文章目录

  • 自定义类型
    • 1. 结构体(struct)
      • 1.1 结构体的基本概念
      • 1.2 结构体的声明
        • 1.2.1 特殊的声明
      • 1.3 结构体的自引用
      • 1.4 结构体的嵌套
      • 1.5 结构体成员的引用
      • 1.6 结构体变量的初始化
      • 1.7 计算结构体的大小(重点)
        • 1.7.1 结构体的内存对齐
        • 1.7.2 修改默认对齐数
      • 1.8 结构体传参
    • 2. 位段
      • 2.1 什么是位段
      • 2.2 位段的内存分配
      • 2.3 位段的跨平台问题
      • 2.4 位段的实际应用
    • 3. 枚举(enum)
      • 3.1 枚举类型的定义
      • 3.2 枚举的优点
      • 3.3 枚举的使用
    • 4. 联合(共用体)(union)
      • 4.1 联合类型的定义
      • 4.2 联合的特点
      • 4.3 联合体大小的计算
    • 5.总结

1. 结构体(struct)

1.1 结构体的基本概念

结构体是一些值的集合,这些值称为成员。结构体的每个成员·可以是不同的类型的变量

1.2 结构体的声明

我们来看结构体声明的基本格式:

struct tag
{
	member - list;
}varlible-list;

超全超详细的C语言结构体、位段、枚举、联合体详解_第1张图片

例如我们要描述一个学生:

struct Student
{
	char name[20];	//姓名
	int age;		//年龄
	long long id;	//学号
};

上面的数组name,变量age,id都叫做这个学生结构体的成员变量

而有了上面的结构体的声明,我们就可以创建出关于这个结构体的结构体变量:

struct Student
{
	char name[20];	
	int age;		
	long long id;	
}Stu1,Stu2,Stu3;

struct Student Stu4, Stu5, Stu6;

1.2.1 特殊的声明

注:此类特殊的声明实际用处不大,大家仅作了解即可

在声明结构体的时候,可以是不完全的声明:

struct
{
	int a;
	float b;
	char c;
};

例如上面的结构体类型省略了标签名,我们就称之为匿名结构体类型

  • 那么匿名结构体类型怎么创建变量呢?

  • 我们需要清楚,如果想要用匿名结构体创建变量,只能在其结构体类型声明好后立刻创建,如:

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

我们继续来看下面的代码:

#include

struct
{
	int a;
	float b;
	char c;
}x;

struct
{
	int a;
	float b;
	char c;
}* p;

int main()
{
    p = &x;   //ok?
    
    return 0;
}
  • 我定义了两个成员列表一模一样的匿名结构体变量x和*p,那么大家认为这串代码p = &x; 是否正确?也就是说,编译器是否会认为这两个匿名结构体类型会一模一样呢?

  • 编译后我们发现,会报警告:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iB32Fr8f-1689169418588)(C:/Users/HUASHUO/AppData/Roaming/Typora/typora-user-images/image-20230712170610947.png)]

  • 说明**在编译器看来,这两个匿名结构体是不一样的,p = &x; 这个操作在本质上也是错误的,**这点大家需要注意

1.3 结构体的自引用

结构体是可以自引用的

那么具体的,结构体该如何自引用呢?

可能有小伙伴会这样写:

struct Node
{
	int data;
	struct Node next;
};
  • 如果是这样写,那么我们来计算一下sizeof(struct Node)

超全超详细的C语言结构体、位段、枚举、联合体详解_第2张图片

  • 如图所示,如果我们采取这种方式来实现结构体的自引用,sizeof()的值便无法计算,显然这种方法是错误的
  • 事实上,正确的自引用方式应该是这样的:
struct Node
{
	int data;
	struct Node* next;
};
  • 我们用结构体指针来实现结构体的自引用

需要注意,下面这种自引用的写法也是错误的

//error example

typedef struct Node
{
	int data;
	Node* next;
}Node;
  • 我们不能在完成typedef之前,就使用已经typedef后重命名的名称
  • 注:对typedef还不太了解到小伙伴可以看看【C语言】typedef的使用

1.4 结构体的嵌套

一个结构体中可以嵌套另一个结构体

例如:

struct Grade
{
	float math;
	float English;
	float Chinese;
};

struct Stu
{
	struct Grade grade;
	char name[20];
	long long id;
};
  • 需要注意,如果一个结构体嵌套了另一个结构体,那么这个被嵌套的结构体在使用之前必须要被声明,例如,下面的写法就是错误的
//error example

struct Stu
{
	struct Grade grade;
	char name[20];
	long long id;
};

struct Grade
{
	float math;
	float English;
	float Chinese;
};

1.5 结构体成员的引用

  • 如果被引用的结构体变量不是结构体指针,那么用操作符.来进行结构体成员引用
  • 如果被引用的结构体变量是结构体指针,那么用操作符->来进行结构体成员引用
  • 例如:
#include

typedef struct Stu
{
	struct Grade grade;
	char name[20];
	long long id;
}Stu;

int main()
{
	Stu s1;
	Stu* s2 = &s1;
    
	printf("math:%f  English:%f  Chinese:%f\nname:%s\nid:%lld\n", 
           s1.grade.math, s1.grade.English, s1.grade.Chinese, s1.name, s1.id);
    
	printf("math:%f  English:%f  Chinese:%f\nname:%s\nid:%lld\n",
           s2->grade.math, s2->grade.English, s2->grade.Chinese, s2->name, s2->id);
	return 0;
}

1.6 结构体变量的初始化

对结构体变量成员的初始化有很多方式

  • 在定义的时候直接初始化:
//方法一:
//按默认顺序依次初始化
struct Grade
{
	float math;
	float English;
	float Chinese;
}g1 = {12,13,14};

struct Stu
{
	struct Grade grade;
	char name[20];
	long long id;
}Stu1 = { {12,13,14},"abcde",123456 };	//结构体的嵌套初始化


//方法二:
//用操作符 . 实现自定义顺序初始化
struct Grade
{
	float math;
	float English;
	float Chinese;
}g1 = {.Chinese = 76, .English = 42, .math = 55};
  • 对结构体成员引用后再初始化:
#include

struct Grade
{
	float math;
	float English;
	float Chinese;
}g1;

int main()
{
	g1.Chinese = 12;
	g1.English = 13;
	g1.math = 15;
    
    return 0;
}

1.7 计算结构体的大小(重点)

我们先来看一串代码:

struct Text1
{
	char a;
	int b;
	char c;
};

struct Text2
{
	int b;
	char a;
	char c;
};

int main()
{
	printf("%d\n", sizeof(struct Text1));
	printf("%d\n", sizeof(struct Text2));
}
  • 可能有小伙伴认为这两个结构体的成员都一样,那打印的结果也就都是1+1+4 = 6,让我们看看结果:

超全超详细的C语言结构体、位段、枚举、联合体详解_第3张图片

  • 是不是出乎意料?至于为什么,就先让我们一起来了解下面的内容,只有这样,我们才能彻底搞清楚这是怎么一回事

1.7.1 结构体的内存对齐

要搞清楚如何计算结构体的大小,就需要搞清楚结构体在内存中是如何存储的,而要弄清楚结构体在内存中是怎么存储的,我们就需要借助一个宏offsetof来帮助我们查看

offsetof (type,member)
  • offsetof()可以计算结构体成员相较于结构体起始位置的偏移量

  • 需要头文件

  • 什么是相较于结构体起始位置的偏移量?我通过画图来说明:

超全超详细的C语言结构体、位段、枚举、联合体详解_第4张图片

现在,我们就以上面的结构体struct Text1为例,看看他在内存中到底是怎么存放的

#include`
#include

struct Text1
{
	char a;
	int b;
	char c;
};

int main()
{
	printf("%d\n", offsetof(struct Text1, a));
	printf("%d\n", offsetof(struct Text1, b));
	printf("%d\n", offsetof(struct Text1, c));

	return 0;
}

可以得到这样的结果:

超全超详细的C语言结构体、位段、枚举、联合体详解_第5张图片

通过画图分析,可以得到结构体成员a,b,c大概是这样存储的:

超全超详细的C语言结构体、位段、枚举、联合体详解_第6张图片

  • 如果是这样的话,也只占9个字节呀,为什么得到的结果却是12个字节呢?

通过上面现象的分析,我们发现结构体成员不是按照顺序在内存中连续存放的,而是有一定的对齐规则的

结构体内存对齐的规则:

  1. 结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置

  2. 从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处对齐数:结构体成员自身大小和默认对齐数的较小值

  • VS上默认对齐数是8

  • gcc没有默认对齐数,对齐数就是结构体成员的自身大小

  1. 结构体的总大小,必须是最大对齐数的整数倍最大对齐数:所有成员的对齐数中的最大值

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

这样,我们就可以对上面的两个例子做出合理的解释了:

  • 对于struct Text1char a的对齐数为1,故存放在偏移量为0的第一个字节,int a的对齐数为4,而1.2.3都不是4的整数倍,因此,这三个字节都被浪费,a从偏移量为4的字节开始存放,共占4个字节,char c的对齐数为1,8是1的整数倍,故存放在偏移量为8的字节处,而整个结构体成员的最大对齐数为4,且此时结构体已经占了9个字节,为了达到所占字节数为4的整数倍,故还要浪费3个字节,因此该结构体所占的字节数就为12
  • 对于struct Text2,仍是同理,这里就不再用文字赘述,我通过画图来分析:

超全超详细的C语言结构体、位段、枚举、联合体详解_第7张图片

我们再来看最后一道题

#include

struct Text1
{
	char a;
	double b;
	int c;
};

struct Text2
{
	struct Text1 text;
	char d;
	double e;
};

int main()
{
	printf("%d\n", sizeof(struct Text2));

	return 0;
}

超全超详细的C语言结构体、位段、枚举、联合体详解_第8张图片

为什么要内存对齐呢?

主要有两点原因:

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

举个例子:

对于32位机器(字长是32位,每次读写数据是32bit)

超全超详细的C语言结构体、位段、枚举、联合体详解_第9张图片

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

因此,在设计结构体的时候,为了做到既节省空间,又要满足对其,要如何做到呢?

让占用空间小的成员尽量集中在一起

例如我们最开始讲的两个成员相同但所占空间不同的结构体:

struct Text1
{
	char a;
	int b;
	char c;
};

struct Text2
{
	int b;
	char a;
	char c;
};

1.7.2 修改默认对齐数

我们可以用#pragma预处理指令,来实现对默认对齐数的修改

例如:

#pragma pack(1)		//将默认对齐数修改为1
struct Text1
{
	int b;
	char a;
	char c;
};

#pragma pack()		//取消设置的对齐数,还原为默认值

struct Text2
{
	int b;
	char a;
	char c;
};

int main()
{
	printf("%d\n", sizeof(struct Text1));
	printf("%d\n", sizeof(struct Text2));

	return 0;
}

可以得到:

超全超详细的C语言结构体、位段、枚举、联合体详解_第10张图片

1.8 结构体传参

直接上实例代码:

#include

struct Stu
{
	char name[20];
	long long id;
	int age;
};

//结构体传参
void Print1(struct Stu s)
{
	printf("%s\n%lld\n%d\n", s.name, s.id, s.age);
}

//结构体地址传参
void Print2(struct Stu* s)
{
	printf("%s\n%lld\n%d\n", s->name, s->id, s->age);
}

int main()
{
	struct Stu s = { "abcde",123456,18 };	//初始化

	Print1(s);	//传结构体
	Print2(&s);	//传结构体地址

	return 0;
}

需要注意:

函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销

如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降

故:结构体传参的时候,要传地址

2. 位段

我们可以用结构体来实现位段

2.1 什么是位段

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

  1. 位段的成员必须是int, unsigned int, signed int, char
  2. 位段的成员名后边有一个冒号和一个数字
  3. 位段的成员名后面的数字的大小不能大于成员类型所占比特位的大小

例如:

struct Text
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

Text就是一个位段类型

那么位段Text的大小又是多少呢?

printf("%d\n", sizeof(struct Text));

2.2 位段的内存分配

  • 位段的空间上是按照需要以4个字节(int)或者1个字节(char)方式来开辟的
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

下面以VS2019编译器为例,分析一个位段在内存中如何存储的例子:

#include

struct Text
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct Text s;
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;

	printf("%d\n", sizeof(struct Text));

	return 0;
}

我们先来进行一波假设:

超全超详细的C语言结构体、位段、枚举、联合体详解_第11张图片

可以得到我们的分析似乎是正确的,那我们继续分析,可以得到位段成员应该在内存中是这么存储的:

超全超详细的C语言结构体、位段、枚举、联合体详解_第12张图片

通过观察内存,可以得到我们的分析是正确的

再来看一个例子:

struct Text
{
	char a : 3;
	int b : 4;
	char c : 5;
	char d : 4;
};

printf("%d\n", sizeof(struct Text)); 	//?

可以得到:

超全超详细的C语言结构体、位段、枚举、联合体详解_第13张图片

为什么?

  • 学到这里,我们可不能忘了位段也是靠结构体来实现的
  • 而我们前面提到,在结构体中存在内存对齐的现象
  • 因此,我们就可以通过画图来分析:

超全超详细的C语言结构体、位段、枚举、联合体详解_第14张图片

可以得出结论:

在VS2019编译器上:

  • 联合体成员是从低位向高位存储的
  • 当一次性开辟的空间不足以存储下一个数据时,应该浪费剩余的空间,重新开辟一块新的内存进行存储
  • 位段由结构体实现,也存在内存对齐的现象,位段的大小也应该是最大对齐数的整数倍

2.3 位段的跨平台问题

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

总结:

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

2.4 位段的实际应用

例如:网络上IP数据包的格式

超全超详细的C语言结构体、位段、枚举、联合体详解_第15张图片

3. 枚举(enum)

枚举顾名思义就是一一列举。

把有可能的取值一一列举。

例如现实生活中:

一周的星期一到星期日我们可以一一列举

一年有十二个月我们也能一一列举

…………

C语言中,同样存在枚举类型:

3.1 枚举类型的定义

需要特别注意:枚举是对可能情况的一一列举,因此使用,号分割,这与结构体成员用;分隔是不同的

enum Day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

上面定义的enum Day就是枚举类型,{}中的内容就是枚举类型的可能取值,叫做枚举常量

这些可能取值都是有值的,默认从0开始,依次递增;也可以在声明枚举类型的时候进行赋值

例如:

enum Color
{
	RED,
	YELLOW,
	BLUE
};

int main()
{
	printf("%d %d %d", RED, YELLOW, BLUE);
}

结果为:

超全超详细的C语言结构体、位段、枚举、联合体详解_第16张图片

enum Color
{
	RED = 3,
	YELLOW = 1,
	BLUE = 5
};

int main()
{
	printf("%d %d %d", RED, YELLOW, BLUE);
}

结果为:

超全超详细的C语言结构体、位段、枚举、联合体详解_第17张图片

enum Color
{
	RED,
	YELLOW,
	BLUE = 5
};

int main()
{
	printf("%d %d %d", RED, YELLOW, BLUE);
}

结果为:

超全超详细的C语言结构体、位段、枚举、联合体详解_第18张图片

3.2 枚举的优点

我们可以用# define定义常量,为什么非要使用枚举呢?其实,枚举有以下几个优点

  1. 增加代码的可读性和可维护性

  2. # define定义的标识符比较,枚举类型具有类型检查

例如,下面这串代码在cpp文件中是不能通过的:

//error example

enum Color
{
	RED,
	YELLOW,
	BLUE = 5
};

int main()
{
	enum Color cc = 3;
    //3是int类型,而cc是枚举类型
}
  1. 便于调试

  2. 使用方便,一次可以定义多个常量

3.3 枚举的使用

#include

enum Color
{
	RED = 3,
	YELLOW = 4,
	BLUE =6
};

int main()
{
	int a = RED;	//将一种可能情况赋值给整形a
	printf("%d\n", a);

	enum Color clr = RED;	//只能拿枚举常量给枚举变量赋值
	printf("%d\n", clr);

	return 0;
 }

4. 联合(共用体)(union)

4.1 联合类型的定义

联合也是一种特殊的自定义类型

这种类型定义的变量也是包含一系列的成员,特征是这些成员公用一块空间(所以联合体也叫共用体)

例如:

//联合类型的声明
union UN
{
	char c;
	int i;
};

//联合变量的定义
union UN un;

4.2 联合的特点

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

我们来看一个例子:

union UN
{
	char c;
	int i;
};

union UN un;

int main()
{
    //输出结果一样吗?
	printf("%p\n", &(un.c));
	printf("%p\n", &(un.i));

    //un.i会是多少呢?
	un.i = 0x11223344;
	un.c = 0x55;
	printf("%x\n", un.i);

	return 0;
}

我们一起来分析(以VS2019编译器为例):

  • 有联合体的特性我们知道,这个联合体的大小是sizeof(int)4个字节
  • 由于联合体的成员共用一块空间,那我们假设成员i和c的起始地址是一样的,由于栈空间是从高地址向低地址开始存放的且读取数据时是从低地址向高地址读取,故可画出示意图:

超全超详细的C语言结构体、位段、枚举、联合体详解_第19张图片

  • 由于VS2019采用的是小端存储(高位字节的数据放高地址,低位字节的数据放低地址),那么un.i的数据在内存中就是这样存储的:
  • 注:对小端字节序存储还不太了解的小伙伴可以看看整数在内存中的存储

超全超详细的C语言结构体、位段、枚举、联合体详解_第20张图片

  • 此时,我们再对只占一个字节的un.c赋值,那么存储的数据就变成了这样:

超全超详细的C语言结构体、位段、枚举、联合体详解_第21张图片

  • 所以打印出的结果应该是0x11223355

超全超详细的C语言结构体、位段、枚举、联合体详解_第22张图片

  • 符合预期。

可以得出结论:

联合体所有成员公用一块空间,并且,每个成员的起始地址应该是一样的

4.3 联合体大小的计算

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

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

例如:

union UN
{
	char c[5];
	int i;
};

union UN un;
printf("%d\n", sizeof(un));
  • 结果是5
  • 尽管这个联合体的大小应该是sizeof(c) = 5,但是最大对齐数为sizeof(int) = 4,因此为了达到最大对齐数的整数倍,要浪费3个字节,故这个联合的大小为8个字节

5.总结

本次我们学习了C语言的自定义类型——结构体(struct)、位段、枚举(enum)、联合(union)

应该重点掌握以下类容

自定义类型的基本使用

熟悉结构体内存对齐的规则

熟悉各自定义类型的特点,并知道计算各自定义类型所占空间的大小


如果觉得本篇文章对你有所帮助,还请点个赞支持一下博主,同时你也可以关注博主,博主会继续发布更多博文与大家分享、学习

共勉!!!( ̄y▽ ̄)╭ Ohohoho…

你可能感兴趣的:(C语言,c语言,开发语言)