C语言学习记录 - 结构体
1. 回顾
结构体变量的创建和初始化
#include<stdio.h>
struct Stu
{
char name[20];
int age;
char sex[5];
char id[20];
};
//👆声明
int main(){
//按照结构体成员的顺序初始化
struct Stu s = {"张三", 20, "男", "20230818001"};
//按照指定的顺序初始化
struct Stu s2 = {.age = 18, .name = "lisi", .id = "20230818002", .sex = "女"};
return 0;
}
结构的特殊声明:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
虽然两个结构体都匿名了,成员也完全一样,但是p=&x却是不合法的
编译器会把上面的两个声明当成完全不同的两个类型,所以非法
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次
结构体自引用
怎么写呢?
struct Node
{
int data;
struct Node next;
};
👆这样写其实不太ok,因为这样结构体变量的大小就无法计算,理论上是无穷大的
struct Node
{
int data;
struct Node* next;
};
👆这样写就ok了
另外,在结构体自引用的过程中,夹杂了typedef
对匿名结构体类型的重命名,也容易引入问题,比如:
typedef struct
{
int data;
Node* next;
}Node;
Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体的内部提前使用Node类型来创建成员变量,这是不行的
解决办法什么呢?
typedef struct Node
{
int data;
struct Node* next;
}Node;
定义结构体就不要使用匿名的结构体了
2. 结构体内存对齐
对齐规则:
-
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
-
其他成员变量要对其到某个数字(对齐数)的整数倍的地址处
-
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
- vs中默认为8
- Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
-
结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
-
如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
为什么存在内存对齐呢
-
平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
-
性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需一次,假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数,如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了,否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中
总的来说:结构体的内存对齐是拿空间来换取时间的做法
那么如何尽量做到既对齐,又节省空间呢:让占用空间小的成员尽量集中在一起
例如
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
//S1和S2类型的成员一样,但是所占空间大小有所区别
修改默认对齐数
#pragma
这个预处理指令,可以改变编译器的默认对齐数
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果为6
printf("%d\n", sizeof(struct S));
return 0;
}
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数
3. 结构体传参
//两种形式
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;
}
一般首选第二种,传地址的方式
原因
函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降
4. 结构体实现位段
什么是位段
位段的声明和结构类似,但有两个不同
- 位段的成员必须是
int, unsigned int或signed int
,在C99中位段成员的类型也可以选择其他类型,比如char
类型 - 位段的成员名后边有一个冒号和一个数字
比如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型,所占内存大小8字节
提一嘴:关于成员名的这个下划线,有的书里会说,建议给结构体的成员变量前面都加上_,这样一看就知道是个成员变量,不过对机器来说不影响,算是一种代码风格吧
那怎么分配内存的呢
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A a = { 0 };
a._a = 1;
a._b = 9;
a._c = 5;
a._d = 20;
printf("%zd\n", sizeof(a));
return 0;
}
创建了一个变量,调试观察其内存
- 按照需要一次以4个字节或1个字节的方式来开辟
- 涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不确定(比如16位机器的最大位数16,32位机器最大32,如果有一个成员27位,在16的机器中会出问题)
- 位段中的成员在内存中从左向右分配还是从右向左分配的标准尚未定义
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时,是舍弃剩余的位,还是利用,这是不确定的
总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题在
位段的应用
下图是网络协议中,IP数据报的格式,可以看到其中有很多属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通有帮助
使用位段的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的,内存中每个字节分配一个地址,一个字节内部的bit没有地址
所以没法对位段的成员使用&操作符,也就不能通过scanf直接给位段成员赋值,只能是先输入了放在一个变量中,然后赋值给位段成员
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A a = { 0 };
//这是错误的↓
//scanf("%d",&a._b);
//正确示范
int b = 0;
scanf("%d", &b);
a._b = b;
return 0;
}