文章目录
结构体的介绍
在c语言中,有着各种各样的数据类型,比如:char、short、int、long、float、double等等,但是这些类型不能够完全描述我们生活中的一些事物的属性,比如一个学生的基本信息,包含其姓名、性别、年龄、学号等等,用char不能完全描述这些东西,用int也不能……用上述任何一个数据类型都不能准确描述学生的属性,此时就需要自己定义一个类型来描述,结构体就发挥其作用,它允许存储不同类型的数据项。
定义和声明结构体
定义结构,必须使用 struct 语句。struct 语句定义了一个包含多个成员的新的数据类型,struct 语句的格式如下:
struct tag {
member-list;
member-list;
member-list;
...
} variable-list ;
tag 是结构体标签。
member-list 是标准的变量定义,比如 int i; 或者 float f;也可以称其为结构体的成员。
variable-list 结构变量,定义在结构的末尾,最后一个分号之前,可以指定一个或多个结构变量,可有可无。
举一个实例,要记录学生的姓名、性别、年龄、学号的结构体变量:
struct student
{
char name[20];
char sex;
int age;
char num[20];
}a,b; //这里的a、b两个变量可有可无
struct student x;//声明结构体变量,结构体本质上也是一种数据类型,所以其声明和int、double等类型类似,都是 struct+标签+变量名;
此外,还可以设计匿名结构体,但是值得注意的是,在自定义匿名结构体的时候,不能够忘了要在结尾的分号前设计需要数量的结构体变量,同样记录学生的姓名、性别、年龄、学号的结构体变量:
struct
{
char name[20];
char sex;
int age;
char num[20];
}a,b; //这里的结构体变量必须要有!!!需要多少个变量就设计多少个变量
也可以使用typedef关键字来定义结构体的名称,比如:
typedef struct student
{
char name[20];
char sex;
int age;
char num[20];
}stu; //这里是对该结构体的新命名,必须要写在这里,写在末尾的分号前
struct student a;//传统声明方法
stu b; //typedef 后,可以选择的第二种声明方法,因为stu是该结构体的新名字
除了这些之外,还需要注意的是,定义了两个结构体,那么这两个结构体在编译器看来就是完全不同的两个数据类型,即使两个结构体内的成员一摸一样,那也是两个类型,比如:
struct student1
{
char name[20];
int age;
}*ps;
struct student2
{
char name[20];
int age;
}a;
ps=&a;//这样子是错误的,因为struct student1 和 struct student2 是不同的类型
还有就是,如果一个结构体A包含在另一个结构体B之内,那么结构体A要么在B之前定义,要么在结构体B之前声明:
struct B; //对结构体B的声明,因为其包含在结构体A中且在结构体A之后定义
struct A
{
char a;
int b;
struct B c;
};
struct B
{
double a;
float x;
struct A y;//结构体A虽然包含在结构体B之内,但是A在B之前定义,所以不用声明结构体A
};
最后,一个结构体里面的成员如果是结构体本身,那么如何设计呢?像下图一样直接设计一个自身结构体变量是错误的做法,从其内存大小方面来看,sizeof(struct student)该如何计算呢?一个struct student里面有第二个struct student ,第二个里面有第三个……这样无限循环,无法计算大小,从这一方面就可以看出这种写法是行不通的。
struct student1
{
char name[20];
struct student a;//结构体内部成员是结构体本身
};
正确 的写法如下,结构体内部设计指向该结构体类型的指针,非指针成员被称作数据域,指针成员被称作指针域,在以后的数据结构方面有用到较多。
struct student1
{
char name[20];
struct student *a;//结构体成员是自身结构体的指针
};
结构体成员的初始化
初始化结构体有两种方法:
由于结构体里面往往不止一个成员,所以初始化的时候要用 { } 大括号将其括起来。
第一种方式: { .结构体成员名=值 , .结构体成员名=值, ……};
第二种方式:{ 值 , 值, ……};
要注意的是,结构体成员是int、double等类型时,值 直接写数据就行,但是如果是char类型的话,只有一个字符那么用 ' ' 单引号括起来,字符串用双引号括起来。具体如下
#include<stdio.h>
struct people
{
int a;
float b;
char c;//char类型且只有一个字符
};
struct test
{
int a;
char x[10];//char类型且是字符串
};
int main()
{
struct people a = { .a = 10,.b = 6.6,.c = 'a' };//这是一种初始化方式
printf("%d %.2lf %c\n", a.a, a.b, a.c);
struct people b = { 10,6.6,'a' }; //这是第二种结构体初始化方式,个人习惯用这一种
printf("%d %.2lf %c", b.a, b.b, b.c);
struct test arr[3]={{1,"abcdefghij"},{2,"usingforar"},{3,"helloworld"}}; //字符串的初始化是 ""
printf("%d %s",arr[0].a,arr[0].x);
return 0;
}
访问结构体成员
结构体变量访问
使用成员访问运算符 . 来访问结构体成员,一般是"结构体变量名.结构体成员名"。
#include<stdio.h>
struct book
{
char auther[20];
int price;
};
int main()
{
struct book a;//定义结构体变量
a.auther="zhangsan"; //访问结构体成员
a.price=30;
printf("%s %d",a.auther,a.price); //打印
return 0;
}
结构体指针访问
使用 -> 来访问结构体成员,一般也是 “结构体变量名->结构体成员名”,也可以使用先解引用,再用 . 来访问,格式为“(*结构体变量名).结构体成员名” 。
#include<stdio.h>
struct book
{
char auther[20];
int price;
};
int main()
{
struct book *a;//定义结构体指针变量
a->auther="zhangsan"; //访问结构体成员
(*a).price=30; //第二种访问方式,先解引用,然后再用.
printf("%s %d",a->auther,a->price); //打印
return 0;
}
结构体传参
当结构体作为函数参数时,在函数内部访问任然和上面两个方式一样。
#include<stdio.h>
struct book
{
char auther[20];
int price;
};
void test1(struct book x)
{
printf("%s",x.auther);//传值和结构体变量访问方法一样
}
void test2(struct book* x)
{
printf("%s\n",x->auther);//传地址和结构体指针访问方法一样
printf("%d",(*x).price);
}
int main()
{
struct book *a;//定义结构体指针变量
a->auther="zhangsan";
(*a).price=30;
struct book b={"lisi",20};
test1(b);//传值
test2(a);//传地址
return 0;
}
结构体的内存对齐
规则
1、结构体的第一个成员直接对齐到 相对于结构体变量起始位置 偏移量为0的地址处
2、从第二个成员开始,每个成员要对齐到【对齐数】的整数倍处
【对齐数】—— 结构体成员自身大小和默认对齐数的较小值
VS情况下默认对齐数:8 Linux环境默认不设对齐数(对齐数是结构体成员自身大小)
3、如果嵌套结构体,那么嵌套结构体对齐到自己的最大对齐数的整数倍处
4、结构体的总大小必须是最大对齐数的整数倍偏移量:在这里指以结构体变量起始位置为起点,某个内存(单位是字节)的位置为终点,起点和终点的距离(单位是字节),偏移量从0开始。(下面第一张图最左边有介绍)
对齐:在这里我理解为结构体成员存放的起始位置,比如成员a要对齐到4的整数倍,那么无论变量a占用多少个空间,其存放的第一个空间都要再偏移量为4*n的地方,详细见下方解读。
占用空间小的结构体成员尽量放在一起,这样能很好的节约结构体空间
规则1
现在对这四个规则还没有了解,咱们一个一个慢慢来看,首先第一个规则容易理解,第一个成员直接对齐到偏移量为0的地址处,无论第一个成员是什么类型,如下图中间部分,结构体struct book第一个成员是int类型,其占四个字节,那么就从起始位置开始(偏移量为0),四个字节空间(标红)存放int a; 下图最右边部分,struct book结构体第一个成员是char类型,char类型的数据占1个字节的内存空间,那么就从起始位置开始(偏移量为0),一个字节空间(标红)存放char a;:
规则2
在介绍规则2之前我们要先了解什么是对齐数,首先对齐数的定义是结构体成员自身大小和默认对齐数的较小值,结构体成员自身大小并不难知道,比如int类型占4个字节,double类型占8个字节等等,默认对齐数就有一些讲究了,在VS环境下的默认对齐数是8,即只要结构体成员自身大小小于等于8,那么这个成员对齐数就等于自身大小;结构体成员自身大小大于8,那么这个成员的对齐数就是8。但是在Linux环境下,没有默认对齐数,所以对齐数是结构体成员自身大小,比如在Linux环境下,int类型对齐数是4,char类型对齐数是1,double类型对齐数是8等等,都是自身大小。
比如下面这张图,中间部分,结构体第二个成员是char b;该成员自身大小是1(char类型所占空间是1个字节),VS环境下默认对齐数是8,两者取较小的,那么该成员对齐数就是1,那么这个成员就要对齐到1的整数倍处,所以从偏移量为4的地方开始存放,标记为蓝色部分。而结构体第三个成员是int c;自身大小时4,VS环境默认对齐数是8,所以该成员对齐数是4,所以该成员要对齐到4的整数倍处,看偏移量,4的整数倍,首先看到4,发现4已经被使用了,不行,再看到8,发现还没有使用过,于是从偏移量为8的地方开始存放,需要4个字节,标记为黄色部分。
下面此图右边部分,结构体第二个成员int b;对齐数不难算出是4,那么从偏移量4*n的地方开始存放,发现偏移量为4的地方可以存,所以从此处存放int b;占用四个内存空间。第三个成员也是int类型,同样从偏移量为4*n的地方开始存放,从偏移量为8处开始存,占四个内存空间。
我们可以用offsetof宏来验证规则2,offsetof是计算一个结构成员相对于结构开头的字节偏移量,其头文件是<stddef.h>,包含两个参数,一个是结构体标签,另一个是结构体内部成员名。此段代码运行结果如下。
#include<stdio.h>
#include<stddef.h>
struct book1
{
int a;
char b;
int c;
};
struct book2
{
char a;
int b;
int c;
};
int main()
{
printf("第一个的偏移情况:\n");
printf("%d\n", offsetof(struct book1,a));
printf("%d\n", offsetof(struct book1, b));
printf("%d\n", offsetof(struct book1, c));
printf("第二个的偏移情况:\n");
printf("%d\n", offsetof(struct book2, a));
printf("%d\n", offsetof(struct book2, b));
printf("%d\n", offsetof(struct book2, c));
return 0;
}
规则3
出现嵌套结构体的情况,那么嵌套结构体也是外层结构体内的一个成员,也应该有一个对齐数,那么该嵌套结构体的对齐数就是自己内部成员的对齐数的最大值(不包括默认对齐数)。比如下方设计的struct B结构体,其内部嵌套了一个struct A c;结构体变量,这个成员的对齐数要看struct A结构体内部的对齐数,其内部三个对齐数分别是2、4、1,最大值是4,所以该成员的对齐数是4,要从偏移量是4*n的地方开始存放第三个成员,但是存放第三个成员,是相当于把struct A结构体所占空间里的内容全部拷贝到struct B里面,struct A的内部成员不需要在struct B的标准下再次对齐,只需要把struct A c;看成一个整体,其整体大小是多少,在struct B里面就占多少空间,其内存结构如下方图片。
#include<stdio.h>
struct A
{
short a; //对齐数为2
int b; //对齐数为4
char c; //对齐数为1
};
struct B
{
int a; //对齐数为4
char b; //对齐数为1
struct A c; //对齐数为4 struct A c;也是struct B的一个成员,所以也有对齐数
};
int main()
{
printf("%d\n", sizeof(struct A));
printf("%d\n", sizeof(struct B));
return 0;
}
上面一段代码放到VS环境下运行,运行结果如下,暂时先不用理解struct A的空间大小为什么是12,只需要知道其空间大小是12即可,下文会详细讲其原因,下方内存布局图片所示,struct A的12个空间,看作一个整体,一起放到struct B里面,struct A有多大,其在struct B内部就占多少空间,如绿框标出的:
规则4
结构体总大小必须是最大对齐数的整数倍,这个就不难理解了,如上图的struct A结构体类型,其三个成员大小分别是2、4、1,不难发现,struct A结构体大小不是简单的2+4+1=7,也不是简单的如上图左边所示的,到最后一个黄色区域为止,一共占了9个内存空间大小就是9。其内存大小是12,就是因为有规则4的存在,总大小必须是结构体内部成员最大对齐数的整数倍,其内部成员最大对齐数是4,那么struct A的总大小必须是4*n(n为整数),而根据其内存分布,已经占了9个内存空间,比9大的且是4的倍数的数字里面,最小的是12,所以其总内存是12。
而对于上图的struct B而言,第一个成员占4个字节内存,第二个成员对齐数为1,而前四个内存空间已经使用,所以在偏移量为1*n的地方开始存,发现偏移量为4处可以存,第三个成员是一个结构体变量,其对齐数是自己内部成员对齐数的最大值,struct A内部最大对齐数是4,所以第三个成员的对齐数是4,从偏移量为4*n的地方开始存,发现只有从偏移量为8开始存,占12个空间,struct B其内部成员对齐数分别是4、1、4,,所以最大对齐数是4,那么struct B的总大小必须是4*n,而由图可知,将struct A放进去之后,正好占了20个内存空间,是4的倍数,所以其大小就是20。(此过程是一套完整的分析过程)
VS环境下修改默认对齐数
VS环境下的默认对齐数是8,但是可以通过人为操作来改变这个值。如下代码和运行结果。
#include<stdio.h>
#include<stddef.h>
struct test2
{
char a;
int b;
char c;
};
#pragma pack(1)//设置默认对齐数,pack()括号内的值就是修改后的默认对齐数的值
struct test1
{
char a;
struct test2 b;
int c;
};
#pragma pack()//恢复默认对齐数
int main()
{
printf("第一个的偏移情况:\n");
printf("%d\n", offsetof(struct test1,a));
printf("%d\n", offsetof(struct test1, b));
printf("%d\n", offsetof(struct test1, c));
printf("第二个的偏移情况:\n");
printf("%d\n", offsetof(struct test2, a));
printf("%d\n", offsetof(struct test2, b));
printf("%d\n", offsetof(struct test2, c));
return 0;
}
分析:struct test1 结构体的默认对齐数被改成了1,由于任意的数据类型,其所占空间至少是1个字节,所以不管struct test1 结构体内部成员是什么,它们的对齐数都是1,其第一个成员是char类型,所以占一个字节的内存,第二个成员默认对齐数是1,所以从偏移量为1的地方开始存储,第二个成员大小是12,故存放了12个字节的空间,然后第三个成员默认对齐数也是1,故从偏移量为13处开始存。
好了,关于结构体的所有基本知识到这里就结束啦,如果有写的不好的地方欢迎评论区多多指正!!!喜欢的话请一键三连哦!!!