前言:
本文将介绍结构体类型,结构体变量的创建和初始化,重点介绍结构体中的内存对齐。
正文:
1.结构体
1.1结构体是什么
结构体是C语言中三大自定义类型之一(另外两个是联合体和枚举),C语言给我们的类型,有char、int、short、float等类型。但当我们要描述一个学生的信息,这个学生的性别、姓名、年龄以及学号等,C语言所给的这些类型不能满足我的使用,这是我们就需要使用结构体,自定义我们所需要的类型。
1.2结构体的申明
1.2.1结构体的一般申明
例如下面我将实现上面所说的结构体:
struct student
{
char name[20];
int age;
char sex[10];
int id[20];
};
上面的代码就是一个描述学生的结构体,该结构体的类型是strcut student 这个就相当于我们所熟知的int、char等类型。 struct和student都是这个结构体类型的关键字。当然我们也可以写一个关于描述房子的结构体:struct house 接下来如上补充自己需要的成员。struct后面的关键字并不一定只能是house,其他的关键字也是可以的,比如:a、aa、aaa这些都是可以的,之所以写house是为了提醒使用者。struct后面又被叫做结构体标签。换句话说,这就struct后面就是一个标签作用,至于是什么标签,这是有我们程序员确定的。
1.2.2结构体的特殊申明
在申明结构体时,可以不完全申明。
struct
{
char name[20];
int age;
char sex[10];
int id[20];
}a1;
向上面这个结构体就是,省去了结构体标签。这样的结构体在申明的时候,就需要给它创建变量,上述代码的变量名就是a1。这样的结构体只能使用一次(就是使用a1),之后在过程中就不能再创建变量了
struct a2 = { "zhangsan",12,"man",20240331 };
这样的代码是不行的,编译器会报错。
这种不完全申明还有一个弊处:
例如:上面这两个结构体,我们看代码,我们可以知道上面的两个结构体中的内容其实是一致的,那么我们就可以很容易联想到
1.3结构体的创建和初始化
结构体的申明一定要在结构体的创建之前,否则编译器是会报错的。
上述代码应该是这样写
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[10];
int id[20];
};
int main()
{
struct student a1;
return 0;
}
这里的a1就是变量名,那现在创建了变量,那如何初始化呢?
第一种初始化方式:就是按顺序初始初始化
第二种初始化方式:使用点操作符(.)进行初始化,可以不按顺序就行初始化。
两种方式的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[10];
int id[20];
};
int main()
{
struct student a1 = {"zhangsan",12,"man",20240331}; //第一种初始化方式
struct student a2 = { .age=13,.name="lisi",.sex="man",.id=20240332 };//第二种初始化方式
return 0;
}
这两种方式在,实际应用中我们根据实际的需求来进行相应的选择就行。
1.4结构体成员的访问
上面介绍了结构体的创建和初始化,下面我将介绍如何访问结构体中的成员。
第一种方式:使用点操作符(.)
使用方式:结构体变量.成员名
代码如下:
printf("%s %d %s",a1.name,a1.age,a1.sex);
(这里无法只使用一次就打印出id数组,使用方法一样,但是要全部打印出需要使用循环)
这样就能打印出结构体中的成员。
第二种方式:使用 箭头操作符(->)
使用方式:结构体指针变量->成员名
代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct student
{
char name[20];
int age;
char sex[10];
int id[20];
};
int main()
{
//struct student a1 = {"zhangsan",12,"man",20240331}; //第一种初始化方式
//struct student a2 = { .age=13,.name="lisi",.sex="man",.id=20240332 };//第二种初始化方式
struct student a1 = { "zhangsan",12,"man",20240331 };
struct student* tmp = &a1;
printf("%s %d %s\n",a1.name,a1.age,a1.sex);
printf("%s %d %s\n", tmp->name, tmp->age, tmp->sex);
return 0;
}
这个访问方式,需要先创建一个对应的结构体指针变量,使用&(取地址符)取出结构体变量的地址,然后使用箭头操作符(->)加上成员名,这样就能访问对应结构体变量的成员。两种方式的输出结果是一致的。
1.5结构体的自引用
上面我们知道一个结构体中可以包含多种类型,那么结构体中是否能包含结构体本身的类型。一开始我想到了下面这种形式。
struct node
{
int date;
struct node next;
};
int main()
{
return 0;
}
但是这种代码是错误的,这样实际上,我们创建一个结构体,这个结构体中又创建了一个同类型的结构体,这样做子子孙孙无穷匮也,内存的占据也是无穷大的,所以这样的写法是不对的。正确的写法应该是,在里面加一个本结构体类型的指针。
struct node
{
int date;
struct node* next;
};
int main()
{
return 0;
}
这样才是结构体自引用的正确方式,因为一个指针的大小是固定的,在结构体的自引用时,不要使用匿名的结构体,这样做很容易产生问题。使用结构体的自引用,我们其实就可以实现链表,通过一个结构体找到下一个结构体。
2.结构体的内存对齐
结构体的大小是多少?不是不里面的各类型的大小相加呢?还是其他?看完下面你就理解了。
2.1对齐规则
对齐规则:
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到对齐数的整数倍的地址处。
对齐数=编译器默认的对齐数与该成员变量大小的较小值。
有些编译器没有设置默认对齐数,那么对齐是就是该成员的大小。
3.结构体的总大小的最大对齐数(结构体成员中的所有对齐数的最大值)的整数倍。
4.嵌套结构体的情况,先算里面嵌套结构体的大小,规则和上面一样,然后里面的嵌套结构体就和其他成员变量一样,同时符合上面的规则。
支持你已经可以算出任何一个结构体的大小。
2.2为什么要内存对齐
1、平台原因:
不是所有的硬件平台都能访问地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据
2.性能原因:
在访问内存未对齐的时候,处理器需要作两次内存访问(这个是指处理一个数据时);但是对齐的内存访问只需要一次访问,这样实际上就是用空间换时间效率。
我们知道上面的对齐原理,那么我们在设计程序的时候就可以一定程度上控制结构体变量的大小。列如下面这两串代码:
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
char c1;
int i;
char c2;
};
上面这两个结构体中的内容其实是一样的,但是它们所占的字节数却不一样,s1所占的字节数是9,而s2所占的字节数是8。这就告诉我们在使用结构体时应该将将占空间小的成员放在前面,这样能节省我们的内存空间。
2.3修改默认对齐数
上面我们讲诉了,我们的一些编译器中使用默认对齐数的,如果我们需要自定义一个对齐数,那也是可以的。
#pragma这个预编译处理指令,可以修改编译器的默认对齐数。
#pragma pack(2)--------这样就把默认对齐数修改为2了。相应的某些结构体的大小也会发生变化。
3.结构体传参
如果我们想使用一个print函数来输出结构体中的成员,我们向这个函数传参时,是传结构体变身的内容还是结构体结构体的指针。一言以蔽之,是使用结构体指针更好,因为函数在传参的时候,参数是需要压栈的,会有时间和空间上的开销。毫无疑问,一个结构体的大小一定是会比一个结构体指针大的,所以使用结构体指针传参,这样在空间和时间上,性能是最好的。
结论:结构体传参的时候,要传结构体的地址。
4.结构体实现位段
上面我们讲结构体的对齐是为了空间换时间,那我们在非常需要节省空间的,这个时候我们就会使用位段来节省空间。
4.1什么是位段
尾端的申明和结构体类似,但是有一下两点不同:
1.位段的成员必须是int、unsigned int、或者signed int,在c99中的位段成员也可以是其他类型。
2.位段的成员后边有一个冒号和一个数字。
struct A
{
int a:2;
int b:5;
int c:10;
int d:10;
};
上面这个就是一个位段。
4.2位段的内存分配
1.位段的成员的类型都是int、unsigned int、signed int或者char等类型.
2.位段的空间是以4个字节或者1个字节的方式来开辟的。
3..位段有一些不确定的因素,所以跨平台性不好。
4.3位段的跨平台问题
1.int是被当作有符号整型还是无符号整型,是不能确定的。
2.位段的最大位的数目是不能确定的,比如在16位的机器上,int占2个字节;而在32位的机器上,int是占4个字节,如果我写 int a:20;这样的代码在32位的机器上是没有问题的,但是在16位的机器上就是错误的,因为16位机器上的int只占有16个字节。
3.位段中成员在内存中是从左向右分配空间,还是从右向左分配空间这是不确定的。
4.如果结构体中有两个成员,有一个较大的那个成员占据一开始所给的空间,剩下的空间不够存放第二个成员,又新申请了空间,那是舍弃一开是所给空间的剩余部分,还是继续使用。这也是不同的。
结论:和结构体相比,位段达到同样的效果,但是节省了大量空间,但是位段的跨平台性不好。
4.4位段使用的注意事项
当位段中的几个成员共用一个字节时,我们不能通过地址来进行赋值,因为一个字节内部是没有分配地址的(地址是以字节位单位划分的)。这是我们想赋值就不能直接使用scanf函数,而是只能先赋值到一个变量中,然后在赋值给位段中的成员。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct node
{
int a : 2;
int b : 5;
}mm;
int main()
{
scanf("%d", mm.b);
return 0;
}
这样是不行的。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct node
{
int a : 2;
int b : 5;
}mm;
int main()
{
int tmp1, tmp2;
scanf("%d%d",&tmp1,&tmp2);
mm.a = tmp1;
mm.b = tmp2;
return 0;
}
要使用上述的形式,才能正确输入。
总结:
看到这里,你因该了解了结构体是什么,如何使用结构体,以及一些结构体的注意事项。在学习结构体是我们应该勤敲代码,在实践中了解结构体。如果文章中有错讲误讲的,欢迎在评论区或者私信中指教。