结构体的基础知识
结构体的含义
在C语言中除了有C语言自己的内置数据类型,如char、short、int、long、float、double,其实还是不够的,因为在现实生活中我们有复杂的对象需要描述,如一个学生,有姓名班级年龄地址等信息;再如一本书,有书名作者定价等信息;所以我们不可能用一个数据类型就可以轻描淡写地略过,这时候我们就需要自定义数据类型,C语言中提供了三种自定义类型供程序员使用,分别是结构体、联合体和枚举。这里我来给大家讲一下结构体的创建。后续会出联合体和枚举的文章!
结构体的创建
首先我们要自定义一个结构体,需要使用一个关键字struct,下面是定义模板:
struct tag
{
member_list;
};
tag 是你需要的结构体的名字,这个根据实际情况定义,然后 member_list 就是这个结构体需要包含的成员信息列表,这个根据实际需要,添加即可。特别注意了,最后一个 } 是有 ; 的,不要遗漏!
话不多说,我们来定义一个学生的结构体:
struct Student
{
char name[20]; //姓名
char sex[5]; //性别
int age; //年龄
char address[30]; /地址
};
结构体应该定义在那个位置也是程序员自己设置的,如果定义在函数内部,那这个结构体就只能在函数内部使用了,如果是定义在函数外,也就是这个工程都可以使用这个结构体来定义变量。
说到变量,我们怎么定义结构体变量,有三种方式,首先就是在结构体 } 后面定义变量,或者直接在结构体下面定义变量,还可以在函数内部定义,区别就是前两个是全局变量,后面一个是局部变量。
变量也是有分三种,一种是普通变量(就只是一个普通变量名),还有一种是结构体数组变量,顾名思义就是个数组来的,最后一种就是结构体指针变量。
我们来给大家定义一下:
struct Student
{
char name[20];
char sex[5];
int age;
char address[30];
}stu, stu1[10], * p1;
struct Student s1, sp1[10], * p2;
int main()
{
struct Student s2, sp2[10], * p3;
return 0;
}
要注意了,除了直接在结构体后面定义变量不用使用struct Student 之外,其他都需要使用struct Student,大家会不会觉得麻烦,没事,我们还有一个关键字就是 typedef,下面会提到怎么使用!
匿名结构体
匿名结构体就是没有名字的结构体,只能使用一次。
struct
{
char name[20];
int age;
char add[30];
}x,b;
只能使用一次就是只能在结构体后面定义变量,其他定义方式都是错的!毕竟这个结构体没有名字,没有名字怎么再次定义变量呢!
typedef 介绍及其使用
这是一个重命名的关键字,直接上示例:
typedef struct Student
{
char name[20];
char sex[5];
int age;
char address[30];
}Stu;
直接在前面加上 typedef ,然后重新定义一个简单的名字,在结构体后面定义,上面代码就可以使用 Stu 来定义结构体变量了。
结构体的操作符
如何使用结构体,我们需要两个操作符,分别是 . 和 ->
. 是普通结构体变量解引用的操作,->是结构体指针操作。
实践一下:
#include <stdio.h>
typedef struct Student
{
char name[20];
char sex[5];
int age;
char address[30];
}Stu;
int main()
{
Stu p = { 0 };
scanf("%s %s %d %s", p.name, p.sex, &p.age, p.address);
Stu* pp = &p;
printf("%s %s %d %s\n", p.name, p.sex, p.age, p.address);
printf("%s %s %d %s\n", pp->name, pp->sex, pp->age, pp->address);
return 0;
}
除了结构体指针要使用->,其余的使用点操作符就可以了,目前编译器使用比较便捷,当我们输入结构体操作符后,就会弹出一个小窗口供我们选择要使用的成员变量名,就可以不用一个一个地输入了。
这里要注意了,我们使用scanf 去输入值的时候,要注意需要传入地址的,由于数组名就是一个地址,所以不需要&,如果想具体了解数组名和指针的关系,大家可以点开找到数组名再深入理解
结构体的大小
结构体的大小不是简单的数据类型大小的叠加,而是需要对齐的,下面我来讲一下结构体大小的对齐规则。
首先我们要确定成员的自身的大小(以字节为单位),例如 char 的大小是 1 ,int 的大小是 4 以此类推,之后我们要确定编译器自身的默认对齐数,例如VS 的默认对齐数是8,Linux中gcc编译器的默认对齐数是成员自身大小。
接着我们要确定每一个成员的对齐数是多大,这个对齐数等于自身的大小和编译器默认对齐数之间取最小值,例如char a 的大小 在VS中的对齐数是1和8进行比较,结果取最小值就是1.
第一个成员从偏移量为0 的位置开始对齐,其他结构体成员要对齐到偏移量和自身的对齐数的整数倍,结构体总大小为最大对齐数(结构体中每个成员变量都有⼀个对⻬数,所有对齐数中最大的)的整数倍。
如果出现嵌套结构体,嵌套的结构体成员对齐到⾃⼰的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
实践
这里以VS2022为例,默认对齐数为8
没有嵌套结构体的情况下:
#include <stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
double d;
char c;
int i;
};
int main()
{
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S2));
printf("%zd\n", sizeof(struct S3));
return 0;
}
S1:
首先char a是第一个元素,所以从偏移量为0开始对齐,int i 大小为4,和默认对齐数8比较,取4,所以从偏移量4开始对齐,接着是char c 的大小为1,默认对齐数为8,取1,这是大小一共为9;三个成员的对齐数分别为1,4,1,取最大的值就是4,结构体的总大小为成员中最大对齐数的整数倍,所以总大小为12,加多三个空白字节。
S2:
char c1从0开始对齐,对齐数为1,char c2 大小为1,和8相比取对齐数1,int i 的大小为4,和8相比对齐数就是4,将后两个对齐到整数倍即可,最后大小为8,整个结构体的成员的最大对齐数是4,由于8是4的倍数,所以结构体最后的总大小为8
S3:
double d 的大小为8,和默认对齐数相比取对齐数8,由于是第一个成员,从偏移量为0开始,char c 的大小为1,和8相比取对齐数为1,int i 的大小为4,和8相比取对齐数为4,这时大小为16,结构体成员中最大对齐数为8,16恰好是8的倍数,所以结构体最后的大小为16.
运行结果如下:
嵌套结构体的情况:
#include <stdio.h>
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd\n", sizeof(struct S4));
return 0;
}
对于嵌套结构体,这个嵌套结构体的对齐数取自身成员的对齐数的最大值,所以是8,然后我们按照规则来就可以了:
算到的大小为32,S4结构体最大对齐数是8,32是8 的倍数,最后的结构体大小为32.
运行结构:
如果结构体出现数组,如何处理结构体的大小?我们把这个数组拆分成一个元素一个元素来存放就可以了,没有多大的区别。
#include <stdio.h>
struct S5
{
char a[5];
int i;
short b[2];
char c;
};
int main()
{
printf("%zd\n", sizeof(struct S5));
return 0;
}
运行结果:
内存对齐的原因
我们拿下面这个结构体来举例:
struct S
{
char a;
int i;
};
假设编译器是四个字节四个字节来读,那读完char a 的数据后,跳过四个字节就读不到完整的int i 的数据,编译器只能倒回去再次读取int i 的数据,就会浪费一定的时间,相反如果结构体对齐,读取数据就会很便捷,这就是空间换时间!
一句话总结,结构体的内存对齐是为了拿空间换时间!
修改默认对齐数
#pragma pack(num)
其中num 是你自己要设置的结构体的默认对齐数的值,一般设置这个对齐数的值为 2 的次方数
#pragma pack()如果在写结构体后面再添加这个语句,就是取消默认对齐数的修改,恢复默认对齐数
合理设计结构体
建议从最小的成员开始写起,把小的成员集中到一起,这样会节省空间,但是注意了,我们一切要根据项目需求走!
结构体传参
结构体传参也很简单,要么直接传数值,要么传地址,这里建议传址调用!因为传值调用需要浪费空间和时间来拷贝一个一模一样的结构体到函数完成任务,传地址的话不仅节省空间还节省时间!所以传址调用是一个不错的选择!
位段
位段的形式:
位段就是结构体成员后面加了冒号:之后跟一个数字就可以了
struct S
{
char a : 2;
char b : 5;
char c : 4;
};
位段的内存分配
冒号的数字就是这个成员的大小,单位是比特
成员的类型可以是int 、unsigned int、signed int 或者 char 等类型
在VS2022中,一般以一个字节(char)或者四个字节(int)的开辟,分配空间从从右向左分配,如果空间不够放入一个完整的成员,就再次申请空间。
数据的存放,如果超过自身的比特位数就会发生截断!
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
当空间不够就会舍弃这空白的空间,所以如下图申请了3个字节的空间:
当位数不够存放就会发生截断现象。
一下是监视和内存窗口:
位段的缺陷
代码不具有跨平台性,我这里举的例子是VS2022的例子,如果放在其他平台上,位段的内存分配可能会不一样。