结构体、枚举、联合体的学习总结
结构体
枚举
联合体
结构体
结构体介绍
结构体自引用
结构体定义和初始化
结构体内存对齐
位段
结构体介绍
结构体声明:
struct student {
//student为结构体名称,可省略,省略后为匿名结构体,只能当次定义当次使用,不可二次使用
char name;
char age;
}A;//A实际上是定义了一个全局变量,可省略
注意:
(1)一个程序中,结构体的名称不能重复;
(2)一个程序中,有几个struct就有几个结构体类型,与结构体内部元素无关;
(3)结构体内部至少包含一个元素(c语言中结构体成员列表不能为空);
(4)结构体是相同/不同类型的元素集合;
(5)结构体内部中写在前面的元素地址小。
#include<stdio.h>
#include<windows.h>
struct student {
char name;
char age;
}A;
int main()
{
printf("%p\n", &A);//结构体地址与结构体内第一个成员变量的地址在数值上相等
printf("%p\n", &A.name);
printf("%p\n", &A.age);
system("pause");
return 0;
}
结构体自引用
#include<stdio.h>
#include<windows.h>
struct student {
char name;
char age;
struct student *next;
//结构体可嵌套,但需开辟空间存储。若直接引用结构体本身,不确定开辟多大空间;若定义指针直接开辟4个字节。故结构体自引用时定义结构体指针。
int num;
}A;
int main()
{
printf("%p\n", &A);
printf("%p\n", &A.name);
printf("%p\n", &A.age);
printf("%p\n", &A.next);
printf("%p\n", &A.num);
printf("%d\n", sizeof(struct student));//结构体大小涉及到内存对齐,下文会详细讲解
printf("%d\n", sizeof(A.next));
printf("%d\n", sizeof(A.num));
system("pause");
return 0;
}
32位平台测试:
64位平台测试:
类型重命名:
typedef struct {
char name;
char age;
struct student *next;
int num;
}A;//结构体类型重命名---结构体名称为A
结构体定义和初始化:
struct student {
char name;
char age;
}A;//声明类型的同时定义变量A
struct App s;//定义结构变量s
struct student={"zhangsan",18};//结构体初始化
结构体初始化方式与数组完全一致。
(1)结构体可整体初始化但不可整体赋值;
(2)结构体初始化用{};
(3)结构体内可嵌套结构体。
结构体访问方式:
(1)指针指向结构体内部变量名。
(2)结构体名称.内部变量名。(.点操作符)
结构体内存对齐(计算结构体的大小):
首先我们先思考第一个问题,为什么结构体要内存对齐?数组需要吗?
答案是数组不需要内存对齐,因为数组是相同元素的集合,只要第一个数组元素对齐,那么剩余的元素均会对齐。又因为结构体是相同或不同元素的集合,若不内存对齐,就存在多次内存访问问题,程序效率会大大减慢。因为我们现在在主观意念上认识的是指针可以访问内存上任意一块已定义的地址,实则不然,内存访问类似于中国管辖领土对其划分为各个省市等区域,内存访问数据也是需要从特定的地方访问特定类型的数据,再访问指定数据。
总之,结构体为什么存在内存对齐可以总结为两句话:
(1)平台问题(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐,原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问只需要一次访问。
#include<stdio.h>
#include<windows.h>
int main()
{
struct student {
char name;
char age;
int number;//学号
struct student *next;//结构体自引用
};
struct students {//结构体内成员顺序发生变化
char name;
int number;
struct student *next;//32位平台下指针占4个字节,64位平台下指针占8个字节
char age;
};
printf("%d\n", sizeof(struct student));
printf("%d\n", sizeof(struct students));
system("pause");
return 0;
}
32位平台运行结果:
64位平台运行结果:
显然,无论是32位平台还是64位平台在结构体内部成员顺序发生变化后,结构体大小也发生了变化,此现象就是因为存在结构体内存对齐。
内存对齐分析:
如图,若结构体内成员变量直接存储,计算机底端在访问时遵守必须以4的整数倍访问。故如int型number存放,第一次内存访问会读取number两个字节,再进行第二次内存访问读取后两个字节,最后再拼接到一起返回给用户,内存未对齐可以访问但效率较低。若结构体内存对齐时,可一次性内存访问,效率较高 。
故内存对齐是一种舍弃空间换取效率的做法。
故如何内存对齐可总结为四句话:
(1)结构体第一个成员在结构体变量偏移量为0的地方。(即第一个成员默认是对齐的)
(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的对齐数与该成员大小的较小值(两者之间取较小值)。
(VS中默认为8,Linux中默认为4,新版的Linux默认为8)
对齐指的是存放该变量的起始偏移量必须要能整除该变量的对齐数,通常是能整除该对齐数的最小整数。(如上图分析中number为int型,占4个字节,内存中放入两个char型后,number的对齐数为自身大小4(因自身大小小于编译器默认对齐数),最小整数能整除4的数字为4,故内存中需空置2个字节,在4字节处放入number)。
(3)结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。(如上图第三个内存访问分析中,成员放置完毕占13个字节,但因此结构体中最大对齐数为4,故结构体大小要为最接近结构体实际大小的最大对齐数的倍数)
(4)如果出现嵌套结构体现象(非自身引用),嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。(结构体的对齐数为内部最大对齐数)
#include<stdio.h>
#include<windows.h>
int main()
{
struct s1 {
char name;//1
char age;//1+2(空置)
int number;//4
}A;//结构体大小为8,对齐数为4
struct s2 {
char name;//1+3
int number;//4
struct s1 A;//8
char age;//1+3(结构体最大对齐数为4)
}B;//20
printf("%d\n", sizeof(B));
system("pause");
return 0;
}
修改默认对齐数
#pragma pack(8)//设置默认对齐数为8
#pragma pack()//恢复默认对齐数
#include<stdio.h>
#include<windows.h>
#pragma pack(2)//设置默认对齐数为2
struct s1 {
char name;//1
char age;//1+2
int number;//2(对齐数为2)
}A;//6
#pragma pack()//恢复默认对齐数(VS中为8)
struct s2 {
char name;//1
char age;//1+2
int number;//4
}B;//8
int main()
{
printf("%d\n", sizeof(A));
printf("%d\n", sizeof(B));
system("pause");
return 0;
}
结论:结构体在对齐方式不合适的情况下,我们可以自己设定默认对齐数。
结构体传参
#include<stdio.h>
#include<windows.h>
struct s {
int data[1000];
int num;
};
struct s s1 = { {1,2,3,4},1000 };
void print1(struct s s1)
{
printf("%d\n", s1.num);
}
void print2(struct s *ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s1);//结构体传参自身,硬拷贝
print2(&s1);//传地址
system("pause");
return 0;
}
那么我们在日常的结构体传参中应该用哪一种呢?
**建议是传地址。**因为我们都知道临时变量是在栈上开辟空间,如果是硬拷贝,假如结构体中有一个很大很大的数组,那么整个结构体大小就会过大,就容易出现压栈现象,整个程序性能会有所下降。如果传结构体地址,就不会有压栈现象,临时变量形成一个指针,再通过指针访问其结构体内部元素即可。
位段(压缩存储)
位段的声明与结构体类似,有两个不同:
(1)位段的类型必须是unsigned int或int或signed int或char。
(2)位段的成员名后边有一个冒号和一个数字。(数字代表冒号前定义的变量使用几个比特位)
#include<stdio.h>
#include<windows.h>
struct s {
int a : 2;
int b : 5;
int c : 10;
int d : 20;
}A;
int main()
{
struct s A = { 0 };
A.a = 10;
A.b = 20;
A.c = 20;
A.d = 3;
printf("%d\n", sizeof(A));
system("pause");
return 0;
}
位段开辟空间根据位段类型中成员变量。(int 32个比特位 char 8个比特位)
位段的内存分配中原则是成员变量所占比特位相加不超过类型开辟的比特位空间,就放在一起,若放不下就另外开辟空间。故上图中前三个成员变量(a、b、c)所占比特位相加小于32,故它们放在一起,第四个变量与前三个相加大于32,故另开辟空间存放,故该位段类型大小为8。
注意(位段的跨平台问题):
(1)int位段被当成有符号数还是无符号数是不确定的。(位段可能发生整形提升)
(2)位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写27,在16位平台会出现错误)
(3)位段中的成员在内存中从左向右分配还是从右向左分配标准尚未定义。
(4)当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。(大部分平台采用舍弃剩下的位)
总结:跟结构相比,位段可达到相同的结果,优点在于可节省空间,但是有跨平台问题存在。
位段主要在网络协议中使用较多。
枚举(enum)
枚举顾名思义就是一一列举。
枚举是一种类型,几乎可以代替宏(除宏定义的函数之外)。宏不是类型,是预处理。
enum sex {
MALE,
FEMALE,
SECRET
};
enum sex为枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量。这些可能取值都是有值的,默认从0开始,一次定增1,在定义时也可以赋初值。
#include<stdio.h>
#include<windows.h>
enum sex {
MALE=1,
FEMALE,
SECRET
};
enum Color {
RED=100,//可自定义
PINK,
BLUE,
GREEN
};
int main()
{
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
printf("%d\n", RED);
printf("%d\n", PINK);
printf("%d\n", BLUE);
printf("%d\n", GREEN);
system("pause");
return 0;
}
枚举的优点:
(1)枚举是类型,编译时可进行类型检查,更加严谨(相比于宏)。
(2)代码可读性较高。
(3)便于调试。
(4)使用方便,一次可定义多个常量。
总之,无特殊情况,枚举可用宏代替。
联合(共用体)(需内存对齐)
定义:联合是一种特殊的自定义类型,这种类型中也包括一系列的成员,特征是这些成员公用同一块空间(所以叫共用体)
联合体内的所有成员共享地址,并且联合体地址和联合体内所有成员地址在数值上是一样的。(也就是联合体中所有成员变量都是第一个)
联合大小的计算:
(1)联合的大小至少是最大成员的大小。
(2)当最大成员大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍。(同结构体)
#include<stdio.h>
#include<windows.h>
union U1 {
char a[5];//1+3(空置)
int i;//4
};//8
int main()
{
printf("%d\n", sizeof(union U1));
system("pause");
return 0;
}
数组对齐拿一个元素即可,因函数元素类型相同,只要第一个元素对齐其余均对齐。