结构是一些值的集合,这些值称为成员变量。
结构的每个成员可以是不同类型的变量。
struct tag{
member-list;
}variable-list;
struct 表明这是个结构体
tag 是结构体标签(自定义的)
member-list 是结构体成员
variable-list 是结构体名(全局变量)
例如描述一个学生:
struct Stu{
char name[20]; //姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
}; //分号不能丢
在声明结构体的时候,可以不完全声明。
//匿名结构体类型
struct {
int a;
char b;
float c;
};
struct {
int a;
char b;
float c;
}a[2], * p;
匿名结构体类型,只能使用一次,省略了结构体标签(tag)。
还可能引发问题,编译器会误判。
struct {
int a;
char b;
float c;
}a[2], * p;
int main(){
p = &s; //err
return 0;
}
在结构体中,能否包含一个类型为该结构本身的成员?
答案:可以
//正确的结构自引用
struct Node {
int data;
struct Node* next;
};
//typedef - 类型重定义,重新定义一个新类型名称
//一般应用于数据结构中
typedef struct Node{
int data;
struct Node* next;
}Node;
有了结构体类型,那么任何定义变量,其实很简单。
举例一
struct Point {
int x;
int y;
}p1; //声明结构体类型的时候,定义变量p1
int main() {
struct Point p2; //定义结构体变量p2
struct Point p3 = { 1,2 }; //定义变量的同时给变量赋值
return 0;
}
举例二
struct Stu {
char name[20]; //姓名
int age; //年龄
};
int main() {
struct Stu s = { "zs",22 }; //初始化
return 0;
}
举例三
struct Point {
int x;
int y;
}p1; //声明结构体同时定义变量p1
struct Node {
int data;
struct Point p;
struct Node* next;
}n1 = { 10,{1,2},NULL }; //结构体嵌套初始化
int main() {
struct Node n2 = { 20,{3,4},NULL }; //结构体嵌套初始化
return 0;
}
举例四
struct S {
char c;
int i;
}s1, s2;
struct B {
double d;
struct S s;
char c;
};
int main() {
struct B sb = { 3.14,{'w',1000},'ccc' };
printf("%lf %c %d %c", sb.d, sb.s.c, sb.s.i, sb.c);
return 0;
}
我们已经掌握了结构体的基本使用。
现在我们深入讨论一下:如何计算结构体的大小?
这就涉及到特别热门的知识点:结构体内存对齐。
试问,这四种结构体计算内存大小,将输出多少?
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
struct S3 {
double d;
char c;
int i;
};
struct S4 {
char c1;
struct S3 s3;
double d;
};
int main() {
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
printf("%d\n", sizeof(struct S4));
return 0;
}
为什么结构体成员一样,仅仅是前后顺序不同就导致内存空间差异?
这我们得先了解结构体的对齐规则:
1. 结构体第一个成员要在与结构体变量偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
3. 结构体总大小为最大对齐数(每个成员变量都有个对齐数)的整数倍。
4. 如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS编译器默认为 8.
Linux中的默认值为 4.
个人理解
结构体的这个成员变量,在内存地址中从几开始?放几个字节?
从几开始,成员变量大小和编译器对齐数对比,取小值的倍数,内存就从几开始。
例如:
struct S1{
char c1;
int i;
}
char c1;c1是1,vs编译器的对齐数是8,取小值1;1的倍数还是1,所以char c1变量在内存中的存储位置从1开始。
int i;i是4,vs编译器对齐数是8,取小值4;4的倍数是4、8,前面有char c1存储的1,i存储下去,整个内存变成5,不对齐了,所以要浪费3位字节;int i此时要在第5字节位开始存储4个字节。
最后总结构体的内存为 1+3+4=8。
为什么存在内存对齐?
大部分参考资料都是这么说的:
总的来说:
结构体的内存对齐是拿空间来换取时间的做法。
所以在设计结构体的时候,我们要满足对齐,又要节省空间,如何做到?
答案:让占用空间小的成员集中在一起。
struct S1 { //12
char c1;
int i;
char c2;
};
struct S2 { //8
char c1;
char c2;
int i;
};
S1和S2类型的成员一样,但是S1占用的空间比S2大。
之前我们见过 #pragme
这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。
//改变前
struct S1 { //12
char c1;
int i;
char c2;
};
//改变后
#pragma pack(2) //设置默认对齐数为2
struct S1 { //8
char c1;
int i;
char c2;
};
#pragma pack() //取消设置默认对齐数
#pragma pack(8) //设置默认对齐数为8
struct S1 { //1 +3(要对齐)+4 +1 +3(要对齐) = 12
char c1;
int i;
char c2;
};
#pragma pack() //取消设置默认对齐数
#pragma pack(1) //设置默认对齐数为1
struct S2 { //1 +1 +4 =6
char c1;
char c2;
int i;
};
#pragma pack() //取消设置默认对齐数
int main() {
printf("%d\n", sizeof(struct S1)); //12
printf("%d\n", sizeof(struct S2)); //6
return 0;
}
结果在对齐方式不适合的时候,我们可以自己改变默认对齐数。
我们可以通过这个函数来计算,结构体成员在内存中相对于首地址的偏移量。
#include
#include
struct s2
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct s2, c1)); //0
printf("%d\n", offsetof(struct s2, i)); //4
printf("%d\n", offsetof(struct s2, c2)); //8
return 0;
}
上代码:
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函数传递地址好些。
函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象过大,导致压栈系统开销变大,最终造成性能下降。
位段的声明和结构是类似的,有两个不同:
例如:
struct A{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型。
那位段A的大小是多少?
printf("%d\n", sizeof( struct A )); //8
8 是怎么得来的?
答:
总结:
和结构体相比,位段可以达到同样的效果。相比之下的优点是:可以很好的节省空间;缺点是位段有跨平台的问题存在。
网络传输协议包:
例如在微信上发送消息,数据需要承载其他验证消息才能发送,这时候就适合使用位段来操作。使用结构体会很复杂并且浪费空间。