目录
结构体
在C语言中,整形我们可以用int来描述,字符我们可以用char来描述。但是如果我们现在要描述一个学生,你就会发现无法用int或char来描述一个学生。结构体,就是用来解决这个问题的
结构体的声明
struct tag
{
member-list;
} variable - list;
如上,struct tag就是结构体变量的变量名,比如我想描述一个学生,那么我就可以写成:
struct student
{
;
};
要描述一个学生,就需要一些该学生的信息,如:姓名,性别,学生证号等等。那么我们就可以这么写:
struct student
{
char name[ 5 ];
char gender[ 6 ]; //male 或 female
int age;
};
就像整形的类型是int,字符的类型是char,我们定义出来的结构体变量的类型就是struct student,如下:
int a;
char a;
struct student a;
同时,下方的variable - list是我们定义变量的地方,如下:
#include<stdio.h>
struct student
{
char name[5];
char gender[6];//male 或 female
int age;
}a;//等价于struct student a
int main()
{
struct student c;
return 0;
}
结构体的初始化
对于结构体,我们应该一个一个初始化,如下:
#include<stdio.h>
struct student
{
char name[20];
char gender[6];//male 或 female
int age;
};
int main()
{
struct student a = { "张三","male",18 };
struct student b = { "王五","male",30 };
struct student c = { .gender = "female",.age = 12,.name = "静香" };
return 0;
}
我们可以看到,结构体的初始化是需要把每一个都初始化的,但是不一定要按照顺序,我们可以通过点操作符—— . 来找到结构体的成员
但是需要注意的是,如果要初始化一个字符的话,那我们就需要用到 “ ”
结构的特殊声明——匿名结构体
在结构体中有这样一种类型——匿名结构体,如下:
struct
{
char name[20];
char gender[6];
int age;
} a;
我们会看到,这个结构体很不一样,因为他都没有名字!而我们也不能去引用他,如上我们可以创建一个结构体变量 struct student a; 但是这个我们会发现根本没法引用
而且使用匿名结构体变量时,我们会碰到一些特殊的场景,如下:
#include<stdio.h>
struct
{
char name[20];
char gender[6];
int age;
} a;
struct
{
char name[20];
char gender[6];
int age;
} *b;
int main()
{
b = &a;
return 0;
}
当我们运行程序的时候,编辑器就会报错
这是因为编辑器会认为,这是两个不同的东西,所以我们没有办法将两者联系到一快儿
所以我们什么时候才要使用匿名结构体变量呢?
只有该结构体只会使用一次时,才会用到匿名结构体变量
当然,如果我们还想使用这个匿名结构体变量的话,我们还可以用 typedef 对结构体进行重命名
//情况1
typedef struct
{
char name[20];
char gender[6];
int age;
}Stu;
//情况2
typedef struct abc
{
char name[20];
char gender[6];
int age;
}Stu;
如上,情况1 和情况2 是完全相等的,都是结构体变量Stu
结构体的自引用
而在结构体里,还有关于结构体的自引用
这方面的知识多用于初阶数据结构中的链表,我们能通过结构体的自引用找到其他相同类型的结构体
在这里我简单说一下链表是一个什么东西:
如上是一个数组,我们通过首元素地址就能顺藤摸瓜找到整个数组
这时我们来想一个问题:如果有8节车厢,每一节车厢之间都需要钥匙才能通过,而我们手上就只有第一节车厢的钥匙,那我们该如何走到尾车厢呢?
答案是:在每一节车厢都放着通往下一节车厢的钥匙
我们在每一个链表节点里面放着下一个节点的地址,而我们通过地址就能够找到下一个节点
而结构体自引用的写法如下:
#include<stdio.h>
struct Stu
{
int age;
struct Stu* next;
};
注意,当我们用到的是结构体指针时,我们就需要使用 -> (箭头操作符)来找到结构体指针指向的内容
还需注意的是,我们如果要使用typedef 来给结构体重命名的话,我们在结构体内部是不能使用我们重命名的名字的,如下:
//正确做法
typedef struct Stu
{
int age;
struct Stu* next;
}Stu;
//错误做法
typedef struct Stu
{
int age;
Stu* next;
}Stu;
结构体内存对齐
结构体的内存对齐是结构体的重点,讲的是结构体在内存中的存储方式
结构体内存对齐有如下几条规则:
- 结构体的第一个成员要对其到结构体变量偏移量为0的地址处
- 对齐数是成员变量大小与编辑器默认对齐数相比之下的较小值,而每个成员变量都要对齐到对齐数的整数倍的地址上
- 结构体的总大小为最大对齐数的整数倍
- 如果在该结构体中嵌套了一个结构体变量作为成员,那么该结构体成员的最大对齐数就是其内部最大成员变量的对齐数
#pragma 这个指令可以修改默认对齐数,如:
#pragma pack(1) // 此处将默认对齐数修改为 1
看到这里可能有些人会觉得很难,接下来我就来逐个给各位讲讲
首先我们先来看这么一串代码:
#include<stdio.h>
struct Stu1
{
char a;
int b;
char c;
};
struct Stu2
{
char a;
char b;
int c;
};
int main()
{
printf("%d\n", sizeof(struct Stu1));
printf("%d\n", sizeof(struct Stu2));
return 0;
}
我们会看到,两个结构体内存放的虽然都是相同的元素,但是大小却不尽相同,我们用画图的方式来理解一下
如上,蓝色部分代表的是char类型,橙色部分代表的是int类型
先看左边的部分:
我们先存了一个char类型的数据,在偏移量为0的位置。而接着存了一个int类型的数据进去,但是根据第二条规则,这个int类型数据不是第一个,所以要对其到对齐数的整数倍处。默认对齐数为8,int的大小为4,8>4,所以对齐数为4,int就要对齐到4的倍数处,如上
而随后又存了一个char的数据进去,易得其对齐数为1,所以直接跟在int的后面
又根据第3条规则,结构体的大小为最大对齐数的整数倍,int的对齐数为4,char的对齐数为1,显然4是最大的对齐数,上面的总大小为9,但9不是4的倍数,所以我们需要浪费3个空间凑到12,12是4的倍数,所以这个结构体的大小为12
再来看右边的部分:
易得char的对齐数为1,int的对齐数为4,两个char连在一起放置,而int需要对其到4的倍数处,而总大小为8,最大对齐数为4,8是4的倍数,所以该结构体的总大小为8
看完了上述的情况,相信你对前三条规则已经有了初步的了解,而第四条又是什么意思呢?如下:
#include<stdio.h>
struct Stu1
{
char a;
int b;
};
struct Stu2
{
char a;
char b;
int c;
struct Stu1 d;
};
int main()
{
printf("%d\n", sizeof(struct Stu2));
return 0;
}
结构体1被嵌套在结构体2中,结构体1的成员里最大对齐数是4(int),又因为默认对齐数8>4,所以该结构体的对齐数就是4
对其到4的倍数的位置之后,其原本的大小为8,所以结构体2的大小就是16
为什么会出现对齐呢?
1. 平台原因
有些平台有特定的搜索数据的方式,某些硬件平台只能在特定的地址处取特定的数据
2. 性能原因
同样的一个数据,以不同的方式存进内存里,第一种编辑器会直接找到数据,但是第二种计算机第一次找只能找到前半段,后半段需要第二次寻找,也就是说效率变低了
所以,内存对齐从本质上来讲就是用空间换时间
结构体传参
首先我们需要知道
int* 是指向 int 的结构体
char* 是指向 char 的结构体
struct Stu* 就是一个指向 struct Stu 的结构体
了解了上述内容,我们接下来来看下面这么一段代码
#include<stdio.h>
struct Stu
{
char a;
int b;
};
void Print1(struct Stu* test)//用结构体指针接收
{
printf("%d\n", test->b);
}
void Print2(struct Stu test)
{
printf("%d\n", test.b);
}
int main()
{
struct Stu test = { 'a',1 };
Print1(&test);//传址
Print2(test);//传值
return 0;
}
两个函数的结果相同,那我们用哪一种方式会好一点呢?
https://blog.csdn.net/2302_80023639/article/details/134412692?spm=1001.2014.3001.5501
在这里我推荐各位了解一下函数栈帧的相关知识点
我们都知道,函数的形参是实参的一份临时拷贝,而如果我们将整个结构体传过去的话,倘若结构体过大,那么程序运行的效率就会下降
而如果我们传一个结构体的地址过去,随后用结构体指针来接收的话,指针的大小为 4 / 8 个字节,对程序运行效率的影响远小于传结构体
结构体实现位段
讲完了结构体,我们就需要了解结构体实现位段的能力
注意:位段的成员只能是int,unsigned int,char类型(ASCII码)的数据
struct A
{
int a : 2;
int b : 5;
};
如上就是一个位段,位段的成员后面都必须跟一个冒号(:)和一个数字
这个数字表示的是什么呢?
—— 一个比特位
也就是说,我们在位段里可以按照我需要的字节分配空间,如上,我们加起来就需要7个比特位
这个位段的大小是多少?
一个int有32个比特位,而内存的开辟是需要按照4个字节(int)或是1个字节(char)的方式来开辟的,我们在位段中用的是int,所以这个位段的大小就为4
也就是说,不足一个int的,用一个int就够了,所以大小就是一个int的大小,没有用到的空间就会被浪费掉
#include<stdio.h>
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
};
struct B
{
int _a : 2;
int _b : 5;
};
struct C
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
printf("%d\n", sizeof(struct A));
printf("%d\n", sizeof(struct B));
printf("%d\n", sizeof(struct C));
return 0;
}
如上,前两个因为大小都没有超过32个比特位,所以只需要一个int的大小就足够了
而第三个需要47个比特位,47>32,一个字int不够用,所以我们就需要2个int,大小就为8
位段的内存分配
我们来看这样一段代码
#include<stdio.h>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
如上我们会看到,位段会先用第一个字节,但是第一个字节用完了之后,是把剩下的空间用了,还是直接使用下一个字节?这个C语言没有明确规定,具体看编辑器
就像上面,一个字节有8个比特位,我的 a 和 b 已经把前 7 个给用了,那我的 c 是把剩下那个比特位用了还是直接开辟一个新的字节,这是不确定的
再看,我们现在是要把 a 改成10,10的二进制是 01010
要把 b 改成 12,12的二进制是 01100
要把 c 改成 3,3 的二进制是 00011
要把 d 改成 4,4 的二进制是 00100
我们 a 只给了3个字节,所以我们只能取到 01010 的前三个 —— 010
同理,b 就取到 1100, c 就取到 00011,d 就取到 0100
在图上就是
而在程序执行时,其会4个4个地划分,如下:
我们在编辑器上看一看效果:
位段的跨平台问题
位段在跨平台方面是存在很大问题的,因为很多C语言没有规定,所以只能看具体的编辑器
- int 时有符号 int 还是无符号 int 时未知的,也就是最高位是否是符号位是未知的
- 一些比较远古的机器是只有16位的,当我们在32位的机器上创建了一个位段,放到16位的机器上就会出现问题
- 位段中的成员是从左边开始分配内存还是从右边开始也是不确定的,如上我们是从左向右依次分配 a、b、c、d 还是从右向左,这是不确定的
- 当剩下的字节空间不足下一个位段成员存放时,是先把剩下的用完,还是重新再开辟一个新的字节空间,这是不确定的
综上,位段虽然比之结构体能更加节省空间,但是位段存在跨平台的问题
位段的应用
位段应用在明确知道要使用几个字节的情况
如下是IP数据报
如上,我们明确知道了版本要用 4个比特位,首部长度要用 8个比特位等等,在明确知道了要用多少个比特位的情况下,我们就可以考虑使用位段
使用位段的注意事项
由于位段可能是几个成员共用一个字节,是按比特位来算的,所以位段的起点不一定是字节的起点
另,位段使用的是比特位,但只有字节有地址,单一的比特位是没有地址的
使用我们不能对变量成员使用 取地址符号(&),也不能使用 scanf 对成员进行赋值
如若要改变位段成员的值的话,就需要先将其赋值到一个变量上,然后再将该变量赋值给位段成员
或者就直接给成员赋值
如下:
#include<stdio.h>
struct S
{
char a : 1;
char b : 4;
};
int main()
{
struct S s = { 0 };
//错误示范
scanf("%d", % s.b);
//正确示范1
s.a = 10;
//正确示范2
int d = 0;
scanf("%d", &d);
s.b = d;
return 0;
}
结语
至此,我们的结构体就讲完了,如果对你有帮助的话,希望可以留下一个赞!