国际访客建议访问 Primers 编程伙伴 国际版站点 > C 教程 > 结构体 struct 以获得更好的体验。

# C 语言的结构体 struct

C 语言中的结构体是一种自定义的数据类型,它允许我们将不同类型的数据组合成一个整体。结构体是 C 语言中实现复杂数据结构的基础,也是面向对象编程中"类"概念的雏形。

结构体的定义使用 struct 关键字:

// 定义结构体类型
struct 结构体类型名
{
    类型 变量名1;
    类型 变量名2;
    类型 变量名3;
    // ...
};

// 定义结构体类型的变量
struct 结构体类型名 变量名;

例如,定义一个表示玩家信息的结构体:

// 定义结构体类型 struct Player
struct Player
{
    char name[64];  // 名称
    int hp;         // 生命值
    int mp;         // 魔法值
};

// 入口函数
int main(void)
{
    // 定义 struct Player 类型的变量 p1 p2
    struct Player p1 = {'Mario', 6, 10};
    struct Player p2 = {'Luigi', 5, 15};
}

这里定义了一个名为 Player 的结构体类型,它包含名称(name),生命值(hp)和魔法值(mp)三个成员。

然后定义了 struct Player 类型的变量 p1p2

  • 前者的名称为 Mario,拥有 6 点生命值和 10 点魔法值

  • 后者名为 Luigi,拥有 5 点生命值和 15 点魔法值

# 访问结构体成员

通过结构体类型的变量访问其内部成员时需要使用 . 运算符,例如:

#include <stdio.h>

// 定义结构体类型 struct Player
struct Player
{
    char name[64];  // 名称
    int hp;         // 生命值
    int mp;         // 魔法值
};

int main(void)
{
    struct Player p1 = {'Mario', 6, 10};

    printf("name: %s\n", p1.name);  // 通过 . 访问成员 name
    printf("hp: %d\n", p1.hp);      // 通过 . 访问成员 hp
    printf("mp: %d\n", p1.mp);      // 通过 . 访问成员 mp

    return 0;
}

通过结构体指针类型的变量访问其内部成员时需要使用 -> 运算符,例如:

#include <stdio.h>

// 定义结构体类型 struct Player
struct Player
{
    char name[64];  // 名称
    int hp;         // 生命值
    int mp;         // 魔法值
};

int main(void)
{
    struct Player p1 = {'Mario', 6, 10};
    struct Player* ptr = &p1;           // 指针指向 p1

    printf("name: %s\n", ptr->name);    // 通过 -> 访问成员 name
    printf("hp: %d\n", ptr->hp);        // 通过 -> 访问成员 hp
    printf("mp: %d\n", ptr->mp);        // 通过 -> 访问成员 mp

    return 0;
}

# 结构体大小与内存对齐

显然,结构体的大小就是其成员大小之和。例如:

#include <stdio.h>

// 定义结构体类型 struct Player
struct Player
{
    char name[64];  // 64 字节
    int hp;         // 4 字节
    int mp;         // 4 字节
};

int main(void)
{
    printf("struct Player 的大小是 %zu 字节\n", sizeof(struct Player));
    return 0;
}

运行结果:

struct Player 的大小是 72 字节

这里 struct Player 的大小为 \(64 + 4 + 4 = 72\) 字节

但是有时候简单的相加结果却不正确。例如:

#include <stdio.h>

// 定义结构体类型 struct Player
struct Player
{
    char name[65];  // 65 字节
    int hp;         // 4 字节
    int mp;         // 4 字节
};

int main(void)
{
    printf("struct Player 的大小是 %zu 字节\n", sizeof(struct Player));
    return 0;
}

运行结果:

struct Player 的大小是 76 字节

这里将数组 name 从 64 个 char 改为了 65 个 char,而 struct Player 并非 \(65 + 4 + 4 = 73\) 字节,而是 76 字节。

这是因为这里发生了内存对齐,导致 namehp 之间空了 3 个字节。

内存对齐(Memory Alignment) 是指数据在内存中的存储位置(地址)必须是某个数值的整数倍。

这样做的目的是提高硬件访问数据的性能。现代计算机硬件中的CPU在数据 自然对齐 的情况下,能够最高效地执行内存读写操作。

