本文简介:本文主要讲解了什么是结构体,结构体类型的声明与结构体的自引用,结构体变量的定义和初始化,以及结构体内存对齐和结构体传参。
众所周知C语言中本身就存在许多内置类型(如 int,short,char,flot,double……等),但这些类型都是孤立的只能单独使用。因此,在需要描述一些复杂的对象时,譬如:人(姓名,性别,年龄等)、书(书名,书号,售价)这些单一的类型就显得难以胜任。且根据使用场景的不同,在描述一个相同的复杂变量时的侧重点也同样不同(比如相同的一个人在学校可能侧重考试成绩,工作后则看重工作经历)。倘若为这些不同场景的需求设置对应的内置类型可以说怎么样都设置不完。因此,C语言就非常聪明的给出了一种可以由使用者根据自己的需求来构建的类型:自定义类型。其中比较常用的有:结构体、枚举、联合体。
目录
1 什么是结构与结构体
结构是一些元素的集合,这些元素被称为成员变量,且每个成员可以是不同类型的变量。结构体是属于用户自定义的数据类型,能够存储不同类型的数据。
2 结构体的声明
2.1 结构的声明
下行为结构的基础代码
struct tag
{
member-list;
}variable-list;
1 struct 为结构体关键字,想要创建一个结构体就必须用到它,不可缺少,不可修改。
2 tag 为结构体标签,这个名称是根据用户的实际需求可自行更改(比如描述对象为学生是可改为stu)。
3 struct tag 为结构体类型,在声明完后就可创建结构体变量。
4 member-list 为成员列表,可以有一个或者多个,且可以是不同的类型。
5 variable-list 为变量列表,该项可写可不写,也可以多写,但多写的结构体变量需要用“ , ”区隔开来。没写则代表创建了一个结构体类型。
注意:结构体类型声明完毕后,无论是否创建变量其末尾必须添加 “ ; ” 此为语法要求。
现在,我们来描述一本书。
struct Book
{
char book_name[100];//书名
char anthor[100]; //作者
int price; //售价
char id[20]; //书号
}book3,book4; //book3,book4都是struct Book类型的结构体变量,且均为全局变量
//book3,book4可填可不填,但是" ; "一定要带,这是语法要求
int main()
{
//创建与初始化结构体变量
struct Book book1 = { "C语言","张三",50, 12345433 }; //局部变量
struct Book book2 = { "怪诞行为学","丹·艾瑞里",50,543241223 };//局部变量
return 0;
}
从这里我们可以看出,只要做好声明 struct Book 其实和 int char 这些内置的变量类型在使用时的逻辑基本上是一样的,我们只是对其使用的场景进行了特殊化处理。
---------------------------------------------------------------------------------------------------------------------------------
2.2 特殊声明
区别于一般的结构体声明,我们在创建结构体是可以不完全声明的,也就是只填写结构体关键字不填写结构体标签,这类不完全声明的结构体类型也叫匿名结构体类型。
但是,匿名结构体类型只能在“ { } ”的后面和“ ; ”的前面创建结构体变量,一旦脱离这个范围是不能创建变量的。
struct //结构体标签已省略
{
char book_name[100];
char anthor[100];
int price;
char id[20];
}book3,book4; //只能在“ {} ”后和“ ; ”前创建结构体变量
int main()
{
struct book1; //这类做法无效,编译器会 book1 是一个结构体标签,而不是结构体变量
return 0;
}
---------------------------------------------------------------------------------------------------------------------------------
另外,哪怕两个匿名结构体的结构体成员完全一样,编译器也认为这是两个完全不同的结构体类型。下面开始验证,我们先创建一个匿名结构体,和一个匿名结构体指针。
struct
{
char book_name[100];
char anthor[100];
int price;
char id[20];
}book3;
struct
{
char book_name[100];
char anthor[100];
int price;
char id[20];
}*pbook;
int main()
{
pbook = &book3;
return 0;
}
倘若pbook= &book3,pbook如果被赋值成功我们则可以理解为:从编译器的角度出发,即使这两个结构体虽然没有名字,但成员类型一模一样的情况下这两个结构体是一样。当我们将这段代码放在编译器上编译时弹出了这样的一个窗口:
由此我们可以看出,等号两边的类型是不相同的,因此是非法的。
所以哪怕两个匿名结构体的结构体成员完全一样,编译器也认为这是两个完全不同的结构体类型。
3 结构体的自引用
数据结构就是数据在内存中存储的结构。其方式多种多样,我们今天拿顺序表与链表来比较。顺序表顾名思义,就是数据在内存中连续存放。与顺序表相反,链表则是乱序存放。我们以存储数字1 2 3 4 5 为例
这样问题就产生了,顺序表可以按照起始位置和偏移量就能找到想要的数据,而链表则不能以这种方式,因为这种方式只适用于连续存放的数据。因此,我们需要将链表中每一个数据作为一个节点,节点既要能够保存数据也要有能够找到下一个节点能力。又因为节点的功能并不单一,所以我们可以认为每个节点就是一个复杂的对象,这样我们就能够把它设计为结构体。现在,我们先声明一个结构体。
struct Node
{
int data;
struct Node next;
};
上述代码表示:一个结构 struct Node 中存储了一个数据 date 和一个类型为结构本身的成员。这时,就出现了一个类似于套娃的bug,结构中包含结构本身,本身又包含本身...... 这样下去无论多大的内存都会被挤爆,所以当我们当开始编译的时候编译器压根不会通过。
---------------------------------------------------------------------------------------------------------------------------------
这时,我们可能会开始质疑这个思路的有效性。其实不然,上述方法其实是我们思维的一个误区,链表的思路其实就是我们只需关注数据本身和如何找到下一个数据,至于其他的都无所谓。
所以,我们可以在前面的思路之上,让结构中不包含结构成员,而是包含下一个节点的地址,也就是指针,又因为下一个数据也是结构体,所以我们的思路就可以转变为:在结构中包含一个指向该结构类型的指针。因此一个节点就可以分为两块区域,一块叫数据域,一块叫指针域。
struct Node
{
int data; //数据
struct Node* next; //指向下一个节点的指针
};
像这样,自己能够找到一个和自己类型相同的结构体变量,就叫做结构体的自引用。
4 结构体的内存对齐
请先观察下面两组代码
struct S1
{
char c1;
char c2;
char c3;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
char c1;
int i;
char c3;
};
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
printf("%d\n", sizeof(struct S3));
return 0;
}
根据之前所学的知识,如果结构体的大小只与存放的数据的类型有关的话,结构体的大小应该是每个成员变量大小的和,那么S1,S2,S3的大小分别是3 6 6。然而,当我们执行时却发现了不一样的地方
通过S1和S2的对比我们能够明确的发现结构体的大小和成员变量的大小有关。我们再来对比S2和S3,S2和S3的成员变量完全一样,但仅仅只是因为排布的顺序的差别导致两个结构体的大小差了4个字节。这便涉及到结构体在内存中的存放方式,也就时结构体内存对齐了。
---------------------------------------------------------------------------------------------------------------------------------
4.1 偏移量
想要了解内存对齐,我们得先了解偏移量
--------------------------------------------------------------------------------------------------------------------------------
4.2 内存对齐规则
1. 第一个成员在与结构体变量偏移量为0的地址处。即结构体的起始地址就是首个成员变量的地址
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 (VS中默认的值为8)
图中 i 的对齐数为4,默认对齐数为8,因为对齐数小于默认对齐数,所以对齐数选 4 。
3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
上图中 struct SS 的成员变量中有 int(4字节)和 char(1字节) 类型,因此最大对齐数是4,所以结构体的大小只能为 4 的倍数,又因为 c3 的偏移量已经为8了,所以其大小只能为12字节。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
如上图所示,我们能够根据前三条规则求出 struct S1 的大小为 16 字节,且 struct S1 所有成员中最大的对齐数为 8 ,同时 struct S2 中最大的,所以更具第四条规则 struct S2的大小只能为 8 的倍数。因此,struct S2的大小为 32 字节。
4.3 为什么存在内存对齐?
大部分的参考资料都是如是说的:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。
2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于。为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总而言之,结构体的内存对齐是拿空间来换取时间的做法。类似与生活中花10元买 4.3元的菜为了省时间只让收营员找5元给你剩下0.7元不要了。
因此,我们在设计结构体的时候,既要满足对齐,又要做到节省空间,其方法是让占用空间小的成员尽量集中在一起。
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
4.4 修改默认对齐数
#pragma 这个是个预处理指令,可以改变我们的默认对齐数。
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
结果为:
5 结构体传参
请看下行代码
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;
}
函数 print1 与 print2 分别为结构体传参和结构体地址传参。 我们都知道:当函数调用的时候,实参传给形参,形参其实是实参的一份临时拷贝。当我们传递的对象是一个结构体时,倘若这个结构体过大时,势必会影响计算机性能的发挥。
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
由此,我们可以得出结论:
在结构体传参时,要传结构体地址。
6 总结
对于结构体,我们其实完全可以把它想象成一个自己定义的一个内置类型(如int char等),只是他在使用前要先根据自己的需求声明一下。其使用的逻辑和内置类型使用的逻辑并无太大的区别(如赋值、传参等)。但是它却能够实现一些内置类型所不能或者说难以实现的功能,如自引用等。在了解其背后的原理以及内存对齐的规则和原因后,其实真正厉害还得是这群开发了C语言的人。
尾巴
好长啊,真的好长啊,这是本人第一次写这么长的文章。我平时是一个比较内向和一个比较笨的人,就是那种在生活中和陌生人说多几句话都会小心翼翼的结巴的人。但当我真的把结构体详解这篇文章写到这里时候还是免不了流露出激动想要分享的心情,嘴角会时不时不自觉的往上弯。
这篇文章虽然是由我本人写完的,但我确确实实在网上查找了许多资料,也看了许多同类型的文章,虽谈不上抄袭,却也还是有借鉴的影子。但哪怕这样,在我又通读一遍我写我文章时我自己都能发现一些错误,如行文不流畅,概念解释存在错误等问题。可想而知在我不知道的地方应该还存在其他的错误。
能完成这样一篇文章,对我来说是真的好不容易值得我好好庆祝。但也正因我觉得不容易,所以在发布时才觉得诚惶诚恐,担心我匮乏的文笔和有限的知识,难以正真完成 “详解” 两字的任务。因此,本文是分享本人对于结构体的理解,对于其中存在的错误也恳请各位大佬多多指正。