C语言--结构体(Struct)
结构体类型
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
就是一些不同类型的数据结构(结合)在一起,变成结构体。
结构在一起的数据是一起出现,一起消失的,就像是一个类型一样,因为他们是一体的。
struct Stu //结构体变量名
{int S; //成员列表char t;
}u4,u5,u6; //变量列表 ---> 表示是全局变量 “;”不能省略int main()
{struct Stu u1;//创建 结构体类型struct Stu u2;struct Stu u3; //局部变量return 0;
}
对于此结构体变量,参考下面理解

结构体变量的创建,初始化,使用
#include <stdio.h>struct Stu //声明
{char name[20]; int age;int id[10];
}; int main()
{struct Stu s1 = {"changsan",20,2025111401}; //顺序初始化struct Stu s2 = { .age = 18,.id = 2025111402,.name = "lisi" }; //随机初始化: '.' + '初始化的成员变量名' = 内容struct Stu s3 = { .age = 18,.age=20,.age=200}; //随机初始化时,or顺序初始化后,还重新初始化成员变量,默认取最后初始化的值printf("名字:%s 年龄:%d \n", s1.name,s1.age);printf("%d ", s3.age);// 使用 -> 变量名 + '.' + 成员变量名 return 0;
}//输出结果:
//名字:changsan 年龄:20
//200
//
如上,是结构体变量的创建+初始化+使用。
结构的特殊声明
在声明结构的时候,可以不完全的声明。
比如:
struct Stu
{char name[20];int age;int id[10];
}s1;struct
{char name[20];int age;int id[10];
}s2;
s2这种声明结构体变量叫匿名结构体。这种结构体变量,只能使用他的全局变量的形式,无法创建他的局部变量。。。如:
#include <stdio.h>struct
{char name[20];int age;int id[10];
}s2;int main()
{s2.age = 10;for (int i = 0;i < 5;i++){s2.age++;s2.age++;}printf("%d ", s2.age); //只能创建该结构体变量的全局return 0;
}//输出结果:
//20
这样只能在声明的时候创建该结构体的全局变量(可以是多个全局变量,但只能一次性全部创建),在后续过程中无法再创建。。。
结构体的自引用 X
在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表(雏形):
struct Node
{int data;struct Node n;
};
上述代码正确吗?如果正确,那sizeof(struct Node) 是多少?
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
正确的自引用方式:
struct Node
{int data;struct Node* next;
};
存放下一个节点(结构体)的地址,指针大小能确定(4/8字节)。
结构体的重命令
在结构体自引用使用的过程中,可以用 typedef 对匿名结构体类型重命名。
typedef struct Node
{int data;struct Node* next; //不能写成Node* next ,因为还没重命令完成,不能使用
}Node;
注:因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
重命令后,无法创建全局变量,只能创建不同的名称。如:
#include <stdio.h>typedef struct Node
{int data;struct Node* next; //不能写成Node* next ,因为还没重命令完成,不能使用
}Node ,s1 ,s2;int main()
{Node s;s.data = 100;s1 ss;ss.data = 200;s2 sss;sss.data = 300;printf("%d ", s.data);printf("%d ", ss.data);printf("%d ", sss.data);return 0;
}//输出结果:
//100 200 300
那还这么使用全局变量?只能用常规创建全局变量的方式创建相对应的全局变量:
#include <stdio.h>typedef struct Node
{int data;struct Node* next;
}Node, s1, s2;Node s;int main()
{s.data = 100;printf("%d ", s.data);return 0;
}//输出结果:
//100
注:创建全局变量时,顺序不能搞错,先声明->再创建。。。
结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点: 结构体内存对齐
对齐规则
首先得掌握结构体的对齐规则:
- 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
(上一个变量最后的存储位置的下一个地址开始)
对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
- 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处
结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
对于下面这段结构体为例:
#include <stdio.h>struct S1
{char C1; //1char C2; //1int n; //4
};struct S2
{char C1; //1int n; //4char C2; //1
};struct S3
{char C1;struct S1 arr;int n;
};struct S4
{char C1;char C2;
};int main()
{printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));printf("%zd\n", sizeof(struct S3));printf("%zd\n", sizeof(struct S4));return 0;
}//输出结果:
//8
//12
//16
//2


没用上的空间呢?浪费了,不用了。
为什么存在内存对齐?
大部分的参考资料都是这样说的:
- 平台原因 (移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
个人理解:
数据是由谁处理?CPU!
我们知道CPU的“笨宝宝”,你给他两个数据,再给他一个加法符号,他会自动相加!
你给他两个数据,再给他一个减号,他却不认识,就报错了(前面的博客中的例子),那么这个时候给他两个数据,一个数据是负数,再给他一个加号,他就认识了,就能计算了(操作符中提到过)。跟这个类似,CPU是一个“笨宝宝”,他没有判断能力,只有超强的计算能力。现在你给他一个地址(struct的地址),然后告诉他查第一个数据,这个数据是char类型,那么CPU会根据char类型的大小偏移的方式查这个数据(不偏移的话他肯定越界访问,系统会给他警告),他查到之后自己记住这段数据的最后一个字节的地址,把该段内的内容发给你。
你要第二个数据(int类型)的时候,他根据struct的地址一个一个偏移,直到越过上端他记住过的地址,直接把内容发送给你,再记住最后一个字节的地址,于此类推。。。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
比如 小 -> 大 or 大 -> 小来 设计。
修改默认对齐数
那么这么修改默认对其数呢?
#include <stdio.h>#pragma pack(1) //修改默认对其数(1)
struct S1
{char C1; //1char C2; //1int n; //4
};
#pragma pack()//恢复默认对齐数struct S2
{char C1; //1char C2; //1int n; //4
};int main()
{printf("%zd\n", sizeof(struct S1));printf("%zd\n", sizeof(struct S2));return 0;
}//输出结果:
//6
//8
这样 S1 和S2 类型的成员一模一样,但是S1 和S2 所占空间的大小有了一些区别。
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
结构体在对齐方式不合适的时候,我们可以自己更改
(不乱改,尽量不使用3/7/11等数字)
结构体传参
注:用指针的时候,“.” 会升级成 “->” 。
#include <stdio.h>struct S
{int data[10];int num;
};void Print_1(struct S s1)
{int c = sizeof(s1.data)/sizeof(s1.data[0]);for (int i = 0;i < c;i++){printf("%d ", s1.data[i]);}printf("\n");printf("%d \n", s1.num);s1.num = 9999999;
}void Print_2(struct S* s1)//用地址指向时,用 ->( 可以抽象的理解为 指向的内容 偏移到 后面变量的位置)
{int c = sizeof(s1->data) / sizeof(s1->data[0]);for (int i = 0;i < c;i++){printf("%d ", s1->data[i]);}printf("\n");printf("%d \n", s1->num);
}int main()
{struct S s1 = { {0,1,2,3,4},200 };Print_1(s1);printf("------\n");Print_2(&s1);return 0;
}//输出结果:
//0 1 2 3 4 0 0 0 0 0
//200
//------
//0 1 2 3 4 0 0 0 0 0
//200
上面的print1 和print2 函数哪个好些?首选print2函数。
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
即:传结构体变量本身的时候,要自己创建变量,时间和空间上都有开销。
传指针的时候,可以直接使用(还能修改内容),可以节省时间和空间。
结论:
结构体传参的时候,要传结构体的地址。
注:用指针的时候,“.” 会升级成 “->” 。
注:s1.data 的本质是数组,看成数组data正常使用即可 ,其他数据类型也一样。
完
