一.结构体
1.1 结构体的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。结构体是为了描述一些糅杂多种类型变量的复杂对象而产生的。
1.2 结构的声明
例如描述一个学生:
struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };
要描述一个学生有很多需要进行描述,所以这是一个结构体。
1.3 特殊声明
看以下代码:
struct { int a; char b; float c; }x; struct { int a; char b; float c; }a[20],*p; p = &x;//这样的代码合法吗?
虽然说两个结构体的成员和位置一模一样,但是p=&x这样的操作是不合法的。
1.4 结构体的自引用
在一个结构体包含一个类型为结构本身的成员是否可以呢?
struct Node { int data; struct Node next; }; //这样的代码合理吗?
这样的结构体是否合理呢?如果你认为合理,那么可以计算一下,sizeof(struct Node)的大小,你会发现,这是一个无限套娃,next里面还有next,这样结构体的空间就是不可知的,所以这样定义结构体是不合法的如果想自引用,那么应该保存它的地址,像这样:
struct Node { int data; struct Node* next; }; 1.5 结构体变量的定义和初始化 struct Stu { char name[15];//名字 int age;//年龄 }p1;//p1 是变量,类型是struct Point struct Stu s = { "zhangsan",20 };//初始化 struct Node { int data; struct Stu p1; struct Node* next; }n1 = { 10,{"zhangsan",20},NULL};//结构体嵌套初始化
1.6 结构体内存对齐(计算结构体的大小)
在这里我们可能有点疑惑,如果单纯按照字节来计算的话两个结构体应该都是6,但是它们的大小不仅不是6,而且它们虽然成员相同,但是大小也不一样。从这里我们可以推测结构体有其独特的内存管理,以及它的大小和成员的位置也有关系。
结构体内存对齐规则
1.第一个成员在与结构体变量偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
3.对齐数==编译器默认的一个对齐数(看编译器)与该成员大小的较小值--vs默认量是8
4.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍
如果按照这样一个对齐规则就可以明白上述编译器为什么会出现那样的结果。
首先看S1:
根据规则我们可以推出它的大小符合编译器的结果12.
同样我们可以分析一下S2:
通过这样的分析,我们就能发现它确实答案是如此,但是具体怎么验证呢?这我们可以借助一个宏offsetof(type,member),这个宏是计算偏移量的,当然我们也要引它的头文件
#include<stddef.h>.
验证:
结果恰如我们推测的那般。
嵌套结构体计算空间大小:
这个题目如果把S3的大小作为对齐数去计算的话,那么很明显S4的大小应该是4,而不是3.所以我们在这里要看一下对齐规则的最后一条
5.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍
所以这里遇到结构体的对齐数应该看这个结构体里面最大对齐数,而不是把结构体的大小当作对齐数。
1.7 结构体对齐数的设置
在vs编译器中默认对齐数是8,而在gcc中是没有默认对齐数的,那么我们有时可能会觉得这个对齐数有点大,浪费了不少空间,那么是否可以更改呢?
答案是肯定的。只不过我们在这里要用到一个预处理指令#pragma.
在修改之前,S的大小应该是12,而在修改后由于对齐规则对齐数==编译器默认的一个对齐数(看编译器)与该成员大小的较小值--vs默认量是8。而发生了改变。那么在这里我们需要注意一点
#pragma pack(num)
之后,如果仅针对这一个结构体记得在该结构体后面再#pragma pack()
复原为默认对齐数。这样不影响后面的结构体对齐。
如果仅仅从节省空间的角度来说,全部修改为1无疑是最节省空间的,但是系统并没有这么做,这是为什么呢?
1.8 内存对齐的优点
1.平台原因(移植原因)--不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能再某些地址处取某些特定类型的数据,比如0 4 8等偏移量处,否则就会抛出硬件异常。
2.性能原因---数据结构(尤其是栈)应该可能地在自然边界上对齐--原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次内存访问。所以这里是拿空间换时间。
1.9 结构体传参
在指针我们知道传参分为传值和传址,那么我们知道形参是要压栈的,对于较小的空间影响不大,但是一个结构体可能很大,这时对栈的需求很大,就可能会导致计算机运行收到影响,所以仅凭分析我们就知道,结构体适合传址。
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; }
如果,仅仅是打印如上而不做修改,可以使用const修饰防止被修改,如果是更改,那么只能使用传址。
ps:我们在设计和计算结构体的时候需要注意什么?
笔者认为需要注意这些:
1、首先关注是否是嵌套结构体类型,如果是,那么需要注意最大对齐数的选取是内嵌的结构体的最大对齐数和默认对齐数的较小值。
2、注意在设计结构体的时候,我们要让占用空间小的成员尽量集中在一起,以此来尽可能地节省空间。
3、如果对于它的内存不清晰,可以画图分析一下。