目录
前言
结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。是一种自定义类型。
一、结构体的声明
1.结构的声明
struct tag{member - list ;} variable - list ;
例如,一个学生可以视为一个结构体,它具有名字,年龄,性别,学号等基本信息。可以表示如下:
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
这里千万要注意分号不要丢~
2.特殊的声明
在声明结构体的时候,也可以不完全声明。即可以匿名声明结构体。如:
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面两个结构在声明的时候直接省略了名称,即上面的标签tag。
那么就会出现这样的问题: 这两个结构体是否相同呢?
我们用代码来检验一下:
p = &x;
把上述几个代码放在一起运行,编译器就会出现警告:
从“*_”到“*_”类型不兼容。
也就是说,编译器认为这两个结构体是不同的类型,所以这样使用是非法的。
二、结构体的定义和初始化
1.定义
结构体类型创建完毕,怎样定义一个变量呢?可以有以下两种方式:
(1)声明类型的同时定义变量
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
(2)先声明,在后面需要时再定义
struct Point
{
int x;
int y;
};
struct Point p2;
2.初始化
初始化也可以有以下几种方式:
(1)定义变量的同时赋值
struct Stu //类型声明
{
char name[15];//名字
int age;//年龄
};
struct Stu s = {"zhangsan", 20};//初始化
(2)结构体嵌套初始化
struct Point
{
int x;
int y;
};
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL};
(3)无顺序初始化
比如上面n1的初始化,还可以这样写
n2 = {.data = 10, .next = NULL, .p = {4,5}};
3.结构的自引用
在结构中包含一个类型为该结构体本身的成员是否可以呢?
我们尝试一下以下代码:
struct Node
{
int data;
struct Node next;
};
这样的代码是否可行呢?
其实不可以,运行时编译器会报出这样的错误:“next”使用未定义的 struct“Node”。这是为什么呢?
原来在声明这个结构体类型时,在第二个类型处定义一个结构体,这个结构体又需要开辟两个类型的空间,而第二个类型又是一个结构体,这样无限循环下去,结构体根本不能完全声明。
那有没有一种方法能够在结构体内部调用本身类型的结构体呢?
答案是肯定的。直接包含结构体类型会无限开辟空间,那么我们只需用一个指针,指向这种类型的结构体就可以了。这样指针变量很容易开辟空间,结构体就能很好地定义出来。如下:
struct Node
{
int data;
struct Node* next;
};
好了,学完匿名声明和自引用,有些人可能就会想到,能不能用下面这种方式来定义呢?
typedef struct
{
int data;
Node* next;
}Node;
答案是否定的。因为Node这个结构体是结构体类型在声明完之后才能定义的,所以在结构体类型定义的过程中出现Node ,计算机无法解析,认为是未定义的符号。
所以,如果要使用结构体自引用,就不能使用匿名声明。
三、结构体内存计算
现在我们已经掌握了结构体的使用方法。那么结构体的大小是多大呢?是不是声明时定义了的类型大小之和呢?(我这样说了,那就肯定不是,要不然我后面没啥讲了嘿嘿嘿)要弄清楚这个问题,我们就要研究一下结构体的内存对齐。
1.结构体内存对齐
(1)对齐规则
第一个成员在与结构体变量偏移量为0的地址处。 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。- 对齐数 = 编译器默认的一个对齐数 与 该成员大小 之中的较小值。
- 每个成员变量都有一个对齐数,结构体总大小为最大对齐数的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
(2)为什么存在内存对齐?
1. 平台原因(移植原因):
不太理解?我们用实际例子来看一下结构体大小是如何计算的。
2.计算方法
(1)普通类型
struct S1
{
char c1;
int i;
char c2;
};
结果是多少呢?我们按照对齐规则来分析。
首先第一个成员在与结构体变量偏移量为0的地址处:
第二个成员是int类型,大小4个字节,vs编译器默认对齐数为8,4<8,因此它的对齐数为4。所以要对齐到4的整数倍的地址处,即int要从”4“这个地方开始存放。
第三个成员类型是char类型,大小1字节,1<8,所以对齐数为1,char可以从”8“处开始存储。
最后,判断一下最大对齐数,1=1<4,三个成员最大对齐数为4,目前结构体大小是9(0~8),而结果必须是4的倍数,所以结构体大小是16.(不是4的倍数,要扩展空间直到达到4的倍数)。学废了吗?学废了!
(2)结构体嵌套问题
观察下面这个结构体:
struct S2
{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S2));
struct S3
{
char c1;
struct S2 s2;
double d;
};
printf("%d\n", sizeof(struct S3));
首先,我们可以根据上面的规则直接算出s3大小为12。接着,我们来看最后一条规则:
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
什么意思呢?接下来我将通过计算帮助大家理解。
首先s3第一个成员是c1,直接从0开始存储:
接着开始存储s2。
结构体s2的成员有三个,对齐数分别是8、1、4,最大对齐数是8,因此结构体s2应该对齐到8的倍数处(嵌套的结构体对齐到自己的最大对齐数的整数倍处)。而s3大小为12,那么嵌套的结构体就应该这样存储:
然后,存储成员d。大小为8字节,默认对齐数是8字节,因此对齐数就是8字节。从24偏移量开始存储:
最后,我们发现目前大小为32,但是规则中还有一句话: 结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。c1对齐数为1,s2对齐数为8,d对齐数为8,因此最大对齐数为8,而32恰好是8的倍数,不需要再扩展,因此最终结果是32。
我看到有人问了,如果成员的排列方式不一样,有没有可能结构体大小也不一样?
问得好!!!就让我们来对比一下下面这两个结构体:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
来吧,算一下它们的大小。。。。。。。。。
算完了吗?公布答案,s1大小为12,s2大小为8。恭喜你回答正确。
咦?这两个结构体的成员好像一模一样诶。没错,虽然我们无法改变结构体浪费内存的现象,但是在设计结构体的时候,可以让占用空间小的成员尽量集中在一起,这样就可以既满足对齐,又能节省空间。
3.修改默认对齐数
这个部分比较简单,使用#pragma这个预处理指令,就可以修改我们的默认对齐数。如:
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
此处的s1与前面的s1一样,但是前面的s1大小是8,但是这里修改默认对齐数为1,大小变成了6。
修改默认对齐数要看具体使用情景,在结构体对齐数不合适的时候,我们可以自己修改默认对齐数。
四、结构体传参
直接来看代码:
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;
}
答案是print2函数更好。 想一想,为什么?
原因: