文章目录
结构体
结构体是一种复合数据类型,结构体将不同的数据组合成一个整体的自定义数据类型,它可以包含不同的类型成员变量,整型、浮点型、字符型等这些成员按照一定的顺序存储在内存中,每个成员都有对应的内存地址和大小。
结构体的定义通过 struct
关键字,和大括号 {};
定义结构体。
结构体定义和声明
在C语言中结构体的格式如下:
c
struct tag//结构体名
{
数据类型 成员名;
数据类型 成员名;
......
};
==例1:==使用结构体定义了一个学生 Student
类型的变量,这个变量包含一个学生的基础信息。
c
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
};
==例2:==使用结构体定义了一个链表
c
struct NodeList
{
int val;
struct NodeList* next;
};
==例3:==定义了一个结构的同时,声明了一个结构体变量。stu1和结构体指针stu2是全局变量。
c
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
}stu1, *stu2;
结构体的初始化和赋值
关键字 struct
和结构体名称,放在一起是一个类型名 struct Student
,后面跟上变量名即可,初始化的方法于数组类似使用花括号({})将内容包裹在一起。
例1:
使用大括号,在声明的时候,按照结构体成员变量一一赋值
c
struct Student stu1 = {2024, xiaoli, 18};
例2:
也可以先声明,在赋值
c
struct Student stu1;
stu1.name = zhangsan;
对结构体类型名的优化
例1:
若每次使用结构体类型的变量,感觉类型名过长,这里可以使用 tepedef
关键字对类型进行重命名。
可以在结构体定义时重命名:将 struct Student
重命名为 Student
c
typedef struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
}Student;
例2:
在结构体定义后重命名:将 struct Student
重命名为 Student
c
struct Student
{
int id;//学号
char name[20];//姓名
int age;//年龄
};
typedef struct Student Student;
例3:
以下这种使用方法时错误的:
还没有执行完typedef就将Node当作结构体类型名来使用,提前使用Node将会导致编译器报错,这是不被允许的。前后顺序混乱。
c
typedef struct NodeList
{
int val;
Node* next;
}Node;
结构体的自引用与嵌套
结构体里还可以引用自己,但只能自引用指针类型的。
c
struct NodeList
{
int val;
struct NodeList* next;
};
若不是指针类型的,结构体的大小将无法计算,sizeof(struct NodeList)
,结构体里包含着一个同类型的结构体变量,结构体大小将会膨胀,无穷大。
c
struct NodeList
{
int val;
struct NodeList next;
};
结构体嵌套,结构体里还可以定义结构体。
结构体访问与操作
访问结构体通过 点运算符(.
)、箭头运算符(->
)进行访问。
点运算符(.
),用于对结构体成员的直接访问,是双目操作符。使用方法:结构体变量名.成员名
c
struct point//声明结构体变量
{
int x;
int y;
};
int main()
{
struct point p1 = { 10, 20 };//初始化
printf("%d %d\n", p1.x, p1.y);
struct point p = { p.x = 10, p.y = 5 };//指定顺序初始化
printf("%d %d", p.x, p.y); //访问结构体内的变量
return 0;
}
箭头运算符(->
),用于间接访问结构成员。对结构体指针p使用 (->
)进行访问,还可以赋值
c
struct stu
{
char name[20];//姓名
int age;//年龄
int num;//学号
};
int main()
{
struct stu s = { "wangwu", 19, 202405027};
struct stu* p = &s;
printf("%s %d %d\n", p -> name, p -> age, p -> num);
return 0;
}
匿名结构体
例1:
匿名结构体,在对结构体初始化是并未进行命名。想要使用该结构体只能在声明结构体的同时声明一个对应的结构体变量。而在后续使用该结构体类型时,也只能使用这几个变量,无法重新声明。
c
struct
{
int x;
int y;
}N1, N2, N3;
这里定义了一个存放坐标的结构体,使用N1,N2,N3来存储x y,但也只能用N1、N2、N3这三个坐标。
c
int main()
{
printf("请输入x,y的坐标: ");
scanf("%d %d", &N1.x, &N1.y);
printf("%d %d\n", N1.x, N1.y);
N2 = N1;//同类型结构体 赋值。
printf("%d %d\n", N2.x, N2.y);
return 0;
}
例2:
这两个结构体属于同种类型的结构体吗?
c
struct
{
int x;
int y;
}N1;
struct
{
int x;
int y;
}N12;
int main()
{
printf("请输入x,y的坐标: ");
scanf("%d %d", &N1.x, &N1.y);
N2 = N1;// error C2440: "=": 无法从""转换为""
printf("N1:%d %d\n", N1.x, N1.y);
printf("N2:%d %d\n", N2.x, N2.y);
return 0;
}
虽然这两个你匿名结构体的成员一致,但它们在编译器眼里,并不是同一种结构体,两者无关联,也就无法赋值。
结构体中的内存对齐(面试常考)
计算结构体字节大小
offsetof
--- 宏,用于计算结构体成员相较于结构体变量起始位置的偏移量。
结构体的偏移量(offset)是指从结构体的起始地址开始,到特定成员的起始地址的距离。
c
offsetof(type, member)
//头文件--<stddef.h>
//返回值--偏移量
//返回类型--无符号整形
c
#include <stdio.h>
#include <stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("S1:\n");
printf("%zd\n", offsetof(struct S1, c1));
printf("%zd\n", offsetof(struct S1, i));
printf("%zd\n", offsetof(struct S1, c2));
printf("S2:\n");
printf("%zd\n", offsetof(struct S2, c1));
printf("%zd\n", offsetof(struct S2, c2));
printf("%zd\n", offsetof(struct S2, i));
return 0;
}
S2
如图:根据结构体S2在内存中的偏移量,可以的出结构体S2在内存中所占字节个数。c1、c2是字符类型,占一个字节,i为整型类型,占4个字节。
根据偏移量得出 ,成员c1从0的位置开始向后占1个字节,成员c2从1的位置开始向后占1个字节,成员 i 从4的位置开始向后占4个字节。计算的出该结构体占8个字节 。根据上图可以发现,S2结构体浪费了2个字节的空间。
S1
如图:根据结构体S1在内存中的偏移量,可以的出结构体S1在内存中所占字节个数。
根据偏移量得出 ,成员c1
从0的位置开始向后占1个字节,成员i
从4的位置开始向后占4个字节,成员c1
从8的位置开始向后占1个字节。看似S1
结构体占9个字节大小,实际上该结构体占12个字节。而且还浪费了6个字节大小的空间。
出现上述问题的,是因为结构体成员的存在着对齐现象。
对齐规则
-
结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处(上述两个结构体c1偏移量为0的原因)
-
其它成员变量要对齐到对齐数的整数倍的地址处。
- 对齐数:编译器默认的第一个对齐数 与 该成员变量大小的较小值。
- vs中默认---8
- linux中gcc 没有默认对齐数,对齐数就是成员自身大小。
-
结构体中每个成员变量都有一个对齐数。
-
结构体总大小为最大对齐数(结构体中最大对齐数)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员中的对齐数)的整数倍。
上述结构体S1,更具对齐规则:
- 第一个成员的对齐到偏移量为0的地址处
- 第二个成员的大小为4,vs默认对齐数位8,取较小的那个。第二个成员的对齐数为4,那第二个成员的偏移量为 4 的整数倍,从上至下依次计数得出,从偏移量为4的地址处存放。
- 第三个成员的大小为1,而vs默认对齐数位8,取较小的那个。第三个成员的对齐数为1,第三个成员的偏移量为对齐数的整数倍,从上至下依次计数得出,从偏移量为8的地址处存放。
- 最后存放完成员变量后,结构体总大小为最大对齐数的整数倍,而此时S1结构体大小占9个字节,最大对齐数位4,最终得出的结果位 12个字节。
练习:计算结构体S3、S4的大小。
c
struct S3
{
double d;
char c;
int i;
};//16字节
struct S4
{
char c1;
struct S3 s3;
double d;
};//32字节
为什么存在内存对齐?
平台原因(移植):
并不是所有硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些待定类型的数据,否则会抛出硬件异常。
性能原因:
访问未对齐的内存,处理器需要作两次访问,而对齐的内存仅需依次访问。结构体的内存对齐是那空间换时间的做法
现在在32位的机器上,它每次读取内存只能读取4个字节
c
struct S
{
char c;
int n;
}
未对齐的情况下。32为机器一次读取4个字节,第一次读取四个字节,int还剩1个字节没有读取,读取完int需要读取两次。
对齐的情况下。读取int只需要读取一次。
设计结构体时,既要内存对齐,又要节省空间,可以这样做:
- 让占空间小的成员尽量集中在一起,集中在一起并不是必须让占空间小的成员必须从第一个位置开始放。
c
struct S1
{
char c1;
int i;
char c2;
};//12字节
struct S2
{
char c1;
char c2;
int i;
};//8字节
修改默认对齐数
使用 #pragma
预处理指令,可以修改vs编译器的默认对齐数。一般设置为2的n次方
c
#include <stdio.h>
#pragma pack(1)//设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//重置默认对齐数,恢复成8
int main()
{
printf("%d\n", sizeof(struct S));
return 0;
}
结构体传参
结构体传参,有两种形式:传地址、传参。
-
传地址,在调用函数,创建函数栈帧时不需要额外的开辟空间来存放形参。
-
传参,需要开辟额外的空间存放形参。会增加时间和空间上的系统消耗。
- 若一个结构体过于庞大,参数压栈的系统开销比较大,会导致代码性能下降
c
#include <stdio.h>
struct S
{
int arr[1000];
int n;
};
void print1(struct S a)
{
for (int i = 0; i < 10; i++)
{
printf("%d", a.arr[i]);
}
printf("\n");
printf("%d", a.n);
}
void print2(struct S* pa)
{
for (int i = 0; i < 10; i++)
{
printf("%d", pa->arr[i]);
}
printf("\n");
printf("%d", pa->n);
}
int main()
{
struct S a = { {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 10 };
print1(a);
print2(&a);
return 0;
}
这里当然是print2效能上更好,调用print1函数,在为函数开辟栈帧时,为结构体占用字节较大,开辟的一千多个整形大小的空间,而print2不需要,它只需要开辟一个整形指针大小的空间即可。
结构体传参的时候,要传结构体的地址。
结构体实现位段
位段的声明和结构体时相似的。
-
位段的成员必须是
int unsigned int 或 signed int
,在C99中位段成员的类型也可以选择其它类型。 -
位段的成员名后边有一个冒号和一个数字。
-
位段的大小不能超过自身的大小
c
struct S1
{
int n;
int m;
int i;
};
struct S2
{
int _n : 2;//占2个比特位
int _m : 4;//占4个比特位
int _i : 30;//占30个比特位
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
位段的实现,本质上是节省空间,将12个字节大小的节省到8个字节。
位段可能会受到编译器的内存对齐规则的影响,导致实际占用的内存可能比位段的总位数更多,使用位段给了2个字节左右的空间,它的大小却是8个字节。
位段的内存分配
- 位段的成员可以是整型家族,和char类型
- 位段的在空间上是按照需要,以4个字节(int)或1个字节(char)的方式开辟的。
- 位段涉及很多不确定因素,位段是不跨平台的。
- 给定空间后,在空间内部是从左到右使用,还是从左向右使用这个是不确定的。取决于编译器vs编译器从左到右
- 当剩下的空间不足以放下一个成员的时候,空间是浪费还是使用。取决于编译器。vs编译器浪费
例:
c
struct S
{
int _a : 3;
int _b : 4;
int _c : 5;
int _d : 4;
};
int main()
{
struct S s;
s._a = 9;
s._a = 12;
s._c = 5;
s._d = 2;
printf("%zd\n", sizeof(struct S));
return 0;
}
一共3个字节大小。0
而存放值的时候,空间不够从最高位开始舍弃,多余的部分补0。
在main函数中,需要将9、12、5、2的值存放的对应的变量中,首先将它们转换为二进制位,再存入其中。
9:1001 12:1100 5:0101 2:0010
位段跨平台问题
int
位段被当为有符号数 还是无符号数是不确定的。- 位段中最大位的数目不能确定。16位机器的最大为16,32位机器最大为32,若32位机器实现的位段,给定的值大于16,将代码移植到16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,没有标准
- 当剩下的空间不足以放下一个成员的时候,空间是浪费额外开辟还是使用完。没有标准。
根结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但存在跨平台的问题。
注意
位段的几个成员共有一个字节,内存中为每一个字节分配地址,而比特位不分配,也就是说,位段部分成员是不存在地址的,也就不可以使用取地址操作符(&),使用scanf对位段成员输入值。
位段的实际运用,目前的级别还接触不了。
节大小。0
而存放值的时候,空间不够从最高位开始舍弃,多余的部分补0。
在main函数中,需要将9、12、5、2的值存放的对应的变量中,首先将它们转换为二进制位,再存入其中。
9:1001 12:1100 5:0101 2:0010
[外链图片转存中...(img-Ox3GvsSd-1723216315121)]
位段跨平台问题
int
位段被当为有符号数 还是无符号数是不确定的。- 位段中最大位的数目不能确定。16位机器的最大为16,32位机器最大为32,若32位机器实现的位段,给定的值大于16,将代码移植到16位机器会出问题。
- 位段中的成员在内存中从左向右分配,还是从右向左分配,没有标准
- 当剩下的空间不足以放下一个成员的时候,空间是浪费额外开辟还是使用完。没有标准。
根结构体相比,位段可以达到同样的效果,并且可以很好的节省空间,但存在跨平台的问题。
注意
位段的几个成员共有一个字节,内存中为每一个字节分配地址,而比特位不分配,也就是说,位段部分成员是不存在地址的,也就不可以使用取地址操作符(&),使用scanf对位段成员输入值。
位段的实际运用,目前的级别还接触不了。