找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2977

积分

0

好友

403

主题
发表于 3 天前 | 查看: 20| 回复: 0

结构体(struct)是由一系列相同或不同类型的数据构成的数据集合。它与 intchar 这类基础数据类型类似,但更为灵活,允许你将多种数据组合成一个新的自定义类型,以便于管理和使用。

在实际的嵌入式项目中,结构体无处不在。由于C语言无法直接操作数据库,开发者常利用结构体来封装一系列属性,将大量数据组织在内存中,从而完成对数据的存储和操作。当我们面对需要多种数据类型共同描述一个实体的场景时,结构体就变得至关重要。例如,一个学生的信息需要学号(字符串)、姓名(字符串)、年龄(整型)等,这些数据类型不同却同属一个整体,结构体正是解决这个问题的理想工具。

结构体在函数中的作用不仅仅是简化代码,其核心价值在于封装。良好的封装提高了代码的复用性,使用者无需关心内部实现细节,只需按照定义使用即可。

需要注意的是,结构体的大小并非其各成员大小的简单相加。出于性能考虑,现代计算机(如32位CPU)通常以4字节为单位高效地存取数据。因此,如果结构体中每个成员的首地址都是特定字节数(如4字节)的整数倍,那么访问效率会更高,这就是内存对齐的由来。

编译器通常有默认的“对齐系数”。开发者可以通过预编译指令 #pragma pack(n) 来修改这个系数,其中 n 可以是1、2、4、8、16等。

内存对齐规则

  1. 数据成员对齐规则:结构体或联合体的第一个数据成员放在偏移量(offset)为0的位置。之后每个数据成员的对齐,将按照 #pragma pack 指定的数值和该成员自身长度两者中较小的那个值进行。
  2. 整体对齐规则:在所有数据成员完成各自对齐后,结构体或联合体本身也要进行对齐。对齐将按照 #pragma pack 指定的数值和结构体中最大数据成员长度两者中较小的那个值进行。
  3. 推论:当 #pragma packn 值大于或等于所有数据成员的长度时,这个 n 值将不起任何作用。

C语言的结构体用于描述对象的状态(属性),但不能包含函数来描述行为。而在C++中,为了保持与C语言的过渡连续性,结构体被扩展为可以包含函数,此时它具有了类的功能,但其成员函数默认是 public 访问权限,与 classprivate 默认权限不同。

结构体声明

下面是一个声明结构体类型的例子:

//声明一个结构体
struct book
{
  char title[MAXTITL]; //一个字符串表示的titile 题目;
  char author[MAXAUTL]; //一个字符串表示的author作者;
  float value; //一个浮点型表示的value价格;
}; //注意分号不能少,这也相当于一条语句;

这个声明描述了一个由两个字符数组和一个 float 变量组成的结构体模板。它本身并未创建任何数据对象,只是定义了这类数据的组织形式。结构体声明也被称为模板,它勾勒了数据存储的蓝图。

声明解析

  1. 使用关键字 struct,表明接下来是一个结构体定义。
  2. 跟随一个可选的标志(或称“标签”,此处是 book),用于后续快速引用该结构体类型。例如:struct book library;
  3. 花括号 {} 内是成员列表,每个成员以自己的声明方式描述,以分号结束。
  4. 结束花括号后的分号表示结构体定义结束。

结构体声明示例与作用域说明

作用域
结构体声明的位置决定了其作用域。如果声明在任何函数之外,则其标记在本文件内该声明之后的所有函数中都可以使用。如果声明在某个函数内部,则其标记只能在该函数内部,且必须在声明之后使用。

结构体在函数内部声明的作用域

关于标志名(标签)的省略
何时可以省略标志名?当你在一个地方定义结构体类型,在另一个地方定义该类型的变量时,必须使用标志名。如果定义结构体类型的同时就创建变量,那么标志名可以省略,但这种定义是一次性的,无法在其他地方复用该类型。

一般格式为:

struct 结构体名 {
    成员变量;
};

