网站开发有哪些软件有哪些,动漫网页设计素材,阿里云虚拟主机怎么建立网站,电子商务网站优点欢迎来到白刘的领域 Miracle_86.-CSDN博客
系列专栏 C语言知识
先赞后看#xff0c;已成习惯 创作不易#xff0c;多多支持#xff01; 结构体作为一种数据结构#xff0c;其定义和特点决定了它在各种应用中的广泛适用性。随着科技的进步和新兴行业的不断涌现#xf…欢迎来到白刘的领域 Miracle_86.-CSDN博客
系列专栏 C语言知识
先赞后看已成习惯 创作不易多多支持 结构体作为一种数据结构其定义和特点决定了它在各种应用中的广泛适用性。随着科技的进步和新兴行业的不断涌现结构体在未来应用中将发挥更加重要的作用。
目录
一、结构体类型的声明
1.1 回顾
1.1.1 结构体的声明
1.1.2 结构体变量的创建和初始化
1.2 结构体的特殊声明
1.3 结构体的自引用
二、结构体内存对齐
2.1 对齐规则
2.2 为什么有对齐规则
2.2.1 平台原因 (移植原因)
2.2.2 性能原因
2.3 修改默认对齐数
三、结构体传参
四、位段
4.1 什么是位段
4.2 位段的内存分配
4.3 跨平台问题
4.4 位段的应用
4.5 位段使用的注意事项 一、结构体类型的声明
1.1 回顾
我们在学习操作符的时候简单的介绍了结构体这里稍微复习一下。 武器大师——操作符详解下-CSDN博客 1.1.1 结构体的声明
struct tag
{member-list;//成员列表
}variable-list //变量列表
其中tag表述结构体名花括号里面放着成员列表也就是要描述对象的各种属性。变量列表用来存放定义为该结构体类型的变量。
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
}; //分号不能丢
1.1.2 结构体变量的创建和初始化
#include stdio.h
struct Stu
{char name[20];//名字int age;//年龄char sex[5];//性别char id[20];//学号
};
int main()
{//按照结构体成员的顺序初始化struct Stu s { 张三, 20, 男, 20230818001 };printf(name: %s\n, s.name);printf(age : %d\n, s.age);printf(sex : %s\n, s.sex);printf(id : %s\n, s.id);//按照指定的顺序初始化struct Stu s2 { .age 18, .name lisi, .id 20230818002, .sex 女 };printf(name: %s\n, s2.name);printf(age : %d\n, s2.age);printf(sex : %s\n, s2.sex);printf(id : %s\n, s2.id);return 0;
}
1.2 结构体的特殊声明
在声明结构体的时候我们可以不完全声明比如
//匿名结构体类型
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}a[20], * p; 上面两个结构体在声明的时候省略了结构体标签(Tag)。
那么试想一下下面这行代码是否合法呢
p x; 编译器会把两个声明当成两个类型所以是非法的。 匿名的结构体类型如果没有对结构体重命名的话基本上只能使用一次。 1.3 结构体的自引用
我们在结构体中包含一个类型为结构体本身的成员是否可以呢
比如说我们定义一个链表的节点首先我们简单介绍一下链表和节点 链表是一种数据结构它由节点组成每个节点包含两部分数据和指向下一个节点的指针。 节点是链表中的基本单元每个节点存储数据并指向下一个节点。节点通常由一个具有数据成员的结构体或类表示数据成员可以是任何类型的数据。 struct Node
{int data;struct Node next;
};
这么定义对吗
仔细想想。这样其实不太合理如果结构体里包含一个结构体那结构体里的结构体就可以又包含一个结构体以此类推就造成了无限大是不合理的。
正确的自引用方式
struct Node
{int data;struct Node* next;
};
在结构体自引用使用的过程中夹杂着typedef对匿名结构体重命名也会容易引发问题看看下面的代码是否可行
typedef struct
{int data;Node* next;
}Node; 仔细想其实很简单不可行因为我在结构体里用了Node而Node还没命名完还是那句心法——从上到下依次执行。
解决方案
typedef struct Node
{int data;struct Node* next;
}Node;
二、结构体内存对齐
2.1 对齐规则 1.结构体的第一个成员要对齐到结构体变量起始位置偏移量为0的地址处 2.其它成员要对齐到某个数字对齐数的整数倍的地址处。 对齐数 编译器默认的一个对齐数 与 该成员变量大小的较小值。 VS中默认的对齐数为8。 Linux中gcc没有默认对齐数对齐数就是成员变量本身的大小。 3.结构体的总大小为最大对齐数结构体中每个成员都有一个对齐数所有对齐数中最大的的整数倍。 4.如果遇到了结构体嵌套嵌套的结构体成员对齐到自己的成员的最大对齐数的整数倍处。结构体整体大小就是所有最大对齐数含嵌套结构体的成员的对齐数的整数倍。 一些练习 //练习1struct S1{char c1;int i;char c2;};printf(%d\n, sizeof(struct S1));//练习2struct S2{char c1;char c2;int i;};printf(%d\n, sizeof(struct S2));//练习3struct S3{double d;char c;int i;};printf(%d\n, sizeof(struct S3));//练习4-结构体嵌套问题struct S4{char c1;struct S3 s3;double d;};printf(%d\n, sizeof(struct S4)); 练习1 首先由于18所以c1c2的对齐数为1由于48所以4为i的对齐数。同时4也是最大对齐数。 过程首先一个字节放c1i的对齐数是4所以为了让i对齐我们需要补充3个字节让i对齐之后4个字节存放i之后存放c2.目前总字节数13(补)419,9不是最大对齐数的倍数所以继续补充3个字节到12故总大小为12个字节。 练习2 对齐数c1、c2 —— 1i —— 4。最大对齐数为4。 过程一个字节放c1一个字节放c2目前两个字节2不是4的倍数补充2个字节然后放ii占4个字节。目前112(补)48,8是4的倍数所以总大小为8。 练习3 对齐数d —— 8c —— 1i —— 4最大对齐数为8。 过程8个字节放d1个字节放c补充3个字节到12,4个字节存放i总字节813(补)416故总大小为16。 练习4 对齐数一个字节放c116个字节放S3目前字节116要补到24然后放d。总字节24832是8的倍数故总大小为32。 运行结果 2.2 为什么有对齐规则
2.2.1 平台原因 (移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的某些硬件平台只能在某些地址处取某些特定类型的数据否则抛出硬件异常。
2.2.2 性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存处理器需要作两次内存访问而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数那么就可以用一个内存操作来读或者写值了。否则我们可能需要执行两次内存访问因为对象可能被分放在两个8字节内存块中。 总体来说结构体的内存对齐是拿空间来换取时间的做法。 所以我们在结构体的设计时要尽量将占用空间小的成员集中在一起。
//例如
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
}; s1与s2的成员一模一样但是s1和s2的空间有着区别。
2.3 修改默认对齐数
我们可以通过#pragam这个预处理指令来修改编译器的默认对齐数。
#include stdio.h
#pragma pack(1)//设置默认对⻬数为1
struct S
{char c1;int i;char c2;
};
#pragma pack()//取消设置的对⻬数还原为默认
int main()
{//输出的结果是什么printf(%d\n, sizeof(struct S));return 0;
}
运行结果 可能有人会问不是又取消对齐数了嘛怎么不应该是12呢这里还是那句心法从上到下依次执行。我们已经创建好结构体大小了它再取消的话其实是没有用的。
三、结构体传参
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;
}
来观察上述两个函数大家认为哪个比较好 答案是print2。 原因是函数传参的时候参数是需要压栈会有时间和空间上的系统开销。 如果传递一个结构体对象的时候结构体过大参数压栈的系统开销较大所以会导致性能的下降。 结论结构体传参的时候要传结构体的地址。 四、位段
4.1 什么是位段
位段是一种用于存储和操作位二进制位的数据结构它允许将多个位组合在一起表示特定类型的数据节省内存空间。
位段的声明和结构体是类似的但有一下两点不同 1.位段的成员必须是 int、unsigned int 或 signed int 在C99中位段成员的类型也可以选择其他类型。 2.位段的成员名后边有一个冒号和一个数字。 eg
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
}; A就是一个位段那A的大小是多少呢这就需要了解位段的内存分配。
4.2 位段的内存分配 1.位段的成员可以是 int、unsigned int、signed int 或者是 char 等类型。 2.位段的空间上是按照需要以4个字节int或者1个字节char的方式来开辟的。 3.位段涉及很多不确定因素位段是不跨平台的注重可移植的程序应该避免使用位段。 //⼀个例⼦
struct S
{char a : 3;char b : 4;char c : 5;char d : 4;
};
struct S s { 0 };
s.a 10;
s.b 12;
s.c 3;
s.d 4;
//空间是如何开辟的 4.3 跨平台问题 1..int 位段被当成有符号数还是无符号数是不确定的。 2.位段中最大位的数目不能确定。16位机器最大1632位机器最大32写成27在16位机器会出问题。 3.位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。 4.当一个结构包含两个位段第二个位段成员比较大无法容纳于第一个位段剩余的位时是舍弃剩余的位还是利用这是不确定的。 总结跟结构相比位段确实可以达到同样的效果并且通常能够很好地节省空间。然而位段确实存在跨平台的问题。由于不同编译器和平台对位段的处理方式可能不同这可能导致在不同环境下程序的行为不一致。因此在需要高度可移植性的场景中使用位段需要特别小心并进行充分的测试和验证。如果节省空间的需求不是特别迫切或者跨平台兼容性更为重要那么使用结构可能是一个更稳妥的选择。
4.4 位段的应用
下图是网络协议中IP数据报的格式。确实我们可以看到其中许多属性只需要较少的bit位就能描述。使用位段bit-fields在这种情况下是一个很好的选择因为它能够实现所需的效果同时节省空间。由于位段可以精确控制每个字段所占用的位数所以它们非常适合用于表示那些只需要少量位数就能完整描述的信息。
在网络传输中数据报的大小对于网络的畅通性至关重要。较小的数据报意味着在传输过程中所需的带宽和资源更少这有助于减少网络拥塞和延迟。通过使用位段来减少数据报中每个字段的大小我们可以有效地减小数据报的整体大小从而提高网络的传输效率。
然而需要注意的是虽然位段在节省空间方面具有优势但它们也带来了一些挑战。例如位段的具体实现可能因编译器和平台的不同而有所差异这可能导致跨平台兼容性问题。此外过度使用位段可能会使代码变得难以理解和维护。因此在决定是否使用位段时需要权衡其优缺点并根据具体的应用场景和需求做出决策。 4.5 位段使用的注意事项
位段的一个特性是其成员可能共享同一个字节的存储空间这导致某些成员的起始位置并非字节的起始边界。由于内存中每个字节分配一个地址而字节内部的bit位是没有独立地址的因此不能直接对位段的成员使用操作符来获取其地址。
由于位段成员的这种地址特性我们不能直接使用像scanf这样的函数来直接给位段的成员输入值。这是因为scanf等函数通常需要能够访问变量的内存地址来执行赋值操作而位段成员并不总是拥有独立的、可寻址的内存位置。
因此在实际编程中我们通常需要先通过scanf等函数将输入值存放在一个临时变量中然后再将这个临时变量的值赋给位段的成员。这样可以确保赋值操作的正确性和可靠性避免因为位段成员地址的特殊性而导致的问题。
需要注意的是位段的使用虽然可以节省空间但也带来了一些限制和复杂性。在实际应用中我们需要根据具体的需求和场景来权衡是否使用位段并确保在使用过程中遵守相关的编程规范和最佳实践。
struct A
{int _a : 2;int _b : 5;int _c : 10;int _d : 30;
};
int main()
{struct A sa { 0 };scanf(%d, sa._b);//这是错误的//正确的⽰范int b 0;scanf(%d, b);sa._b b;return 0;
} 完