浮山网站建设,用wordpress 弄面包,广州增城网站建设,wordpress怎么做优化引言✨我们知道C语言中存在着整形(int、short...)#xff0c;字符型(char)#xff0c;浮点型(float、double)等等内置类型#xff0c;但是有时候#xff0c;这些内置类型并不能解决我们的需求#xff0c;因为我们无法用这些单一的内置类型来描述一些复杂的对象#xff0c…引言✨我们知道C语言中存在着整形(int、short...)字符型(char)浮点型(float、double)等等内置类型但是有时候这些内置类型并不能解决我们的需求因为我们无法用这些单一的内置类型来描述一些复杂的对象如一个学生一本书等等。出于这个原因C语言还给我们提供了一些自定义的数据类型使我们可以自己来构建类型如结构体、枚举、联合体。其中最常使用的就是我们本期的主题:结构体。可能有很多人已经使用过结构体类型来解决一些实际问题了。但是对于结构体还是有很多细节值得我们去深挖的下面就让我们来看看吧温馨提示可以通过目录进行快速定位哦结构体的声明2.1 结构体的基础知识在开启本期内容之前我们先来回顾以下结构体的基本概念:结构体是C语言中一个非常重要的数据类型。该数据类型是由一组称为成员变量的数据组成其中每个成员可以是不同类型的变量甚至可以是另一个结构体变量。结构体通常用来表示类型不同但又相关的若干数据。2.2 结构体的声明结构体的声明格式如下struct tag
{member-list;
}variable-list;struct是结构体关键字我们要定义结构体类型时必须使用它tag是结构体标签它用来区分不同的结构体类型结构体关键词与标签共同组成了结构体的类型与intfloat这些是一个意思我们可以使用struct tag变量名来定义一个结构体变量。member-list代表成员列表它包含了结构体的成员变量。variable-list表示变量列表我们可以在声明结构体类型的同时创建结构体变量。当然我们也可以不写仅声明一个结构体类型。结构体大括号后面的分号必不可少。例如我们可以这样使用结构体来描述一个学生//声明一个学生类型
struct Student
{char name[20];//姓名char sex[5];//性别char id[20];//学号int age;//年龄float score;//绩点
};int main()
{struct Student s1;//定义一个学生结构体变量s1
}当然如果你嫌结构体的类型名太长写起来麻烦可以使用typedef对类型进行重命名如下//声明一个学生类型并用typedef类型重定义为Stu
typedef struct Student
{char name[20];//姓名char sex[5];//性别char id[20];//学号int age;//年龄float score;//绩点
}Stu;int main()
{Stu s1;//相当于sturuct Student s1
}2.3 特殊的声明除以上的声明方式我们也可以使用不完全的声明。例如//声明匿名结构体类型
struct
{int a;char b;float c;
}x;struct
{int a;char b;float c;
}a[20], *p;
上面两个结构体的声明省略了结构体标签tag我们把这样的结构体类型称作匿名结构体类型。但是这样子的声明往往是一次性的。由于我们省略了标签我们就无法在其他地方使用这个类型来创建一个结构体变量。毕竟连名字都没有怎么用来定义变量。当然如果你只想用一次你创建的类型或者你不想要这个结构体类型被别人使用你可以声明一个匿名结构体类型。那么问题来了int main()
{//在上面匿名结构体声明的基础上下面的代码合法吗?p x;
}答案是编译器会报警告尽管两个匿名结构体的成员列表一模一样但是编译器依然会将其当作两个完全不同的类型两个不透类型的指针相互赋值自然是非法的。结构体的自引用我们在创建链表时往往用结构体来表示链表的结点。结构体的成员分为数据域与指针域数据域用来存储当前结点的值指针域用来存储指向下一结点的地址typedef int ListDataType;
struct ListNode
{ListDataType val;//数据域struct ListNode* next;//指针域
};我们将上面这种结构体中包含有指向自身结构体变量的指针的方式称作结构体的自引用。其中val占4个字节next是个指针占4/8个字节结构体具有一个确定的大小。那既然我们这样声明结点目的是为了能够找到下一结点的位置那我们可不可以这样设计结点:typedef int ListDataType;
struct ListNode
{ListDataType val;//数据域struct ListNode next;//保存下一结点
};答案是不行的。假如可以这样设计那么sizeof( struct ListNode)的大小该是多少呢我们是求不出来的因为假设我们用这个类型创建了一个结构体变量n那么n中包含着nextnext也是结构体变量又包含着一个next变量next又包含着next...这样下去就变成了无限套娃。既然不知道大小我们又要如何分配空间给结构体变量呢注意://这样写代码可行否
typedef struct
{int data;Node* next;
}Node;显然是不行的凡是都要讲究个先来后到。当我们在成员列表中定义Node*类型的变量时此时编译器还不知道Node是什么鬼东西自然会报错。我们可以这样修改代码//解决方案
typedef struct Node
{int data;struct Node* next;
}Node;Node* pn;//定义一个结构体指针pn4. 结构体变量的定义和初始化有了结构体类型那我们要如何定义变量呢实则很简单struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1struct Stu //类型声明
{char name[15];//名字int age; //年龄
};
int main()
{//定义结构体变量p2struct Point p2; //初始化定义变量的同时赋初值。struct Point p3 { 3, 4 };//初始化struct Stu s { zhangsan, 20 };
}结构体嵌套结构体的初始化方式如下struct Point
{int x;int y;
}p1; //声明类型的同时定义变量p1
struct Node
{int data;struct Point p;struct Node* next;
}n1 { 10, {4,5}, NULL }; //结构体嵌套初始化int main()
{struct Node n2 { 20, {5, 6}, NULL };//结构体嵌套初始化
}5.结构体的内存对齐快醒醒别睡了终于到了本期的重点内容了,我们来看下面例题//练习
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf(%d\n, sizeof(struct S1));printf(%d\n, sizeof(struct S2));return 0;
}答案如下这里可能有人就纳闷了欸char类型占1个字节int类型占4个字节s1与s2的大小不应该都是1146吗怎么会是12和8呢这就要谈到结构体在内存中的存储了即结构体的内存对齐。实际上S1在内存中的存储方式是这样子的我们看到c1存放完后i并不是紧挨着c1进行存放而是从偏移量为4的地方开始存储中间空出三个字节的空间。这就是结构体的内存对齐下面我们来了解其内存对齐的规则结构体的第一个成员在与结构体变量偏移量为0的地址处其他成员变量要对齐到某个数字(我们称作对齐数)的整数倍的地址处对齐数编译器默认的一个对齐数与该变量大小的较小值。vs的默认对齐数为8结构体的总大小为最大对齐数每个成员变量都有一个对齐数的整数倍对于嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。我们可以模拟一下s1的内存对齐方式同样S2的内存对齐方式如下如果你还是不确定C语言给我们提供了offsetof宏来计算结构体成员的偏移量原型如下需要注意使用时我们需要先包含stddef.h头文件#includestddef.h
#includestdio.h
struct S1
{char c1;int i;char c2;
};
struct S2
{char c1;char c2;int i;
};
int main()
{printf(结构体S1中c1的偏移量为%zd\n,offsetof(struct S1,c1 ));printf(结构体S1中i的偏移量为%zd\n, offsetof(struct S1, i));printf(结构体S1中c2的偏移量为%zd\n, offsetof(struct S1, c2));printf(结构体S2中c1的偏移量为%zd\n, offsetof(struct S2, c1));printf(结构体S2中c2的偏移量为%zd\n, offsetof(struct S2, c2));printf(结构体S2中i的偏移量为%zd\n, offsetof(struct S2, i));return 0;
}结果如下与我们上述的分析过程如出一辙我们再来看一个例子//结构体嵌套问题
struct S3
{double d;char c;int i;
};
struct S4
{char c1;struct S3 s3;double d;
};int main()
{printf(%d\n, sizeof(struct S4));return 0;
}怎么样你做对了吗步骤如下根据内存对齐算出s3所占的空间大小为16根据对齐规则的第5点得出s3的要对齐到8的整数倍即对齐到偏移量为8处double d的对齐数为8因此对齐到偏移量为24处最终大小为最大偏移量8的整数倍即为32。想必有人会有疑问内存对齐那么麻烦为什么存在内存对齐主要有以下两点原因平台原因(移植原因)不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。性能原因数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器可能需要作两次内存访问而对齐的内存访问仅需要一次访问。总的来说结构体的内存对齐是拿空间来换取时间的做法。那在设计结构体的时候我们既要满足内存又要节省空间我们要如何做到让占用空间小的成员尽量集中在一起//例如
//c1与c2不相邻
struct S1
{char c1;int i;char c2;
};//c1与c2相邻
struct S2
{char c1;char c2;int i;
};虽然S1和S2类型的成员一模一样但是S1占12个字节S2占8个字节这就是合理安排位置所带来的好处。6.默认对齐数的修改在C语言中我们也可以修改结构体的默认对齐数只需用#pragma这个预处理指令即可。如下#includestdio.h
#pragma pack(1) //修改默认对齐数为1
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};
int main()
{printf(%d\n, sizeof(struct S1));printf(%d\n, sizeof(struct S2));return 0;
}上面我们将默认对齐数设置成1由于对齐数是默认对齐数和成员大小较小者因此默认对齐数为1相当于不对齐S1与S2的结果相同都为67. 结构体的传参话不多说我们直接上代码来说明#includestdio.h
struct S
{int data[1000];int size;
};
//传值
void print(struct S s)
{printf(%d, s.size);
}
//传址
void print(struct S* sp)
{printf(%d, sp-size);
}
int main()
{struct S s1;print1(s1);print2(s1);
}
print1()和print2()哪个函数好呢 答案是print2()函数。为什么呢print1()和print2()分别对应着传值调用和传址调用。我们知道无论是传值还是传址函数在将要调用时实参都会形成临时拷贝并压入栈中。压栈的这个过程是需要成本的成本体现在时间和空间上。 如果传递一个结构体对象的时候结构体过大(例如我们上面的s1)参数压栈的的成本比较大就会导致性能的下降。所以我们传递像结构体这种数据量较大的变量时一般传地址地址占4个或者8个字节极大程度上减少了所需的成本。 综上所述我们进行结构体传参时要传结构体的地址。8.位段8.1 位段的特征与声明讲完结构体后我们就必须再来讲讲结构体实现位段的能力位段满足以下两点特征1.位段的成员必须是int、unsigned int或者char这些整型家族的成员2.位段的成员名后有一个冒号和数字数字表示成员占多少个二进制位(bit位)3.位段的空间上是按照需要以4个字节 int 或者1个字节 char 的方式来开辟的。例如下面的A就是一个位段类型struct A
{char a : 1;char b : 4;char c : 5;char d : 5;
};其中a占1个二进制位b占4个二进制位c占5个二进制位d占5个二进制位。那么位段A的大小是多少呢这就要来谈谈位段的内存分配了。8.2 位段的内存分配事实上C语言并没有明确规定位段的内存分配方式也就是说1.我们并不知道位段中的成员在内存中是从左向右分配二进制位还是从右向左分配二进制位2.我们不清楚当一个结构包含两个以上位段第二个位段成员比较大第一个位段剩余的二进制位无法容纳第二个位段是舍弃剩余的位还是将其利用这是不确定的。正因如此位段在不同的编译环境下所展现出来的效果很可能会有所不同。我们可以探究一下A当其从右向左分配并且不足时舍去剩余位时的内存分配情况如下(VS2022环境下)struct A
{char a : 1;char b : 4;char c : 5;char d : 5;
}s{0};
int main()
{s.a 11;s.b 12;s.c 3;s.d 4;printf(%d, sizeof(s));//计算s所占大小return 0;
}我们发现按照我们的假设计算出来的结果与vs2022监视器中内存的分配结果一模一样因此我们可以得知在vs2022编译器下位段是从右向左分配且不足时舍弃剩余位。8.3 位段的跨平台问题由于以下问题的存在位段的可移植性很差即存在着跨平台问题1. int 位段被当成有符号数还是无符号数是不确定的。2. 位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机 器会出问题。3. 位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。4. 当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。8.4 位段的应用位段在网络中的应用比较多例如以下ip数据包格式当我们在网络上给某人发送一个消息时这个消息就会封装成如上图所示的一个数据包用于在网络上精确地找到接收人。我们可以看出每一行都恰好的被设计成了int型宽度每个部分我们使用位段来进行排列封装使得空间最大利用。而如果我们使用结构体来进行封装每个部分由于内存对齐的原因势必会额外浪费空间造成数据包变得巨大从而使网络状态变差。总的来说跟结构体相比位段也可以达到一样的效果其可以帮助我们节省空间但是也带来了跨平台性的问题。以上就是本期的全部内容啦制作不易能否点个赞再走呢