指针+结构体

在讲解结构体之前,需复习指针的以下核心内容,确保能理解结构体的定义、成员访问、传参等关键操作:
一、指针基础:定义、本质与核心操作
1. 指针的定义与本质
- 定义格式:
数据类型 *指针变量名;
示例:int *p; char *pc;
-
核心本质:指针变量存储的是内存地址(32位系统占4字节,64位系统占8字节),通过地址间接访问目标数据。
-
关键区分:
-
p:指针变量本身,存储地址(如0x0012ff44); -
*p:解引用操作,通过地址访问目标变量的值; -
&变量名:取地址操作,获取变量的内存地址(给指针赋值的核心方式)。
-
2. 指针的核心操作(必练)
(1)指针赋值与解引用
#include <stdio.h>
int main() {
int a = 10;
int *p = &a; // 给指针p赋值:存储a的地址
printf("a的地址:%p\n", &a); // 输出a的地址(与p的值一致)
printf("p存储的地址:%p\n", p);
printf("通过p访问a的值:%d\n", *p); // 解引用,输出10
*p = 20; // 通过指针修改a的值
printf("修改后a的值:%d\n", a); // 输出20
return 0;
}
(2)空指针与野指针规避
-
未初始化的指针是野指针(指向随机地址,危险),需避免;
-
无指向时赋值
NULL(空指针,指向地址0,不可解引用):int *p = NULL; // 合法空指针 // *p = 30; 错误:空指针不可解引用
二、指针与数组(适配结构体成员为数组的场景)
1. 数组名的本质
-
数组名是数组首元素的地址常量(不可修改),即
arr == &arr[0]; -
指针访问数组元素的两种方式:
int arr[5] = {1,2,3,4,5}; int *p = arr; // 等价于 int *p = &arr[0]; printf("arr[2] = %d\n", arr[2]); // 下标法:3 printf("arr[2] = %d\n", *(p+2)); // 指针法:3(p偏移2个int大小)
2. 指针偏移的核心逻辑
-
指针
p+i的地址 = 原地址 +i × sizeof(数据类型); -
示例:
int占4字节,p+1实际地址+4,指向数组下一个元素(与结构体成员地址计算逻辑一致)。
三、指针与函数(适配结构体传参场景)
1. 指针作为函数参数的核心用途
-
实现“双向通信”:函数内部通过指针修改外部变量的值(避免值拷贝,结构体传参的核心逻辑);
-
示例:
void changeVal(int *p) { *p = 100; // 解引用修改外部变量 } int main() { int a = 10; changeVal(&a); // 传入a的地址 printf("a = %d\n", a); // 输出100 return 0; }
2. 结构体传参的前置铺垫(关键)
-
若直接传递结构体变量,会发生值拷贝(结构体越大,开销越大);
-
最优方案:传递结构体指针(仅拷贝地址,开销极小),函数内部通过指针访问成员;
-
预演逻辑:
// 后续结构体传参的核心格式(提前熟悉) void printStruct(结构体类型 *ps) { // 通过指针访问成员(后续讲 -> 操作符) } int main() { 结构体类型 s; printStruct(&s); // 传入结构体地址 return 0; }
四、指针与结构体的直接关联(核心前置知识)
1. 结构体指针的定义与初始化
-
定义格式:
struct 结构体名 *指针变量名 = &结构体变量; -
示例(提前熟悉格式):
// 假设已定义结构体struct Stu struct Stu s = {"张三", 20}; struct Stu *ps = &s; // 结构体指针ps指向s
2. 结构体指针访问成员的两种方式
-
方式1:
(*指针变量名).成员名(括号不可少,.优先级高于*); -
方式2:
指针变量名->成员名(结构体指针专用,简洁常用); -
示例(后续结构体重点内容,提前预热):
// 两种方式等价,均访问s的age成员 printf("age = %d\n", (*ps).age); printf("age = %d\n", ps->age);
五、结构体自引用的指针基础(适配结构体自引用场景)
1. 指针自引用的核心逻辑
-
结构体自引用(如链表节点)需用自身类型的指针,不可用结构体变量(否则大小无限递归);
-
正确与错误写法对比(提前牢记):
// 错误:结构体内部包含自身变量,大小无限 struct Node { int data; struct Node next; // 错误 }; // 正确:包含自身类型的指针(指针大小固定,4/8字节) struct Node { int data; struct Node *next; // 正确(后续链表/结构体自引用核心) };
2. typedef与自引用的坑(提前规避)
-
匿名结构体+typedef自引用错误(后续结构体重点易错点):
// 错误:Node是重命名结果,内部提前使用未定义 typedef struct { int data; Node *next; // 错误 } Node; // 正确:先声明结构体标签,再自引用 typedef struct Node { int data; struct Node *next; // 正确 } Node;
六、总结
-
指针三大核心操作:取地址(
&)、解引用(*)、指针偏移(p+i); -
结构体相关指针关键:结构体指针定义、
->访问成员、自引用需用指针; -
函数传参关键:指针传参=传地址,是结构体传参的最优方案(减少开销);
-
后续衔接:掌握以上内容后,可直接学习结构体的定义、初始化、内存对齐、传参、位段等核心知识点,无理解障碍。
结构体
-
结构体类型的声明
-
结构体变量的创建和初始化
-
结构体内存对齐
-
结构体传参
- 结构体实现位段
正文开始
1. 结构体类型的声明
前面我们在学习操作符的时候,已经学习了结构体的知识,这里稍微复习一下。
1.1 结构体回顾
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。
1.1.1 结构的声明
代码块
struct tag
{
member-list;
}variable-list;
例如描述一个学生:
代码块
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
题目1
分析以下代码,理解结构体指针
#define FORMAT "Number=%d Name=%s Sex=%c Score=%lf\n"
struct stu{
int num;
char name[20];
char sex;
float score;
}s1={102,"XinHao_Li",'M',78.5},*pstu;
int main(){
pstu=&s1;
printf(FORMAT,s1.num,s1.name,s1.sex,s1.score);
printf(FORMAT,(*pstu).num,(*pstu).name,(*pstu).sex,(*pstu).score);
printf(FORMAT,pstu->num,pstu->name,pstu->sex,pstu->score);
}
1. 结构体与指针的初始化
struct stu { ... } s1 = {...}, *pstu;
pstu = &s1;
-
s1:是一个结构体变量,在内存中占据一块连续空间(大小受内存对齐影响,根据 PDF 第 20 讲,
int、char、float会按规则排列)。 -
pstu:是一个结构体指针变量。执行
pstu = &s1;后,pstu存储了结构体变量s1的首地址。
2. 核心:访问成员的三种等价方式
代码中通过三个 printf 展示了三种访问成员的方法,其效果完全相同:
方法 A:通过变量名访问(直观方式)
C
s1.num, s1.name ...
-
语法:
结构体变量名.成员名 -
原理:直接定位到变量
s1的内存块,根据成员的偏移量(Offset)访问对应数据。
方法 B:通过指针解引用访问(底层逻辑)
C
(*pstu).num, (*pstu).name ...
-
语法:
(*结构体指针).成员名 -
原理:
-
*pstu先对指针进行解引用,找回指针指向的“本体”——即变量s1。 -
再通过
.访问成员。
-
-
注意:必须加圆括号
(),因为点操作符.的优先级高于解引用操作符*。
方法 C:通过箭头操作符访问(工程最常用)
C
pstu->num, pstu->name ...
-
语法:
结构体指针->成员名 -
原理:这是 C 语言专门为结构体指针设计的“语法糖”。它等价于
(*pstu).num。 -
优势:书写简洁,在处理链表或复杂数据结构时,比方法 B 更具可读性。
3. 内存与类型细节分析
-
关于
name的地址: 在printf中,s1.name、(*pstu).name、pstu->name传递给%s的都是数组的首地址。根据 PDF 第 13 讲,数组名在此时退化为指向首元素的指针。 -
关于
printf的格式: 代码中定义了宏#define FORMAT。这里有一个微小的潜在问题:在结构体定义中score是float类型,但在FORMAT中使用了%lf(通常用于double)。在printf传参时,float会自动提升为double,所以程序能正常运行,但在严格的代码规范中,建议保持类型一致。 -
传参效率(对比思考): 虽然这个例子只是打印,但根据 PDF 第 20 讲关于结构体传参的结论:
-
如果你把
s1传给一个函数,系统会拷贝整个结构体(102 + 字符串 + 性别 + 分数),效率低。 -
如果你把
pstu传给函数,系统只拷贝一个指针(4或8字节),效率极高。
-
#define FORMAT "Number=%d Name=%s Sex=%c Score=%lf\n"
struct stu{
int num;
char name[20];
char sex;
float score;
}s1={102,"XinHao_Li",'M',78.5},*pstu;
int main(){
pstu=&s1;
printf(FORMAT,s1.num,s1.name,s1.sex,s1.score);
printf(FORMAT,(*pstu).num,(*pstu).name,(*pstu).sex,(*pstu).score);
printf(FORMAT,pstu->num,pstu->name,pstu->sex,pstu->score);
}
根据上述代码理解完成下面题目
- 基础语法辨析
已知结构体变量 s1 和指针 pstu = &s1,下列选项中能够正确输出 num 成员值且语法完全等价的一组是:
{{ select(1) }}
-
A. s1.num, *pstu.num, pstu->num
-
B. s1.num, (*pstu).num, pstu->num
-
C. s1->num, (*pstu).num, pstu.num
-
D. s1.num, *pstu->num, pstu.num
- 操作符优先级
在表达式 *pstu.name 中,由于操作符优先级的原因,系统会先执行 {{ input(2) }} 操作,这会导致编译错误,因为指针变量本身没有成员。(建议填写:“点”或“.”)
- 内存地址与数组名
在 printf(FORMAT, pstu->name) 中,传递给 %s 的实际值是:
{{ select(3) }}
-
A. 字符数组 name 的第一个字符的值(‘X’)
-
B. 整个结构体变量 s1 的起始地址
-
C. 字符数组 name 在内存中的首地址
-
D. 存放指针 pstu 变量自身的地址
- 结构体数组与指针进阶
若定义 struct stu s[2], *p = s;,欲访问第二个元素的 score 成员,下列写法错误的是:
{{ select(4) }}
-
A. (p + 1)->score
-
B. (*(p + 1)).score
-
C. p[1].score
-
D. *p + 1->score
- 传参效率分析
根据代码分析,若定义函数 void func(struct stu temp) 和 void func(struct stu *ptr)。在调用时,func(s1) 比 func(pstu) 的执行效率低,其根本原因是:
{{ select(5) }}
-
A. 指针方式需要手动计算偏移量,速度慢
-
B. 变量传参涉及整个结构体内存块的拷贝(副本创建)
-
C. 指针方式会自动跳过内存对齐,节省空间
-
D. 变量方式无法访问结构体内的数组成员
第 1 题:基础语法辨析
-
答案: B
-
解析:
-
变量访问:
s1.num是最基本的用法(对象.成员)。 -
指针解引用访问: 由于
.的优先级高于*,必须写成(*pstu).num。若写成*pstu.num,编译器会先寻找pstu.num(而pstu是指针,没有成员),导致报错。 -
指针直接访问:
pstu->num是 C 语言为指针设计的专用语法,等价于(*pstu).num。 -
错误点: 选项 C 中的
s1->num错误,因为s1是对象而非指针;选项 D 中的pstu.num错误,指针不能直接用点号。
-
第 2 题:操作符优先级
-
答案: 点 .
-
解析: 在 C 语言操作符优先级表中,后缀操作符(如
.和->) 的优先级处于第一梯队(Priority 1),而单目操作符(如*解引用) 处于第二梯队。 因此,*pstu.name会被解析器理解为*(pstu.name)。由于pstu是一个地址变量(指针),它内部并没有名为name的成员,所以程序会在编译阶段报错。
第 3 题:内存地址与数组名
-
答案: C
-
解析:
-
pstu->name指向结构体中的字符数组。 -
在 C 语言中,除了作为
sizeof或&的操作数外,数组名会自动退化(Decay)为指向该数组首元素的指针。 -
printf的%s格式符需要的是一个内存地址,它会从该地址开始逐字节读取字符,直到遇到\0为止。因此,传递过去的是name数组在内存中的首地址。
-
第 4 题:结构体数组与指针进阶
-
答案: D
-
解析:
-
A/B/C 均为正确写法:
(p+1)->score和p[1].score都是访问数组第二个元素的标准方式。 -
D 错误原因: 优先级陷阱。
*p + 1->score会被解析为(*p) + (1->score)。-
1->score是非法操作(整型常量不能使用指向符)。 -
即使写成
*(p + 1).score也会因为点号优先级高而报错。
-
-
正确写法应该是:
(*(p + 1)).score或(p + 1)->score。
-
第 5 题:传参效率分析
-
答案: B
-
解析:
-
值传递(func(s1)): 调用时,系统会在栈上开辟一块等大的空间,并将
s1中的所有成员(num、name数组、sex、score)逐字节拷贝过去。如果结构体很大,拷贝会消耗大量 CPU 时间和内存。 -
地址传递(func(pstu)): 调用时,仅拷贝指针本身的值(通常是 4 或 8 字节)。函数内部通过地址直接访问原数据。
-
根据 PDF 第 20 讲结论:在涉及结构体传递时,优先使用指针传递以提高程序性能。
-
1.1.2 结构体变量的创建和初始化
代码块
#include <stdio.h>
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
};
int main()
{
//按照结构体成员的顺序初始化
struct Stu s = { "张三", 20, "男", "20230818001" };
printf("name: %s\n", s.name);
printf("age : %d\n", s.age);
printf("sex : %s\n", s.sex);
printf("id : %s\n", s.id);
//按照指定的顺序初始化
struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "女" };
printf("name: %s\n", s2.name);
printf("age : %d\n", s2.age);
printf("sex : %s\n", s2.sex);
printf("id : %s\n", s2.id);
return 0;
}
1.2 结构的特殊声明
在声明结构的时候,可以不完全的声明。比如:
代码块
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;
上面的两个结构在声明的时候省略掉了结构体标签(tag)。
那么问题来了?
代码块
//在上面代码的基础上,下面的代码合法吗?
p = &x;
警告:
编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。
匿名的结构体类型,如果没有对结构体类型重命名的话,基本上只能使用一次。
以下是基于课件内容的详细解析:
3.1 为什么不行?
虽然这两个结构体的成员列表(int a, char b, float c)完全相同,但由于它们在声明时都省略了结构体标签(tag),它们被视为匿名结构体类型 ()()()。
+1
-
编译器的逻辑:编译器会将这两个声明当成完全不同的两个类型 ()。
-
结果:指针
p属于第二种结构体类型,而&x是第一种结构体类型的地址。由于类型不匹配,赋值操作p = &x;会导致编译器报出警告或错误 ()。
3.2 匿名结构体的特性
匿名结构体类型(没有标签的结构体)具有以下特点:
-
一次性使用:如果没有通过
typedef对其重命名,这种类型基本上只能在声明变量时使用一次 ()。 -
类型唯一性:即便两个匿名结构体的成员一模一样,它们在编译器眼中也是“陌生人”。
3.3 如何修改使代码合法?
如果您希望 p = &x; 能够正常运行,需要确保它们属于同一种类型。
方法 A:添加结构体标签(推荐)
给结构体起一个名字(tag),这样编译器就能识别它们是同一类型 ()()()()。
struct Node { // 添加标签 Nodeint a;
char b;
float c;
};
struct Node x; // 使用标签定义变量struct Node a[20], *p; // 使用相同的标签定义指针
p = &x; // 合法!
方法 B:使用 typedef 重命名
通过 typedef 为匿名结构体创建一个类型别名 ()()()()。
typedef struct {
int a;
char b;
float c;
} S; // 将该匿名结构体类型重命名为 S
S x;
S a[20], *p;
p = &x; // 合法!
1.3 结构的自引用
在结构中包含一个类型为该结构本身的成员是否可以呢?
比如,定义一个链表的节点:
代码块
struct Node
{
int data;
struct Node next;
};
上述代码正确吗?如果正确,那sizeof(struct Node) 是多少?
仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的大小就会无穷的大,是不合理的。
正确的自引用方式:
代码块
struct Node
{
int data;
struct Node* next;
};
在结构体自引用使用的过程中,夹杂了typedef 对匿名结构体类型重命名,也容易引入问题,看看下面的代码,可行吗?
代码块
typedef struct
{
int data;
Node* next;
}Node;
答案是不行的,因为Node是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内部提前使用Node类型来创建成员变量,这是不行的。
解决方案如下:定义结构体不要使用匿名结构体了
代码块
typedef struct Node
{
int data;
struct Node* next;
}Node;
2. 结构体内存对齐
我们已经掌握了结构体的基本使用了。
现在我们深入讨论一个问题:计算结构体的大小。
这也是一个特别热门的考点:结构体内存对齐
2.1 对齐规则
首先得掌握结构体的对齐规则:
-
结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。
-
从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
-
VS 中默认的值为 8
-
Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
-
-
结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。
代码块
//练习1
struct S1
{
char c1;
int i;
char c2;
};
printf("%zu\n", sizeof(struct S1));
//练习2
struct S2
{
char c1;
char c2;
int i;
};
printf("%zu\n", sizeof(struct S2));
//练习3
struct S3
{
double d;
char c;
int i;
};
printf("%zu\n", sizeof(struct S3));
//练习4-结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
printf("%zu\n", sizeof(struct S4));
根据您提供的资料,这四个练习考察的是**结构体内存对齐(Memory Alignment)**的规则。在 VS 环境(默认对齐数为 8)下,输出结果及详细解释如下:
核心对齐规则回顾
-
起始位置:第一个成员对齐到偏移量为 0 的地址处 ()。
-
成员对齐:其他成员要对齐到“对齐数”(成员大小与默认对齐数中的较小值)的整数倍地址处 ()。
-
总大小:结构体总大小必须是“最大对齐数”的整数倍 ()。
-
嵌套结构体:嵌套的结构体对齐到其自身成员中最大对齐数的整数倍处 ()。
练习 1:struct S1
输出结果:12
-
char c1: 占用 1 字节,放在偏移量 0 处 ()。 -
int i: 大小为 4,对齐数是 4。由于偏移量 1, 2, 3 不是 4 的倍数,需要浪费掉,对齐到偏移量 4 处,占用 4 字节(4-7) ()。 -
char c2: 大小为 1,对齐数是 1。偏移量 8 是 1 的倍数,占用 1 字节(8)。 -
计算总大小:当前已使用 9 字节(0-8)。最大对齐数是 4(
int的大小),总大小必须是 4 的倍数,因此增加到 12 ()。
练习 2:struct S2
输出结果:8
-
char c1: 占用 1 字节,放在偏移量 0 处 ()。 -
char c2: 大小为 1,对齐数是 1。偏移量 1 是 1 的倍数,直接存放,占用 1 字节(1)。 -
int i: 大小为 4,对齐数是 4。偏移量 2, 3 不是 4 的倍数,对齐到偏移量 4 处,占用 4 字节(4-7)。 -
计算总大小:当前已使用 8 字节(0-7)。最大对齐数是 4,8 正好是 4 的倍数,因此总大小为 8 ()。
-
优化提示:让占用空间小的成员(如
char)集中在一起可以节省空间 ()()()()。
练习 3:struct S3
输出结果:16
-
double d: 大小为 8,对齐数是 8。放在偏移量 0 处,占用 8 字节(0-7) ()。 -
char c: 大小为 1,对齐数是 1。偏移量 8 是 1 的倍数,占用 1 字节(8)。 -
int i: 大小为 4,对齐数是 4。偏移量 9, 10, 11 不是 4 的倍数,对齐到偏移量 12 处,占用 4 字节(12-15)。 -
计算总大小:当前已使用 16 字节(0-15)。最大对齐数是 8,16 正好是 8 的倍数,因此总大小为 16 ()。
练习 4:结构体嵌套 struct S4
输出结果:32
-
char c1: 占用 1 字节,放在偏移量 0 处 ()。 -
struct S3 s3: 这是一个嵌套结构体。已知S3内部最大的对齐数是 8(来自double)。根据规则,它要对齐到 8 的整数倍,即偏移量 8 处 ()。S3大小为 16,占用 16 字节(8-23)。 -
double d: 大小为 8,对齐数是 8。偏移量 24 正好是 8 的倍数,占用 8 字节(24-31)。 -
计算总大小:当前已使用 32 字节(0-31)。整个结构体中最大的对齐数是 8,32 正好是 8 的倍数,最终大小为 32 ()。
2.2 为什么存在内存对齐?
大部分的参考资料都是这样说的:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:
让占用空间小的成员尽量集中在一起
代码块
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
S1 和S2 类型的成员一模一样,但是S1 和S2 所占空间的大小有了一些区别。
2.3 修改默认对齐数
#pragma 这个预处理指令,可以改变编译器的默认对齐数。
代码块
#include <stdio.h>
#pragma pack(1)//设置默认对齐数为1
struct S
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S));
return 0;
}
结构体在对齐方式不合适的时候,我们可以自己更改默认对齐数。
3. 结构体传参
代码块
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函数。
原因:
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
4. 结构体实现位段
结构体讲完就得讲讲结构体实现位段的能力。
4.1 什么是位段
位段的声明和结构是类似的,有两个不同:
-
位段的成员必须是int、unsigned int 或signed int,在C99中位段成员的类型也可以选择其他整型家族类型,比如:char。
-
位段的成员名后边有一个冒号和一个数字。
比如:
代码块
struct A
{
int _a:2;
int _b:5;
int _c:10;
int _d:30;
};
A就是一个位段类型。
那位段A所占内存的大小是多少?
代码块
printf("%d\n", sizeof(struct A));
4.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;
//空间是如何开辟的?
4.3 位段的跨平台问题
-
int 位段被当成有符号数还是无符号数是不确定的。
-
位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
-
位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
-
当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
4.4 位段的应用
下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。
| 0-15 | 16-31 |
|---|---|
| 4位版本号 | 4位首长度 |
| 16位标识符(identifier) | 3位标志 |
| 32位源IP地址(source address) | |
| 32位目标IP地址(destination address) | |
| 32位选项(若有) | |
| 数据 |
4.5 位段使用的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位置处是没有地址的。内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段的成员输入值,只能是先输入放在一个变量中,然后赋值给位段的成员。
代码块
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
int main()
{
struct A sa = {0};
scanf("%d", &sa._b);//这是错误的
//正确的示范
int b = 0;
scanf("%d", &b);
sa._b = b;
return 0;
}

浙公网安备 33010602011771号