前言
结构体和联合体是C语言数据结构中的重要组成部分。结构体能够将不同的类型变量固定到一起,形成一个整体,其中的变量分别占据了不同的内存空间。联合体则是拿出一块空间,这个空间可以让多种不同的类型数据共用,在使用的时候选择一种,这样能够节省空间。除此之外结构体中还包括位段的,位段的特殊性也会在本篇博客中提到。
一. 结构体
1. 结构体的结构
结构体能够将不同的类型变量固定到一起,形成一个整体,其中的变量分别占据了不同的内存空间。如果想要表示一本书,那么就需要它的:名字、价格等信息。那么就可以用到结构体。
struct Book
{
char name[20];//书名
int price;//价格
};
除了结构体之外,结构体也能是结构体指针,例如在上面的基础上增加代码:
struct Book book;
struct Book* pbook = &book;
这种表达方式是可以的。
如果想要简化结构体的名称可以使用“typedef”对其重命名。例如:
typedef struct Book
{
char name[20];
int price;
}Book;
或者:
struct Book
{
char name[20];
int price;
};
typedef struct Book Book;
2. 匿名结构体
对于只使用一次的结构体可以匿名表示,例如:
struct
{
char name[20];
int price;
};
如果想要使用匿名结构体,需要再定义的时候就规定结构体的名称。
struct
{
char name[20];
int price;
}book;
对于匿名结构体来说,出了定义之后就无法再使用它定义名称了,计算机也不会记得这种类型。如果分别定义指针和变量,即使内容一致也是无法赋值的,例如:
struct
{
char name[20];
int price;
}book;
struct
{
char name[20];
int price;
}*pb;
pb = &book;
这种写法编译器是不支持的,出了匿名结构体之后这种类型对于计算机来说就销毁了。所以“*pb”不能作为“book”的指针。
3. 结构体的应用
结构体应用于顺序表、链表、栈、队列之中。
对于链表来说,需要结构体找到下一个结构体,那么要怎么找呢?
//方法一
struct work
{
int data;
struct work next;
};
//方法二
struct work
{
int data;
struct work* next;
};
对于结构体来说是支持嵌套结构体的,但是由于第一种方法是嵌套自己,而且自己里面还有一个数据,这样就会一直嵌套下去,计算机会不知道它的大小。所以第一种方法会出错。正确的方法是使用指针,指针的大小是确定的4或者8个字节的大小。
对于指针指定下一个结构体的位置,如果需要重命名结构体,还有需要注意的地方。例如:
typedef struct work
{
int data;
struct work* next;
}work;
这是正确的写法,那么错误的写法呢?
typedef struct work
{
int data;
work* next;
}work;
结构体还没有重命名就直接使用重命名的名字,编译器不认识,请注意结构体的重定义在结构体建立之后。
4. 结构体大小规则
(1) 规则
结构体所占内存的大小规则如下:
1)结构体的第一个成员对齐到结构体变量的起始位置偏移量为0的地址。
2)其他成员要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个队期数与该成员变量大小的较小值。
·VS中默认为8
·Linus中gcc没有默认对齐数,大小直接为该成员的大小。
3)结构体总大小为最大对齐数(成员中最大的对齐数)的整数倍。
4)如果潜逃了结构体的情况,嵌套的结构体对齐到自己的成员中最大对齐数的整数倍出,结构体整体大小就是所有最大对齐数(包含嵌套结构体中成员的对齐数)的整数倍。
5)数组的大小在结构体中,按照个数分别计算。
(2) 举例
#include<stdio.h>
struct S1
{
char a;
int num;
char b;
}s1;
struct S2
{
char a;
char b;
int num;
}s2;
int main()
{
printf("sizeof(s1)=%d\n", sizeof(s1));
printf("sizeof(s2)=%d\n", sizeof(s2));
return 0;
}
在vscode上运行代码结果为:
那么为什么如此呢?这就需要用到之前的规则了。对于结构体s1来说
struct S1
{
char a;
//对齐到0处
int num;
//默认对齐数为8,大小为4,所以对齐数为4
//所以会放到能被4整除的位置,所以放到4
//而前面空的3个字节被浪费
char b;
//默认对齐数为8,大小为1,所以对齐数为1
}s1;
//成员最大为4,总计大小为9,最终大小需要被4整除,所以为12
而对于结构体s2来说:
struct S2
{
char a;
//对齐到0处
char b;
//默认对齐数为8,大小为1,所以对齐数为1
//所以会放到能被1整除的位置,所以放到1
int num;
//默认对齐数为8,大小为4,所以对齐数为4
//所以会放到能被4整除的位置,所以放到4
//而前面空的2个字节被浪费
}s2;
//成员最大为4,总计大小为8,最终大小需要被4整除,所以为8
通过这些例子,想必前3条规则都清楚了。那么再举一例了解一下后两条规则吧。
struct S3
{
char a[5];
//对齐到0处
struct S2 s;
//默认对齐数为8,成员大小有1有4,所以对齐数为4
//所以会放到能被4整除的位置,所以放到8
//而前面空的3个字节被浪费
char b[5];
//默认对齐数为8,大小为1,所以对齐数为1
//放5个,总大小为21
}s3;
//成员最大为4,总计大小为21,最终大小需要被4整除,所以为24
int main()
{
printf("sizeof(s3)=%d\n", sizeof(s3));
return 0;
}
包含头文件运行后结果为:
这样所有规则都得到了验证,读者也可以自行编辑,体会。
(3) 对齐的原因
1)平台原因
在有的硬件平台上不能直接访问内存上的所有空间。
2)性能原因
对齐的内存仅访问一次就能访问到,未对齐则需要两次。实际的意义就是牺牲内存空间换取时间上的运行速度。
5. 默认对齐数的修改
使用#pragma pack()可以修改默认对齐数。如:
#pragma pack(1)
这样就能将默认对齐数修改为1。
如果需要还原回默认,那么不填数字就行。
#pragma pack()
二. 位段
1. 位段的介绍
位段是存在于结构体中的概念,它可以规定结构体中成员所占位置的大小。例如:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
后面的数字表示占多少bit位。比如“a”占3个bit位,“b”占4个bit位,“c”占5个bit位。
2. 位段的大小
那么位段的大小又如何呢?
在不同的编译器里,有不同的规定,那么会存在以下问题:
1)int段有没有符号位。
2)最大位的数目不确定(16位机器上int大小为2字节)。
3)在内存中从左向右分配空间,还是从右向左分配空间,标准未定义。
4)当剩余空间无法分配给下一个成员的时候,是浪费还是继续用?
对于VS来说的话,简单的举个例子。
struct S
{
char a : 3;
char b : 4;
//a和b分别占据一个字节的3个bit位和4个bit位
char c : 5;
//第一个字节不足了,再分配一个字节,c占5个bit位
char d : 4;
//字节又不足4个bit位,再分配一个字节,d占4个bit位
};
其他规则和结构体大小规则一致,所以大小为3个字节。
int main()
{
printf("sizeof(struct S)=%d\n", sizeof(struct S));
return 0;
}
运行结果:
需要注意的是自己分配的bit位不能超过类型字节的大小,这里用的是char所以不能大于8。
3. 位段的存在意义
位段的存在是为了节省空间,但是位段不跨平台,不同平台需要不同代码。而且因为地址是按照字节计算位置的,所以位段不能取地址。
三. 联合体
1. 联合体介绍
联合体也叫共用体,意思是所有成员都共用一块内存大小。也就是说这块内存会被选择用来放什么类型。和结构体一样,联合体也可以匿名。
union Un
{
char c;
int i;
};
2. 联合体的大小
联合体的大小规则:
1)大小为最大成员大小的整数倍。
2)至少为最大内存大小。
举例:
#include <stdio.h>
union Un
{
char c;
int i;
};
int main()
{
printf("sizeof(union Un) = %d\n", sizeof(union Un));
return 0;
}
对于以上结构体,最大的大小为4,最大成员为4个字节,所以最后大小为4。
#include <stdio.h>
union Un
{
char c[9];
int i;
};
int main()
{
printf("sizeof(union Un) = %d\n", sizeof(union Un));
return 0;
}
对于以上结构体,最大的大小为9,最大成员为4个字节,所以最后大小为12。
3. 联合体的应用
比如在需要描述不同类型商品的时候,比如描述衣服、书本、食品等内容细节,有需要描述有共同特性的结构,比如商品。就需要用到联合体。
如果只用结构体,对于衣服、书本、食品,都需要定义到不同结构体。
//书
struct Book
{
char member_number[20];
int price;
char book_name[20];
char author[20];
char brief_introduction[20];
};
//衣服
struct Clothing
{
char member_number[20];
int price;
char colour[20];
int size;
char style[20];
};
//食品
struct Tool
{
char member_number[20];
int price;
char type[20];
char name[20];
double weight;
};
如果加入联合体就能节省空间,把它们都视为商品。
struct goods
{
char member_number[20];
int price;
union work
{
struct
{
char book_name[20];
char author[20];
char brief_introduction[20];
}book;
struct
{
char colour[20];
int size;
char style[20];
}clothing;
struct
{
char type[20];
char name[20];
double weight;
}tool;
};
};
这样既省去了重复定义成员编号和价格,又能节省空间。方便记忆。这个例子也证明了结构体和联合体可以相互嵌套,能够使使用起来更加方便。
另一方面,之前使用取地址的方式判断了电脑端是小端存储数据还是大端存储数据,现在可以直接用联合体判断了。
#include <stdio.h>
int Cheak_union()
{
union Un
{
int a;
char c;
}u;
u.a = 1;
return u.c;
}
int main()
{
int tmp = Cheak_union();
if(tmp == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
如果是小端返回1,如果是大端返回0。这样就能用联合体判断大小端了。
作者小结
结合体和联合体的内容还是比较多,比较复杂的,而且结合体在不同的编译器里计算大小的方式是不一样的,需要注意。位段的限制条件就更多了,不同的编译器请一定要注意。
从数据的储存形式,到结构体、联合体。数据结构的基础就讲的差不多了,之后也会继续更多的更新行的内容,希望得到大家的支持。