一. 结构体类型的声明和定义
在实际问题时,有时候我们需要其中的几种一起来修饰某个变量,例如一个学生的信息就需要成绩(整型),姓名(字符串),年龄(整型)等等,这些数据类型都不同但是他们又是表示一个整体,要存在联系,那么我们就需要一个新的数据类型,结构体。
(数组是一组相同类型的元素集合)
1.1结构体相关概念
1.11结构的声明
结构体由不同类型的数据组合成一个整体,以便引用,这些组合在一个整体中的数据是互相联系的。
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
声明一个结构休类型的一般形式如下:
struct 结构体名
{成员列表};
1.12成员列表
成员列表称为域表,第一个成员也称为结构体中的一个域。成员名定名规则写变量名同。
成员列表:
类型名 成员名;
1.2定义结构体类型变量的方法
1.21先声明结构体类型再定义变量名
struct student{
成员表列
}student1, student2 //结构体变量名
1.22在声明类型的同时定义变量
struct 结构体名
{
成员表列
}变量名表列;
1.23直接定义结构类型变量
struct
{
成员表列
}变量名表列;
二、结构体变量的创建、初始化和访问
2.1结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数
使用方式:结构体变量.成员名
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;
}
2.2结构体成员的间接访问
有时候我们得到的不是一个结构体变量,而是得到了一个指向结构体的指针。
使用方式:结构体指针->成员名
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = { 3, 4 };
struct Point* ptr = &p;
ptr->x = 1;
ptr->y = 2;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
2.3匿名的结构体类型
匿名结构体类型,也称为未命名结构体,是指在定义结构体成员时省略了结构体的名字,直接定义其成员。由于没有名称,因此不会创建它们的直接对象(或变量),通常我们在嵌套结构或联合中使用它们。匿名结构体类型的作用域仅限于包含它的联合体,它不能在其他地方被引用。
//匿名结构体类型 //只能使用一次 struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], * p;
问:在上面代码的基础上,下面的代码合法吗?
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
.
三、结构的自引用
注:结构体自引用方式里面必须包含同类型的结构体指针
在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表的节点:
如果这样编写
struct Node
{
int data;
struct Node next;
};
sizeof(struct Node) 是多少?
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大
小就会无穷的大,是不合理的。
正确的自引用方式:
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;
自引用的使用案例:
链表:
在链表中,每个节点都包含数据和指向下一个节点的指针,这个指针就是自引用,它指向下一个相同类型的节点。这种结构可以使得链表在内存中灵活存储,并且不要求数据元素连续存放,从而大大提高存储器的使用效率。
四、计算结构体的大小(结构体内存对齐)
对齐规则:
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的 整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
问:为什么结构体中的成员相同但占用的空间不同?
答:因为要对齐。
4.1偏移量计算的示例:
此处也有一个偏移量的概念。
在C语言中,偏移量通常用于描述结构体成员相对于结构体起始地址的内存位置。C语言的结构体是由一系列不同类型的成员组成的,这些成员按照声明顺序在内存中连续存放。由于不同的成员可能具有不同的大小和对齐要求,因此它们在内存中的位置不是简单的线性关系。
偏移量是一个表示成员变量相对于结构体开始位置的内存位置的数值。计算偏移量可以帮助我们了解结构体成员在内存中的布局情况。
offsetof - 计算结构体成员相较于起始位置的偏移量
#include<stddef.h>
struct Example { char a; // 1字节 int b; // 4字节 short c; // 2字节 double d; // 8字节 };
#include <stdio.h> #include <stddef.h> // 定义一个简单的结构体 struct Example { char a; // 1字节 int b; // 4字节 short c; // 2字节 double d; // 8字节 }; int main() { // 使用 offsetof 宏来获取结构体成员 b 的偏移量 size_t offset_a = offsetof(struct Example, a); // 使用 offsetof 宏来获取结构体成员 b 的偏移量 size_t offset_b = offsetof(struct Example, b); // 使用 offsetof 宏来获取结构体成员 b 的偏移量 size_t offset_c = offsetof(struct Example, c); // 使用 offsetof 宏来获取结构体成员 b 的偏移量 size_t offset_d = offsetof(struct Example, d); // 打印偏移量 printf("a的偏移量为%d\n", offset_a); printf("b的偏移量为%d\n", offset_b); printf("c的偏移量为%d\n", offset_c); printf("d的偏移量为%d\n", offset_d); return 0; }
4.2计算结构体大小示例:
- int a:大小为4字节,对齐到4字节。
- char b:大小为1字节,但由于后面的short大小为2字节,所以char b;后面会有1字节的填充,使得short c;对齐到2字节。
- short c:大小为2字节,已经对齐到2字节。
- short d:大小为2字节,已经对齐到2字节。
- 计算时结构体的总大小为:4 (int) + 1 (char) + 1 (填充) + 2 (short) + 2 (short) = 10字节。
- 但是,由于默认对齐数为8字节,所以结构体的总大小需要对齐到8字节的整数倍,即16字节。
因为其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,所以有浪费的内存空间。
对齐是从对齐数的倍数地址开始。
在上述结构体S1和S2中,c1和c2都为char类型,占一个字节,对齐数为1。
i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。
所以此处S1、S2大小分别为8、12个字节。
struct S1 { char c1; char c2; int i; }; struct S2 { char c1; int i; char c2; }; int main() { struct S1 s1 = { 0 };//8 struct S2 s2 = { 0 };//12 printf("%d\n", sizeof(struct S1));//8 printf("%d\n", sizeof(struct S2));//12 return 0; }
4.3嵌套结构体的计算:
计算时把嵌套中的结构体作为一个数据类型去计算,最大对齐数也要与其比较,故有时候会出现最大对齐数为3,5的情况(结构体中包含一个char数组,数组大小为奇数)。
在上述结构体S3和S4中,c和c1都为char类型,占一个字节,对齐数为1。
i为整型,占四个字节,对齐数为4个字节,从4的倍数的地址开始对齐。
d为双精度浮点型,占8个字节,对齐数为8个字节,从8的倍数的地址开始对齐。
S3为结构体,占16个字节,但最大对齐数为8个字节,所以从8的倍数开始对齐。
所以S3占16个字符,S4占32个字符。
.
4.4为什么存在内存对齐?
结构体的内存对齐是拿空间来换取时间的做法
1. 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起。
4.5修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
#pragma pack(1)//设置默认对齐数为1 struct S { char c1; int i; char c2; }; #pragma pack()//取消设置的对齐数,还原为默认 int main() { //输出的结果是什么? printf("%d\n", sizeof(struct S)); return 0; }
五、结构体传参
- 值传递: 在值传递中,结构体的副本被传递给函数。这意味着函数内部对结构体所做的任何修改都不会影响到原始的结构体。这种传递方式适用于小型结构体,因为结构体的副本需要占用额外的内存空间。
- 指针传递: 在指针传递中,结构体的地址被传递给函数,函数内部通过使用指针来访问和修改结构体的内容。这种方式可以避免结构体的副本创建,因此对于大型结构体更为高效。同时,函数内部对结构体的修改会影响到原始的结构体。
// 定义一个结构体 S,包含一个整数数组 data 和一个整数 num struct S { int data[1000]; // data 是一个可以存储1000个整数的数组 int num; // num 是一个整数 }; // 通过值传递的方式打印结构体的内容 void printf1(struct S t) // 通过值传递接收一个 S 类型的结构体 { printf("%d %d\n", t.data[0], t.num); // 打印结构体的 data 数组的第一个元素和 num 的值 } // 通过指针传递的方式打印结构体的内容 void printf2(struct S* ps) // 通过指针传递接收一个 S 类型的结构体的指针 { printf("%d %d\n", ps->data[0], ps->num); // 使用指针访问并打印结构体的 data 数组的第一个元素和 num 的值 } int main() // 主函数 { // 初始化一个 S 类型的结构体 s,并为其 data 数组和 num 赋值 struct S s = { {1,2,3,4,5},100 }; // data 数组初始化为 {1,2,3,4,5},num 初始化为 100 printf1(s); // 通过值传递调用 printf1 函数,打印 s 的内容 printf2(&s); // 通过指针传递调用 printf2 函数,打印 s 的内容(取 s 的地址传递给函数) return 0; // 主函数返回 0,表示程序正常结束 }
问:上面的 print1 和 print2 函数哪个好些?
答:首选print2函数。
原因:
- 函数传参的时候,参数是需要压栈,会有时间和空间上的损耗。
- 如果传递一个结构体对象的时候,结构体过大,参数压栈的的损耗比较大,所以可能导致性能的下降。
六、结构体位段(位域)实现
6.1位段(位域)的介绍
(有些资料里称为“位段”,也有的称为“位域”)
C 语言的位域(bit-field)是一种特殊的结构体成员,允许我们按位对成员进行定义,指定其占用的位数。
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以选择其他类型。
- 位段的成员名后边有一个冒号和一个数字。
以下是VS 中的实例:
struct A { int _a : 2; int _b : 5; int _c : 10; int _d : 30; // 2+5+10+30 = 47bit }; //变量名 //1.字母,数字,下划线 //2.不能是数字开头 int main() { printf("%d\n", sizeof(struct A));//8 - 64bit }
此处为什么struct A是64bit而不是48bit呢?
这就涉及到位段的内存分配了!
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
所以对于6.1中的实例,占用内存上为a,b,c共申请一个32bit空间,d申请一个32bit空间,总计64bit。
6.2内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char 等类型。
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
以下是VS 中的实例:
struct S { char a : 3; char b : 4; char c : 5; char d : 4; }s; int main() { struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; printf("%d\n", sizeof(s)); //3 }
6.3位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
- 总结:跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
6.4位段的应用
下图是⽹络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要⼏个bit位就能描述,这⾥使⽤位段,能够实现想要的效果,也节省了空间,这样⽹络传输的数据报⼤⼩也会较⼩⼀些,对⽹络的畅通是有帮助的。
6.5位段使用的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用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; }
今天就先到这里了!!!
看到这里了还不给博主扣个:
⛳️ 点赞☀️收藏 ⭐️ 关注!
你们的点赞就是博主更新最大的动力!
有问题可以评论或者私信呢秒回哦。