文章目录
前言
要精确描述一个事物,需要得到多方面的数据。在日常生活中,数据总是成组的出现,例如当我们需要区记录一个 学生的信息的时候,我们需要知道该学生的姓名,年龄,性别等多个信息,但是这些信息并不是同一种类型的数据,所以为了达到目标我们需要自定义一些类型的数据。自定义类型可以根据我们的需要创建出满足要求的数据类型。
1.结构体
结构体的基础知识:
结构是一些值的集合,这些值被称为成员变量。结构的每个成员可以是不同的类型。
结构的声明和变量的创建:
struct tag{
member-list;//成员列表
}variable-list;//变量列表--全局变量
//在声明的时候变量列表可以为空,即不在此处创建变量,在后括号后直接加上分号即可
struct tag{
member-list;//成员列表
};
注意:在声明结构时,必须列出它包含的所有成员。
结构体经过声明后,我们就可以视为存在该种类型的数据,而这种数据类型的名称就是struct tag
下面我们按照这个形式来声明一个结构(以学生为例子)并且创建一个变量:
//一个学生的信息包括名字,年龄,性别
struct Stu{
char name[10];
int age;
char sex[5];
};//这只是一个结构体类型,有了这个类型就可以区创建变量
int main()
{
struct Stu s1;//创建了一个结构体变量
return 0;
}
下面的图片可以更清晰的了解结构体类型变量的创建
结构体在变量在不同位置创建的区别
结构体变量有三种创建方式(仍然以一个学生为例子)
第一种:在声明结构体的时候就创建变量,这种变量属于全局变量。
struct stu{
char name[10];
int age;
char sex[5];
}s1;//
第二种:在声明后并且在函数外面单独创建一个变量,这种变量也属于全局变量。
struct stu{
char name[10];
int age;
char sex[5];
};
struct stu s2;
int main()
{
return 0;
}
第三种:在函数内部创建,这种变量属于局部变量。
struct stu{
char name[10];
int age;
char sex[5];
};
int main()
{
struct stu s3;
return 0;
}
特殊的声明
在声明结构体的时候可以不完全的声明
匿名结构体类型:
struct
{
char name[5];
int age;
float c;
}s1;//变量只能在这个位置创建
//该结构体在声明时省略了 tag 位置的信息,这是一个匿名结构体标签
需要注意的是:
-
1.匿名结构体变量必须在声明的时候就创建好。
-
2.连续声明两个匿名结构体变量,编译器会认为这是两个不同类型的数据
使用typedef可以更方便的创建变量
typedef
工具是一个高级数据特性,利用它可以为某一类型自定义名称,该方面与#define
类似,但是两者也有不同:
- 与
#define
不同,typedef
创建的符号名只受限于类型,不能用于值 typedef
由编译器解释,不是预处理器- 在受限范围内,
typedef
比#define
更灵活
当我们每次创建变量认为写变量类型很麻烦的时候,我们可以使用typedef
,这样可以更方便的创建变量
typedef struct stu{
char name[10];
int age;
char sex[5];
}stu;
int main()
{
stu s1;//现在我们可以更方便的创建变量
return 0;
}
结构体变量的初始化:
和其他一般类型相同:可以在创建的时候初始化,也可以后面再赋值。
#include<stdio.h>
typedef struct stu{
char name[10];
int age;
char sex[5];
}stu;
int main()
{
stu s1 ;
stu s2 = {
"小明",5,"男"};//创建的时候就赋值
s1 = {
"小红",6,"女"};//后面赋值
return 0;
}
计算结构体的大小:
下面是一个计算结构体大小的例子( sizeof
操作符可以得出一个结构的整体长度。)
struct S1{
char c1;
int i ;
char c2;
};
int main()
{
printf("%d\n",sizeof(struct S1));
return 0;
}
在打印结构前,用累加的方式来计算一下该结构体的大小:两个char成员占2字节,一个int成员占4字节,在了解"结构体内存对齐"之前,我们应该都会得到6 这个答案。
打印结果
但是最终打印的结果是12,说明该结构体的大小是12字节,这个结果是由于“结构体内存对齐”的作用。下面我们来了解结构体内存对齐。
结构体内存对齐
在此之前,我们需要先了解这样一个函数–offsetof
:
该函数可以得到一个结构体的成员相较于该结构体初始位置的偏移量(单位是字节)。函数的头文件是stddef.h
。
该函数的原型是:
size_t offsetof(structName,memberName);
我们使用这个函数来计算下面这个结构中各成员的偏移量:
#include<stdio.h>
#include<stddef.h>
struct S1 {
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));
printf("%d\n", offsetof(struct S1, i));
printf("%d\n", offsetof(struct S1, c2));
return 0;
}
现在我们得到了每一个成员的偏移量。
前面我们知道该类型的大小是12,如果我们创建一个这样类型的变量s1
,那么为s1
创建的就是一个12字节大小的空间。
C语言中结构体类型内存对齐的规则:
- 结构体的第一个成员,存放在结构体变量开始位置的0偏移处;
- 其他成员要对齐到某个数字(对齐数)的整数倍的地址处
对齐数:编译器默认的一个对齐数与该成员大小的较小值
vs环境下有一个默认对齐数:8 - 结构体的总大小为最大对齐数(每一个结构体成员都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍的地址处,结构体的整体大小就是最大对齐数(包括嵌套的那个结构体)的整数倍
利用这个规则我们来解释为什么上面那个结构体大小是12:
第一个成员一定会放在0偏移处,第一个成员的大小是1,所以
第二个成员的大小是4,默认对齐数是8,得到第二个成员的对齐数是4,此时我们不能从偏移量为1的位置开始次昂后排,我们需要找到对齐数的整数倍的地址处,由于偏移量是4,最近的一个满足条件的位置是偏移量为4的位置,然后为第二个成员开辟出的空间应该是这样的:
第三个成员的大小是1,默认对齐数是8,得到第三个成员的对齐数是1,满足要求的地址是偏移量为8的位置,所以第三个成员的空间开辟在偏移量为8的位置
在每一个成员的空间都确定好了以后,就计算结构体的总大小,规则是最大对齐数的整数倍,这个例子中的最大对齐数是4(第一个成员是1,第二个成员是4,第三个成员是1),所以总大小是4的整数倍,此时已经利用了9个字节的大小,要满足条件就需要继续向后占用空间,当结构体空间大小为12个字节时(偏移量为11),就满足所有的条件,得到结果
该结构体的大小是12字节。
为什么存在内存对齐?
-
平台原因
不是所有的硬件平台都可以访问任意地址上面的任意数据的,某些硬件平台只能访问某些地址处的某些特定的数据,否则抛出硬件异常 -
性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于为了访问未对齐的内存处理器需要做出两次内存访问,而访问对齐的内存,处理器只需要访问一次
那在设计结构体的时候,即想要满足节约空间,有想要节约时间,可以这样做:
让占用空间小的成员尽量集中在一起
修改默认对齐数
利用#pragma pack()
;
注意:子啊修改默认对齐数的时候一般会改成2的n次方
结构体作为函数的参数需要注意
尽量选择传址调用,不会浪费很多的空间,需要用const 保护它
函数传参的时候会压栈,如果拷贝一份很大的空间,这样会降低效率
2. 位段
什么是位段
位段的声明和结构是类似的有两个不同:
位段的成员后面有一个数字和冒号
比如:
struct A{
int _a :2;//该变量只占2个比特的空间
int _b :5;//该变量只占5个比特的空间
int _c :10;//该变量只占10个比特的空间
int _d :30;//该变量只占30个比特的空间
};
结构A是一个位段,改为u单 的大小是
位段空间大小的计算
位段的内存分配规则:
- 位段的成员可以是
unsigned int
signed int
int
也可以是char
; - 位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
位段的跨平台问题:
- int位段被当作有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定。
- 位段中的成员在内存中从左向右分配还是从右向左分配是不确定的
- 当一个结构包括两个位段,第二个位段的成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位还是利用,这一点是不确定的。
下面重点解释第四点:
这是一个位段已经计算位段大小的代码段,不同的机器上面对于位段的空间开辟方式有两种情况:
struct S{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
printf("%d\n",sizeof(struct S));
return 0;
}
情况一:
四个变量一共占用16个bit,中间储存的内容是连续的。一共需要2字节大小。
情况二:
如果前面变量使用后的空间不能完整的赋予下一个变量,那么下一个变量会重新开辟处一个空间
前面俩个变量占用一个字节,变量c占用一个字节,变量d占用一个字节。一共是3字节大小
不同的环境下可能会出现不同的值
而vs环境下是第二个情况
2.联合
联合的声明方式和结构类似:
union tag{
int i ;
char ch;
float f;
};
联合的特点是每一个成员都是引用的都是内存中相同的地址,。。
计算联合的大小:
如果联合的各个成员具有不同的长度,那么联合的长度就是它最长成员的长度。
联合的初始化
联合的初始化满足两个条件:
- 初始值必须是联合变量第一个成员的类型
- 初始值必须位一对花括号中间
例如:
简单的运用一下联合
利用联合判断所用机器使用的字节序(大端还是小端)
可以用这个代码段
#include<stdio.h>
union {
int i;
char ch;
}endian;
int main()
{
endian.i = 1;
printf("%d", endian.ch);
return 0;
}
3.枚举
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
-
一周的星期一到星期日是有限的7天,可以一一列举。
-
性别有:男、女、保密,也可以一一列举。
-
月份有12个月,也可以一一列举
枚举类型的定义
enum Day//星期
{
//枚举的可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
int main()
{
enum DAY d = Sun; //枚举类型应该是在可能取值范围中取值。
return 0;
}
枚举中常量的取值
枚举的可能取值是有初始值的,下面我们来打印看一看
enum Day//星期
{
//枚举的可能取值
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
int main()
{
printf("%d\n",Mon);
printf("%d\n",Tues);
printf("%d\n",Wed);
printf("%d\n",Thur);
printf("%d\n",Fri);
printf("%d\n",Sat);
printf("%d\n",Sun);
return 0;
}
打印结果是:
我们需要知道的是:在缺省状态下,整型值从零开始,如果对列表的某个标识符惊醒了赋值,那么紧接其后的那个标识符的值就比所复制的值大1。
下面这里我们在声明枚举的时候对于其中一个
注意:
-
这些值都是常量,这一步操作相当于给他们初始化,他们的初始值后面不能更改。
-
枚举的大小是一个整形
枚举的优点
-
增加了代码的可读性
-
和#define定义的标识符比较枚举有类型检查,更加严谨
-
防止了命名污染(封装)
-
便于调试
#define定义的标识符不利于调试,因为在刚开始调试的时候就已经完成替换了。 -
使用方便,一次可以定义多个常量
总结
- 编程过程中要表示的信息通常不是一个数字或者一些列数字,结构可以把这些信息都放在一个单元内这样可以把相关的信息都 储存在一处,而不是风扇哎多个变量中。
- 联合声明和结构声明类似,但是联合的成员共享相同的存储空间,而且在联合中同一时间内只能由一个成员。实质上,可以在联合变量中储存一个类型不唯一的值。
- enum工具提供一种定义符号常量的方法。
- typedef工具提供一种为基本或者派生类型创建新标识的方法。