一、结构体类型的声明
1.1 结构体的声明
结构体声明的关键字为struct,下面以声明一个结构体变量为例:
struct student
{
int num;//学号
char name[20];//姓名
char sex[10];//性别
};//注意此处有一个分号
1.2 结构体变量的创建和初始化
#include<stdio.h>
struct Stu
{
char name[20]; //名字
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
需要注意的使,在打印结构体变量是不可以一次性打印全部成员的,必须一个成员一个成员的打印
1.3 结构体的特殊声明
我们在声明结构体的时候,可以不完全的声明,即可以省略结构体名(如struct student 中的student可以省略),当是我们一般不这样做,因为它可能会导致一些问题,具体例子看下面的代码:
#include<stdio.h>
//匿名结构体类型1
struct
{
int a;
char b;
float c;
}x;
//匿名结构体类型2
struct
{
int a;
char b;
float c;
}a[20], * p;
int main()
{
//将结构体变量1的地址赋值给结构体变量2创建的指针变量
p = &x;
return 0;
}
请问,上面主调函数的赋值操作是否合法?
答案是否定的,因为我们知道,结构体类型是结构体关键字+结构体名(如struct student)构成的,我们知道,什么类型的变量就要使用什么类型的指针去接收(如int类型的变量要用int*类型的指针去接收),而现在由于上面的两个结构体都是匿名的,因此它们的类型是不一样的,故不能将变量x的地址赋值给p。
1.4 结构体的自引用
我们是否可以在结构中包含⼀个类型为该结构本⾝的成员?如定义一个链表的节点:
struct Node
{
int data;
struct Node next;
};
仔细分析,其实是不⾏的,因为⼀个结构体中再包含⼀个同类型的结构体变量,这样结构体变量的⼤⼩就会⽆穷的⼤,是不合理的,正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
若在匿名结构体的自引用过程中使用了typedef重命名结构体类型名,很有可能会出现一些问题,如:
typedef struct
{
int data;
Node* next;
}Node;
由于Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使
⽤Node类型来创建成员变量,这是不行的,我们在使用变量时要注意先声明在使用。
二、结构体内存对齐
2.1 示例
结构体内存对齐是计算结构体大小的重要方法,在引入这个概念之前我们不妨先计算一下下面两个个结构体类型的大小:
#include<stdio.h>
struct stu1
{
char a;
char b;
int c;
};
struct stu2
{
char a;
int c;
char b;
};
int main()
{
printf("%d %d", sizeof(struct stu1), sizeof(struct stu2));
return 0;
}
或许很多人认为,这两个结构体类型的大小是一样的,因为它们都由一个整形和两个字符型的变量构成,并且它们的大小都为6个字节,那我们来看一下程序的运行结果:
可以看到,我们运行结果不仅不是6,而且这两个结构体的大小也并不一样。
2.2 结构体的内存对齐规则
根据上面的结果我们可以知道,结构体大小的计算一定是由规则的,这个规则叫做结构体的内存对齐,下面是它的对齐规则:
1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
2. 其他成员变量要对⻬到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的⼀个对齐数与该成员变量⼤⼩的较⼩值。
------VS 中默认的值为 8
------Linux中gcc没有默认对齐数,对齐数就是成员⾃⾝的⼤⼩
3.结构体总⼤⼩为最⼤对齐数(结构体中每个成员变量都有⼀个对齐数,所有对齐数中最⼤的)的
整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对齐数的整数倍处,结构
体的整体⼤⼩就是所有最⼤对齐数(含嵌套结构体中成员的对齐数)的整数倍
我们可以用画图的方法来理解2.1中的代码:
在上面的图中漏了一点,在我们计算出结构体的全部成员的大小时,这个还不是结构体的大小,结构体的最终大小必须是结构体中最大对齐数的整数倍,在上面的结构体中,结构体的最大对齐数为4,且计算出的所有成员所占大小为8个字节(有两个字节因为内存对齐浪费了),而8刚好为最大对齐数的整数倍,因此结构体的大小为8个字节。总的来说,判断结构体大小有两大部,
《1》根据内存对齐规则计算出结构体中所有成员的大小;
《2》计算结构体总大小,因该满足——结构体总大小为结构体内最大对齐数的整数倍 && 结构体总大小>=结构体中所有成员的大小。
2.3 为什么结构体中存在内存对齐
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。
2.4 修改默认对齐数
在有默认对齐数的编译器里,默认对齐数是可以修改的,我们可以通过#pramga这个预处理指令来修改,具体操作如以下代码所示:
#include <stdio.h>
#pragma pack(1) //设置默认对⻬数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack() //取消设置的对⻬数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
三、结构体传参
结构体传参有两种方式,分别是传值和传址,如:
#include<stdio.h>
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;
}
需要注意的是,我们在进行结构体传参时,应该尽量传它的地址,因为如果传递⼀个结构体对象的时候,结构体过⼤,参数压栈的的系统开销⽐较⼤,所以会导致性能的下降。