C语言结构体定义的三种方式

  1. 最标准的方式(类型说明与定义分开)

    #include <stdio.h>
    struct student //结构体类型的说明与定义分开。声明
    {
      int age;   /*年龄*/
      float score; /*分数*/
      char sex;    /*性别*/
    };
    int main ()
    {
      struct student a = {20, 79, 'f'}; //定义
      printf("年龄:%d 分数:%.2f 性别:%c\n", a.age, a.score, a.sex);
      return 0;
    }
  2. 声明时直接定义变量(不推荐,复用性差)

    #include <stdio.h>
    struct student /*声明时直接定义*/
    {
      int age;   /*年龄*/
      float score;  /*分数*/
      char sex;    /*性别*/
      /*这种方式不环保,只能用一次*/
    } a = {21, 80, 'n'};
    int main ()
    {
      printf("年龄:%d 分数:%.2f 性别:%c\n", a.age, a.score, a.sex);
    }
  3. 直接定义结构体变量(无类型名,最不推荐)

    #include <stdio.h>
    struct //直接定义结构体变量,没有结构体类型名。这种方式最烂
    {
      int age;
      float score;
      char sex;
    } t = {21, 79, 'f'};
    int main ()
    {
      printf("年龄:%d 分数:%f 性别:%c\n", t.age, t.score, t.sex);
      return 0;
    }

定义结构体变量

结构体类型的定义只是告诉编译器数据的表示方法,并不会分配内存。要使用结构体,必须创建变量,即结构体变量

struct book library; 这条语句创建了一个 struct book 类型的变量 library,此时编译器才按照 book 模板为其分配内存空间。

分析
struct book 在声明中的作用类似于 int 这样的基础类型名。struct book s1, s2, *ss; 定义了两个结构体变量和一个指向该结构体的指针

struct book library; 等效于:

struct book{
    char …;
    ….;
    …..;
} library;

第一种写法可以减少重复代码量。

标志符何时可以省略?
情况一

struct {
    char title[MAXTITL];
    char author[MAXAUTL];
    float value;
} library;

这里直接创建了结构体变量 library,编译器会分配内存。这本质上是将声明类型和定义变量合并了,且成员未初始化。由于没有类型名,此结构体“模板”无法在其他地方复用。

情况二:使用 typedef 为已有类型(包括结构体)定义新名称。
一般格式:typedef 已有类型 新类型名;

typedef int Elem;
typedef struct {
    int date;
    .....
} STUDENT;
STUDENT stu1, stu2; // 使用新类型名定义变量

typedef定义结构体类型示例

结构体变量定义总结

  1. 先定义类型,后定义变量
    struct 结构体名 变量名列表;
    struct book s1, s2, *ss; // 需先定义struct book类型
  2. 定义类型的同时定义变量
    struct 结构体名 {
        成员列表;
    } 变量名列表; // 结构体名可以省略,但不建议
  3. 直接定义结构体类型变量(即方式二省略结构体名)
    这种方式无法指明结构体类型名,因此该类型无法重复使用。除非再次书写完全相同的 struct { ... } 定义。

结构体变量的初始化

初始化结构体变量与初始化数组类似,使用花括号括起逗号分隔的初始化列表。每个初始化项目必须与对应成员的类型匹配。

struct book s1 = { //对结构体初始化
    "yuwen",    //title为字符串
    "guojiajiaoyun", //author为字符数组
    22.5        //value为float型
};

注意

  • 如果结构体变量具有静态存储期(如全局变量或 static 变量),初始化列表中的值必须是常量表达式。
  • 只能在定义变量时进行整体初始化。定义之后,无法再使用 s1 = { ... }; 这样的方式整体赋值,只能对成员逐个赋值。
    struct book s1;
    // s1 = { “guojiajiaoyun", "yuwen", 22.5 }; // 错误!
    s1.value = 22.5; // 正确,逐个赋值

C99标准支持指定初始化(Designated Initializers),可以不按顺序初始化成员,但某些编译器(特别是嵌入式领域的旧版本编译器)可能不支持。
C99指定初始化示例与编译器错误

访问结构体成员

使用结构成员运算符点(.) 访问成员:结构体变量名.成员名
该运算符结合性为自左向右,优先级最高。

printf(“%s\n%s\n%f”, s1.title, s1.author, s1.value); //访问结构体变量元素

s1.valuefloat 类型,可以像普通 float 变量一样使用。
注意 scanf(“%d”, &s1.value); 中,. 的优先级高于 &,因此无需括号。

嵌套访问:如果成员本身是结构体类型,可通过多级 . 运算符访问。
结构体变量名.成员.子成员……最低一级子成员;

struct date {
    int year;
    int month;
    int day;
};
struct student {
    char name[10];
    struct date birthday;
} student1;
// 引用出生年份:student1.birthday.year;

结构体变量的整体与分开操作

  • 整体赋值:允许将同类型的一个结构体变量整体赋值给另一个。
  • 禁止整体I/O:不能将结构体变量作为一个整体进行输入 (scanf) 或输出 (printf),必须分别指明其各成员。

结构体整体赋值示例
小结:除“同类型结构体变量可整体赋值”外,其他情况下不能整体引用,必须对成员分别引用。

结构体长度与内存对齐

