[TOC]

在C语言中,有几个比较特殊的自定义类型:结构体枚举联合

本篇博客,就让我们来认识一下这些自定义类型吧!😶

1.结构体

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

1.1结构体的声明

以个人信息为例,有姓名、性别、年龄、身高等几个元素。可以定义结构体如下

1
2
3
4
5
6
7
struct Stu
{
char name[20];
char sex[5];
int age;
int hight;
}s2, s3, s4;//s2,s3,s4全局变量

1.2特殊声明

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

1
2
3
4
5
6
7
8
9
10
11
12
13
struct
{
char c;
int a;
double d;
}sa;

struct
{
char c;
int a;
double d;
}*ps;

这两个结构体就是两个匿名结构体类型,省略了结构体标签。

匿名结构体类型只能定义一次,后续无法使用

这两个结构体的内容是完全一样的。

在第二个结构体里面,定义了一个结构体指针*ps,请问这个指针能存放&sa吗?

1
2
3
4
5
6
int main()
{
//编译器认为等号两边是不同的结构体类型,所以这种写法是错误的
ps = &sa;
return 0;
}

1.3结构体的自引用

在定义结构体的时候,可以包含一个该结构体本身的成员

1
2
3
4
5
6
7
8
9
10
11
12
13
//代码1-错误
struct Node
{
int data;
struct Node next;
};

//正确的自引用方式
struct Node
{
int data;
struct Node* next;
};

在很多时候,我们会使用typedef函数来为结构体重命名

  • typedef函数不能重命名匿名结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
