一、结构体指针变量
结构体指针变量:本质上是一个指针变量,保存的是结构体变量的地址。
1.结构体变量的地址
结构体变量的地址:对结构体变量名取地址。
- 代码演示
typedef struct stu
{
char name[32];
int age;
float score;
}STU;
int main()
{
STU jack = {"jack", 18, 88.8};
printf("%p\n", &jack);
printf("%p\n", &jack.name);
return 0;
}
- 运行结果
000000472c7ffaa0
000000472c7ffaa0
- 说明:
- 结构体变量地址与结构体首个成员地址相同,都指向同一个位置;
- 结构体变量地址为
STU *
类型,结构体首个成员地址为char **
类型,即二者取值的宽度与指针偏移的跨度不相同。
2.结构体指针
2.1结构体指针变量的定义
结构体指针变量定义:结构体类型 *指针变量名 = &结构体变量
- 代码演示
int main()
{
STU jack;
STU *p = &jack;
// 键盘输入为结构体成员赋值
printf("请输入(姓名 年龄 分数):");
scanf("%s %d %f", p->name, &p->age, &p->score);
// 访问结构体成员
printf("我叫:%s,今年%d岁,考了%.2f分\n", jack.name, jack.age, jack.score);
printf("我叫:%s,今年%d岁,考了%.2f分\n", (*p).name, (*p).age, (*p).score);
printf("我叫:%s,今年%d岁,考了%.2f分\n", p->name, p->age, p->score);
return 0;
}
- 运行结果
请输入(姓名 年龄 分数):jack 18 88.8
我叫:jack,今年18岁,考了88.80分
我叫:jack,今年18岁,考了88.80分
我叫:jack,今年18岁,考了88.80分
- 说明:
- 结构体成员可以通过结构体变量访问,也可以通过结构体指针访问;
- 当通过结构体变量通过
.
访问结构体成员,结构体指针或地址通过->
访问结构体成员。
2.2结构体数组在栈区,指针成员指向堆区
首先看清要求:类型为结构体数组,存储在栈区,结构体数组中的结构体对象中存在指针成员,且指针成员指向堆区。
- 代码演示:
int main()
{
// 定义一个结构体数组
STU stu_s[5];
// 为结构体指针成员申请堆区空间
int i = 0;
for (i = 0; i < 5; i++)
{
stu_s[i].name = (char *)malloc(32);
if (NULL == stu_s[i].name)
{
printf("内存申请失败\n");
return 0;
}
}
// 键盘输入赋值
printf("请输入(姓名 年龄 分数):");
for (i = 0; i < 5; i++)
{
scanf("%s %d %f", stu_s[i].name, &stu_s[i].age, &stu_s[i].score);
}
// 遍历结构体成员内容
printf("-----------------------------------\n");
for (i = 0; i < 5; i++)
{
printf("我叫:%-7s,今年%02d岁,考了%.2f分\n", stu_s[i].name, stu_s[i].age, stu_s[i].score);
}
// 释放堆区空间
for (i = 0; i < 5; i++)
{
if (NULL != stu_s[i].name)
{
free(stu_s[i].name);
stu_s[i].name == NULL;
}
}
return 0;
}
- 运行结果
请输入(姓名 年龄 分数):小明 18 88.8
小红 20 86.6
王刚 25 75
李华 18 92
王翠花 28 68
-----------------------------------
我叫:小明 ,今年18岁,考了88.80分
我叫:小红 ,今年20岁,考了86.60分
我叫:王刚 ,今年25岁,考了75.00分
我叫:李华 ,今年18岁,考了92.00分
我叫:王翠花 ,今年28岁,考了68.00分
- 说明:当结构体存在指针成员指向堆区的时候,要先为指针成员申请堆区空间,再为其赋值操作,用完记得释放堆区空间。
2.3结构体数组在堆区,指针成员指向堆区
首先类型为数组,数组存储在堆区,数组每个成员为结构体类型,结构体某些成员为指针,指针指向堆区空间。既然结构体数组存储的堆区,就需要通过指针变量指向堆区空间。
- 代码演示
int main()
{
// 定义一个结构体指针,并为其申请堆区空间
STU *stu_s = (STU *)calloc(5, sizeof(STU));
// 为结构体指针成员申请堆区空间
int i = 0;
for (i = 0; i < 5; i++)
{
(stu_s + i)->name = (char *)malloc(32);
if (NULL == (stu_s + i)->name)
{
printf("内存申请失败\n");
return 0;
}
}
// 键盘输入赋值
printf("请输入(姓名 年龄 分数):");
for (i = 0; i < 5; i++)
{
scanf("%s %d %f", (stu_s + i)->name, &(stu_s + i)->age, &(stu_s + i)->score);
}
// 遍历结构体成员内容
printf("-----------------------------------\n");
for (i = 0; i < 5; i++)
{
printf("我叫:%-7s,今年%02d岁,考了%.2f分\n", (stu_s + i)->name, (stu_s + i)->age, (stu_s + i)->score);
}
// 释放堆区空间
for (i = 0; i < 5; i++)
{
if (NULL != (stu_s + i)->name)
{
free((stu_s + i)->name);
(stu_s + i)->name == NULL;
}
}
if (NULL != stu_s)
{
free(stu_s);
stu_s == NULL;
}
return 0;
}
- 运行结果
请输入(姓名 年龄 分数):隆冬强 43 60
王大锤 32 75
旺财 10 95.5
常威 35 25
来福 30 33
-----------------------------------
我叫:隆冬强 ,今年43岁,考了60.00分
我叫:王大锤 ,今年32岁,考了75.00分
我叫:旺财 ,今年10岁,考了95.50分
我叫:常威 ,今年35岁,考了25.00分
我叫:来福 ,今年30岁,考了33.00分
- 说明:
- 先要申请堆区空间,再使用;
- 申请的时候先申请结构体数组的堆区空间,再申请结构体指针成员指向的堆区空间;
- 释放堆区空间的时候,先释放结构体指针成员指向的堆区空间,再释放结构体数组的堆区空间;
- 结构体数组的每个成员为
STU
类型,那指向堆区的指针变量类型为STU*
。
2.4结构体指针数组在堆区,每个元素指向堆区,元素的指针成员指向堆区
-
分析:本质上是一个数组,数组在堆区,数组每个元素为结构体指针变量,结构体指针变量指向堆区,结构体中有指针成员,且指针成员也指向堆区。
-
代码演示:
int main()
{
int n = 0;
printf("请输入学生数量:");
scanf("%d", &n);
// 为结构体指针数组申请堆区空间
STU **stu_s = (STU **)calloc(n, sizeof(STU *));
// 为结构体指针申请堆区空间
int i = 0;
for (i = 0; i < n; i++)
{
// *(stu_s + i) = (STU *)malloc(sizeof(STU *));
stu_s[i] = (STU *)malloc(sizeof(STU *));
if (NULL == stu_s[i])
{
printf("内存申请失败\n");
return 0;
}
// 为指针成员申请堆区空间
// (*(stu_s + i))->name = (char *)malloc(32);
stu_s[i]->name = (char *)malloc(32);
if (NULL == stu_s[i]->name)
{
printf("内存申请失败\n");
return 0;
}
}
// 键盘输入为结构体成员赋值
printf("请输入(姓名 年龄 分数):");
for (i = 0; i < n; i++)
{
scanf("%s %d %f", stu_s[i]->name, &stu_s[i]->age, &stu_s[i]->score);
}
// 遍历数据
printf("-----------------------------------\n");
for (i = 0; i < n; i++)
{
printf("我叫:%-7s,今年%02d岁,考了%.2f分\n", stu_s[i]->name, stu_s[i]->age, stu_s[i]->score);
}
// 释放堆区空间
for (i = 0; i < n; i++)
{
// 释放指针成员指向的堆区空间
if (NULL != stu_s[i]->name)
{
free(stu_s[i]->name);
stu_s[i]->name == NULL;
}
// 释放结构体指针指向的堆区空间
if (NULL != stu_s[i])
{
free(stu_s[i]);
stu_s[i] == NULL;
}
}
// 释放结构体指针数组的堆区空间
if (NULL != stu_s)
{
free(stu_s);
stu_s == NULL;
}
return 0;
}
- 运行效果
请输入学生数量:3
请输入(姓名 年龄 分数):张大炮 32 85
吕小布 30 83.5
曾小莲 29 78.5
-----------------------------------
我叫:张大炮 ,今年32岁,考了85.00分
我叫:吕小布 ,今年30岁,考了83.50分
我叫:曾小莲 ,今年29岁,考了78.50分
- 说明:
- 此案例中:数组的元素为结构体指针变量,其类型为
STU *
,则指向结构体指针数组的堆区空间的指针变量为一个二级指针STU **
; - 申请堆区空间的时候先申请结构体指针数组的堆区空间,再申请结构体指针指向的堆区空间,再申请结构体指针变量指向的堆区空间;
- 释放空间的时候,先释放结构体指针变量指向的堆区空间,再释放结构体指针指向的堆区空间,最后释放存储结构体指针数组的堆区空间;
- 从上面几个案例可以总结出:堆区空间在申请的时候,先申请外层堆区空间,再逐级申请内层堆区空间;释放的时候先逐层释放内层堆区空间,再释放外层堆区空间;
- 涉及到指针、数组的时候,使用数据的方式不止一种,可以根据习惯选择最方便的方案;数据存储的空间分布比较复杂的时候,拆开逐层分析。
- 此案例中:数组的元素为结构体指针变量,其类型为
3.结构体指针成员为函数指针
C语言中结构体成员不能为函数名,但是可以是函数指针。
- 代码演示
void stu_action(char *name)
{
printf("%s正在打篮球\n", name);
}
typedef struct stu
{
char *name;
int age;
// 结构体成员为函数指针类型
void (*action)(char *);
}STU;
int main()
{
// 定义结构体变量并初始化
STU jack = {"jack", 18, stu_action};
jack.action(jack.name);
return 0;
}
- 运行结果
jack正在打篮球
- 说明:结构体成员为函数指针的时候,在定义这个结构体成员前应先定义函数或声明函数。
二、结构体默认对齐规则
结构体占用的空间大小并不是我们想象中的将各种类型的数据的空间大小累加起来,而是遵循一定的规则。
1.结构体的大小
- 代码演示
typedef struct stu
{
char ch;
int a;
}STU;
int main()
{
printf("%zu\n", sizeof(STU));
return 0;
}
- 运行结果
8
- 上面的案例中:结构体成员有一个
char
类型的变量,1字节;一个int
类型的变量,4字节。但结构体类型的大小却占8字节,因为为了方便数据的读写,结构体有其默认的对齐规则。其内层分布见下图:
2.结构体对齐步骤
2.1成员为基本类型
我们需要将结构体在内存中的存储抽象为一行一行来看:
-
确定分配单位:一行分配多少字节,结构体中最大的基本类型长度就是分配单位,上面案例分配单位就是一个
int
的长度4字节; -
成员的偏移量:相对于结构体的起始位置的偏移字节数,成员自身大小的整数倍,同时注意后一个成员要在前一个成员的后面,即使前一个成员前面有足够空间存储它;
typedef struct stu { char ch1; int a; char ch2; }STU; int main() { printf("%zu\n", sizeof(STU)); // 12 return 0; }
上面案例,成员
char ch2;
定义在成员int a
后面,即使int a
前面有空间放ch2
,也得放在后面。 -
结构体总大小:分配单位的整数倍,即分配单位长度乘分配的行数;
-
为了节约内存,定义结构体类型的时候,需要合理分配成员先后顺序;
typedef struct stu { char ch1; char ch2; short b; int a; }STU; int main() { printf("%zu\n", sizeof(STU)); // 8 return 0; }
这种写法多存储了一个
short
类型的数据,但消耗的空间比上面的写法还节约了4字节,其内存分布:0 1 2 3 ch1 ch2 b b 4 5 6 7 a a a a
2.2数组成员的对齐
结构体中最大的基本类型长度为分配单位,数组不是基本类型,因此不能将数组看成一个整体,而是把数组拆成一个个基本类型来分布。
- 代码演示
typedef struct stu
{
char ch1;
char ch2;
short b;
int a;
char str[5];
short c;
}STU;
int main()
{
printf("%zu\n", sizeof(STU));
return 0;
}
- 运行结果
16
- 说明:如果结构体成员为字符数组,就将数组拆分为对应个数字符分布,如果数组元素的基本类型长度最大,那么分配单位就为数组元素基本类型的长度,上面定义的结构体的内存分布为:
0 | 1 | 2 | 3 |
---|---|---|---|
ch1 | ch2 | b | b |
4 | 5 | 6 | 7 |
a | a | a | a |
8 | 9 | 10 | 11 |
str[0] | str[2] | str[3] | str[4] |
12 | 13 | 14 | 15 |
str[5] | c | c |
2.3结构体嵌套对齐
- 分配单位等于所有结构体中最大的基本类型,如果内层结构体的基本类型最大,本质上也是外层基本类型最大,因为内层属于外层,但如果内层最大基本类型小于外层最大基本类型,内层分配单位为其自身最大基本类型长度;
- 结构体成员的偏移量等于该结构体最大基本类型整数倍,结构体成员中的成员偏移量是相对于结构体成员的偏移量而言的;
- 代码演示
struct test
{
char a;
int b;
};
typedef struct stu
{
char ch1;
struct test tes;
short c;
}STU;
int main()
{
printf("%zu\n", sizeof(STU)); // 16
return 0;
}
其内存分布:
0 | 1 | 2 | 3 |
---|---|---|---|
ch1 | |||
4 | 5 | 6 | 7 |
a | |||
8 | 9 | 10 | 11 |
b | b | b | b |
12 | 13 | 14 | 15 |
c | c |
- 案例:下方结构体的内存大小为多少
struct A
{
char a;
short b;
};
struct B
{
short c;
struct A d;
short e;
int f;
};
int main()
{
printf("%zu\n", sizeof(struct B));
return 0;
}
- 答案为12字节,对照下面的内存分布表逐步分析:
0 | 1 | 2 | 3 |
---|---|---|---|
c | c | a | |
4 | 5 | 6 | 7 |
b | b | e | e |
8 | 9 | 10 | 11 |
f | f | f | f |
- 外层基本分配单位为4字节,c先占两个字节;
- 结构体成员d的偏移量等于该结构体最大基本类型
short
整数倍,因此结构体成员偏移量为2,分配单位为2; - 结构体成员从2号位开始,a占2号位,后一个位
short b
,3不是2的倍数,b占4、5号位; - 内层结构体对齐完成,接着层结构体分配单位的整数倍后面,6号位满足内层结构体分配单位的整数倍,e内层整数倍,e占6、7位;
- 8为
int
长度的整数倍,f占8~9四个字节。
三、结构体强制对齐规则
1.强制对齐
在定义结构体类型之前写上:#pragma pack (value)
指定对齐值 value,但value并非就是分配单位,真正的分配单位是min(value,结构体中最大的基本类型长度)
,即:如果给定的value值为8,但结构体中成员最大长度为4,那么分配单位是4,取给定值和成员类型最大值两者的最小值。
- 代码演示·
#pragma pack(1)
struct A
{
char a;
int b;
};
int main()
{
printf("%zu\n", sizeof(struct A)); // 5
return 0;
}
将value值设定为1的时候,结构体总大小就是每个成员大小的累加和了。
- 验证分配单位是
min(value,结构体中最大的基本类型长度)
;
#pragma pack(8)
struct A
{
int a;
char b;
int c;
};
int main()
{
printf("%zu\n", sizeof(struct A));
return 0;
}
如果上面代码的运行结果为16,则按照8字节分配,验证不成立,如果允许结果为12字节,则按照4字节分配,验证成立。
- 运行结果
12
因此分配单位是min(value,结构体中最大的基本类型长度)
。
2.结构体位域
2.1位域的概念
结构体中成员所占空间以二进制位计算称之为位域或位段。
struct A
{
unsigned int a : 3;
};
- 说明:
- 位域所占的位数,不能超过自身类型的二进制位数,如:上面案例 a 的位域占用位数不得超过32位;
- 位域的类型一般位无符号整数类型。
2.2位域赋值
- 代码演示
struct A
{
// 范围 0~7
unsigned int a : 3;
int b : 3;
unsigned float c : 3; // 报错
};
int main()
{
struct A obj;
obj.a = 5;
printf("%d\n", obj.a);
obj.a = 10; // 1010
printf("%d\n", obj.a); // 取3位010
obj.b = 14;// 1110
printf("%d\n", obj.b); // 110
return 0;
}
- 运行结果
5
2
-2
- 说明:
- 给位域赋值的时,注意不要超过它所占位的最大值。上面案例中赋值范围为0~7,10超过范围,其二进制数为 1010,只会取其后三位010,转化十进制为2;
- 位域为无符号整数,如果定义为有符号,取值时会把第一位看成符号位。上面案例:14二进制1110,取后三位110,第一位为符号位,为1,因此为负数,求十进制-2;
- 将位域定义为浮点数会直接报错。
2.3位域压缩
相邻且类型相同的位域称为相邻位域,相邻位域才能压缩。但是相邻位域压缩的位数不能超过自身类型的位数。
- 代码演示
struct A
{
// 范围 0~7
unsigned char a : 3;
unsigned char b : 4;
};
struct B
{
// 范围 0~7
unsigned char a : 3;
unsigned char b : 4;
unsigned char c : 2;
};
int main()
{
printf("%zu\n", sizeof(struct A));
printf("%zu\n", sizeof(struct B));
return 0;
}
- 运行结果
1
2
- 说明:
- 如果相邻位域压缩的位数超过自身类型的位数,会另起一行存储;
- 上面案例
char
位数为8,A 的位数和为7,用一个字节可以放;但 B 的位数和为9,一个字节放不下,需要两个字节,其中一个字节放成员 a 和 b,另外一个字节放 c(这里不会把 c 拆为两份)。
2.4位域分隔
当我们不希望相邻位域压缩在一块,想要将其分为两个单元存储时,可以将它们分隔。
-
直接另起一个存储单元
struct A { // 范围 0~7 unsigned char a : 3; unsigned char : 0; unsigned char b : 4; }; int main() { printf("%zu\n", sizeof(struct A)); // 2 return 0; }
这种方式是直接在
unsigned char : 0;
后面另起了一个单元。 -
无意义位宽
struct A { // 范围 0~7 unsigned char a : 2; unsigned char : 2; unsigned char b : 4; }; int main() { printf("%zu\n", sizeof(struct A)); // 2 return 0; }
这种方式是在两个位域间间隔了指定位数,如果两个位域与无意义位宽的和超过了一个单元,要另起一个单元。无意义位宽应用较广,寄存器单片机,网络通信都会用到。
四、其它构造类型
1.共用体
结构体的每个成员独立拥有一个空间,但共用体的所有成员共享同一块空间。共用体定义方式与结构体类似,只是将关键字struct
换成了 union
。
- 代码演示
union A
{
char a;
short b;
int c;
};
int main()
{
printf("%zu\n", sizeof(union A)); // 4
return 0;
}
- 说明:上面共用体中a b c 三个成员共享同一块空间,空间的大小由最大的成员大小决定。
- 案例:下面代码计算结果:
union A
{
unsigned char a;
unsigned short b;
unsigned int c;
};
int main()
{
union A obj;
obj.a = 10;
obj.b = 20;
obj.c = 256;
printf("%u\n", obj.a + obj.b + obj.c);
return 0;
}
- 运行结果
512
- 说明:因为后面的赋值会覆盖前面的赋值,因此最终共用体的值16进制表示位
0x00 00 01 00
,又因为成员操作空间大小由自身类型大小决定,因此a取到的值为0x00 00 = 0
,b取到的值为0x01 00 = 256
,c取到的值为0x00 00 01 00 = 256
,求和为512.
2.枚举
枚举:将变量将要赋的值 一一列举出来。枚举定义方式与结构体类似,只是将关键字struct
换成了 enum
。
- 代码演示
enum DIRECTION {EAST, SOUTH, WEST = 8, NORTH};
int main()
{
enum DIRECTION direction;
direction = EAST;
printf("%d %d %d %d\n", EAST, SOUTH, WEST, NORTH);
return 0;
}
- 运行结果
0 1 8 9
- 说明:
- 在C语言里枚举值没有作用域的限制,一个枚举值只能定义一次;
- 枚举值默认从0开始递增,赋值的话默认从赋的值开始递增;
- 定义一个枚举变量,为其赋值只能赋枚举值,赋其它值没有任何意义。