本篇文章我们来学习一下啊C语言中一个重要的知识点——结构体,如果想学好C语言的话那学好结构体必然是前提,废话不多说,我们直接进入正题。
目录
一、结构体的声明和初始化
1.1 结构体是什么
1.2 结构体的声明
1.3 结构体的特殊声明
1.3.1匿名结构体声明
1.3.2 typedef重定义的结构体
1.3.3 结构体的自引用。
1.4 结构体变量的定义与初始化
二、结构体的内存对齐
2.1 计算结构体的内存大小
2.2 offsetof测量偏移量
2.3 结构体内存对齐的意义
2.4 修改默认对齐数
结构体是一些值的集合,这些值被称为结构体成员变量。结构体中每个成员可以是不同的类型。
比如我们可以在一个结构体中定义整形、浮点数、指针、数组等等,如果我们需要使用它们,则可以直接通过操作符来访问这些结构体变量。
struct stu
{
char name[20];
int age;
char sex[5];
char id[20];
};//一定不要忘记这有一个分号;
这里我们定义了一个结构体student,有name数组、整形age、sex数组、id数组,这些就可以存放一个学生的数据,它的姓名是什么、年龄是什么……
struct
{
int a;
char b;
}x;
struct
{
int a;
char b;
}*ps;
上面两个结构体都省略了结构体标签(比如上面的struct student,student就是一个结构体标签),所以在编译器看来是它们是不相同的两种类型,因为它们失去了结构体标签,即是匿名的结构体。
就比如说struct student型的结构体的地址给struct student型的结构体指针,但struct student型的结构体不能给struct teacher型的结构体指针,因为它们标签不同,所以编译器会将它们看作不同的结构体类型,而匿名结构体则是完全没有结构体标签,所以如果想用一个匿名结构体给另一个匿名结构体指针赋值是完全行不通的。
这样的匿名结构体也不能再创建一个相同类型的结构体(即使使用typedef)。
所以说我们使用匿名结构体通常是只使用一次这个结构体,不会使用第二次,但是我们完全也没必要使用匿名的结构体,但是这种书写却也是编译器允许的。
typedef 为C语言的关键字,作用是为一种数据类型定义一个新名字,这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
我们可以使用typedef对代码进行简化,增强代码的可读性。
以下是使用的介绍:
typedef struct student
{
char name[20];
int age;
char sex[5];
char id[20];
};
如果我们不使用typedef重定义struct student,那我们下一次使用想要用struct student定义学生李四的时候会是这样定义的。
struct student s1={lisi",18,"nan","2102010"};
接下来我们来看看使用了typedef重定义struct student
student s1={lisi",18,"nan","2102010"};
当然,我们也可以在分号的前面再加上一个student。
typedef struct student
{
char name[20];
int age;
char sex[5];
char id[20];
}student;
这样,我们在定义李四的时候就可以这样定义。
student s1={lisi",18,"nan","2102010"};
大大减少了代码量,而且增强了可读性。
但是这样做有什么用呢,除了再每次定义一个相同的结构体类型的时候少打struct,还有什么用呢? 接下来我们来了解一下结构的自引用。
我们再以后的学习中会学习到数据结构,其中有一个叫链表的知识点,而一个链表的定义则会使用到结构体自引用,即一个结构体中定义着一个指向自己相同类型的指针。我们来看看具体是什么样的。
typedef struct Node
{
int data;
struct Node* next;
}Node;
这样,使用typedef就会使下一次定义一个这样的链表变得十分简洁,就像定义整形变量i(int i;)一样清晰。
Node n2;
但是如果我们省略结构体标签,再定义next指针:
typedef struct
{
int data;
Node* next;
}Node;
这样编译器就会报错,因为我在定义一个结构体的时候将它重命名为Node,但是我在定义Node时候就要使用Node,这样很明显是错误的。所以说,我们的结构体标签不能省略,而且在其中嵌套定义一个此类型的结构体指针的时候,要写全它的类型,要不然则会引起出错。
如果不使用typedef重定义struct Node 则是下面这种书写方式:
struct Node
{
int data;
struct Node* next;
};
那这样的话,我们在定义一个新的链表n2的时候则要这样定义
struct Node s1;
很明显,我们可以使用typedef将我们的代码变得更加简洁、清晰,尤其是在以后的使用中,代码量的繁杂,typedef的作用就会很明显的体现出来。
结构体变量的定义上面其实已经提到过了。例如:
struct Point
{
int x;
int y;
} p1; //声明类型的同时定义变量p1;
struct Point p2;定义结构体变量p2;
接下来我们看看怎么样对一个结构体变量初始化并赋值:
struct Point p3 ={10,20};
同时我们可以做到结构体的嵌套初始化,即在定义一个结构体类型的时候就对结构体变量n1进行了初始化。
struct Node
{
int data;
struct point p;
struct Node* next;
}n1={10,{4,5},NULL}; //结构体的嵌套初始化。
在我们了解了结构体的初始化和定义后,接下来我们来了解一下如何计算结构体的大小,那结构体的大小是有一定规则的,而这个规则就是我们接下来要了解的——结构体内存对齐。
首先,我们对比一下下面两个例子 :
这里我们会发现,s1和s2中的成员是相同的,但是我们在打印出它们所占的大小的时候发现它们所占的内存大小不同,这是为什么呢? 这即是结构体存储的特殊之处——结构体内存对齐。
1.第一个成员在与结构体变量偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍数。
对齐数=编译器默认对齐数 与 该成员大小的 较小值。
-VS中默认的值是8
3.结构体总大小为最大对齐数的整数倍(每个成员变量都有一个对齐数)的整数倍。
4.如果结构体中嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体)的整数倍。
接下来我举几个例子,来计算一下这几个结构体的大小。
struct s1
{
char c1;
int i;
char c2;
};
struct s2
{
double d;
char c;
int i;
};
struct s3
{
char a;
struct s2 z;
double d;
};
我们如果想在编译器下清晰的看到结构体给成员的偏移量,我们可以使用offsetof,offsetof可用于计算结构体成员相对于起始位置的偏移量(头文件:
- 结构体名
- 结构体成员名
使用方法如下:
1.平台原因(移植原因):
不是所有的硬件平台都能访问任意地址的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出异常。
2.性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说:结构体内存对齐是一种拿空间换时间的做法。
所以我们再创建结构体的时候,我们既要满足对齐,又要节省空间,那我们可以这样做:
让占用小的成员尽量集中一起。
修改结构体的内存大小,我们可以使用#pragma这个预处理指令来使默认对其数发生改变。
#pragma pack这个指令我们要将一个结构体括起来,表示这个结构体的默认对齐数被修改,在这个结构体定义完成之后我们要取消设置的默认对齐数,还原为默认,接下来我来举例使用一下:
这里s1、s2成员相同,而且成员顺序也相同,两个结构体占的内存空间不同,因为我们将默认对齐数改为了2。
在我们以后使用结构体的过程中,但遇到结构体在对齐方式不合适的时候,我们可以自己使用#pragma pack()来人为修改默认对齐数。
本篇博客就到此介绍完了,下篇博客会介绍完结构体位段的知识,并且还会有一些其他的自定义结构体类型。如果看到这里感觉对你有帮助的话,在文章的下面点一个赞呗,蟹蟹~~~~