结构体
结构体的声明
结构体的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。(而数组是相同类型的变量)。
结构的声明
struct tag//标签名,可以自定义)
{
member-list;//成员列表
}variable-list;//变量列表
例如描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[5];//学号 };//分号不能丢
特殊的声明
在声明结构的时候,可以不完全声明。(无标签名)
比如:
//匿名结构体类型 struct { int a; char b; float c; }x;//注:利用现有的内容直接创建出结构体变量,在其他任何地方都无法创建
上面的结构在声明的时候都省略掉了结构体标签。
结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
//代码1 struct Node { int data; struct Node next; }; //不可行!
显然这种形式是不可行的,因为,结构体中又创建了一个自己作为成员,该成员中又会含有自己作为成员,这样做会形成套娃,无穷无尽的套下去。
正确的自引用方式:
//代码2 struct Node { int data;//数据域 struct Node* next;//指针域 //创建了一个结构体指针 };
注意:
//代码3 typedef struct { int data; Node* next; }Node; //这样写代码是否可行? //不可行,因为第一次使用Node时就有问题,不知道Node是什么
结构体变量的定义和初始化
有了结构体类型,那应该如何定义变量?其实很简单!
struct Point { int x; int y; }p1;//声明类型的同时 struct Point p2;//定义结构体变量p2 //初始化:定义变量的同时赋初值 struct Point p3 = { 1,2 }; struct Stu { char name[15];//名字 int age;//年龄 }; struct Stu s = { "zhangsan",20 };//初始化 struct Node { int data; struct Point p; struct Node* next; }n1 = {10,{4,5},NULL};//结构体嵌套初始化
结构体的内存对齐
我们已经掌握了结构体的基本使用了。
现在我们来讨论一个问题:如何计算结构体的大小。
这也是一个特别热门的考点:结构体的内存对齐。
我们先来看这样一道题吧。
struct s1 { char c1; int i; char c2; }; //计算s1大小
我们首先来看一看这三个变量在内存中的分布情况。
注:使用宏(offsetof())可以计算结构体成员相较于结构体起始位置偏移量.(stdeff为头文件)
#include <stddef.h> struct s1 { char c1; int i; char c2; }; int main() { printf("%d\n", offsetof(struct s1, c1)); printf("%d\n", offsetof(struct s1, i)); printf("%d\n", offsetof(struct s1, c2)); printf("%d\n", sizeof(struct s1)); return 0; }
运算结果:
为什么会有这样的情况呢,按一般思维我们可能想的是0,1,5 ,大小是6.
但事实并非如此,那么就让我们看看结构体的对齐规则吧。
1.结构体的第一个成员可以放在相较于结构体变量的起始位置的偏移量为0的位置。
2.从第二个成员开始,往后的每个成员都要对齐到某个对其数的整数倍处。
对齐数:结构体成员自身大小与默认对齐数大小的最小值(不同编译器对齐数可能不同)。
3.结构体的总大小必须是最大对齐数的整数倍。最大对齐数是所有成员最大对齐数的最大值。
下面再用上面的规则完成一下这个题。(平台以VS为例,VS默认对齐数是8)
再来做几个练习:
#include<stdio.h> struct s3 { double d; char c; int i; }; int main() { printf("%d", sizeof(struct s3)); return 0; }
结果是16.
#include <stdio.h> struct s3 { double d; char c; int i; }; struct s4 { char c1; struct s3 s; double d; }; int main() { printf("%d", sizeof(struct s4)); return 0; }
这里要注意结构体的嵌套,s3的大小是16,直接算进去,但最大对齐数(含嵌套结构体的对齐数)是基本数据类型的大小,即double,结果算出来是32。
为什么会存在内存对齐?
1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些特定地址处取某些特定类型的数据,否则会抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要两次内存访问,而对齐的只需要一次。
总体来说:结构体的内存对齐是用空间换时间的做法。
那在设计结构体时,我们既要满足内存对齐,又要节省空间,就要做到:
让占用小的成员尽可能聚集在一起。
比如:
struct s1 { char c1; int i; char c2; }; //改为 struct s2 { char c1; char c2; int i; };
修改默认对齐数
我们可以使用#pragma这个预处理指令,可以修改对齐数。
eg.#pragma pack(8),修改默认对齐数为8。
#pragma pack( ),取消设定的默认对齐数,还原为默认。
结论:在默认对齐数不合适时,我们可以自己修改默认对齐数。
结构体传参
#include <stdio.h> struct S { int data[100]; int num; }; struct S s = { 1,2,3,4 }; //结构体传参 void print1(struct S s) { printf("%d\n", s.num); } //结构体传参 void print2(const struct S* ps)//加上const防止参数被改 { printf("%d\n", ps->num); } int main() { print1(s);//传结构体 print2(&s);//传地址 return 0; }
上面哪个函数更好些?
显然是print2.原因:函数传参的时候,参数需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象时,结构体过大,参数压栈是开销大,导致性能下降/
结论:结构体传参时,要传结构体地址。