提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、结构体基础知识
- (一)、结构体类型的声明、变量的创建与初始化
- (二)、结构成员访问操作符
- (三)、结构体的自引用
- (四)、结构体传参
- 二、结构体内存对齐!!
- (一)、对齐规则
- (二)、分析结构体大小的详细过程
- (三)、为什么存在内存对齐
- (四)、默认对齐数的可修改性
- 三、结构体实现位段!
- (一)、位段的声明
- (二)、位段的内存分配
- (三)、位段的跨平台问题
- (四)、位段的应用
- (五)、位段的使用注意事项
- 总结
前言
提示:这里可以添加本文要记录的大概内容:
在C语言中共有三种自定义类型——结构体、联合体、枚举。本文主要介绍第一种结构体,后面文章中会介绍联合体与枚举。本文主要围绕以下几个方面对结构体进行介绍——结构体的基础知识、结构体的内存对齐、结构体实现位段。结构体内存对齐,和实现位段是我们比较陌生的知识,需要努力掌握一下。
提示:以下是本篇文章正文内容,下面案例可供参考
一、结构体基础知识
结构是一些值的几何,这些值称为成员变量。值得一提的是结构的每个成员可以是不同类型的变量。
结构体的关键字为struct
(一)、结构体类型的声明、变量的创建与初始化
- 我们以描述一个学生为例:
#include<stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//注意分号不能丢
我们列举了学生应该有的一些特征,利用不同的数据类型给定义变量值作为结构体的成员,将这些成员组合在一起就构成了一个描述学生的结构体。这就是它的声明.
- 对于结构体的变量创建以及初始化,我们还是以描述一个学生为例:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};//注意分号不能丢
int main()
{
struct Stu s={"张三",20,“男”,“20230818001”};
//这里我们就创建了结构体变量s,并对其进行了初始化的操作
}
同时我们也可以在声明部分直接定义结构体变量
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}s;//注意分号不能丢
这里我们直接在声明中大括号后面,分号之前直接定义了结构体变量s。
- 结构体的特殊声明
在声明结构体的时候,可以不完全声明,即将结构体的名字给抹掉。这样的话存在两个个弊端:
第一个就是就是我们进行定义结构体变量的时候,如果没有对结构体进行typedef重命名的话,基本上只能在声明后面定义变量,且只能使用一次。
第二个就是尽管两个结构体里面成员完全一致,但会将这两个结构的声明当成完全不同的类型。
//匿名结构体声明:
struct
{
int a;
char b;
float c;
}x;//如果没有重定义的话只能在声明部分,定义变量
struct
{
int a;
char b;
float c;
}a[20],*p;//如果没有重定义的话只能在声明部分,定义变量
根据第二个弊端可以得出:
p=&x;
这句代码是完全错误的,因为编译器会将上面两个匿名结构体声明当成两种不同的类型。
(二)、结构成员访问操作符
成员访问操作符一个是: . ;另外一个是:-> ;
这两个一个针对非指针结构体变量,一个针对指针型结构体变量。
#include<stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}a,*b;//注意分号不能丢
int main()
{
//我们要访问stu结构体中的成员两种方式:
//1.非指针型
a.age=20;
printf("%d\n",a.age);
//2.指针型
printf("%s",b->name);
}
(三)、结构体的自引用
- 结构体的自引用就是结构体的成员中有类型为结构体本身。
- 例如定义一个单链表的节点
struct Node
{
int data;
struct Node next;
};
其实上面的代码是有一些问题的,我们考虑一下啊,一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷大,这样是不合理的,正确的引用应该用指针型:
struct Node
{
int data;
struct Node*next;
};
这样的话指针在X86环境下只占4个字节,就可以避免了结构体变量大小为无穷大的现象。
- 对于不完全声明的匿名结构体类型是不能进行自引用的(尽管进行了重定义)
typedef struct
{
int data;
Node* next;
}Node;
上面这段代码是错误的,因为自定义类型Node在后面声明的,不能提前使用。
故而定义结构体(自引用)不要使用匿名结构体。
(四)、结构体传参
- 传值调用
struct S
{
int data[1000];
int num;
};
void print1(struct S s)
{
printf("%d",s.num);
}
int main()
{
struct S s={{1,2,3,4},4};
print1(s);//传参传的是结构体
}
- 传址调用
struct S
{
int data[1000];
int num;
};
void print2(struct S* p)
{
printf("%d",p->num);
}
int main()
{
struct S s={{1,2,3,4},4};
print2(&s);//传参传的是结构体的地址
}
上面两端代码打印的结果是相同的,分别是结构体的传值调用,以及传址调用,但是我们首先选择的是传址调用,因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。而传址传的是指针,在X86环境下只有4个字节,比较小。故而结构体传参的时候,我们优先选择传结构体的地址。
二、结构体内存对齐!!
(一)、对齐规则
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
宏 offsetof(type,member)(头文件 stddef.h)是计算结构体成员相较于结构体变量起始位置的偏移量。 - 其他成员要对齐到某个数字(对齐数)的整数倍的地址处
在这里对齐数=编译器默认的一个对齐数 与该成员变量大小的较小值。
VS中默认对齐数为8;Linux gcc中没有默认对齐数,对齐数就是成员自身的大小 - 结构体总的大小为最大对齐数(结构体中每一个成员变量都有一个对齐数,所以对齐数中最大的)的整数倍
- 对于镶嵌了结构体的情况,镶嵌的结构体成员对齐到自己成员中最大对齐数的整数倍处。故而结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
(二)、分析结构体大小的详细过程
- 练习1:
#include<stdio.h>
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd",sizeof(struct S2));
}
运行结果如下:
根据结构体对齐规则:对齐如下图:
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;然后第二个成员为n,它的内存大小为4个字节,VS默认对齐数为8,所以取较小值,故而它的对齐数为4,要从对齐数的整数倍开始,故而n变量的地址起始位置应该在偏移量为4的位置,顺至到偏移量为7的位置(共4个字节);最后一个成员为c2,它的内存大小为1个字节,VS默认对齐数为8,所以它的对齐数为1.故而偏移量8即为它的起始地址。结构体成员的最大对齐数为4,所以结构体变量的大小应该是最大对齐数的整数倍,即4的倍数,现在已经是9个字节了,所以还应该浪费3个字节,变成12个字节故而此结构体变量大小为12个字节。
- 练习2:
#include<stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
int main()
{
printf("%zd",sizeof(struct S1));
}
运行结果如下:
根据结构体对齐规则:对齐如下图:
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;然后第二个成员为c2,它的内存大小为1个字节,VS默认对齐数为8,所以取较小值,故而它的对齐数为1,顺利的占据偏移量为1的地址处;最后第三个成员为int型,它的内存大小为4,VS默认对齐数为8,所以它的对齐数为4,要从对齐数的整数倍开始,故而n变量的地址起始位置应该在偏移量为4的位置,顺至到偏移量为7的位置(共4个字节)。它的所有成员的最大对齐数为4,根据对齐规则,此结构体变量的大小应该是最大对齐数的整数倍,所以占据8个字节。
- 练习3:
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%zd",sizeof(struct S3));
}
运行结果如下:
根据结构体对齐规则:对齐如下图:
结构体第一个成员为d,它位于结构体变量起始位置偏移量为0的地址处,占用8个字节;第二个成员为c,它的内存大小为1个字节,VS默认对齐数为8,所以它的对齐数为1,故而它的起始位置是偏移量为8的位置,共占据一个字节;它的第三个成员为i,它的内存大小为4个字节,VS默认对齐数为8,所以它的对齐数为4,因为对齐数位置要是4的倍数,所以它的起始位置的偏移量为12,共占据4个字节,此时偏移量来到15。此结构体变量成员中最大对齐数为8,所以该结构体变量所占字节大小为8的倍数,此时正好为16个字节,正好满足为8的倍数,所以不用额外扩充,浪费字节。
- 练习4:结构体嵌套问题
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd",sizeof(struct S4));
}
运行结果如下:
根据结构体对齐规则,对齐如下图:
结构体第一个成员为c1,它位于结构体变量起始位置偏移量为0的地址处,占用一个字节;紧接着,第二个成员为s3,它的内存大小为16(上面以求),VS默认对齐数为8,所以它的对齐数为8,由于它的起始 位置要是8的倍数,所以它的起始位置偏移量为8,共计16个字节,所以来到了偏移量为23的位置;该结构体的第三个成员为d,它的内存大小为8个字节,VS默认对齐数为8,所以它的对齐数为8,由于它的起始 位置要是8的倍数,所以它的起始位置偏移量为24,共计8个字节,来到了偏移量为31的位置。该结构体成员的最大对齐数为8,所以该结构体变量的大小为8的倍数,此时正好为32个字节,正好满足为8的倍数,所以不用额外扩充,浪费字节。
(三)、为什么存在内存对齐
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。故而应该对齐指定位置。 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐地内存,处理器需要作两次内存访问;而对齐地内存访问仅需要一次访问。假设一个处理器总是从内存中取4个字节,则地址必须是4的倍数。如果我们能保证所有的int类型的数据的地址都能对齐成4的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节的内存块中。
以下面例子来解释:
我们一次要访问4个字节,如果没对齐,我们要完整的访问n变量需要两次访问,而对齐后我们只需要一次访问即可,这节省了时间。
总体来说:结构体的内存对齐实质上是拿空间来换取时间的做法。
而我们肯定会想,设计结构体的时候,如何做到又能对齐,又能节省空间。我们的做法是应该让占用空间小的成员尽量集中在一起!!
#include<stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
struct S2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%zd\n",sizeof(struct S1));
printf("%zd\n",sizeof(struct S2));
}
运行结果如下:
S1和S2结构体的成员一样,但是S1中让占用空间小的成员c1,c2集中在一起,那么它占用的内存就小
(四)、默认对齐数的可修改性
- 我们用#pragma 这个预处理指令, 可以改变编译器的默认对齐数
#pragma pack(1)
struct S
{
char c1;
int n;
char c2;
};
int main()
{
printf("%zd\n", sizeof(struct S));
}
运行结果如下:
此时VS的默认对齐数为1.
三、结构体实现位段!
(一)、位段的声明
- 位段的声明与结构体的声明类似,但需要注意以下几点:
1.位段的成员必须是 int、unsigned int、signed int、c类型,但在C99中也可以选择其他类型;
2.位段的成员名后边有一个冒号和一个数字。
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
在这里A就是一个位段类型,这里里面成员_a冒号后的数字代表所给变量定义所占的二进制位数。
那么这个结构体的大小会是多少呢,要解决这个问题,我们得先了解位段的内存分配规则
(二)、位段的内存分配
-
位段成员可以是int、unsigned int、signed int或者是char等类型
-
位段的空间是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
-
位段涉及很多不确定因素,位段是不跨平台的,要注意可移植的程序应该避免使用位段
-
分析下面位段的大小:
#include<stdio.h>
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s={0};
int main()
{
printf("%zd",sizeof(struct S));
}
运行结果如下:
咱们假设给定空间后,在空间内部都是从右向左使用;当剩下空间不足以放下一个成员的时候,空间就浪费掉;且位段的哦那关键是按照需要以1个字节的方式来开辟。
这样,在开辟第一个字节时候,a从右向左存储三个bit位,紧接着b存储4个比特位,这个字节就浪费了一个bit位,紧接着开辟下一个字节,这是c从右向左占据5个比特位,剩下3个bit位不足以存放d变量,所以又重新开辟一个字节。故而这个位段的大小为3个字节。
(三)、位段的跨平台问题
- int 位段被当作有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(例如16位机器最大为16,而32位机器最大是32。故而写成27,在16位机器中会出错)
- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义
- 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳第一个位段剩余的位时候,是舍弃剩余的位还是将剩余位利用,这个是不确定哒。
总而言之:与结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是位段存在跨平台问题。
(四)、位段的应用
- 在网络协议中,IP数据报的格式,很多属性只需要几个bit位就可以描述,在这里我们可以使用位段,来达到理想的效果,充分节省空间,这样下来,在网络传输的时候,数据报的大小也会较小一些,这对网络的畅通是有帮助的。
(五)、位段的使用注意事项
- 位段的几个成员共用一个字节,这样有些成员的起始位置并不是某个字节的起始位置,故而这些位置处是没有地址的。因为在内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。故而我们在对没有地址的位段成员不能使用取地址操作符(&),所以在用scanf函数的时候,我们应该采用间接赋值。
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = { 0 };
int d = 0;
scanf("%d", &d);
sa._d = d;
printf("%d", sa._d);
}
为了避免位段中的成员可能不能进行取地址操作,我们引入中间变量d,来对位段成员间接赋值。
总结
本文介绍了C语言自定义类型中的第一种结构体的相关知识,着重介绍了结构体内存对齐以及位段的相关知识,如有错误,请批评指正。