数据类型的字节数因编译器位数而异(常见16位/32位/64位)。思考下例结构体占几个字节?

typedef struct {
    char addr;
    char name;
    int  id;
} PERSON;

通过 printf("PERSON长度=%d字节\n", sizeof(PERSON)); 可看到结果为 8字节,而非 1+1+4=6 字节。这就是内存对齐的结果。

结构体PERSON长度为8字节

内存对齐实验

  1. 定义字符数组并初始化:
    char ss[20] = {0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29};
  2. PERSON 指针指向该数组:
    PERSON *ps = (PERSON *)ss;
  3. 打印成员:
    printf(“0x%02x,0x%02x,0x%02x\n”, ps->addr, ps->name, ps->id);
    printf(“PERSON长度=%d字节\n”, sizeof(PERSON));

    内存对齐实验输出1

可以看到 addrname 各占1字节,但因 idint 型需要4字节对齐,所以编译器在它们之后插入了2字节的“空洞”(Padding),然后才是 id 的值 0x17161514

内存布局表格1

调整成员顺序以优化空间
如果将结构体定义为:

typedef struct {
    char addr;
    int  id;
    char name;
} PERSON;

输出显示总长度变为 12字节
内存对齐实验输出2
内存布局表格2

如果定义为:

typedef struct {
    int  id;
    char addr;
    char name;
} PERSON;

输出显示总长度变回 8字节
内存对齐实验输出3
内存布局表格3

结论:合理安排结构体成员的顺序,可以节省内存空间。通常将要求对齐字节数大的成员(如 int, double)放在前面,对齐字节数小的成员(如 char)放在后面。

如果所有成员都是 char 型,则会按照1字节对齐,总大小就是成员数量之和。

typedef struct {
    char addr;
    char name;
    char id;
} PERSON;

全char成员结构体长度
全char成员内存布局

结构体嵌套

结构体可以嵌套另一个结构体。

typedef struct {
    char addr;
    char name;
    int  id;
} PERSON;

typedef struct {
    char age;
    PERSON ps1;
} STUDENT;

通过指针和数组进行内存布局分析:

STUDENT *stu = (STUDENT *)ss;
printf(“0x%02x,0x%02x,0x%02x,0x%02x\n”, stu->ps1.addr, stu->ps1.name, stu->ps1.id, stu->age);
printf(“STUDENT长度=%d字节\n”, sizeof(STUDENT));

嵌套结构体STUDENT内存布局1
嵌套结构体内存表格1

调整 STUDENT 成员顺序:

typedef struct {
    PERSON ps1;
    char age;
} STUDENT;

嵌套结构体STUDENT内存布局2
嵌套结构体内存表格2

嵌套结构体访问遵循规律:

// 方式一:定义时同时创建变量
struct A {
    struct B {
        int c;
    } b;
} a;
a.b.c = 10;

// 方式二:定义内部结构体B后,再定义B的变量
struct A {
    struct B {
        int c;
    };
    struct B sb;
} a;
a.b.c = 11;   // 访问匿名结构体成员b
a.sb.c = 22;  // 访问具名结构体变量sb

结构体占用内存空间总结

  • 结构体定义不分配内存,定义变量时才分配。
  • 大小通常是各成员大小之和,但受内存对齐影响。对齐系数通常是4字节(32位机)或8字节(64位机),以提高访问效率。可使用 #pragma pack(n) 修改对齐方式,n=1 时即为紧凑排列。
  • 和C++类不同,C语言结构体不能在定义时直接初始化成员变量(C99的指定初始化除外)。

为什么函数参数常使用结构体指针?

当函数参数较多时,使用多个独立参数会导致代码重复和调用繁琐。

int handle_video(char *name, long address, int size, time_t time, int alg);

更优雅的方式是使用结构体封装所有相关参数:

struct video_info {
    char *name;
    long address;
    int size;
    int alg;
    time_t time;
};
int handle_video(struct video_info *vinfo);

使用指针的好处

  1. 避免数据拷贝:当结构体较大时,传值(struct video_info vinfo)会导致整个结构体数据被复制到函数栈帧中,开销大。传递指针只复制一个地址,效率更高。
  2. 允许函数修改实参:如果函数需要修改结构体内容,必须使用指针。
  3. 代码简洁:调用时只需传递一个结构体变量的地址,大大简化了代码。
    因此,即使函数(如 handle_video, send_video)不需要修改参数内容,从性能角度考虑,也常使用 const struct video_info *vinfo 形式的指针参数。

嵌入式开发中的位结构体(位域)

