先赞后看,养成习惯(😁),几天不见,甚是想念,今天肖恩带大家拿捏结构体~~~
1. 结构体类型的声明
那首先,什么是结构体呢?
C语⾔已经提供了内置类型,如:char、short、int、long、float、double等,但是只有这些内置类型还是不够的,假设我想描述学⽣,描述⼀本书,这时单⼀的内置类型是不⾏的。描述⼀个学⽣需要名字、年龄、学号、⾝⾼、体重等;描述⼀本书需要作者、出版社、定价等。C语⾔为了解决这个问题,增加了结构体这种⾃定义的数据类型,让程序员可以自己创造适合的类型。、
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量,如:标量、数组、指针,甚至是其他结构体。
结构体的关键字是 struct
struct tag
{
member-list;
}variable-list;
eg描述⼀个学⽣:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
结构的特殊声明
在声明结构的时候,可以不完全的声明。
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上⾯的两个结构在声明的时候省略掉了结构体标签(tag)。那么问题来了?
//在上⾯代码的基础上,下⾯的代码合法吗?
p = &x;
警告:
编译器会把上⾯的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用⼀次。
结构的自引用
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢?
⽐如,定义⼀个链表(后面也会讲哦)的节点:
struct Node
{
int data;
struct Node next;
};
上述代码正确吗?如果正确,那 sizeof(struct Node) 是多少?
仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的大小就会⽆穷的⼤,是不合理的。
正确的自引用方式:
struct Node
{
int data;
struct Node *next;
};
2. 结构体变量的创建和初始化
给大家就两个例子大家一下就看明白了,这里不需要多做讲解,我们重点在后面呢
eg1
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
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;
}
eg2
//代码1:变量的定义
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//代码2:初始化。
struct Point p3 = {10, 20};
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s1 = {"zhangsan", 20};//初始化
struct Stu s2 = {.age=20, .name="lisi"};//指定顺序初始化
//代码3
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化
3. 结构成员访问操作符
3.0 结构体指针
在前面,肖恩曾讲过指针点击链接查看,但是前面指针少讲了结构体指针,今天我们来补上,这个也不是很难哦,我们看看下面的就欧克了
- 结构体指针的定义:结构体指针是指向结构体变量的指针,可以通过结构体指针来访问和修改结构体的成员变量。
- 结构体指针的声明:结构体指针的声明方式为struct_name *ptr_name;,其中struct_name是结构体的名称,ptr_name是指针变量的名称。
- 结构体指针的初始化:结构体指针可以通过赋值给结构体变量的地址来进行初始化,例如ptr_name = &struct_var;。
- 通过结构体指针访问成员变量:可以使用->运算符通过结构体指针来访问结构体的成员变量,例如ptr_name->member_name。
- 动态分配内存:结构体指针通常与动态内存分配函数malloc()、calloc()和realloc()一起使用,以便在运行时为结构体分配内存空间。(这个肖恩在后面会更的,很快很快)
- 指针的指针:结构体指针也可以是指向指针的指针,这在需要修改指针本身指向的地址时非常有用。
- 指针的运算:结构体指针也可以进行指针运算,例如指针加法、指针减法等操作。
- 结构体指针作为函数参数:结构体指针可以作为函数的参数传递,这样可以在函数内部直接修改结构体变量的值。
#include <stdio.h>
struct Rectangle {
int width;
int height;
};
void calculateArea(struct Rectangle *ptr_rect) {
int area = ptr_rect->width * ptr_rect->height;
printf("Area of rectangle: %d\n", area);
}
int main() {
struct Rectangle rect = {5, 10};
calculateArea(&rect);
return 0;
}
- 空指针检查:在使用结构体指针之前,最好进行空指针检查,以避免出现空指针异常。
3.1 结构体成员的直接访问
结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
}p = {1,2};
int main()
{
printf("x: %d y: %d\n", p.x, p.y);
return 0;
}
使用方式:结构体变量.成员名
3.2 结构体成员的间接访问
有时候我们得到的不是⼀个结构体变量,⽽是得到了⼀个指向结构体的指针。如下所⽰:
#include <stdio.h>
struct Point
{
int x;
int y;
};
int main()
{
struct Point p = {3, 4};
struct Point *ptr = &p;
ptr->x = 10;
ptr->y = 20;
printf("x = %d y = %d\n", ptr->x, ptr->y);
return 0;
}
使用方式:结构体指针->成员名
综合举例:
#include <stdio.h>
#include <string.h>
struct Stu
{
char name[15];//名字
int age; //年龄
};
void print_stu(struct Stu s)
{
printf("%s %d\n", s.name, s.age);
}
void set_stu(struct Stu* ps)
{
strcpy(ps->name, "李四");
ps->age = 28;
}
int main()
{
struct Stu s = { "张三", 20 };
print_stu(s);
set_stu(&s);
print_stu(s);
return 0;
}
接下来就是我们今天的重头戏
我们已经掌握了结构体的基本使⽤了。
现在我们深⼊讨论⼀个问题:计算结构体的⼤⼩。
这也是⼀个特别热⻔的考点: 结构体内存对⻬
4. 结构体内存对齐(重点)
4.1对齐规则
⾸先得掌握结构体的对⻬规则:
- 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
- 其他成员变量要对⻬到某个数字(对齐数)的整数倍的地址处。 对齐数=编译器默认的⼀个对齐数与该成员变量大小的较小值。 VS 中默认的值为 8 ,Linux中gcc(编译器)没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
这么说大家看的也是有点懵吧,那通过下面几个练习,我来给大家一一画图讲解,相信大家看完后肯定会比我还明白!
4.2实战演练
例1:
struct S1
{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
我想我画得这么好你肯定看明白了😎
那么大小是不是12字节呢?当然是了
那这个又是多少呢?
例2:
struct S2
{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
没有问题吧,你肯定明白了
后面这两个我就不给大家画图一一解释了,都会了我还讲干嘛哈哈哈
例3:
struct S3
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
你做对了吗?当然对了,讲这么好你肯定懂了
那这个呢?
例4:
结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
4.3为什么存在内存对齐?
存在内存对齐的主要原因是为了提高计算机系统的性能。内存对齐可以使数据在内存中的存储更加高效,减少内存访问的时间。当数据按照特定的字节边界对齐存储时,CPU可以更快地访问这些数据,而不需要额外的处理来处理未对齐的数据。这样可以减少内存访问的次数和提高数据读取的速度,从而提高系统的整体性能。
——GPT3.5
内存对齐的存在主要有以下几个原因:
提高访问效率:当数据按照对齐要求存储在内存中时,处理器可以更快地读取和存储数据,从而提高程序的执行效率。这是因为CPU每次寻址和访问内存都需要消耗时间,而且CPU访问内存时并不是逐个字节访问,而是以字长(word size)为单位访问。如果访问未对齐的内存,处理器可能需要进行额外的操作,比如两次内存访问,这会增加处理时间。而通过对齐内存访问,处理器只需要进行一次访问,从而提高访问效率。
满足硬件要求:许多硬件平台对数据的访问有对齐的要求。例如,某些处理器在读取未对齐的数据时可能会产生异常,导致程序崩溃或结果不正确。内存对齐可以避免这些问题,确保程序在各种硬件平台上的正确性和稳定性。
增强平台可移植性:不同硬件平台在访问内存数据时具有差异性。有些CPU可以访问任意地址上的任意数据,而有些CPU只能在特定地址访问数据。因此,为了确保代码在不同平台上的可移植性,通常在编译时会对分配的内存进行对齐处理。
综上所述,内存对齐是为了提高访问效率、满足硬件要求以及增强平台可移植性而存在的。
——文心一言3.5
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到:
让占用空间小的成员尽量集中在⼀起
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和 S2 类型的成员⼀模⼀样,但是 S1 和 S2 所占空间的大小有了⼀些区别。
4.4修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
(关于预处理肖恩在后面也会讲哦,很快的嘿嘿)
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
是不是跟上面就不一样了
本章的重点可算讲明白了
接下来我们来看结构体的传参
5. 结构体传参
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;
}
上⾯的 print1 和 print2 函数哪个好些?
答案是:首选print2函数。
为什么捏?
传递⼀个结构体对象的时候,结构体过大,系统开销⽐较⼤,所以会导致性能的下降,传递一个地址是不是就方便多了。
结论:
结构体传参的时候,要传结构体的地址
6. 结构体实现位段*
结构体讲完就得讲讲结构体实现位段的能力
6.1什么是位段
位段是一种数据结构,用于在内存中按位对数据进行存储
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以
选择其他类型。 - 位段的成员名后边有⼀个冒号和⼀个数字。
⽐如:
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是⼀个位段类型。
那位段A所占内存的大小是多少?
printf("%d\n", sizeof(struct A));
6.2位段的内存分配
位段(bit-field)的内存分配涉及对结构体(或联合体)中成员变量的位级控制。位段允许开发者以位为单位来定义结构体中的成员变量所占的空间,从而既能够节省空间,又方便于操作。
位段的内存分配有以下几个关键要点:
- 成员类型:位段的成员可以是int、unsigned int、signed
int或char类型,这些都属于整型家族。这些类型决定了位段的基本存储单元大小,通常是按照int(4个字节)或char(1个字节)的方式来开辟空间。 - 位段大小:位段中的成员变量会指定一个位数,用来限制该变量可以使用的二进制位数。例如,int
a:3;表示变量a只使用3个位来存储数据。这意味着即使给a赋一个更大的整数值,它也会在存储时被截断到指定的位数。 - 内存布局:位段在内存中的布局是从低位到高位进行填充的。如果当前字节的剩余位数不足以存储下一个位段成员,则会开辟新的存储单元(按int或char的大小)来继续存储。这意味着位段可能会跨越多个存储单元,但每个成员仍然只使用其指定的位数。
- 未使用空间:由于位段是以位为单位进行分配的,因此可能会产生未使用的空间。这些未使用的空间在内存中并不会被其他变量使用,而是被浪费掉了。然而,相较于直接使用完整的int或char变量,使用位段通常仍然能够节省大量的空间。
- 平台依赖性:需要注意的是,位段的具体实现和内存布局可能因编译器和平台的不同而有所差异。不同的编译器可能会对位段进行不同的填充和对齐处理,这可能导致在不同的平台上,相同的位段定义可能会产生不同的内存布局和大小。因此,在编写需要跨平台运行的代码时,应谨慎使用位段,并进行充分的测试。
总的来说,位段的内存分配是一种灵活且有效的空间优化技术,但使用时需要注意其潜在的平台依赖性和内存布局问题。
——文心一言3.5
⼀个例子
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
空间是如何开辟的?
6.3位段的应用
位段(Bit-field)是一种在C语言中用于优化数据存储与处理的技术,它允许程序员在结构体中对成员进行位级别的操作和管理。位段的主要应用包括节省内存空间、简化位操作处理以及提高数据处理效率。
首先,位段可以有效地节省内存空间。通过将多个成员变量打包在一个字节或更大的存储单元中,位段能够显著减少结构体的总体大小。这对于需要处理大量数据的后端应用程序来说尤为重要,因为降低内存占用可以提高性能。
其次,位段可以简化对位操作的处理。在处理二进制数据、网络协议、文件格式等任务时,位段能够将相关的位操作封装在结构体中,使代码更加清晰、可读性更高。通过位段,程序员可以使用适当的位掩码和位运算来访问和修改特定的位,而无需手动进行繁琐的位操作。
此外,位段在处理控制信息时特别有用。在数据通信领域,控制信息往往只占一个字节中的一个或几个二进制位。通过使用位段,可以将这些控制信息紧凑地存储在一个结构体中,方便进行读取和修改。
需要注意的是,位段的具体实现和内存布局可能因编译器和平台的不同而有所差异。不同的编译器可能会对位段进行不同的填充和对齐处理,这可能导致在不同的平台上,相同的位段定义可能会产生不同的内存布局和大小。因此,在使用位段时,程序员需要了解目标平台的特性和编译器的行为,并进行充分的测试以确保代码的正确性和可移植性。
总的来说,位段是一种强大的工具,可以用于优化数据存储和处理,提高程序的性能和可读性。然而,使用时需要谨慎处理平台依赖性和内存布局问题。
——文心一言3.5
6.4位段使用的注意事项
位段的几个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位
置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的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;
}
文心一言3.5{
使用位段(bit-field)时,需要注意以下关键事项:
- 类型限制:位段的类型只能是整型家族的一部分,如int、unsigned int、signed int、char、unsigned
char和signed char。不能使用浮点型或其他非整型类型。 - 位数限制:位段所占的二进制位数不能超过其所属基本类型所能表示的最大位数。例如,如果int类型在特定编译器或平台上占4个字节(32位),那么一个int类型的位段最多只能有32位。
- 访问与操作:无名位段不能被直接访问,但它们会占据内存空间。此外,不能对位段进行取地址操作,因为位段通常不是连续的存储单元。
- 长度为零的位段:长度为零的位段在定义中是有意义的,它强制下一个位段从新的存储单元开始。这对于控制位段的存储布局特别有用。
- 整型升级:当位段出现在表达式中时,它们会自动进行整型升级,转换为int或unsigned
int类型。这意味着在进行算术运算或比较时,位段的行为类似于它们升级后的整型。 - 赋值范围:对位段赋值时,应确保不超过其所能表示的最大范围。超出范围的赋值可能导致数据丢失或不可预测的行为。
- 跨平台问题:位段的具体实现和内存布局可能因编译器和平台的不同而有所差异。不同的编译器可能会对位段进行不同的填充和对齐处理,导致相同的位段定义在不同的平台上可能产生不同的结果。因此,在使用位段进行跨平台编程时,需要特别小心。
- 数组形式的限制:位段不能定义为数组形式。这意味着你不能创建一个包含多个位段的数组。
- 速度与空间的权衡:位段的主要优势在于节省空间,但可能牺牲一些速度。这是因为位段的操作通常涉及更复杂的位运算和内存布局管理。
- 可移植性:由于位段的具体实现可能因编译器和平台而异,因此使用位段可能会降低代码的可移植性。在编写需要跨多个平台运行的代码时,应谨慎考虑是否使用位段。
总的来说,位段是一种强大的工具,可用于优化数据存储和处理。然而,它们也有一些限制和潜在问题,需要在使用时仔细考虑和处理。}
GPT 3.5{
在使用位段时,需要注意以下几个方面:
-
内存对齐:位段中的字段通常是按照位数进行定义的,因此需要考虑字段在内存中的对齐情况。确保每个字段都从正确的内存位置开始存储,以避免访问数据时出现错误或性能下降。
-
字段大小:定义位段时需要确保每个字段的大小是合适的,不要超出字段所能表示的范围。过大的字段可能会导致内存浪费,而过小的字段可能无法满足数据存储需求。
-
字段顺序:位段中的字段顺序通常会影响内存布局和数据访问的效率。需要根据实际需求合理安排字段的顺序,以便提高数据访问的效率。
-
跨平台兼容性:由于不同平台对于位段的实现可能存在差异,因此在跨平台开发时需要谨慎使用位段,以避免出现不同平台之间的兼容性问题。
-
可读性和维护性:位段的代码通常比较复杂,需要注意代码的可读性和维护性。建议添加详细的注释说明每个字段的含义和用途,以便他人能够理解和维护代码。
综上所述,使用位段时需要注意以上几个方面,以确保数据的正确存储和访问,并提高代码的可维护性和可读性。
}
感谢大家的阅读❤️,那么本期文章就到这里咯~~~
期待与你的下次相见!
Looking forward to seeing you all next time!
下期预告(联合和枚举)~~~