目录
结构体
结构体类型的声明
结构是一些值的集合,这些值称为成员变量,每个成员可以是不同类型的变量。
struct tag
{
member-list;
}variable-list;
struct - 结构体关键字
tag - 结构体标签名
struct tag - 结构体类型名,同理与int、char
member-list - 成员列表
variable-list - 改结构体定义出来的变量
struct Book
{
int name[20];
char id[20];
}b1,b2;//定义的同时允许创建结构体变量,b1,b2是全局变量
int main()
{
struct Book b3;
//b3就是我们用结构体创建的变量,局部变量
}
结构体的特殊声明
匿名结构体类型
这种声明方式,就只能使用声明时定义出来的变量,之后无法在函数中再定义该类型下的其他变量。
struct
{
int a;
char b;
long c;
}s1,s2;
注意事项
struct
{
int a;
char c;
double b;
}x;
struct
{
int a;
char c;
double b;
}*p;
int main()
{
p = &x; //err, 就算两个匿名成员变量完全相同,其类型也不兼容
return 0;
}
结构体的自引用
结构体中包含同类型结构体的指针(但是不能结构体中包含同类型的结构体)
例如链表结点:
struct Node
{
int data;
struct Node* next;
};
typedef struct Node
{
int data;
struct Node* next;//typedef的规则
}Nd;
struct Node
{
int data;
struct Node next;//err,会发生无限套娃,内存出错,相当于结构体中有子结构体,子结构体也有其子结构体,...
`` };
结构体变量的定义和初始化
struct Point
{
int x;
int y;
}p1 = {10, 20};//可以在创建结构体类型时创建变量并初始化
struct Stu
{
int num;
char ch;
struct Point p;//这里的p就是定义的一个结构体成员名,这个成员的类型是struct Point结构体类型
float d;
};
int main()
{
struct Point p2 = {1, 2};//可以在main函数中创建变量并初始化
struct Stu s1 = { 100, 'w', {2, 5}, 3.14f };
printf("%d %c %d %d %f\n", s1.num, s1.ch, s1.p.x, s1.p.y, s1.d);//打印结构体内容 100 w 2 5 3.140000
struct Stu s2 = {.d = 1.2f, .p.x = 3, .p.y = 5, .num = 200};//C语言也支持乱序、不完全初始化
}
结构体内存对齐
结构体内存对齐实际上讨论的是计算结构体所占内存大小
//引例
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d\n", sizeof(struct S1"));//12
printf("%d\n", sizeof(struct S2"));//8
return 0;
}
所以为什么会发生以上情况?我们如何在声明结构体时,更加节省空间呢?
结构体对齐规则:
- 结构体的第一个成员,对齐到结构体在内存中存放位置的0偏移处
偏移的解释:
struct S1
{
char a;
int b;
char c;
};
//offsetof()是计算偏移量的宏,其头文件:#include<stddef.h>
int main()
{
struct S1 s = { 'x', 10, 'y' };
printf("%zu\n", offsetof(struct S1, a));//0
printf("%zu\n", offsetof(struct S1, b));//4
printf("%zu\n", offsetof(struct S1, c));//8
return 0;
}
2、从第二个成员开始,每个成员都要对齐到某个对齐数的整数倍的地址处
这个对齐数是编译器默认对齐数与该成员变量自身字节大小中的较小值
VS中默认对齐数是8, Linux gcc:没有默认对齐数,那么对齐数就是结构体成员的自身字节大小,并且一个编译器中的默认对齐数是可以修改的,后面会讲到
3、结构体的总大小(看所占空间,不看偏移量),必须是所有成员的对齐数中最大的对齐数的整数倍
4、如果有两个结构体,分别为结构体1,结构体2,如果结构体2是结构体1的成员,在计算结构体1中的结构体2成员的对齐数时,要将结构体2对齐到其自身的成员(结构体2的成员)中最大对齐数的整数倍的地址处(也就是不看结构体2总和所占的内存大小,只看结构体2中成员的最大对齐数)。并且对于结构体1的最大对齐数,也需要与结构体2中的成员的对齐数选取较大值。
分析具体结构体所占大小:
例一:
所以该结构体所占大小为12
例二:
结构体大小为8
例三:
结构体大小为16
例四:
这里S3占16个字节,是在例三中已经计算好的,所以并没有在计算S4所占大小时表示S3中浪费的字节,最终计算得到S4的大小是32
例五:
struct S1
{
char c1[20];//0~19
int c2[5];//20~39
char c3;//40 ,0~40,总共41个字节,结构体所占大小需要是最大对齐数的整数倍,所以需要浪费掉41~43, 即总共44个字节
};
printf("%d", (int)sizeof(struct S1));//44
//遇到结构体数组时看数组每个元素的类型去判断对齐数
结构体内存对齐的原因
1、平台原因(移植原因):
不是所有硬件平台都能访问任意地址上的任意数据;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
2、性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要访问一次,也就是说结构体的内存对齐是用空间换取时间
下例:
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S2更好,成员变量完全相同,但使用的空间更少,所以以后在声明结构体时,为了更好的节省空间,就需要注意结构体对齐问题。
修改默认对齐数
#pragma pack(N) N就是我们想要设置为的默认对齐数,在声明完成后,需要用#pragma pack()来取消我们自行设置的默认对齐数,还原为编译器默认大小。
#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));//12
printf("%d\n", sizeof(struct S2));//6
return 0;
}
结构体传参
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 函数哪个好些?
答案是:print2传址更好
原因:函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下。
结构体实现位段(位段的填充&可移植性)
定义:
位段的声明和结构是类似的,位段的成员必须是int、unsigned int、signed、int、char,每个成员后面都会跟着一个冒号和一个数字 并且一般统一用同一种类型,(char类型的话只适用于部分平台)
举例:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
//a、b、c、d是位段的成员,冒号后面的是成员对应使用的比特位
int main()
{
printf("%d\n", sizeof(struct A));//8个字节
return 0;
}
根据上例进行解释,"位段"中的"位"是二进制位,2个二进制位,5个二进制位,10个二进制位,30个二进制位,位段的主要目的是为了节省空间
当我们知道结构体中每个变量会占用多少个空间时,就使用位段的方式,明确告诉编译器,a变量开辟2个比特位,b开辟5个比特位,…,上例中都是int类型,所以冒号后面的数字需要≤32个比特位(int占4个字节,也就是32比特位,当然需要根据不同机器int的大小,如果是16位机器,那么就是≤16个比特位)
位段内存分配
以int的形式开辟32个比特位,_a占了2个比特位,然后紧挨着在这32个比特位中让_b占5个比特位,_c占10个比特位,然后发现剩下15个比特位,_d占不下,所以跳到下一个int大小中,使用30个比特位
ps:C语言标准没有明确规定第一个32个比特位中剩下的15个比特位是否会使用,完全取决于编译器。
在内存中,这些位是从int中的高地址开始使用,还是低地址开始使用,这里是看不出来的
检测VS2019:
检测一:
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };
s.a = 10;//位段赋值的时候也可能发生截断
s.b = 12;
s.c = 3;
s.d = 4;
return 0;
}
检测二:
因此位段在VS中是以成员的类型为分块,从高地址向低地址使用,该块使用完或者不足时,开辟下一块,再进行使用
PS:注意大小端问题,VS中是小端,小端是低地址存数据的低位,高地址存数据的高位
位段的问题总结:
位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
int 位段被当成有符号数还是无符号数是不确定的。
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,若是冒号后是27在16位机器会出问题。
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
所以对于跨平台编程,会因为这些不确定的原因造成错误
段位的实际运用
上图是我们在进行网络传输数据时需要的所有信息,我们会发现,像版本号,服务类型,协议这类信息的位长度都是确定的,因此,在海量数据的网络传输中,我们就需要使用位段来节省空间,以此提高效率。
枚举
枚举类型的定义
和结构体相同,也是一种自定义类型
枚举顾名思义就是一一列举。
把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
enum Day//星期
{
Mon, //注意枚举类型中是以,间隔开的
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
enum Color//颜色
{
RED,
GREEN,
BLUE
};
以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。 {}; 中的内容是枚举类型的所有可能取值,也叫枚举常量。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值,当枚举类型完成定义赋初值后,这些常量的值都不能再改变(相当于一次性define了一堆值)
enum Color
{
RED,
GREEN,
BLUE
};
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
如果只赋了其中一个值,那么该枚举常量后面的常量,都依次递增1
再例如很多项目中的switch中的不同的case
enum Cal
{
ADD = 1,
SUB,
MUL,
DIV,
EXIT = 0
};
int main()
{
int input = 0;
scanf("%d", &input);
switch (input)
{
case ADD:
add();
break;
case SUB:
sub();
break;
case MUL:
mul();
break;
case DIV:
div();
case EXIT:
break;
default:
}
return 0;
}
ps:通常不会让枚举类型参与运算
枚举的优点
增加代码的可读性和可维护性
和#define定义的标识符比较,枚举有类型检查,更加严谨。
防止了命名污染(封装)
便于调试
使用方便,一次可以定义多个常量
define出来的常量没有类型
#define MAX 100
enum SEX
{
MALE,
FEMALE,
SECRET
};
int mian()
{
int m = MAX;
enum SEX s1 = MALE;
return 0;
}
//对于m和s1,在调试、运行后,define MAX就已经不存在了,MAX会被替换成100,而s1还是MALE,因此方便调试
枚举的使用
类比于unsigned int类型的数据可以赋值0~2^32 - 1这些整数,而枚举类型的所有赋值情况,就是大括号中包含的所有常数,这些常数可以用变量使用,也可以直接使用(当成define来用)
RED GREEN本身都是int类型,所以在打印clr内的值时,用%d打印
typedef enum Sex
{
MALE,
FEMALE=5,
SECRET
}Sex;
//枚举类型也可也类型重定义
联合(共用体)
联合类型的定义
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
//联合类型的声明
union Un
{
char c;
int i;
};
int main()
{
//联合变量的定义
union Un un1;
printf("%d\n", sizeof(un1));//4
printf("%p\n", &un1);//0x006FFCC4
//联合也可以用 . 和 -> 取到其成员变量
printf("%p\n", &(un1.c));//0x006FFCC4
printf("%p\n", &(un1.i));//0x006FFCC4
return 0;
}
第一个字节被i和c共用
用i的时候不能用c,用c的时候不用i
也就是公用同一块空间
联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
union Un
{
int i;
char c;
};
int main()
{
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));//是一样的,都是同一个随机值
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);//11223355,也就是说对un.c的赋值改变了un.i的值
return 0;
}
联合大小的计算
联合的大小至少是最大成员的大小,数组类型按照整个数组的大小计算
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍
union UN
{
char c[5];//5
int n;//4
};
int main()
{
printf("%d\n", sizeof(union UN));//8
return 0;
}
存在对齐现象,char的对齐数是1,int的对齐数是4,4是最大对齐数,对齐到8
union Un2
{
short c[7];//14
int i;//4
};
int main()
{
printf("%d\n", sizeof(union Un2));//(4, 14) -> 16
return 0;
}
联合的使用
指针判断计算机大小端
int main()
{
int a = 1;
if(*(char*)&a == 1)
printf("小端\n");
else
printf("大端\n");
}
联合体判断大小端
union Un
{
char c;
int i;
};
int main
{
union Un un1;
un1.i = 1;
if(un1.c == 1)
printf("小端存储");
else
printf("大端存储");
return 0;
}