结构体类型声明
结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。 (区别于数组,数组是一组相同类型元素的集合。)
上图中的struct Stu是结构体类型,可以用来创建变量。
//声明一个结构体类型
//声明学生类型Stu是想通过该类型创建学生变量(对象)
//描述一个学生需要有相关信息,姓名性别年龄
struct Stu
{
char name[20];
char sex[10];
int age;
}s4,s5,s6; //创建结构体变量s4,s5,s6(全局变量)
struct Stu s3; //创建结构体变量s3(全局变量)
int main()
{
//创建结构体变量s1,s2(局部变量)
struct Stu s1;
struct Stu s2;
return 0;
}
特殊结构体类型的创建
匿名结构体类型: 上图中的结构体类型只有一种创建变量的方式,在变量列表的位置创建变量。 匿名结构体指针类型:
struct
{
char a;
int b;
}*pab;
::: tip 结构体指针类型创建变量psa。如果有一个成员变量一模一样的匿名结构体类型,创建变量ab,指针变量psa中不能存放ab的地址,编译器会认为两个声明是两个完全不同的类型。 ::: 匿名结构体类型只能在创建变量的时候使用一次,因为没有名字,后面无法再使用。
结构的自引用
结构体类型不能再内部包含自己类型的变量,例如struct type中不能出现struct type类型的变量。 如下: 原因是这种写法无法计算变量大小,反复包含使得变量非常大,变量的空间无法创建。 ::: warning 上述代码的形式不是递归。 ::: 数据结构的链表中通过一个节点可以找到下一个节点。可以将一个节点分为两个部分,一部分存放节点本身的数据,另一部分存放下一个节点的地址。 当一个节点的类型是struct Node时,下一个节点的类型也是struct Node,下一个节点的地址存放在指针变量next中,next的类型就是struct Node*。如下:
struct Node
{
char data;
struct Node* next;
};
int main()
{
return 0;
}
::: tip 结构体找到同类型的其他变量,使用指针串联。 :::
结构体类型重命名
typedef struct Node
{
char data;
struct Node* next;
}Node;
int main()
{
struct Node n1;
Node n2;
return 0;
}
上述代码使用typedef把struct Node重命名为Node,两种类型名都存在,再使用struct Node创建变量时,可以使用新的名字Node创建变量。 ::: tip 结构体重命名时原来变量列表的位置上放置的时类型名,不再是变量名。 ::: 当对函数匿名结构体进行重命名时,不能在结构体内部使用新的命名:
typedef struct
{
char data;
Node* next;
}Node;
int main()
{
return 0;
}
上述代码运行失败的原因是,结构体重命名操作结束后才可以使用Node,在上述代码第4行时新的变量名Node还未被定义,不能使用。 ::: tip 结构体重命名时不建议省略原有的tag,以便在结构体内使用struct tag的指针。 :::
结构体变量的定义和初始化
struct T
{
double e;
short f;
};
struct S
{
int a;
char b;
double c;
char d[50];
struct T t;
};
int main()
{
struct S s = { 10,'a',3.14,"hello world",{20.5,3} };
printf("%d %c %lf %s %lf %d\n", s.a,s.b,s.c,s.d,s.t.e,s.t.f);
return 0;
}
10 a 3.140000 hello world 20.500000 3
结构体内存对齐
计算结构体大小
struct S1
{
char a;
int b;
char c;
};
struct S2
{
char a;
char c;
int b;
};
int main()
{
struct S1 s1 = { 0 };
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s1));
printf("%d\n", sizeof(s2));
return 0;
}
嵌套结构体的结构体大小:
struct S1
{
char a;
int b;
double c;
};
struct S2
{
char d;
struct S1 s1;
int e;
};
int main()
{
struct S1 s1 = { 0 };
struct S2 s2 = { 0 };
printf("%d\n", sizeof(s2));
return 0;
}
::: tip 内存对齐的意义: 1.平台原因(移植原因)︰不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则会抛出硬件异常。 ⒉性能原因︰数据结构(尤其是栈)应尽可能在自然边界上对齐。原因是为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。 ::: 内存对齐是空间换取时间的做法。浪费一部分内存空间,提升内存读取的效率。 ::: warning 在设计结构体的时候,既要满足内存对齐的要求,又要尽量节省空间,需要将占用空间较小的结构体成员尽量集中在一起。 ::: 编译器的默认对齐数可以自行设置:
#pragma pack(1)
//设置默认对齐数为1
struct S1
{
char a;
int b;
double c;
};
#pragma pack()
//取消设置的默认对齐数
::: tip 自行设置的默认对齐数一般是2、4、8、16等,不会设置成3、5、7之类的数字。 :::
offsetof-计算偏移量
offsetof实际是宏,宏的参数可以传递类型。 offsetof的参数为结构体名和成员名,返回偏移量。
#include <stddef.h>
struct S
{
char c;
int i;
double d;
};
int main()
{
printf("%d\n", offsetof(struct S, c)); //0
printf("%d\n", offsetof(struct S, i)); //4
printf("%d\n", offsetof(struct S, d)); //8
}
画图验证如下:
结构体传参
struct S
{
char a;
int b;
double c;
}s;
int main()
{
s.a = 'a';
s.b = 10;
s.c = 3.14;
printf("%c %d %lf", s.a, s.b, s.c);
return 0;
}
类似上述代码,对结构体的操作都是在主函数中完成。有的情景选需要将结构体进行传参,让其他函数对结构体进行操作。如下:
struct S
{
char a;
int b;
double c;
}s;
void Init(struct S* ps)
{
ps->a = 'a';
ps->b = 10;
ps->c = 3.14;
}
void Print(struct S tmp)
{
printf("%c %d %lf", tmp.a, tmp.b, tmp.c);
}
int main()
{
Init(&s);
Print(s);
return 0;
}
a 10 3.140000
::: warning 上述代码第7行应写成“&s”传址调用,如果直接传s,tmp只是函数Init中一份s的临时拷贝,在Init函数中的操作都是针对s的临时拷贝tmp来做的,不改变s的值。(s和tmp的地址不同)。 函数外部想要改变函数内部的变量值时,必须传入函数变量地址。 ::: 上述代码第13行函数结构体打印时,不需要改变结构体成员的值,可以使用传值调用。 ::: tip 在可以使用传值调用的时候,如上述代码的Print函数,也应尽量写成传址调用。 原因是传值调用会在函数中建立一份临时拷贝,如果原数据所占空间过大,函数传参时参数压栈,系统开销过大,影响系统性能。而传址调用时,指针所占的空间是4个或8个字节。
如果担心传址后误操作改变原值,在函数接收时对参数加上const修饰即可。 :::
结构体实现位段
位段是结构体式的类型,位段和结构的区别是,位段的成员必须是int、unsigned int、signed int,有的也有char。位段的成员名后面有冒号和数字。
struct A
{
int a : 2;
int b : 5;
unsigned int c : 10;
signed int d : 30;
};
int main()
{
struct A a = { 0 };
printf("%d\n", sizeof(a));
return 0;
}
上述代码中,假设a只有1、2、3、4这4种取值,一个整型是4字节32比特,可以表示2^32种状态,而两个比特位就可以表示种状态,占用一个字节过于浪费。 ::: tip 位段的成员里,冒号后的数字就是所占比特位的数量,最大不可以超过一个int所占空间4字节,即≤32。 ::: 上述代码中成员所占比特位之和是47比特,使用6个字节就可以满足需要。
位段的内存分配规则: - 位段的成员可以是int、unsigned int、signed int或者是char(属于整形家族)类型。 - 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。 - 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的作用是节省空间,如下: 本来需要4个int16字节存放的成员,现在只需要两个字节即可存放。 上图中每次开辟一个字节,空间不够时就按需再开辟一个字节。
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 = 20;
s.c = 3;
s.d = 4;
printf("%d\n", sizeof(s)); //3
return 0;
}
验证内存中的存储情况: ::: tip 上述代码中位段的成员类型是char,每次开辟一个字节,空间不够时就按需再开辟一个字节。每个字节的使用方式是由右向左填充比特位,先使用低位再使用高位。当当前字节剩余比特位不能满足接下来的比特位填充需求时,将当前字节剩余比特位浪费掉,重新开辟一个字节的空间,直到把所有的变量都填充进去。 ::: 上述代码占用3个字节的内存空间,节省了1个字节。由上证明位段可以节省空间。
位段的跨平台问题 - int位段被当成有符号数还是无符号数是不确定的。 - 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。) - 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。 - 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:跟结构相比,位段可以达到同样的效果?,但是可以很好的节省空间,但是有跨平台的问题存在。
位段应用(TCP协议):