结构体
1 结构体的声明
1.1 结构的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.2 结构的声明
struct tag
{
member - list;
}variable - list;
其中struct为结构体关键字,tag是结构体标签名,struct tag是结构体类型,member - list为成员变量的列表,variable - list为变量列表。
例如描述一个学生:
struct student//创建一个struct student类型
{
char name[20] ;//姓名
int age ;//年龄;
char sex[10] ;//性别
char id[20] ;//学号
};//分号不能丢
int main()
{
struct student a1;//用struct student类型创造一个变量a1
struct student a2;//用struct student类型创造一个变量a2
struct student a3;//用struct student类型创造一个变量a3
return 0;
}
也可以这么写:
struct student//创建一个struct student类型
{
char name[20];//姓名
int age;//年龄;
char sex[10];//性别
char id[20];//学号
}a4, a5, a6;
int main()
{
struct student a1;
struct student a2;
struct student a3;
return 0;
}
上面代码中的a4、a5、a6,也是根据struct student类型创造出的结构体变量,a1、a2、a3和a4、a5、a6的类型都是一摸一样的,但这两者的区别区别在于,a1、a2、a3为局部变量,而a4、a5、a6为全局变量。
注:结构体中不可以直接初始化成员
1.3 特殊的声明
在声明结构的时候,可以不完全的声明。 例如:
//代码1
struct
{
int a;
char b;
float c;
}x;
//省略结构体标签名
//代码2
struct
{
int a;
char b;
float c;
}a[20], * p;
像上面这几种省略结构体标签(tag)的结构体类型称为匿名结构体类型
那么问题来了,在上面代码的基础上,
p = &x;这条代码合理吗?
答案是不合理,虽然上面代码1和代码2的结构体成员是一模一样的,但编译器还是会把上面的两个声明当成完全不同的两个类型,所以指针p里面就不能存x的地址,是非法的。
1.4 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
//代码1
struct Node
{
int data;
struct Node next;
};
代码1这样写可以吗?如果可以,那sizeof(struct Node)是多少?
答: 当然不行,假如要算sizeof(struct Node)的大小,int的大小是4字节,那么struct Node next的大小怎么算,struct Node next里面不是也有一个int和struct Node next它自己吗?可想而知,这样一直下去就会变成无限套娃,那么结构体到底能不能自引用呢,方法又是什么样的呢?
正确的自引用方式:
//代码2
struct Node
{
int data;
struct Node* next;//同类型的结构体的指针
};
注意:
//代码3
typedef struct
{
int data;
Node* next;
}Node;
代码3这样写可以吗?
答案是不能,因为这涉及到一个先后顺序的问题,到底是typedef struct定义Node之后再定义typedef struct Node中的Node这个成员呢还是Node先定义出来它的成员再对这个类型重命名产生Node呢?
解决方案:
typedef struct Node
{
int data;
struct Node* next;
}Node;
在typedef struct前面加上自定义类型名即可。
1.5 结构体变量的定义和初始化
有了结构体类型,那如何定义变量,其实很简单,访问一个结构的成员的方法如下:
例如:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
struct s//另一个结构体
{
int c;
double b;
char n;
};
struct stu
{
struct s sb;//另一个结构体变量
char name[20];//姓名
char num[20];//学号
char cla[40];//班级
int age;
};
int main()
{
//{20,3.14,'w'}: 这些都是sb的结构成员
struct stu t = { {20,3.14,'w'},"zhangsan","20181020175","15gz计算12班",18 };
struct stu* ps = &t;//struct stu类型的指针
printf(" %d ", t.sb.c);//输出20
printf(" %f %c\n", t.sb.b, t.sb.n); //输出3.14,w
printf(" %s %s %s %d\n", t.name, t.num, t.cla, t.age);
//用指针:
printf(" %d %f %c\n", (*ps).sb.c, (*ps).sb.b, (*ps).sb.n);
printf(" %d %f %c\n", ps->sb.c, ps->sb.b, ps->sb.n);//因为sb不是指针 ,所以*ps->后面可以直接用.。
return 0;
}//指针ps先找到s,再找到sb=ps->.sb.c=(*ps).sb.c
输出结果:
*1.6 结构体内存对齐
现在我们深入讨论一个问题:计算结构体的大小。
要计算结构体的大小,我们不得不了解一个概念:结构体内存对齐。
考点
如何计算? 首先得掌握结构体的对齐规则:
第一个成员在与结构体变量偏移量为0的地址处。
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
注:VS编译器中默认的值为8
结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的对齐数中(含嵌套结构体的对齐数)的最大对齐数的整数倍。
这样光看的话咱可能不是很理解,下面我们来看几个例子:
练习1
//练习1
#include<stdio.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
struct S1 s = { 0 };
printf("%d\n", sizeof(struct S1));
}
该程序输出的结果是多少?我们一步一步来:
第一步:简单的画出内存图,假设结构体变量S在箭头所指处开辟空间,则图中蓝色的字节区域相对于箭头所指的地方的结构体变量的偏移量就为0,下面那块字节所处的空间相对于起始位置的偏移量就为1,以此类推往下递增:
第二步,为第一个结构体成员分配空间:因为结构体成员char c1最先创建,所以首先为c1分配空间,又因为第一个成员要分配在与结构体变量偏移量为0的地址处,且char为1个字节,1小于编译器默认的对齐数8,所以c1的对齐数是1,则图中的橙色区域(把蓝色给覆盖了)就是c1所占的内存空间;
第三步,根据上面的对齐规则,为剩下的结构体成员分配空间:int为4个字节,4小于编译器默认的对齐数8,因此i的对齐数是4,所以要在偏移量为4的倍数的区域为i分配空间,则图中的绿色区域就是为i分配的空间(8是4的倍数),以此类推,图中的紫色区域就是为c2分配的空间(因为char为1个字节,除0以外的任何自然数都是1的倍数);
第四步,计算结构体总大小:此时结构体的总大小为9个字节(从图中的橙色区域到紫色区域,包括中间被浪费的空白的三块空间),因为在结构体变量S1中,int的对齐数最大(char的对齐数为1,int的对齐数为4),所以最终结构体的总大小要为4的整数倍,所以最少又要浪费3块空间,因此最终结构体的总大小为9+3=12个字节。 (图中的红色区域为被浪费的空间)
输出结果:
练习2:
#include<stdio.h>
struct S2
{
char c1;
char c2;
int i;
};
int main()
{
struct S2 s = { 0 };
printf("%d\n", sizeof(struct S2));
}
该程序输出的结果是多少?根据上面的步骤一步一步套就行了,内存空间分配图如下:
输出结果:
练习3:
//练习3
#include<stdio.h>
struct S3
{
double d;
char c;
int i;
};
int main()
{
struct S3 s = { 0 };
printf("%d\n", sizeof(struct S3));
}
该程序输出的结果是多少?老套路,根据对齐规则一步一步来即可:
输出结果:
练习4-结构体嵌套问题
//练习4-结构体嵌套问题
#include<stdio.h>
struct S4
{
double d1;
char c2;
int i;
};
struct S5
{
char c1;
struct S4 s4;
double d2;
};
int main()
{
struct S4 s1 = { 0 };
struct S5 s2 = { 0 };
printf("%d\n", sizeof(struct S5));
return 0;
}
该程序输出结果是多少?嵌套问题也是一样的步骤,不过要注意的是,嵌套的结构体要对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的对齐数中(含嵌套结构体的对齐数)的最大对齐数的整数倍,在这道题中,也就是嵌套的结构体s4要对齐到偏移量为8的区域处(因为在s4中,d1的对齐数为8,大于int和char),结构体的整体大小就是8的整数倍 ,内存空间分布图如下:
输出结果:
那么在我们懂得怎么计算结构体大小之后,是否想过为什么存在内存对齐呢?
大部分的参考资料都是这样说的:
1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,要怎么做到呢:
让占用空间小的成员尽量集中在一起。
例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
S1内存空间分配简图 (大小为12个字节):
S2内存空间分配简图 (大小为8个字节):
这里我们来思考一个问题,编译器中的默认对齐数是否能修改呢?
其实是可以的。
1.7 修改默认对齐数
#pragma 这个预处理指令,可以改变我们的默认对齐数。 比如:
#pragma pack(8) :设置默认对齐数为8
#pragma pack(1) :设置默认对齐数为1
#pragma pack( ) : 取消设置的默认对齐数,还原为默认
注:一般我们在设置默认对齐数时,括号里的值通常是2的N次方。
结构在对齐方式不合适的时候,我们可以自己更改默认对齐数:
#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;
};
int main()
{
printf("%d\n", sizeof(struct S1 ));
printf("%d\n", sizeof(struct S2));
return 0;
}
输出结果:
1.8 计算偏移量
实际上,offsetof宏可以计算结构体中某变量相对于首地址的偏移量
形式 :offsetof (结构体类型,成员名)
返回值:返回成员的偏移量(以字节为单位)
头文件:#include<stddef.h>
例如:
#include<stdio.h>
#include<stddef.h>
struct S1
{
char c1;
int i;
char c2;
};
int main()
{
printf("%d\n", offsetof(struct S1, c1));//计算c1的偏移量
printf("%d\n", offsetof(struct S1, i));//计算i的偏移量
printf("%d\n", offsetof(struct S1, c2));//计算c2的偏移量
return 0;
}
输出结果:
内存空间分配简图:
1.9 结构体传参
直接上代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
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函数。
原因:
函数传参时,参数需要压栈,会有时间和空间上的系统开销;
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降,而传递结构体对象的地址就不会有这样的问题。
结论:结构体传参的时候,要传结构体的地址。
2. 位段
结构体讲完就得讲讲结构体实现 位段 的能力。
2.1什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是
int、unsigned int、signed int或char。2.位段的成员名后边有一个冒号和一个数字。
比如:
#include<stdio.h>
struct A
{
int _a : 2; `//成员占2个比特位`
int _b : 5; `//成员占5个比特位`
int _c : 10;`//成员占10个比特位`
int _d : 30;`//成员占30个比特位`
};
int main()
{
printf("%d\n", sizeof(struct A));
return 0;
}
A就是一个位段类型。
那位段A的大小printf("%d\n", sizeof(struct A));是多少?
输出结果:
8怎么来的? 在开辟空间时,先为_a开辟一个整型(4个字节,32个比特位),因为_a只占2个比特位,用完后还剩30个比特位,_b也只占5个比特位,而且剩下的30个比特位完全够_b来用,所以_b占用的5个比特位的空间也是来自于那剩下的30个比特位里的,_b用完后则还剩下25个比特位,_c也一样,用完后还剩下15个比特位,但此时轮到_d时,剩下的15个比特位就不够存储需要占30个比特位的_d了,所以需要再次开辟一个整型,也就是4个字节,32个比特位,来存储_d,所以总共就开辟了8个字节。 那么位段这个东西到底有什么用? 打个比方:比如说我们要创建一个变量表示性别,无非就三种表示方式:男,女,保密;如果以二进制的形似存储起来,要么是01,要么10,要么00,要么11,总共就4种可能性,此时只需要2个比特位就可以表示这4种可能性,避免了为了表示性别而开辟一个整型大小(32个比特位)的空间来存储的情况,所以位段这个东西就是可以帮我们在一些特定的情况下节省空间。
2.2 位段的内存分配
- 位段的成员可以是
int,unsigned int,signed int或者char(属于整形家族)类型;- 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的;
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
举个例子:
//一个例子
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?
该abcd四个结构体成员的空间在内存中是如何开辟的?
在VS编译器中,我们先假设这几个成员变量存储时都是从右往左存储的,因为a、b、c、d都是char类型,所以开辟空间时要一个字节一个字节(1个字节==8个比特位)的开辟,则存储情况如图:
将a、b、c、d初始化后,a的二进制形式为10010,因为a中只能存3个比特位,所以10010存入a中时就会变成010,b、c、d也是如此,则存储情况如图:
若转换为16进制输出(4个2进制数转换1个16进制数),则结果就会是62 03 04,编译结果如下:
所以在VS编译器中,位段中的结构体成员在内存中的存储方式就是按我们假设的方式---从右往左来存储的。
注:此时不需要考虑大小端的问题,因为大小端是以一个字节为单位来存储,而此时是以比特位为单位来存储的。
2.3 位段的跨平台问题
int 位段被当成有符号数还是无符号数是不确定的;
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题;
位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义;
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结 :
跟结构相比,位段可以达到同样的效果,可以很好的节省空间,但是有跨平台的问题存在。
3. 枚举
枚举顾名思义就是一一列举,也就是把可能的取值一一列举。
比如我们现实生活中:
一周的星期一到星期日是有限的7天,可以一一列举。
性别有:男、女、保密,也可以一一列举。
月份有12个月,也可以一一列举
这些都叫枚举。
3.1 枚举类型的定义
例如:
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,当然在定义的时候也可以赋初值。
例1(未初始化):
#include<stdio.h>
enum Color
{
RED
GREEN,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
输出结果:
例2(初始化):
#include<stdio.h>
enum Color
{
RED=1,
GREEN=3,
BLUE
};
int main()
{
printf("%d\n", RED);
printf("%d\n", GREEN);
printf("%d\n", BLUE);
return 0;
}
3.2 枚举的优点
我们可以使用 #define 定义常量,为什么非要使用枚举?
枚举的优点:
- 增加代码的可读性和可维护性;
- 和
#define定义的标识符相比枚举有类型检查,更加严谨;- 防止了命名污染(防止命名相同);
- 便于调试;
- 使用方便,一次可以定义多个常量。
比如说我们写一个计算器程序的菜单:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
int ret, x, y;
int input = 0;
do
{
menu();
printf("请输入您的选择: (1 2 3 4 0)\n");
scanf_s("%d", &input);
switch (input)
{
case 1:
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Add(x, y);
printf("ret=%d\n\n", ret);
break;
case 2:
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Sub(x, y);
printf("ret=%d\n\n", ret);
break;
case 3:
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Mul(x, y);
printf("ret=%d\n\n", ret);
break;
case 4:
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Div(x, y);
printf("ret=%d\n\n", ret);
break;
case 0:
printf("程序退出\n\n");
break;
default:
printf("输入错误,请重新输入:\n");
break;
}
} while (input != 0);
}
当我们读这串代码(还没开始看menu函数中的内容),读到case时可能不知道这个case 1中的1是什么意思,或者说不知道case 2中的2是个什么意思,这是我们就可以用枚举类型来让代码读起来更加清晰:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
enum Option
{
EXIT, //0
ADD, //1
SUB, //2
MUL, //3
DIV //4
};
int main()
{
int ret, x, y;
int input = 0;
do
{
menu();
printf("请输入您的选择: (1 2 3 4 0)\n");
scanf_s("%d", &input);
switch (input)
{
case ADD://加
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Add(x, y);
printf("ret=%d\n\n", ret);
break;
case SUB://减
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Sub(x, y);
printf("ret=%d\n\n", ret);
break;
case MUL://乘
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Mul(x, y);
printf("ret=%d\n\n", ret);
break;
case DIV://除
printf("请输入两个整数:\n");
scanf_s("%d%d", &x, &y);
ret = Div(x, y);
printf("ret=%d\n\n", ret);
break;
case EXIT://退出
printf("程序退出\n\n");
break;
default:
printf("输入错误,请重新输入:\n");
break;
}
} while (input != 0);
}
又比如说我们要定义多个常量,而用#define的话可能要写很多遍,而用枚举直接全部写在{}里就行了:
3.3 枚举的使用
我们在使用枚举类型时要注意一个点,那就是枚举类型的成员都是常量,而常量的值在初始化过后是不能再修改的, 比如说:
#include<stdio.h>
enum Color//颜色
{
RED = 1,//赋初值1
GREEN = 2,//赋初值2
BLUE = 4//赋初值4
};
int main()
{
printf("%d ", RED);
printf("%d ", GREEN);
printf("%d ", BLUE);
return 0;
}
输出结果:
![]()
RED = 5;像这样修改RED的值就会报错。
拿枚举常量赋值时,只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。 例如:
#include<stdio.h>
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
int main()
{
enum Color PINK = RED;//用枚举常量RED给枚举变量PINK赋值
printf("%d\n", PINK);
return 0;
}
输出结果:
![]()
enum Color PINK = 5;像这样赋值是不被允许的。
4. 联合(共用)体
4.1 联合类型的定义
联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。
比如:
联合类型的声明
//联合类型的声明
union Un
{
char c;
int i;
};
联合变量的定义
//联合变量的定义
union Un un;
计算联合变量的大小
//计算联合变量的大小
printf("%d\n",sizeof(un));
4.2 联合的特点
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。
举个例子:
#include<stdio.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un;
//这三行代码的输出结果一样吗?
printf("%p\n", &(un.c)); //输出c的地址
printf("%p\n", &(un.i)); //输出i的地址
printf("%p\n", &un); //输出un的地址
return 0;
}
输出结果:
该程序得出的结论就是:联合的成员是共用同一块内存空间的,因此联合体也被称为共用体。
也可以画一个内存分配简图来理解:
假设内存空间是从红色箭头处开始分配,,图中的蓝色区域就为char所占的内存空间;
图中绿色区域就为int所占的内存空间(跟char共用一个字节的空间,蓝色被绿色覆盖了):
但是联合体的这种特征导致了一个情况,当共用体进行赋值时,比如说下面这串代码:
union Un
{
int i;
char c;
};
int main()
{
union Un un;
un.i = 5;
un.c = 48;
printf("%d\n", un.i);//输出结果是多少?
return 0;
}
输出结果:
我们会发现,i的值不是5,而是48,其原因就是,因为联合体共用空间的缘故,所以当i往空间里面放一个值,c再往空间里面放一个值时,c的值就把i里面的值给修改(覆盖)了,也就是往i的四个字节中的第一个字节里面放了个48,所以最后输出的结果才会是c的值。
因此,联合体的另一个特点是:在同一时间,只能使用联合体中的一个成员。
4.3 联合大小的计算
计算联合体的大小时有两点规则:
1.联合的大小至少是最大成员的大小。
2.当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。 比如说, 例1:
#include<stdio.h>
union Un1
{
char c[5];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1));
//输出结果是多少?
return 0;
}
首先c[5]虽然是一个数组,但是数组类型是char类型,所以对齐数还是要按1来算,因为c[5]中有5个元素,所以c所占的空间为5个字节,所以Un1中最大的成员是c[5],但Un1中的最大对齐数为4,5不是4的倍数,根据计算联合体大小的规则可以得知,c[5]要对齐到4的整数倍也就是8处,所以要再浪费3个字节,因此联合体的大小为8。 输出结果如下图:
例2: ``
#include<stdio.h>
union Un2
{
short c[7];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un2));
//输出结果是多少?
return 0;
}
这题跟例1也是大同小异,首先找Un2中的最大成员和最大对齐数,由代码可知,最大成员为c[7],大小为14个字节;最大对齐数为4(short类型大小为2,int为4),因为14不是4的倍数,所以要对齐到最大对齐数的整数倍也就是16处,所以要再浪费2个字节,因此联合体的大小就为16。 输出结果如下图:
到这里结构体的介绍就结束了,如果你觉得本篇文章对你多少有些帮助,可以点个赞或者点一波收藏支持一下哦,欢迎各位大佬批评指正,咱们下篇博客见!