int data;`
Node* next;
}Node;
//不能重命名匿名结构体

//正确写法
typedef struct Node
{
int data;
struct Node* next;
}Node;

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

1
2
3
4
5
6
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.5结构体内存对齐

结构体在内存中的存储方式比较特殊

1
2
3
4
5
6
struct S1
{
char c1;//1
int i;//4
char c2;//1
};

结构体里面一共存放了6个字节的内容,按理来说,它所占的空间应该也是6个字节。

但当我们用sizeof计算它的长度的时候,却得到了12个字节。

image-20220128113007239

这是为什么呢?

对齐规则

结构体在内存中存放的时候,为了保证内存读取效率,需要进行内存对齐

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

对齐数 = min(编译器默认的一个对齐数, 该成员变量的类型大小),在vs2019中默认对齐数是8;

  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

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

offsetof

offsetof函数:查看结构体成员变量相对于首地址的偏移量

image-20220128114128701

  • i变量是int类型,4个字节,默认对齐数是8,对齐数是4。C2变量是1个字节,对齐数就是1。

  • 所以int i就要从内存的4的整数倍的位置开始存放,也就是第5个字节的位置。c2在int后,在对齐数1的整数倍的位置进行存放,也就是第9个字节的位置。

  • 而结构体的总大小应该是最大对齐数(这里就是4)的整数倍,9并不是4的整数倍,所以结构体的总大小应为12个字节

如图,结构体的3个成员在内存中存放并不是完全连续的。char c1int i之间相隔了3个字节,char c2后还空出了3个字节。

image-20220128114636606

特殊情况:数组

1
2
3
4
5
struct Stu 
{
int i; //4 4,8 - 4
char c[5]; //5 1,8 - 1
};

数组在计算对齐数的时候,是看单个元素的大小,而不是看整个数组的大小。

  • char c[5]数组占5个字节
  • 计算对齐数的时候,用char类型的1个字节和8对比,对齐数是1

(请忽略图中char数组的下标)

image-20220128173750326


那么为什么需要内存对齐呢?

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

  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。

image-20220128115200293

在设计结构体的时候,我们可以把占用空间小的结构体成员尽量放在一起,这样可以减小结构体所占的空间

image-20220128140322397


1.6修改默认对齐数

我们可以利用#pragma预处理指令来更改默认对齐数

1
2
3
4
5
6
7
8
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

image-20220128135855099


1.7结构体传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

在结构体传参的时候,我们最好传结构体的地址。

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

image-20220128140558021


2.位段

位端是一种特殊的结构体

2.1什么是位段?

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

1.位段的成员必须是 int、unsigned int 或signed int

2.位段的成员名后边有一个冒号和一个数字。

1
2
3
4
5
6
7
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};

成员名后的数字表示,该成员需要几个比特的空间来存储

在结构体内,最小的成员char类型都需要1个字节的空间。但有些数据并不需要1个字节来存储,这时候就可以用位段来减少空间占用

1
int _a:2;//2bit - 00 01 10 11 -四种情况

位端的大小如下:先是开辟4个字节的空间,进行a、b、c的连续存放。存放完毕后还剩15个bit的空间,不够存放d的30个bit,所以开辟了第二个4个字节的空间,用来存放成员d

image-20220128141207420


2.2位段在内存中的存储

  • 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

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

  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

1
2
3
4
5
6
7
8
9
10
11
12
13
//一个例子
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;//1010-截断-010
s.b = 12;//1100-完整保存-1100
s.c = 3;//0011-前补0-00011
s.d = 4;//0100-完整保存-0100
  • a和b占一个字节
  • c占独立的一个字节
  • d占一个字节

它在内存中的存储如下图所示:

image-20220128145404821

2.3位段的跨平台问题

在不同编译器下,位段的存储方式会有很大的不同

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

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

2.4位段的应用

在互联网中,数据在服务器和服务器之间传递的时候,会参照以下的范式。

图里的4位(4bit)版本号首部长度,以及3位标志,都没有达到1个字节的大小。这时候如果用结构体来保存,就会存在较大的空间浪费,从而加大服务器的压力。

这时候就适合采用位段来存储这样的内容,节省空间。

image-20220128150137035


3.枚举

枚举的意思是一一列举

在生活中有一些事物的类型是可以一一列举出来的(有限的)。

比如:人的性别、星期、12个月。

在C语言中,就可以用枚举类型来定义这种有限的元素

3.1枚举类型的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};

需要注意的是,枚举类型和结构体是不同的。

  • { }中的内容是枚举类型的可能取值,也叫枚举常量

  • 枚举类型代表具体的数值。默认是从0开始,以1递增。

  • 枚举类型可以用来替代数值

比如在day的枚举类型里面,每一个元素分别代表一个数字。默认是从0开始,以1递增。

1
2
3
4
5
6
7
8
9
10
enum Day//星期
{
Mon,//0
Tues,//1
Wed,//2
Thur,//3
Fri,//4
Sat,//5
Sun//6
};

我们在定义的时候也可以赋初值

1
2
3
4
5
6
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};

如果你只对其中一个常量赋值了,后面的常量也是以1递增的

1
2
3
4
5
6
7
8
9
10
enum Day//星期
{
Mon,//0
Tues,//1
Wed=5,//5
Thur,//6
Fri,//7
Sat,//8
Sun//9
};

3.2枚举的优点

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

枚举的优点:

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较枚举有类型检查,更加严谨。
  3. 防止了命名污染(用了{ }封装)
  4. 便于调试
  5. 使用方便,一次可以定义多个常量

其中第四点的内容见下图:

image-20220128161009911


3.3枚举的使用

1
2
3
4
5
6
7
8
9
10
11
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
//在定义完枚举常量之后,无法在外部进行更改!
RED=3,//err

enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5; //err
  • enum Color里面的内容是枚举常量
  • enum Color clr是一个枚举变量

需要注意的是,如果你用数字给枚举变量赋值,在.c文件下不会报错,但是在.cpp文件下会报错

  • CPP文件的语法检查更严格!

image-20220128164300898

那么,如何日常使用中应用枚举类型呢?

在计算器的代码中👉【博客链接】

我们可以使用枚举常量来替代干巴巴的case 0case 1等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//这只是个示例
//详细代码见之前的博客
enum Options
{
EXIT,//0
ADD,//1
SUB,//2
MUL,//3
DIV//4
};

void menu()
{
printf("**********************************\n");
printf("***** 1. add 2. sub *****\n");
printf("***** 3. mul 4. div *****\n");
printf("***** 0. exit *****\n");
printf("**********************************\n");
}

int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case ADD:
//用枚举类型代替原本的数字,增强代码可读性
break;
case SUB:
break;
case MUL:
break;
case DIV:
break;
case EXIT:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);

return 0;
}

4.联合体

4.1联合类型的定义

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

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

1
2
3
4
5
6
7
8
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;

当我们计算这个联合体的大小的时候,发现它只有4个字节,并不是5个字节。

而且char cint i元素的起始地址相同,这说明它们是公用这4个字节的空间的。

image-20220128163113396

4.2联合体的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小

(因为联合至少得有能力保存最大的那个成员)

1
2
3
4
5
6
7
8
9
10
union Un
{
int i;
char c;
};
union Un un;
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);

image-20220128164958206

4.3用联合体判断大小端

在之前数据存储的学习中,我们了解到了什么是编译器的大小端,并写了一个函数来判断当前编译器的大小端👉链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int check_sys()
{
int a=1;
char*p=(char*)&a;
if(1==*p)
return 1;
else
return 0;
}

int main()
{
int b=check_sys();
if(1==b)
printf("小端\n");
else
printf("大端\n");

return 0;
}

将int类型地址强制转换成char*类型

  • 如果是小端,第一个地址是01
  • 如果是大端,第一个地址是00

今天我们就利用联合体的成员在内存中共享同一空间的特点,来改进这个代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int cheak_sys()
{
union
{
char c;
int i;
}u;
u.i = 1;//将int类型改成1
return u.c;//返回该类型的第一个字节
}

int main()
{
int ret = cheak_sys();
if (1 == ret)
printf("小端\n");
else
printf("大端\n");

//如果返回1,表示小端
//如果返回0,表示大端
return 0;
}

使用联合体,就无需进行指针类型的强制转换

对char c类型的定义可以覆盖掉int i的第一个字节


4.4联合体在内存中的存储

和结构体一样,联合体也需要进行内存对齐

  • 联合的大小至少是最大成员的大小
  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
1
2
3
4
5
6
7
8
9
10
11
union Un1
{
char c[5];//5 1,8 - 1
int i; //4 4,8 - 4
};

union Un2
{
short c[7];//14 2,8-2
int i; //4 4,8-4
};

需要注意的是,联合体在计算对齐数的时候,数组是按一个元素的大小进行计算,而不是以整个数组的大小进行计算!(这一点和结构体是一样的)

Un2中short数组的对齐数:

  • short c[7]一共14个字节
  • 每个元素是2个字节
  • 默认对齐数是8
  • 所以它的对齐数是2

image-20220128172445587

Un1为例,它在内存里的存储方式如图 (请忽略图中char数组的下标)

  • char c[5]和int i类型共用前5个字节(其中int i占4个字节)
  • 因为需要对齐到最大对齐数的整数倍,所以大小是8

image-20220128172848392


结语

自定义类型的内容非常丰富

你学会了吗?

码字不易,若有帮助,点个赞呗!