在嵌入式开发中,位结构体常用于高效表示硬件寄存器状态或进行紧凑的数据通信。它允许我们按位(bit)来定义结构体成员的长度。

1. 位结构体类型设计

typedef struct symbol_struct {
    uint_32 SYMBOL_TYPE   : 5;  // 5位
    uint_32 reserved_1    : 4;  // 4位
    uint_32 SYMBOL_NUMBER : 7;  // 7位
    uint_32 SYMBOL_ACTIVE : 1;  // 1位
    uint_32 SYMBOL_INDEX  : 8;  // 8位
    uint_32 reserved_2    : 8;  // 8位
} SYMBOL_STRUCT, *SYMBOL_STRUCT_PTR;

关键点:成员的数据类型(如 uint_32)决定了编译器分配内存的基本单位,而后面的位域指定了在该单位内使用的有效位数。

位结构体调试窗口查看内存地址
观察发现,前5个成员共占33位,超过了4字节(32位),因此 reserved_2 被分配到了新的地址空间(0x1fff0830)。同时,reserved_2 虽然只用了8位,但因其类型是 uint_32,所以实际占用了4个字节。

2. 修改数据类型为 uint_8

typedef struct symbol_struct {
    uint_8 SYMBOL_TYPE    : 5;
    uint_8 reserved_1     : 4;
    uint_8 SYMBOL_NUMBER  : 7;
    uint_8 SYMBOL_ACTIVE  : 1;
    uint_8 SYMBOL_INDEX   : 8;
    uint_8 reserved_2     : 8;
} SYMBOL_STRUCT, *SYMBOL_STRUCT_PTR;

将类型改为 uint_8(1字节)后,编译器以1字节为单位进行分配。
uint_8类型的位结构体内存地址
此时,reserved_2 只占用1个字节。SYMBOL_TYPEreserved_1 共9位,超过1字节,因此它们被分配在两个不同的字节地址上。

3. 分析位域的紧凑性

当所有位域的总位数不超过其基础类型(如 uint_32)的大小时,编译器会尝试将它们紧凑地排列在同一块内存中。
紧凑排列的位结构体调试信息
此例中,所有位域加起来正好32位,因此它们被紧密打包在4个字节内。通过联合体(union)和直接赋值可以验证位域的跨越存储。

typedef union {
    uint32 WORD;
    struct {
        uint32 LAYER_LEVEL : 2;
        uint32 OBJECT_ACTIVE_CUR : 4;
        uint32 OBJECT_ACTIVE_PRE : 4; // 赋值为 0b1111
        uint32 NUMBER_ACTIVE : 4;     // 赋值为 0b0101
        // ... 其他位域
    } BIT;
} STATUS_STRUCT;

通过联合体验证位域紧凑排列
可以看到 OBJECT_ACTIVE_PRENUMBER_ACTIVE 的位域在内存中是连续紧凑存放的,可能跨越字节边界。

4. 混合类型与空隙

如果位域的基础类型混合了 uint_8uint_32,或者位域无法紧凑放入当前分配单元,就会产生“空隙”。
混合类型位结构体产生空隙
图中 RESERVED 成员已不在前4字节的地址空间内,说明中间存在空隙。

5. 结构体/联合体组合的影响

外层是普通结构体还是联合体,并不影响内部位结构体的内存分配策略。
结构体与位域组合
联合体与位域组合

总结与注意事项

  1. 空间分配单位:位域的基础数据类型(uint_8uint_32等)决定了编译器分配内存的最小单位。位域只是指在该单位内使用的有效位数。
  2. 紧凑存储:编译器会尽可能将位域紧密排列在当前分配的内存单位内,直到放不下,再分配新的单位。
  3. 位域大小限制:位域长度不能超过其基础类型的位数(如 uint_8 test : 15 会编译报错)。
  4. 平台与编译器差异最重要的一点:位结构体的具体内存布局高度依赖于编译器、操作系统以及编译选项。上述结论基于特定环境(CodeWarrior 10.2, MQX 3.8)得出,在其他环境下可能不同。在涉及跨平台或硬件直接操作时,务必通过调试工具(如查看变量地址)仔细验证内存布局。

掌握结构体,特别是理解其内存对齐和位域机制,对于编写高效、紧凑的嵌入式C代码至关重要。如果你想了解更多关于内存管理或底层开发的细节,可以关注云栈社区,那里有更多深入的讨论和资源分享。




上一篇:集成PD快充与半桥驱动的CH32M030 MCU,如何简化BLDC电机控制设计
下一篇:深入解析嵌入式C语言:结构体(struct)的应用与最佳实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-5 18:48 , Processed in 0.554469 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表