目录
概念
结构是⼀些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
声明
struct tag { member-list; }variable-list;
member-list是结构体成员清单
variable-list是变量清单
举例
一个有关图书的结构体,包含名字,编号,价格这三个成员
struct Book
{
char name[20];
char number[20];
double price;
};
那么我们在主函数要怎么引用呢?
下面进行一个具体的使用,大家通过代码来参考
#include<stdio.h>
struct Book
{
char name[20];
char number[20];
double price;
};
int main()
{
struct Book b1 = { "ABCD","A0001",9.9 };
struct Book b2 = { "EFGH","A0002",19.9 };
printf("%s %s %f\n", b1.name, b1.number, b1.price);
printf("%s %s %f", b2.name, b2.number, b2.price);
return 0;
}
我们可以在主函数中声明,也可以在所有函数外声明成一个全局变量
声明过后要跟上变量名称,即上面的“b1”和“b2”,此时通过b1和b2就能找到两者分别代表的结构体及其内容
我们同样可以把结构体的变量名称写在结构体的声明后面,如下
#include<stdio.h>
struct Book
{
char name[20];
char number[20];
double price;
}b1,b2;
int main()
{
b1 = { "ABCD","A0001",9.9 };
b2 = { "EFGH","A0002",19.9 };
printf("%s %s %f\n", b1.name, b1.number, b1.price);
printf("%s %s %f", b2.name, b2.number, b2.price);
return 0;
}
此时的输出的结果和上面是一样的
特殊声明
在声明结构体时,可以不完全的声明,如下
struct
{
char name[20];
char number[20];
double price;
};
我们把结构体的标签(Book)省去了
在这种情况下,若没有对匿名的结构体类型重命名,通常情况下只能使用一次
也就造成了下面的代码会报错
#include<stdio.h>
struct
{
char name[20];
char number[20];
double price;
}b1;
struct
{
char name[20];
char number[20];
double price;
}*p;
int main()
{
p = &b1;
return 0;
}
在p=&b1这一步出错,因为编译器会把上面两个声明当成2个不同的类型
自引用
常用于一个链表的节点,在一个结构体中写上下一个结构体的指针,如下
struct Book
{
char name[20];
char number[20];
double price;
struct Book* next;
};
注意:自引用这一步不能写成struct Book next,这样写会导致该结构体的大小无穷大,编译器会报错
同时在使用了typedef重命名的匿名结构体中也不能进行自引用,如下:
typedef struct
{
char name[20];
char number[20];
double price;
book* next;
}book;
因为book是在结构体后才重命名的,但在匿名结构体中却已经用到了
这种情况就像命令一个人去找他的儿子,给的命令是去找张三的儿子,但是这个人此时还不叫张三,他就不能找到儿子
解决办法:此时定义结构体不要用匿名结构体,更正如下
typedef struct book { char name[20]; char number[20]; double price; struct book* next; }book;
内存
内存对齐规则
1. 结构体的第⼀个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对⻬到对⻬数的整数倍的地址处。
对⻬数=编译器默认的⼀个对⻬数与该成员变量大小的较小值
VS 中默认的值为8,Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的大小
3. 结构体总大小为最大对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对齐数中最大
的)的整数倍
4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最⼤对齐数的整数倍处,结构体的整体大小就是所有最⼤对齐数(含嵌套结构体中成员的对齐数)的整数倍
下面我们举一个例子来具体说明(VS环境)
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", sizeof(struct S1));
return 0;
}
上面的输出结果为:12
我们用一张图来说明为什么是12而不是6
首先我们的第一个成员c1是char型,占一个字节,第0位就是c1的位置
第二个成员i是int型,我们计算一下它的对齐数:int型占4个字节,VS对齐数默认为8,4<8,所以i的对齐数就是4,我们就需要对齐到偏移量为4的整数倍的地址(即4),所以4,5,6,7这四个字节放的就是i
第三个成员c2是char型,占一个字节,1<8,接下来我们就需要对齐到偏移量为1的整数倍的地址(即接下来的第8位)
所以三个成员存放的地方分别是:c1->0 i->4,5,6,7 c2->8
此时我们占了0-8的地址,一共9个字节,但是由于规则:结构体的大小为最大对齐数的整数倍
此时的对齐数分别是4和1,那就必须是4的倍数,2*4<9<3*4
所以该结构体所占的空间是12字节(1,2,3,9,10,11位置为空)
修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对⻬数
#pragma pack(1)
在VS中上面这个代码可以将默认的对齐数(8)改成1,下面的代码可以恢复默认
#pragma pack()
注意:更改后对结构体使用strlen时,数值有可能出现变化
结构体传参
我们在需要传递结构体的内容时,优先使用传址调用,尽量少使用传值调用
原因:函数传参时,参数需要压栈,有时间和空间上的系统开销,若结构体过大,则会浪费时间和空间,导致性能的下降
结构体实现位段
声明须知
1.位段的成员可以是int , unsigned int , signed int 或者是char 等类型
2.位段的成员名后有一个冒号和一个数字
作用
位段可以修改成员所占的空间,专门用来节省内存
比如在结构体中我把int a;修改成int a:2,此时a只占2个bit位,a存的数据超过2个bit位时会省略前面的数据只取2个bit位的内容
在实际使用中,要根据具体情况决定位段的大小
位段的内存分配
1.位段的空间上是需要以4个字节(int)或1个字节(char)的方式来开辟的
2.位段存在许多不确定因素,在跨平台开发(使用不同编译器)时要避免使用位段
部分不确定因素:
1.int位段被当做有符号数还是无符号数是未知的
2.位段中最大位的数目不能确定(32位机器和64位机器就有差别)
3.申请的一块内存中,从前向后使用还是从后向前使用,方向取决于编译器
4.当一块内存的空间不足下一位成员使用时,另外开辟空间还是继续使用?
应用场景
在某些网络协议中,某些IP传输数据只需几个bit,此时使用位段就能节省空间,从而提升了网速
使用须知
位段的几个成员共有同⼀个字节,因此有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。
内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输⼊值,只能是先输入放在⼀个变量中,然后赋值给位段的成员
比如这样一个结构体
struct S1
{
char c : 2;
}b1;
直接使用scanf("%c",&b1.c); 就是错误的
需要先写 char a; 然后输入给它 scanf("%c",&a); 最后把a的值赋值给目标 b1.c = a;
花了好久才把结构体部分的知识整理清楚,对齐规则部分写的稍微有点差劲,还望大家海涵
有不足之处欢迎指出