指针+结构体

在这里插入图片描述

在讲解结构体之前,需复习指针的以下核心内容,确保能理解结构体的定义、成员访问、传参等关键操作:

一、指针基础:定义、本质与核心操作

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;
    

六、总结

  1. 指针三大核心操作:取地址(&)、解引用(*)、指针偏移(p+i);

  2. 结构体相关指针关键:结构体指针定义、-> 访问成员、自引用需用指针;

  3. 函数传参关键:指针传参=传地址,是结构体传参的最优方案(减少开销);

  4. 后续衔接:掌握以上内容后,可直接学习结构体的定义、初始化、内存对齐、传参、位段等核心知识点,无理解障碍。

结构体

  1. 结构体类型的声明

  2. 结构体变量的创建和初始化

  1. 结构体内存对齐

  2. 结构体传参

  1. 结构体实现位段

正文开始

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 讲,intcharfloat 会按规则排列)。

  • pstu:是一个结构体指针变量。执行 pstu = &s1; 后,pstu 存储了结构体变量 s1 的首地址。


2. 核心:访问成员的三种等价方式

代码中通过三个 printf 展示了三种访问成员的方法,其效果完全相同:

方法 A:通过变量名访问(直观方式)

C

s1.num, s1.name ...
  • 语法结构体变量名.成员名

  • 原理:直接定位到变量 s1 的内存块,根据成员的偏移量(Offset)访问对应数据。

方法 B:通过指针解引用访问(底层逻辑)

C

(*pstu).num, (*pstu).name ...
  • 语法(*结构体指针).成员名

  • 原理

    1. *pstu 先对指针进行解引用,找回指针指向的“本体”——即变量 s1

    2. 再通过 . 访问成员。

  • 注意:必须加圆括号 (),因为点操作符 . 的优先级高于解引用操作符 *

方法 C:通过箭头操作符访问(工程最常用)

C

pstu->num, pstu->name ...
  • 语法结构体指针->成员名

  • 原理:这是 C 语言专门为结构体指针设计的“语法糖”。它等价于 (*pstu).num

  • 优势:书写简洁,在处理链表或复杂数据结构时,比方法 B 更具可读性。


3. 内存与类型细节分析
  1. 关于 name 的地址: 在 printf 中,s1.name(*pstu).namepstu->name 传递给 %s 的都是数组的首地址。根据 PDF 第 13 讲,数组名在此时退化为指向首元素的指针。

  2. 关于 printf 的格式: 代码中定义了宏 #define FORMAT。这里有一个微小的潜在问题:在结构体定义中 scorefloat 类型,但在 FORMAT 中使用了 %lf(通常用于 double)。在 printf 传参时,float 会自动提升为 double,所以程序能正常运行,但在严格的代码规范中,建议保持类型一致。

  3. 传参效率(对比思考): 虽然这个例子只是打印,但根据 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);
}

根据上述代码理解完成下面题目

  1. 基础语法辨析

已知结构体变量 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


  1. 操作符优先级

在表达式 *pstu.name 中,由于操作符优先级的原因,系统会先执行 {{ input(2) }} 操作,这会导致编译错误,因为指针变量本身没有成员。(建议填写:“点”或“.”)


  1. 内存地址与数组名

在 printf(FORMAT, pstu->name) 中,传递给 %s 的实际值是:

{{ select(3) }}

  • A. 字符数组 name 的第一个字符的值(‘X’)

  • B. 整个结构体变量 s1 的起始地址

  • C. 字符数组 name 在内存中的首地址

  • D. 存放指针 pstu 变量自身的地址


  1. 结构体数组与指针进阶

若定义 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


  1. 传参效率分析

根据代码分析,若定义函数 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)->scorep[1].score 都是访问数组第二个元素的标准方式。

    • D 错误原因: 优先级陷阱。*p + 1->score 会被解析为 (*p) + (1->score)

      1. 1->score 是非法操作(整型常量不能使用指向符)。

      2. 即使写成 *(p + 1).score 也会因为点号优先级高而报错。

    • 正确写法应该是: (*(p + 1)).score(p + 1)->score

第 5 题:传参效率分析
  • 答案: B

  • 解析:

    • 值传递(func(s1)): 调用时,系统会在栈上开辟一块等大的空间,并将 s1 中的所有成员(numname 数组、sexscore)逐字节拷贝过去。如果结构体很大,拷贝会消耗大量 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. 结构体的第1个成员对齐到和结构体变量起始位置偏移量为0的地址处。

  2. 从第2个成员变量开始,都要对齐到某个对齐数的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。

    • VS 中默认的值为 8

    • Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小

  1. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。

  2. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

代码块

//练习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)下,输出结果及详细解释如下:

核心对齐规则回顾

  1. 起始位置:第一个成员对齐到偏移量为 0 的地址处 ()。

  2. 成员对齐:其他成员要对齐到“对齐数”(成员大小与默认对齐数中的较小值)的整数倍地址处 ()。

  3. 总大小:结构体总大小必须是“最大对齐数”的整数倍 ()。

  4. 嵌套结构体:嵌套的结构体对齐到其自身成员中最大对齐数的整数倍处 ()。


练习 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 为什么存在内存对齐?

大部分的参考资料都是这样说的:

  1. 平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  1. 性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取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 什么是位段

位段的声明和结构是类似的,有两个不同:

  1. 位段的成员必须是int、unsigned int 或signed int,在C99中位段成员的类型也可以选择其他整型家族类型,比如:char。

  2. 位段的成员名后边有一个冒号和一个数字。

比如:

代码块

struct A
{
    int _a:2;
    int _b:5;
    int _c:10;
    int _d:30;
};

A就是一个位段类型。

那位段A所占内存的大小是多少?

代码块

printf("%d\n", sizeof(struct A));
4.2 位段的内存分配
  1. 位段的成员可以是int、unsigned int、signed int 或者是char 等类型

  2. 位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。

  1. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

代码块

//一个例子
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 位段的跨平台问题
  1. int 位段被当成有符号数还是无符号数是不确定的。

  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

  1. 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。

  2. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:

跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。

4.4 位段的应用

下图是网络协议中,IP数据报的格式,我们可以看到其中很多的属性只需要几个bit位就能描述,这里使用位段,能够实现想要的效果,也节省了空间,这样网络传输的数据报大小也会较小一些,对网络的畅通是有帮助的。

0-1516-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;
}
posted @ 2026-01-16 10:28  dearbi  阅读(0)  评论(0)    收藏  举报  来源