聊城宏远网站建设优化,如果建网站,高端网站建设解决方案,何为网站开发 #x1f4dd;个人主页#xff1a;Sherry的成长之路 #x1f3e0;学习社区#xff1a;Sherry的成长之路#xff08;个人社区#xff09; #x1f4d6;专栏链接#xff1a;C语言进阶 #x1f3af;长路漫漫浩浩#xff0c;万事皆有期待 文章目录1.结构体1.1 概述个人主页Sherry的成长之路 学习社区Sherry的成长之路个人社区 专栏链接C语言进阶 长路漫漫浩浩万事皆有期待 文章目录1.结构体1.1 概述1.2 结构的声明1.3 特殊声明1.4 结构的自引用1.5 结构的定义与初始化1.6 重点结构体内存对齐1.7 修改默认对齐数1.8 结构体传参2.位段2.1 位段概述2.2 位段的内存分配2.3 位段的跨平台问题3.枚举3.1 定义3.2 枚举类型的优点3.3 枚举类型的使用4.联合共用体4.1 联合类型的定义4.2 联合类型的特点4.3 联合类型大小的计算5.总结1.结构体
1.1 概述
C 语言允许用户自己指定这样一种数据结构它由不同类型的数据组合成一个整体以便引用这些组合在一个整体中的数据是互相联系的这样的数据结构称为结构体它相当于其它高级语言中记录。结构是一些值的集合这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2 结构的声明
以描述 “ 学生 ”为例
#define _CRT_SECURE_NO_WARNINGS 1
#includestdio.h
//结构体的声明
struct student
{char name[20];int age;char sex[5];float score;
}s1s2;
//定义结构体变量s1、s2
//此处定义的结构体变量是全局的
struct student s3, s4;
//定义结构体变量s3、s4
//此处定义的结构体变量等同于声明时定义也是全局的
int main()
{struct student s5, s6;//定义结构体变量s5、s6//此处定义的结构体变量是局部的return 0;
}1.3 特殊声明
关于结构体的不完全声明即匿名结构体类型
#define _CRT_SECURE_NO_WARNINGS 1
#includestdio.h
struct
//没有声明结构体标签即为匿名结构体类型
{char name[20];int age;char sex[5];float score;
}student { Zhang,21,Man,91.7 };
//匿名结构体类型必须在生声明的同时进行定义int main()
{printf(%s %d %s %.1f\n, student.name, student.age, student.sex, student.score);return 0;
}我们把这种在声明时省略掉结构体标签的结构体称为匿名结构体类型在使用这种方式进行声明时由于没有声明结构体标签导致一旦该结构体结束声明将无法再次进行定义所以对于该类型的结构体来说就必须在声明结构体的同时进行定义可以不初始化。 再来看下面这个例子
//结构体类型1
struct
{char name[20];int age;char sex[5];float score;
}x;//结构体类型2
struct
{char name[20];int age;char sex[5];float score;
}*p;在这个示例中虽然两个结构体类型内的结构体成员完全一样但因为两者都使用了匿名结构体的声明方式编译器会把上面的两个声明当成完全不同的两个类型 于是在下面的代码中将被视为非法
p x;
//一种类型的指针指向另一种不同类型将被视为非法1.4 结构的自引用
结构的自引用就是指结构体在自己的声明中引用了自己的一种声明方式。
struct Test
{int data;struct Test n;
};
int main()
{struct Test n;return 0;
}我们说这种引用方式是非法的。这是因为当我们这样进行引用后在我们定义结构体变量时会进行自引用但在自引用中又嵌套了对自身的引用如此循环往复而编译器并不知道该在何时停止自引用。
正确的自引用形式
struct Test
{int data;struct Test* NEXT;//使用指针指向确定的引用空间
};
int main()
{struct Test n;return 0;
}当我们在进行结构体变量的定义时同样进行了自引用不同的是这一次我们使用了一个指针指向了下一个结构体变量的空间而在这次指向之后指针指向的空间被固定不再指向其它空间如此就实现了真正的结构体自引用。 同时我们还可以结合关键字 typedef 进行使用
typedef struct Test
{int data;struct Test* NEXT;//但在这里必须仍使用struct Test//在结构体声明结束后才会进行重命名
}Test;
//使用tepydef关键字将struct Test类型重命名为Test类型int main()
{Test n;//经过重命名在进行定义时可以直接使用重命名后的类型名进行定义return 0;
}我们可以结合关键字 typedef 来将我们声明的结构体变量进行重命名方便我们对结构体变量定义与初始化。但要注意的是在使用 typedef 时在结构体声明内部进行自引用时仍需写成完全形式这是因为只有在结构体声明结束后才会对我们声明的结构体类型进行重命名。
1.5 结构的定义与初始化
举个例子
#define _CRT_SECURE_NO_WARNINGS 1
#includestdio.h
struct student
{char name[20];int age;char sex[5];float score;}s1 { Zhang,21,Man,98.4 };
//初始化结构体变量s1此处的结构体变量是全局的struct student s2 { Wang,20,Woman,99.5 };
//初始化结构体变量s2此处初始化的结构体变量等同于声明时初始化也是全局的int main()
{struct student s3 { Jiao,21,Man,67.2 };//初始化结构体变量s3此处的结构体变量是局部的printf(%s %d %s %.1lf\n, s1.name, s1.age, s1.sex, s1.score);printf(%s %d %s %.1lf\n, s2.name, s2.age, s2.sex, s2.score);printf(%s %d %s %.1lf\n, s3.name, s3.age, s3.sex, s3.score);return 0;
}1.6 重点结构体内存对齐
经过上面的学习我们就已经基本掌握了结构体的使用了。接下来我们将要深入研究结构体大小的计算过程即结构体内存对齐而这也是近年来的热门考点。 先来看看下面这段计算结构体变量大小的代码
#includestdio.h
struct test1
{char a;int b;char c;
}test1;
struct test2
{char d;char e;int f;
}test2;
int main()
{printf(The size of test1 is %d\n, sizeof(test1));printf(The size of test2 is %d\n, sizeof(test2));return 0;
}我们将其编译运行起来看看结果 The size of test1 is 12 The size of test1 is 8 我们看到实际的计算结果与我们的猜想大相径庭那么到底是哪里出现了问题呢这就是我们在这里需要研究的内容结构体内存对齐。 要想弄清楚究竟是如何进行结构体变量大小计算的我们首先得掌握 结构体的对齐规则 第一个成员在与结构体变量偏移量为0的地址处。偏移量该成员的存放地址与结构体空间起始地址之间的距离其他成员变量要对齐到对齐数的整数倍的地址处。对齐数 编译器默认的一个对齐数与该成员大小的较小值。对齐数在VS中的默认值为8结构体总大小为最大对齐数的整数倍如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数的整数倍。 知晓了结构体的对齐规则我们再回过头来分析上面的结构体变量大小计算过程。 分析如图 但是我们发现这样的方式造成了很大程度上的空间浪费以 test1 为例12个字节的大小中有六个字节的空间申请了但却没有被使用。那么为什么还要采用这样的办法呢 主要有以下两个原因 平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。性能原因数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访问。通俗来说结构体的内存对齐就是一种用空间换时间的处理方法。而我们能做的就只有以上面的 test1 与 test2 为例尽可能的选取 test2 这样使占用空间小的成员尽可能的集中在一起。 1.7 修改默认对齐数
在我们的代码编写过程中默认的对齐数可能会不够合适。而当这个时候我们就可以通过使用下面这个预处理指令来修改我们的默认对齐数
#pragma pack(8)
//修改默认对齐数为8我们也可以通过该指令在修改过默认对齐数之后取消设置的默认对齐数将其还原
#pragma pack()
//取消设置的默认对齐数还原为默认1.8 结构体传参
结构体传参与函数传参类似,我们直接来看下面的示例
#includestdio.h
struct TEST
{int data[1000];int num;
};
struct TEST test { {1,2,3,4}, 1000 };
//结构体传参
void Print1(struct TEST test)
{printf(%d\n, test.num);
}
//结构体地址传参
void Print2(struct TEST* p)
{printf(%d\n, p-num);
}
int main()
{Print1(test); //传结构体Print2(test); //传地址return 0;
}而在上面这段代码中我们一般认为 Print2 函数更为优秀。原因是当函数传参的时候参数是需要压栈的在这个过程中就会产生时间和空间上的系统开销。如果传递一个结构体对象时结构体过大那么将会导致参数压栈的的系统开销较大最终将会导致程序性能的下降。
2.位段
结构体实现位段
2.1 位段概述
位段bit-field以位为单位来定义结构体或联合体中的成员变量所占的空间。含有位段的结构体联合体称为位段结构。采用位段结构既能够节省空间又方便于操作。 位段的声明和结构体十分相似但同时有两个不同点 位段的成员必须是 int、signed int 、unsigned int 或 char 类型。位段的成员名后边有一个冒号和一个数字该成员所占内存空间大小单位为 bit位。 #includestdio.h
struct test
{int _a : 2;//成员 a 只占用 2 个比特位signed int _b : 5;//成员 b 只占用 5 个比特位unsigned int _c : 10;//成员 c 只占用 10 个比特位char _d : 4;//成员 d 只占用 4 个比特位
};
int main()
{printf(The size of struct test is %d\n, sizeof(struct test));//4return 0;
}优点能够节省大量的空间通过有条件地根据实际使用需求限制每个变量所占内存空间的大小从而减少了整体结构的空间占用
2.2 位段的内存分配
位段存在的意义便是最大程度上去减少空间的浪费所以在进行存储时位段不会进行内存对齐操作。那么位段的内存空间是如何进行分配的呢 注意位段的内存分配并没有严格的规则在不同的编译器上产生的结果可能不同我们今天的讲解将以Visual Studio 2019 为例进行研究。
首先需要知道位段进行内存分配的规则
1. 位段的成员可以是 int 、unsigned int 、signed int 或者是 char属于整形家族类型
2. 位段的空间上是按照需要以4个字节 int 或者1个字节 char 的方式来开辟的。
3. 位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段。
4. 位段的内存分配是逐4字节一个 int 类型的大小进行分配的。
5. 当字节内空间不足以放下下一个成员变量时剩余的空间不再使用而是再从内存中申请一个字节的空间继续分配。
6. 不同类型char 与 int类型数据进行存储时将会另起4个字节一个 int 类型的大小进行存储。#includestdio.h
struct test
{char a : 3;char b : 4;char c : 5;char d : 4;
};
int main()
{struct S s{0};s.a10;s.b12;s.c3;s.d4;return 0;
}分析如图
至此该位段结构的内存分配结束共占据3个char 类型数据的大小即 3 个字节
2.3 位段的跨平台问题
我们上面说过位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段并且在未来位段结构的使用过程中我们一定要提前仔细地研究好位段在不同编译器下使用时究竟是如何进行内存分配的再结合我们的实际需求实现跨平台使用。 而在位段进行跨平台使用时我们通常需要注意以下四个关键点 int 位段被当成有符号数还是无符号数是不确定的。位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机器会出问题位段中的成员在内存中从左向右分配还是从右向左分配的标准尚未定义。当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。 总结下来跟结构相比位段可以达到跟结构相同的效果并且可以更好的利用空间但同时存在着跨平台问题。
3.枚举
枚举是列出某些有穷序列集的所有成员的程序或者是一种特定类型对象的计数。这两种类型经常但不总是重叠。是一个被命名的整型常数的集合。简单来说就将某种特定类型的对象一一进行列举。 枚举的声明与结构和联合相似, 其形式为 enum 枚举名{ 标识符整型常数 标识符整型常数 … 标识符整型常数 } 枚举变量 3.1 定义
#includestdio.h
//枚举类型1
enum Sex
{MALE,FEMALE,SECRET
}s1 MALE;
//声明时进行定义与初始化全局
enum Sex s2 FEMALE;
//枚举类型2
enum Day
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
int main()
{enum Day s3 Mon;//定义与初始化局部return 0;
}我们可以看到枚举类型的声明、定义与初始化与结构十分类似。然后我们再来看一看枚举类型内部各成员的值我们以日期为例
#includestdio.h
enum Day
{Mon,Tues,Wed,Thur,Fri,Sat,Sun
};
int main()
{//打印各成员的值printf(The value of Mon is %d\n, Mon);printf(The value of Tues is %d\n, Tues);printf(The value of Wed is %d\n, Wed);printf(The value of Thur is %d\n, Thur);printf(The value of Fri is %d\n, Fri);printf(The value of Sat is %d\n, Sat);printf(The value of Sun is %d\n, Sun);return 0;
}将上面这个示例编译运行起来看看结果的反馈 我们看到枚举类型内部各成员的默认值是从 0 开始依次递增的。 但是成员的值不仅限于默认值同时也允许我们在定义时给各成员附合适的初值
#includestdio.h
enum Day
{Mon,Tues5,Wed,Thur,Fri,Sat15,Sun
};
int main()
{printf(The value of Mon is %d\n, Mon);printf(The value of Tues is %d\n, Tues);printf(The value of Wed is %d\n, Wed);printf(The value of Thur is %d\n, Thur);printf(The value of Fri is %d\n, Fri);printf(The value of Sat is %d\n, Sat);printf(The value of Sun is %d\n, Sun);return 0;
}我们可以依照上面这种方式对枚举类型成员的初值进行修改 我们看到经过修改本应按序赋值为 1 的枚举成员 Tues 被赋值成了 5 于是接下来的成员就从 5 开始依次赋值直到成员 Sat 被赋值为 15 后接下来的成员就从 15 开始依次递增。
3.2 枚举类型的优点
枚举类型的成员均为常量不可在使用中被修改那么我们同样可使用宏 #define 去定义常量为什么非要使用枚举类型呢 这是因为相比于宏枚举类型具有很多优点 优点 增加代码的可读性和可维护性。和 #define 定义的标识符相比较枚举有类型检查更加严谨。防止了命名污染通过封装实现。便于调试。使用方便一次可以定义多个常量。 3.3 枚举类型的使用
同时我们要注意在使用枚举类型时只能用枚举常量给枚举变量赋值只有这样才不会出现类型差异
#includestdio.h
//声明枚举类型
enum TEST
{test1,test2,test3
};
//其中test1、test2、test3为枚举常量
int main()
{//定义枚举变量enum TEST t;//使用枚举常量给枚举变量赋值t test3;//验证赋值结果printf(The value of t is %d\n, t);return 0;
}
4.联合共用体
在进行某些算法的编程的时候需要将几种不同类型的变量存放到同一段内存单元中。也就是使用覆盖技术使几个变量互相覆盖。这种几个不同的变量共同占用一段内存的结构在C语言中被称作 “ 联合体 ” 类型结构简称联合也叫共用体。
4.1 联合类型的定义
联合是一种特殊的自定义类型这种类型定义的变量也包含有一系列的成员但不同的是这些成员共用同一块空间也被称作共用体 它的定义也基本与结构体一致
#includestdio.h
union TEST
{char a;int b;
}test;
//在定义联合体的同时定义联合体变量test
int main()
{//查看联合体的占用空间printf(The size of test is %d\n, sizeof(test));//查看联合体成员的存储地址printf(The address of test is %p\n, test);printf(The address of a is %p\n, test.a);printf(The address of b is %p\n, test.b);return 0;
}但不同的是我们编译运行后发现联合体成员 char 类型变量 a 与 int 类型变量 b 共同占用同一片空间一个 int 类型所占的空间 这种方式定义的联合体结构是三种结构中最节省空间的一种但同时极致的空间节省能力导致了它在使用时需要满足的条件极为苛刻。
4.2 联合类型的特点
联合体最大的特点就是联合体的成员是共用同一块内存空间的则联合至少得有足够的空间容纳最大的成员这样一个联合变量的大小就至少得是最大成员的大小。 既然联合体的大小会随着内部成员大小的变化而变化那么是不是联合体类型也可以通过判断内容大小来帮助我们判断机器的大小端存储模式呢
#define _CRT_SECURE_NO_WARNINGS 1
#includestdio.h
int check_sys()
{union CHECK{char check1;int check2;}check;check.check2 1;return check.check1;
}int main()
{if (1 check_sys()){printf(您的机器采用小端存储模式\n);}else{printf(您的机器采用大端存储模式\n);}return 0;
}我们将其编译运行发现该思路可以帮助我们检查机器的大小端存储模式
4.3 联合类型大小的计算
联合体类型的大小计算需要按照以下规则进行计算
联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍时要对齐到最大对齐数的整数倍。
#includestdio.h
//联合体1
union TEST1
{char c[5];int i;
};
//联合体2
union TEST2
{short c[7];int i;
};
int main()
{//检查联合体的大小printf(The size of TEST1 is %d\n, sizeof(union TEST1));printf(The size of TEST2 is %d\n, sizeof(union TEST2));return 0;
}1.在联合体 TEST1 中占用空间最大的成员是 char 类型数组 c 且其中含有 5 个元素则其所占空间大小为 5 个字节而我们都知道 VS 的对齐数默认为 8 则将会对齐至默认对齐数的整数倍即 8 个字节。 2.而联合体 TEST2 中占用空间最大的成员是 short 类型数组 c 且其中含有 7 个元素则其所占空间的大小为 14 个字节那么就将会对齐至对齐数的整数倍即 16 个字节
5.总结
今天我们对结构体的相关原理与使用等知识又有了新的了解,学习了结构体、位段、枚举、以及联合共用体的相关知识完成了通过联合体类型判断机器的大小端存储模式希望我的文章和讲解能对大家的学习提供一些帮助。 当然本文仍有许多不足之处欢迎各位小伙伴们随时私信交流、批评指正我们下期见~