目录
前言
在前面C语言初阶中,我们一起初步了解了自定义类型–结构体,本章将带大家此基础上继续学习C语言中的自定义类型:结构体、枚举、联合!
1.结构体
在C语言初阶中我们已经一起初步学习了结构体的内容:
1.结构体的声明、定义及初始化
2.结构体成员的访问
3.结构体的传参
内容不多但很重要,大家可以阅读C语言初阶之初始结构体的进行巩固后,然后我们一起继续结构体的学习!
1.1结构体特殊声明
在声明结构体的时候,可以不完全的声明(相当于直接定义结构体变量)
eg:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}*p;
#include<stdio.h>
int main()
{
p = &x;
return 0;
}//上面的代码合法吗?
上面代码中分别用相同的匿名结构体类型去定义变量、指针,并在主函数main中用指针去接收变量的地址,虽然在test.c的文件中编译运行没有问题,但是合理不一定合法。虽然两个结构体类型一样,但编译器会把上面两个相同的匿名结构体类型当成完全不同的两个类型,并且在(.cpp)文件中会发生报错。
在test.c文件中运行的结果:
在test.cpp文件中的运行的结果:
1.2结构体自引用
在结构体包含一个类型为结构本身的成员是否可以呢?
//代码1
struct Node
{
int data;
struct Node next;
};//是否可行?
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct Node));
return 0;
}//输出结果是多少?
代码运行的结果:
上面代码代码运行的结果会报错,因为结构体里面在用相同的结构体定义变量,会无限为结构体开辟空间导致空间不足,那正确的自引用方式是怎么样的呢?
正确的自引用方式:
struct Node
{
int data;
struct Node *next;
};
用结构体指针记录下一个变量的地址,可以找到下一个结构体变量,同时结构体指针的大小是固定的(4/8个字节)。
代码运行的结果:
注意:
typedef struct Node
{
int data;
Node*next;
}Node;//是否可行?
解析:
typedef struct Node
{
int data;
Node*next;
}Node;
//不可行,typedef重命名类型在后面;
//不能先在结构体内部使用新命名的类型
代码运行的结果:
正确的方式为:
typedef struct Node
{
int data;
struct Node* next;
}Node;
1.3结构体变量的定义和初始化
按序初始化:
struct Stu//声明类型
{
char name[20];//名字
int age;//年龄
}p1;//定义
struct Stu p1 = { "zhangsan",20 };//按序初始化
乱序初始化:
struct Stu//声明类型
{
char name[20];//名字
int age;//年龄
}p2;//定义
struct Stu p2 = { .age=38,.name="wujing"};//乱序初始化
按序初始化,就是按着结构体成员的顺序进行初始化;乱序初始化,则是不按结构体成员的顺序进行初始化,但是要指定初始化的成员。
1.4结构体内存对齐
基于前面结构体的学习,我们已经掌握了结构体的基本使用规则,那现在我们讨论一个问题:结构体的大小怎么计算呢?这时我们就得学习结构体的对齐规则了!
结构体的对齐规则:
1.结构体的第一个成员,对齐到结构体内存中存放位置的0偏移处。
2.从第二个成员开始,每个成员都要对齐到(对齐数)的整数倍数;对齐数=结构体成员的自身大小和默认对齐数的较小值;VS:默认对齐数为8;Linux没有默认对齐数,该环境下的对齐数就是结构体成员自身大小。
3.结构体的总大小,必须是所有成员最大对齐数的整数倍。
4.如果结构体中嵌套了结构体成员,嵌套的结构体对齐到自己成员中最大对齐数的整数倍数处。
5.如果结构体中嵌套了结构体成员,结构体的总大小是最大对齐数的整数倍,这里的最大对齐数包含了嵌套结构体成员中的对齐数。
eg1:
//练习1
struct s1
{
char c1;//1/8,对齐数为1
int i;//4/8,对齐数为4
char c2;//1/8,对齐数为1
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s1));
return 0;
}//该结构体的大小为多少呢?
图形理解:
代码运行的结果:
小知识:
offsetof是C语言库中的一个宏,返回结构体成员类型在内存中的偏移量的,包含在头文件#include<stddef.h>中。
验证:
struct s1
{
char c1;
int i;
char c2;
};
#include<stdio.h>
#include<stddef.h>
int main()
{
printf("%d\n", sizeof(struct s1));
printf("%d\n", offsetof(struct s1, c1));
printf("%d\n", offsetof(struct s1, i));
printf("%d\n", offsetof(struct s1, c2));
return 0;
}
eg2:
//练习2
struct s2
{
char c1;//1/8,对齐数为1
char c2;//1/8,对齐数为1
int i;//4/8,对齐数为4
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s2));
return 0;
}//该结构体的大小为多少呢?
图形理解:
验证:
struct s2
{
char c1;
char c2;
int i;
};
#include<stddef.h>
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s2));
printf("%d\n", offsetof(struct s2, c1));
printf("%d\n", offsetof(struct s2, c2));
printf("%d\n", offsetof(struct s2, i));
return 0;
}
eg3:
//练习3
struct s3
{
double d;//8/8,对齐数为8
char c;//1/8,对齐数为1
int i;//4/8,对齐数4
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s3));
return 0;
}//该结构体的大小为多少呢?
图形理解:
验证:
struct s3
{
double d;//8/8,对齐数为8
char c;//1/8,对齐数为1
int i;//4/8,对齐数4
};
#include<stdio.h>
#include<stddef.h>
int main()
{
printf("%d\n", sizeof(struct s3));
printf("%d\n", offsetof(struct s3, d));
printf("%d\n", offsetof(struct s3, c));
printf("%d\n", offsetof(struct s3, i));
return 0;
}
eg4:
//练习4-练习结构体嵌套问题
struct s3
{
double d;//8/8,对齐数为8
char c;//1/8,对齐数为1
int i;//4/8,对齐数4
};//s3的最大对齐数为8
struct s4
{
char c1;//1/8,对齐数为1
struct s3 s;
double d;//8/8,对齐数为8
};//包括嵌套结构体在内的成员最大对齐数为8
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s4));
return 0;
}//该结构体的大小为多少呢?
图形理解:
验证:
struct s3
{
double d;//8/8,对齐数为8
char c;//1/8,对齐数为1
int i;//4/8,对齐数4
};//s3的最大对齐数为8
struct s4
{
char c1;//1/8,对齐数为1
struct s3 s;
double d;//8/8,对齐数为8
};//包括嵌套结构体在内的成员最大对齐数为8
#include<stdio.h>
#include<stddef.h>
int main()
{
printf("%d\n", sizeof(struct s4));
printf("%d\n", offsetof(struct s4, c1));
printf("%d\n", offsetof(struct s4, s));
printf("%d\n", offsetof(struct s4, d));
return 0;
}
eg5:
struct s5
{
char ch[5];//1/8,对齐数为1
int i;//4/8,对齐数4
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s5));
return 0;
}
图形理解:
验证:
struct s5
{
char ch[5];//1/8,对齐数为1
//相当于char c1,char c2,char c3
//char c4,char c5等5个变量
int i;//4/8,对齐数4
};
#include<stddef.h>
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(struct s5));
printf("%d\n", offsetof(struct s5, ch));
printf("%d\n", offsetof(struct s5, ch[2]));
printf("%d\n", offsetof(struct s5, ch[3]));
printf("%d\n", offsetof(struct s5, ch[4]));
printf("%d\n", offsetof(struct s5, i));
return 0;
}
相信通过上面的一些练习,你对结构体内存对齐有了一定了解,那为什么结构体会存在结构体内存对齐呢?
1.平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。
2.性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而在对齐的内存访问中仅需要一次访问。
3.在32位环境下,操作系统按4个字节进行访问,如果进行第一次访问的时候读取到不完整的数据,则需要进行第二次访问,以此类推;如果进行第一次访问的时候读取到都是完整的数据,则算一次访问。
总体来说: 结构体的内存对齐就是拿空间换时间的做法;如果想在设计结构体的时候既要满足内存对齐,又要节省空间,尽量让空间小的成员尽量集中在一起。
1.5修改默认对齐数
#pragma这个预处理指令,可以用来改变默认对齐数
#include<stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct s1
{
char c1;//1/8,对齐数为1
int i;//4/8,对齐数为4
char c2;//1/8,对齐数为1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct s2
{
char c1;//1/1,对齐数为1
int i;//4/1,对齐数为1
char c2;//1/1,对齐数为1
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n",sizeof(struct s1));
printf("%d\n", sizeof(struct s2));
return 0;
}//输出结果是什么?
图形理解:
代码运行结果如下:
结论: 结构在对齐方式不合适的时候,我们可以自己修改默认对齐数。
2.位段
2.1什么是位段?
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是int \unsigned int\signed int类型。
2.位段的成员名后边有一个冒号和一个数字。
eg:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
#include<stdio.h>
int main()
{
printf("%d", sizeof(struct A));
return 0;
}//A是一个位段类型,其大小是多少呢?
代码运行的结果:
为什么输出结果是8呢?这和位段的内存分配有关,接下来我们一起学习位段内存的分配规则吧!🐇🐇🐇
2.2位段的内存分配
位段内存分配规则:
1.位段成员可以是int、unsigned int、signed int、或者char(属于整型家族的类型)。
2.位段的空间是按照需要以4个字节或者1个字节的方式来开辟。
3.位段涉及很多不确定因素,位段是不跨平台的,注意可移植的程序应该避免使用位段。
了解基本规则之后,让我们在VS2019的环境下进行测验吧!
eg:
struct S
{
int a : 3;
int b : 4;
int c : 5;
int d : 4;
};
#include<stdio.h>
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}//空间是如何开辟的呢?
因为位段中是char类型,预先开辟一个字节(8个比特位)的内存空间,但是位段内存分配的时候是向右分配还是向左分配?剩下的空间,是丢弃还是重新利用呢?在这里我们假设是从右向左分配(低地址–>高地址),剩下的空间被丢弃。
图形理解:
调试的结果为:
结论: 在VS2019环境下,位段分配内存是从右向左分配的,剩下的空间被丢弃掉。
2.3位段的跨平台问题
1.位段成员被当成是有符号数还是无符号数是不确定的。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出现问题。)
3.位段成员中是从左向右分配,还是从右向左分配,标准尚未定义。
4.当一个结构包含两个位段,第二个位段成员比较答,剩余的位无法容纳第二个成员时,是舍弃剩余的位还是利用,这是不确定的。
总结: 跟结构相比,位段可以达到同样的效果,但可以很好的空间,但是有跨平台的问题存在。
2.4位段的作用
总结: 如图我们可以将版本号、长度、协议、标识符、地址等网络相关的东西使用位段封装起来,大大地节省了空间,从而使网络的速度得到了有效地提升。
3.枚举
枚举顾名思义就是一一列举;把可能的取值一一列举,比如在我们的现实生活中:一周的星期一到星期天是有限的7天,可以一一列举;性别有:男、女、保密,也可以一一列举;月份有12个月,也可以一一一列举。
3.1枚举类型的定义
enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的enum Day、enum Sex、enum Color都是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。🐼🐼🐼
enum Color//颜色
{
RED,
GREEN,
BLUE
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(enum Color));
printf("%d\n", RED);
printf("%d\n",GREEN);
printf("%d\n",BLUE );
return 0;
}
代码运行的结果:
枚举类型的大小为4个字节,因为枚举常量的值为整型,而使用枚举类型变量的的时候只能赋枚举常量其中的一个值,所以为开辟4个字节的即可。 这些值在最开始没有赋值的时候,是默认从0开始依次递增,也可以在定义的时候进行赋初值。
eg:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=5
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(enum Color));
printf("%d\n", RED);
printf("%d\n",GREEN);
printf("%d\n",BLUE );
return 0;
}
代码运行的结果为:
3.2枚举的优点
学了枚举之后,也许心中会有疑问:我们可以使用#define定义常量,为什么还要使用枚举?这可能是你不知道枚举的优点的原因。
枚举的优点:
1.增加代码的可读性和可维护性
在通讯录的博客中,在功能分块的时候,往往会忘记应该定义什么样的功能书写;在这时候,我们使用使用枚举常量会帮助我们记忆。有利于我们读懂代码,进行代码维护和书写。
eg:
enum Option
{
Exit,
Add,
Del,
Search,
Modify,
Show,
Sort,
Clean,
};
2.和#define定义的标识符相比,枚举有类型检查,更加严谨。
#define定义的常量,在代码运行的过程,只是把定义的常量替换成对应的数字;而枚举类型定义的变量属于枚举类型,会有类型的检查。
3.防止命名污染(封装)
#define定义变量的位置是随意的,而枚举常量的位置在{}内,可读性更强,一定程度防止了命名污染。
4.便于调试。
在调试过程中,#define定义的常量把定义的常量替换成对应的数字,常量观察不到;而用枚举类型定义的变量时枚举常量,还可以观察到。
#define max 5
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 5
};
#include<stdio.h>
int main()
{
enum Color s = BLUE;
int i = max;
return 0;
}
调试的结果为:
5.使用方便,一次可以定义多个变量。
#define在定义多个变量时,需要多次使用#define;而枚举类型在定义多个常量的时候,仅需要使用一次,相对比较方便。
3.3枚举的使用
#define max 5
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 5
};
#include<stdio.h>
int main()
{
enum Color s = BLUE;
enum Color s1 = 5;//是否可行?
return 0;
}
在上面的代码中给枚举变量赋值整型显然是不可行的,会出现类型差异。结论:只有拿枚举常量给枚举赋值,才不会出现类型的差异。
4.联合(共用体)
4.1联合类型的定义
联合也是一种特殊的自定义类型,这种类型的定义的变量也包括一系列的成员,特征是这些成员公用同一块空间(所以联合体也叫共用体)。
eg:
//联合类型的声明
union Un
{
char C;
int i;
};
4.2联合的特点
联合的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合体至少得有能力保存最大的那个成员大小)。
eg1:
union Un
{
char c;
int i;
};
union Un un;
#include<stdio.h>
int main()
{
printf("%p\n", &(un.i));
printf("%p\n", &(un.c));
return 0;
}
代码运行的结果:
eg2:
union Un
{
char c;
int i;
};
union Un un;
#include<stdio.h>
int main()
{
un.i = 0x11223355;
un.c = 0x66;
printf("%x\n", un.i);
return 0;
}//输出的结果是什么?
代码运行的结果为:
图形理解:
从上面的代码运行的结果可以看出,联合体成员共用一块内存空间,联合体可以说是非此即彼(不能同时完整的存在),不能同时使用!
4.3联合大小的计算
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。
eg1:
union Un1
{
char c[5];//1/8,对齐数为1
int i;//4/8,对齐数为4
//上面最大成员c[5]大小为5,最大对齐数为4
//对齐到最大对齐数的整数倍为8
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(union Un1));
return 0;
}
代码运行的结果为:
eg2:
union Un2
{
short c[7];//2/8,对齐数为2
int i;//4/8,对齐数为4
//上面最大成员c[7]大小为14,最大对齐数为4
//对齐到最大对齐数的整数倍为16
};
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(union Un2));
return 0;
}
代码运行的结果为:
在之前数据内存的存储的时候,我们一起解答了一道面试题:设计一个小程序判断当前编译环境是大端模式还是小端模式?现在我们学习了联合体,是否能联合体的知识测试呢?让我们一起在VS2019的环境下试试吧!
eg1:
//指针访问的方法
#include<stdio.h>
int check_sys()
{
int i = 1;
//00 00 00 01--大端模式
//01 00 00 00--小端模式
return (*(char*)&i);
}
int main()
{
int ret = check_sys();
if (1 == ret)
{
printf("小端模式\n");
}
else
{
printf("大端模式\n");
}
return 0;
}
代码运行的结果:
eg2:
//联合体访问的方法
union Un3
{
char c;
int i;
};
#include<stdio.h>
int check_sys()
{
union Un3 un;
un.i = 1;
//00 00 00 01--大端模式
//01 00 00 00--小端模式
return un.c;
}
int main()
{
int ret = check_sys();
if (1 == ret)
{
printf("小端模式\n");
}
else
{
printf("大端模式\n");
}
return 0;
}
代码运行的结果为:
总结
本章我们一起学习了的内容有
1.结构体的特殊声明
2.定义及初始化
3.结构体的自引用
4.结构体的内存对齐
5.修改默认对齐数
6.位段的概念
7.位段的内存分配
8.位段的跨平台问题
9.位段的作用
10.枚举的概念
11.枚举的定义
12.枚举的优点
13.枚举的使用
14.联合体的概念
15.联合类型的定义
16.联合体大小的计算
希望对大家认识自定义类型有些许帮助!最后,感谢大家阅读,如有错误,欢迎纠正!🎠🍭🐇