今天要介绍的是:结构体的相关内容
一.结构体类型的声明
1.结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
2.结构的声明
结构的声明的原型:
struct tag
{
member-list;
}variable-list;
eg:
struct Student { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 }; //分号不能丢
3.特殊的声明
匿名结构体:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20], *p;
4.结构的自引用
我们也许回想在结构中包含一个类型为该结构本身的成员是否可以呢?像下面这样
struct Node { struct Node a1; int date[2]; };
这种情况下,需要确保结构体类型的定义是在使用它的结构体类型之前。否则,编译器将无法确定结构体类型的大小。
所以是不行的,正确的自引用方式如下:
struct Node { int data; struct Node* next; };
注意:
typedef struct { int data; Node* next; }Node;
在定义指针变量next时,使用了Node类型。由于Node类型的定义在当前代码中尚未完成,编译器无法识别Node类型
正确的如下:
typedef struct Node { int data; struct Node* next; }Node;
5.结构体变量的定义和初始化
struct Point { int x; int y; }p1; //声明结构体的同时定义变量p1 struct Point p2; //定义结构体变量p2 //也可以 struct Point p3 = {x, y};//初始化:定义变量的同时赋初值。 struct Stu //类型声明 { char name[15];//名字 int age; //年龄 }; struct Stu s = {"zhangsan", 20};//初始化 struct Node { int data; struct Point p; struct Node* next; }n1 = {10, {4,5}, NULL}; //声明结构体的同时嵌套初始化 struct Node n2 = {20, {5, 6}, NULL};//单独结构体嵌套初始化
6.结构体传参
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); printf("%d\n", s.data[0]); printf("%d\n", s.data[1]); printf("%d\n", s.data[2]); } //结构体地址传参 void print2(struct S* ps) { printf("%d\n", ps->num); printf("%d\n", ps->data[0]); printf("%d\n", ps->data[1]); printf("%d\n", ps->data[2]); } int main() { print1(s); //传结构体变量 printf("_________"); print2(&s); //传地址 return 0; }
两种调用,传参的结果都是一样的:
但是,还是用printf2来传地址时更好:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降
二.结构体内存对齐
struct S1
{
char c1 ;
int i ;
char c2 ;
}; 这个结构体有多大呢?
第一反应大抵是:1+4+1=6吧
但其实:
1.对其规则
1. 第一个成员在与结构体变量偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的 较小值 。 VS中默认的值为8
3. 结构体 总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍 。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
按照对其规则,这是上述答案的分析:
接下来再看另外一个例子:
eg:
struct S2 { char c1; char c2; int i; }; int main() { printf("%d", sizeof(struct S2)); return 0; }
答案便是:
2.存在原因
结构体的内存对齐是拿空间来换取时间的做法
一些资料也显示说:
- 访问效率:内存对齐可以使结构体的成员在内存中连续存储,减少内存访问次数,提高访问效率。如果结构体成员没有进行内存对齐,可能会导致成员之间存在空隙,需要多次访问内存才能获取到所有成员的值
- 数据对齐:某些硬件平台要求访问特定类型的数据必须按照特定字节对齐,否则可能会导致访问错误或性能下降。例如,某些处理器要求访问双字节数据(如short类型)必须按2字节对齐,访问四字节数据(如int类型)必须按4字节对齐。如果结构体成员没有进行内存对齐,可能会导致访问错误或性能下降
3.减少浪费
那我们如何尽量减少内存对齐所产生的浪费呢?
让占用 空间小的成员尽量集中 在一起
所以上述S1的例子我们可以改造成:
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d", sizeof(struct S2)); return 0; }
结果也确实减小了内存使用:
4.修改默认对齐数
这里我们使用#pragma,可以改变我们的默认对齐数
#pragma pack(8)//设置默认对齐数为8 struct S1 { char c1; int i; char c2; }; #pragma pack()//取消设置的默认对齐数,还原为默认 #pragma pack(1)//设置默认对齐数为1 struct S2 { char c1; int i; char c2; }; int main() { printf("%d\n", sizeof(struct S1)); printf("%d", sizeof(struct S2)); return 0; }
这次S2的大小便是我们最初认为的6了:
三.位段
1.什么是位段
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是 int、unsigned int 或signed int 。
- 位段的成员名后边有一个冒号和一个数字
struct A { int a : 2; //a占用2个bit位 int b : 5; //b占用5个bit位 int c : 10; int d : 30; }; int main() { struct A a; printf("%d", sizeof(a)); }
不使用位段的话是占16个字节,现在是:
2.位段的内存分配
- 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型
- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的(一次增加4/1个字节)
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段
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; }
3.位段的跨平台问题
- int 位段被当成有符号数还是无符号数是不确定的。
- 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定
这次关于结构体相关的内容就先到这里啦! 在下一篇文章中,我们将详细介绍枚举和联合体的内容。感谢大家的支持,加油!!!