所谓 自然对齐 就是 N 字节的数据类型按照 N 字节对齐,即内存地址是 N 的整数倍

上述结构体中的 hp 是 4 个字节,自然对齐即 4 字节对齐,也就是内存地址是 4 的整数倍。

假设 struct Player 的内存地址从 0 开始,那么如果不对齐的话,hp 的内存地址应当是 65。

而 4 字节对齐时,65 不是 4 的正数倍,大于 65 的 4 的最小整数倍数是 68,因此 hp 会空 3 个字节放置到地址 68 的内存位置。

#include <stdio.h>

// 定义结构体类型 struct Player
struct Player
{
    char name[65];  // 65 字节
    int hp;         // 4 字节
    int mp;         // 4 字节
};

int main(void)
{
    struct Player p;
    printf("struct Player 的大小是 %zu 字节\n", sizeof(p));
    printf("p 的地址是 %p\n", &p);
    printf("p.name 的地址是 %p,偏移量为 %td\n", &p.name, (char*)&p.name - (char*)&p);
    printf("p.hp 的地址是 %p,偏移量为 %td\n", &p.hp, (char*)&p.hp - (char*)&p);
    printf("p.mp 的地址是 %p,偏移量为 %td\n", &p.mp, (char*)&p.mp - (char*)&p);
    return 0;
}

运行结果:

struct Player 的大小是 76 字节
p 的地址是 0x7ffd69a20b90
p.name 的地址是 0x7ffd69a20b90,偏移量为 0
p.hp 的地址是 0x7ffd69a20bd4,偏移量为 68
p.mp 的地址是 0x7ffd69a20bd8,偏移量为 72

# 修改对齐方式

可以使用预处理指令 #pragma pack 可以修改对齐方式。

#pragma pack(n)     // 设为 n(1, 2, 4, 8, 16...)字节对齐
#pragma pack()      // 设为默认的对齐方式,通常是自然对齐
#pragma pack(push)  // 保存当前的对齐方式
#pragma pack(pop)   // 恢复之前保存的对齐方式

例如:

#include <stdio.h>

#pragma pack(push)  // 保存当前的对齐方式
#pragma pack(1)     // 设为 1 字节对齐,也就是不对齐

// 定义结构体类型 struct Player
struct Player
{
    char name[65];  // 65 字节
    int hp;         // 4 字节
    int mp;         // 4 字节
};

#pragma pack(pop)   // 恢复之前保存的对齐方式

int main(void)
{
    struct Player p;
    printf("struct Player 的大小是 %zu 字节\n", sizeof(p));
    printf("p 的地址是 %p\n", &p);
    printf("p.name 的地址是 %p,偏移量为 %td\n", &p.name, (char*)&p.name - (char*)&p);
    printf("p.hp 的地址是 %p,偏移量为 %td\n", &p.hp, (char*)&p.hp - (char*)&p);
    printf("p.mp 的地址是 %p,偏移量为 %td\n", &p.mp, (char*)&p.mp - (char*)&p);
    return 0;
}

运行结果:

struct Player 的大小是 73 字节
p 的地址是 0x7ffe1e9ef8c0
p.name 的地址是 0x7ffe1e9ef8c0,偏移量为 0
p.hp 的地址是 0x7ffe1e9ef901,偏移量为 65
p.mp 的地址是 0x7ffe1e9ef905,偏移量为 69

# 结构体的位域

定义结构体类型时,可以在其成员后面通过添加 : bit 的方式注明该字段的位数。

这个功能常用在硬件操作、格式解析等底层开发中。例如:

// 定义 UART 的 SR 寄存器
struct UART1_Register_SR
{
    int PE : 1;     // 奇偶校验错误
    int FE : 1;     // 帧错误
    int NF : 1;     // 噪声标识
    int OR : 1;     // 溢出错误
    int IDLE : 1;   // 空闲
    int RXNE : 1;   // 接收寄存器非空
    int TC : 1;     // 传输完成
    int TXE : 1;    // 发送寄存器非空
};

这里定义了一个结构体类型用于表示 UART(通用异步收发器)的 SR(状态) 寄存器。

该结构体包含 8 个字段,每个字段都只有 1 位;不考虑内存对齐的前提下,该结构体的大小是 1 字节。

本文 更新于: 2025-11-27 09:38:06 创建于: 2025-11-27 09:38:06