一、前言
结构体与数组都是C语言中的自定义类型,他们的包含的值,样式都是又程序员自己决定的。
结构体是什么?
结构体是一种用户自定义的复合数据类型,它允许将不同类型的数据组合在一起,形成一个新的数据类型
结构体有什么特点?
- 封装性: 结构体可以将相关的数据封装在一起,隐藏内部细节,只暴露必要的接口。
- 灵活性: 结构体的成员可以是基本数据类型,也可以是其他结构体类型,甚至可以包含指向自身的指针。
- 内存对齐: 结构体的大小不一定等于所有成员大小的简单相加,编译器可能会在成员之间插入填充字节,以满足内存对齐的要求。
对于以上的特点,我们将会在下面的文章中展示
二、结构体类型的声明
结构体类型声明十分的简单,我们可以按照下面的这种方式:
struct 结构体名称 {
成员类型1 成员名称1;
成员类型2 成员名称2;
...
成员类型n 成员名称n;
};
就像这样:
struct stu {
char name[20];
int ID;
int score;
};
注:无法在结构体内直接对变量的值进行设定,必须在结构体初始化的时候才可以
这种是声明,已经有名字的结构体了,但是我们也可以把名字不进行声明,也称为匿名结构体,如:
struct {
成员类型1 成员名称1;
成员类型2 成员名称2;
...
成员类型n 成员名称n;
};
struct {
char name[20];
int ID;
int score;
};
匿名结构体的限制:
- 不能直接赋值: 不能将一个匿名结构体变量直接赋值给另一个匿名结构体变量
- 不能直接比较: 不能直接比较两个匿名结构体变量
- 成员访问: 可以通过点运算符访问成员,但每次访问时都需要明确指定变量名
注:
如是连续声明了两个匿名结构体,这两个结构体也是不同的,如下:
#include <stdio.h>
struct {
int a;
int b;
} s1 = {20,24};
struct {
int a;
int b;
}*p;
int main() {
*p = &s1;
return 0;
}
这两个匿名的结构体是不同的类型,所以*p = &s1是错误的!(上面涉及的结构体创建我们会在下面讲)
三、结构体的创建与初始化
下面我用代码介绍三种结构体的初始化方式,分别是按顺序初始化、不按顺序初始化、指针初始化,同时也可以在结构体后面直接创建,如s0,此时创建出来的结构体是全局变量
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu {
char name[20];
int score;
char ID[20];
} s0 = {"李白", 78, "2400320224"};
int main() {
//按顺序初始化
struct stu s1 = { "张三",98,"2400320225" };
//不按顺序初始化
struct stu s2 = { .ID = "2400320226" ,.name = "李四",.score = 99 };
//指针初始化
struct stu* ps;
struct stu* tmp = (struct stu*)malloc(sizeof(struct stu));
if (tmp != NULL) {
ps = tmp;
strcpy(ps->ID, "2400320227");
ps->score = 100;
strcpy(ps->name, "王五");
}
return 0;
}
对于结构体的创建与初始化大家就多了解学会就行
四、结构体的重命名
我们可以观察上面的代码,我们要创建结构体时需要写很长的一个类型名struct stu,这是很麻烦的,所以我们可以通过C语言中的关键词typedef对结构体类型进行重命名,如下:
#include <stdio.h>
typedef struct stu {
char name[20];
int score;
char ID[20];
}stu;
int main() {
stu s1 = { "张三",98,"2400320225" };
return 0;
}
这里我们将struct stu重命名为了stu,使用起来更加简便
结构体的自引用
前面中我们说过结构体中可以定义许多的数据类型,那么我们能不能在结构体中定义自己的结构体类型呢?如下:
struct stu {
char name[20];
int score;
char ID[20];
struct stu next;
};
其实这样是不行的,我们可以知道struct stu结构体中有一个名为next的结构体,类型也为struct stu,那么同理这个next里面也有一个next,这样就会无限循环的创建,那么这个结构体是非常大,无穷尽的! 如果我们需要在结构体中定义有另一个结构体,我们可以这样写:
struct stu {
char name[20];
int score;
char ID[20];
struct stu* next;
};
我们在结构体中定义一个结构体的指针,我们也可以通过这个指针找到里面的结构体!
不可以提前使用结构体名称
请看下面的代码:
typedef struct {
char name[20];
int score;
char ID[20];
stu* next;
}stu;
对于匿名的结构体,我们重命名称为了stu,但是我们不能在其内部使用stu的,因为编译器还没重名成功,是无法认识stu这个类型的!
注:为了避免这种情况,建议少使用匿名结构体
五、结构体的内存对齐
《数据在内存中的存储》一文中,我们讲解了整数、浮点数在内存中的存储方式,这里我们来讲讲结构体在内存中的存储,结构体是内存对齐的,而与结构体内存对齐有最大关联的是结构体大小的计算
对齐规则
-
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
-
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值
VS 中默认的值为 8
Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
-
- 结构体总大小为最大对齐数 (结构体中每个成员变量都有一个对齐数,所有对齐数中最大的) 的
整数倍
- 结构体总大小为最大对齐数 (结构体中每个成员变量都有一个对齐数,所有对齐数中最大的) 的
-
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构
下面我们举几个例子
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main() {
printf("%zd\n", sizeof(struct S1));
}
S1的内存大小是12,为什么呢?我们可以画图分析
首先我们需要第一个成员c1对齐到内存的第一个位置,所有成员中对齐数最大为4,也正因为c1后面只有三个字节,所以 i 必须存到下4个字节块中,c2再存入四个字节块,其实意思就是每个成员都占了4个字节,这样才满足 “结构体总大小为最大对齐数 (结构体中每个成员变量都有一个对齐数,所有对齐数中最大的) 的
整数倍” 的规则
我们再看下面的代码
#include <stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
int main() {
printf("%zd\n", sizeof(struct S1));
}
S1的内存为8,为什么呢?我们继续画图分析
同样的我们按成员顺序存储,最大对齐数为4,我们将c1存进去后,后面还有3个字节,但是c2只占一个直字节,我们可以直接把c2存入c1后面,再存入i的时候,我们发现2个字节无法融入i,所以我们需要在下个4字节内存块存放i,所以整个结构体只占8个字节
不难看出,成员摆放的方式不同,对结构体大小有着影响
下面我们来看结构体嵌套结构体的案例
#include <stdio.h>
struct S1
{
char c1;
char c2;
int i;
};
struct S2 {
int i;
struct S1 s;
double d;
};
int main() {
printf("%zd\n", sizeof(struct S2));
}
S2的大小为24字节,我们继续画图分析
其实结构体中嵌套结构体也不难计算,对其套路一致,只需要把嵌套的结构体所占内存大小算出,将它当作一个变量即可,这里我不过多解释了
为什么需要内存对齐?
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常 - 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要
作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地
址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以
用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两
个8字节内存块中,效率也会降低
如果我们要减小结构体的大小就尽量将内存小的成员变量集中在一起,如第二个例子
六、结构体的传参
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降,所以结构体在传参时,我们应该传结构体的地址,在通过地址调用结构体
七、位段
在C语言中,位段(Bit Field)是一种特殊的结构体成员,用于将一个字节中的多个位分配给不同的变量。它允许程序员精确地控制内存的使用,通常用于需要节省内存空间或者与硬件设备进行交互的场景,这里的位指的就是比特位(Bit),位段的成员必须是int、unsigned int 或 signed int ,在C99中位段成员的类型也可以选择其他类型
位段的实现
struct 结构体名称{
数据类型 成员名 : 位宽;
};
如下:
struct BitSegment {
int _a : 3;
int _b : 4;
int _c : 1;
};
位段的内存开辟
上面代码中_a、_b、_c分别占3、4、1个比特位,一共占用了八个比特位,那么整个结构体的大小是1个字节吗?不是的,其实是四个字节,因为int类型的数据大小为4字节,结构体内存的存储必须满足对齐规则,所以大小为4个字节,那么_a、_b、_c是怎么存储的呢?我们看下面的图:
- 位段的成员可以是int unsigned int signed int 或者是char 等类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的,也就是说,如果是int类型会先开辟4个字节,如果是char类型则先开辟一个,存不下时会开辟新的内存
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
位段的存储大致如上,编译器会根据变量所占的位数截断,放入字节中,若是字节不满足放下一个完整数据则编译器会开辟新的字节
位段的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的,所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员,如下:
#include <stdio.h>
struct BitSegment {
int _a : 3;
int _b : 4;
int _c : 1;
};
int main() {
struct BitSegment s1;
int tmp;
scanf("%d", &tmp);
s1._a = tmp;
return 0;
}
End
对于自定义类型-结构体的介绍我们就讲到这里,小编对结构体的理解还是有限的,希望有错误的地方大家帮忙指正,谢谢大家的浏览!