C 语言结构体全解析:声明 + 内存对齐 + 位段 + 传参优化

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

  • 【C语言自定义类型】结构体全解析:声明/内存对齐/位段(含实战案例)📦
    • [前景回顾:数据存储核心速记 📝](#前景回顾:数据存储核心速记 📝)
    • [一、结构体基础:类型声明与核心特性 📝](#一、结构体基础:类型声明与核心特性 📝)
      • [1. 结构体的声明](#1. 结构体的声明)
      • [2. 特殊声明:匿名结构体(仅用一次)](#2. 特殊声明:匿名结构体(仅用一次))
      • [3. 高频考点:结构体的自引用(实现链表核心)](#3. 高频考点:结构体的自引用(实现链表核心))
    • [二、结构体成员访问:. 和 -> 操作符详解 🔍](#二、结构体成员访问:. 和 -> 操作符详解 🔍)
      • [1. 错误案例:传值调用无法修改实参](#1. 错误案例:传值调用无法修改实参)
      • [2. 正确方式:传址调用(高效修改实参)](#2. 正确方式:传址调用(高效修改实参))
    • [三、高频考点:结构体内存对齐(笔试必问) 📏](#三、高频考点:结构体内存对齐(笔试必问) 📏)
      • [1. 内存对齐规则(必须牢记)](#1. 内存对齐规则(必须牢记))
      • [2. 用offsetof宏验证偏移量](#2. 用offsetof宏验证偏移量)
      • [3. 为什么需要内存对齐?(空间换时间)](#3. 为什么需要内存对齐?(空间换时间))
      • 4. 修改默认对齐数(#pragma指令)
    • [四、结构体传参:效率优化的关键 ✨](#四、结构体传参:效率优化的关键 ✨)
    • [五、结构体进阶:位段(节省空间的利器) 🪀](#五、结构体进阶:位段(节省空间的利器) 🪀)
      • [1. 位段的声明规则](#1. 位段的声明规则)
      • [2. 位段的空间优势](#2. 位段的空间优势)
      • [3. 位段的不确定性(跨平台问题)](#3. 位段的不确定性(跨平台问题))
      • [4. 位段的使用注意事项](#4. 位段的使用注意事项)
    • [写在最后 📝](#写在最后 📝)

【C语言自定义类型】结构体全解析:声明/内存对齐/位段(含实战案例)📦

告别基础数据类型,我们正式进入C语言自定义类型的核心------结构体!结构体是描述复杂数据的关键工具,小到存储学生信息,大到实现链表、网络协议,都离不开它。这一篇我们用表格+案例的形式,从结构体的声明、成员访问,到高频考点内存对齐、传参优化,再到进阶的位段实现,全方位拆解,帮你彻底掌握结构体的使用与底层逻辑!

前景回顾:数据存储核心速记 📝

C 语言底层核心:数据在内存中的存储(大小端 + 整数 + 浮点型全解析)

回顾数据存储的核心知识点,能帮我们更好理解结构体的内存布局:

  1. 不同数据类型(char/int/double)占用的内存大小不同,且存在对齐要求。
  2. 多字节数据存在大小端存储差异,会影响结构体成员的内存读取。
  3. 指针存储的是内存地址,占用固定大小(32位平台4字节,64位平台8字节),这是结构体自引用的基础。

一、结构体基础:类型声明与核心特性 📝

结构体是"不同类型数据的集合",和数组(同类型数据集合)形成鲜明对比,用表格对比更清晰:

特性 结构体 数组
成员类型 可以不同 必须相同
核心作用 描述复杂对象(如学生) 存储同类型有序数据
访问方式 通过成员名(. / ->) 通过下标索引
大小计算 遵循内存对齐规则 元素大小 × 元素个数

1. 结构体的声明

最常规的声明方式(带类型名):

c 复制代码
// 声明结构体类型struct Stu
struct Stu
{
    char name[20];  // 姓名
    int age;        // 年龄
    double score;   // 成绩
};  // 分号不能丢!

// 创建结构体变量(三种方式)
struct Stu s1;          // 声明后单独创建
struct Stu s2 = {"lisi", 18, 95.5};  // 创建时初始化
struct Stu s3[3];       // 创建结构体数组

2. 特殊声明:匿名结构体(仅用一次)

如果结构体只需要使用一次,可以省略类型名,直接创建变量,这就是匿名结构体

c 复制代码
#include <stdio.h>

// 匿名结构体类型,直接创建变量A
struct 
{
    int a;
    char b;
    float c;
}A;

// 另一个匿名结构体类型,创建指针ps
struct 
{
    int a;
    char b;
    float c;
}*ps;

int main()
{
    ps = &A;  // 错误!编译器判定两个匿名结构体是不同类型
    return 0;
}

💡 关键提醒:匿名结构体仅能使用一次,即使成员完全相同,编译器也会视为不同类型,无法相互赋值或赋值给指针。

3. 高频考点:结构体的自引用(实现链表核心)

结构体自引用是数据结构的基础,用于实现链表、树等线性/非线性结构。核心是用指针自引用,而非直接包含同类型结构体变量。我们用表格对比正确与错误写法:

写法类型 代码示例 正误判断 核心原因
直接包含变量 struct Node{int data; struct Node n;}; ❌ 错误 结构体包含自身变量,导致无限递归,大小无法计算
包含自身指针 struct Node{int data; struct Node* next;}; ✅ 正确 指针仅存储地址,大小固定,不会引发递归
typedef+指针 typedef struct Node{int data; struct Node* next;}Node; ✅ 正确 先声明struct Node,内部用指针,最后重命名
typedef+提前用Node typedef struct{int data; Node* next;}Node; ❌ 错误 Node重命名在结构体声明后生效,内部无法识别

二、结构体成员访问:. 和 -> 操作符详解 🔍

访问结构体成员有两种核心方式,根据访问对象是"结构体变量"还是"结构体指针"选择,用表格总结用法:

操作符 使用场景 语法格式 等价写法
. 访问结构体变量的成员 结构体变量.成员名 -
-> 访问结构体指针指向的成员 结构体指针->成员名 (*结构体指针).成员名

1. 错误案例:传值调用无法修改实参

很多新手会踩"传值调用修改结构体"的坑,我们通过代码演示:

c 复制代码
#include <stdio.h>
#include <string.h>

struct Stu
{
    char name[20];
    int age;
    double score;
};

// 传值调用:形参ss是实参s的临时拷贝
void set_stu(struct Stu ss)
{
    strcpy(ss.name, "zhangsan");  // 修改的是拷贝的成员
    ss.age = 20;
    ss.score = 100.0;
}

void print_stu(struct Stu ss)
{
    printf("%s %d %lf\n", ss.name, ss.age, ss.score);
}

int main()
{
    struct Stu s = {0};  // 初始化为0
    set_stu(s);          // 传值调用,实参s未被修改
    print_stu(s);        // 输出:(空) 0 0.000000
    return 0;
}

❌ 原因:传值调用时,形参是实参的一份临时拷贝,对形参的修改不会影响实参本身。

2. 正确方式:传址调用(高效修改实参)

想要修改实参的结构体成员,必须传递结构体的地址(传址调用):

c 复制代码
#include <stdio.h>
#include <string.h>

struct Stu
{
    char name[20];
    int age;
    double score;
};

// 传址调用:形参ps指向实参s的地址
void set_stu(struct Stu* ps)
{
    strcpy(ps->name, "zhangsan");  // 等价于 (*ps).name
    ps->age = 20;                  // 等价于 (*ps).age
    ps->score = 100.0;             // 等价于 (*ps).score
}

void print_stu(struct Stu ss)
{
    printf("%s %d %lf\n", ss.name, ss.age, ss.score);
}

int main()
{
    struct Stu s = {0};
    set_stu(&s);  // 传递实参地址
    print_stu(s); // 输出:zhangsan 20 100.000000
    return 0;
}

✅ 核心优势:传址调用仅传递4/8字节的地址,避免了结构体拷贝的空间和时间开销,效率更高。

三、高频考点:结构体内存对齐(笔试必问) 📏

结构体的大小不是成员大小的简单相加,而是遵循内存对齐规则计算的,这是笔试的热门考点。我们先看一个反常识的例子:

c 复制代码
#include <stdio.h>
struct S1
{
    char c1;
    char c2;
    int n;
};
struct S2
{
    char c1;
    int n;
    char c2;
};

int main()
{
    printf("%zd\n", sizeof(struct S1));  // 输出:8
    printf("%zd\n", sizeof(struct S2));  // 输出:12
    return 0;
}

为什么成员类型相同,顺序不同,大小就不一样?答案就是内存对齐

1. 内存对齐规则(必须牢记)

规则序号 规则内容 补充说明
1 结构体第一个成员,对齐到结构体起始地址(偏移量为0) 无例外,所有结构体都遵循
2 其他成员对齐到"对齐数"的整数倍地址处 对齐数 = min(编译器默认对齐数, 成员自身大小);VS默认8,GCC无默认对齐数
3 结构体总大小,是所有成员"最大对齐数"的整数倍 不足的部分会自动填充字节
4 嵌套结构体时,嵌套的结构体对齐到自身内部最大对齐数的整数倍 总大小是外层+内层最大对齐数的整数倍

2. 用offsetof宏验证偏移量

offsetof宏(需包含<stddef.h>)可计算成员相对于结构体起始位置的偏移量,帮我们直观理解内存布局。以struct S2为例:

c 复制代码
#include <stdio.h>
#include <stddef.h>  // offsetof宏的头文件

struct S2
{
    char c1;
    int n;
    char c2;
};

int main()
{
    printf("c1的偏移量:%d\n", offsetof(struct S2, c1));  // 0
    printf("n的偏移量:%d\n", offsetof(struct S2, n));    // 4(对齐到4的整数倍)
    printf("c2的偏移量:%d\n", offsetof(struct S2, c2));  // 8(对齐到4的整数倍)
    return 0;
}

📊 S2的内存布局(VS环境,默认对齐数8):

偏移量区间 0 1-3 4-7 8 9-11
存储内容 c1 填充字节 n c2 填充字节
占用大小 1 3 4 1 3

总大小12,是最大对齐数4的整数倍。

3. 为什么需要内存对齐?(空间换时间)

原因类型 具体说明
平台兼容性 某些硬件平台只能在特定地址(如4的整数倍)读取特定类型数据,否则抛出硬件异常
提升访问效率 未对齐的内存需要处理器访问两次,对齐的内存仅需一次。例如int成员在偏移量1处,需读两次内存再拼接

4. 修改默认对齐数(#pragma指令)

可通过#pragma pack(n)修改默认对齐数,n为新的对齐数;#pragma pack()还原默认:

c 复制代码
#include <stdio.h>
#pragma pack(1)  // 设置默认对齐数为1(取消对齐,紧凑存储)

struct S2
{
    char c1;
    int n;
    char c2;
};

#pragma pack()  // 还原默认对齐数

int main()
{
    printf("%zd\n", sizeof(struct S2));  // 输出:6(1+4+1)
    return 0;
}

四、结构体传参:效率优化的关键 ✨

结构体传参有两种方式:传值调用和传址调用,我们用表格对比两者差异:

传参方式 语法格式 空间开销 效率 能否修改实参
传值调用 函数名(结构体变量) 大(拷贝整个结构体) ❌ 不能
传址调用 函数名(&结构体变量) 小(仅拷贝地址) ✅ 可以(加const可禁止修改)

代码演示对比:

c 复制代码
#include <stdio.h>

struct S
{
    int data[1000];  // 4000字节
    int num;         // 4字节
};

// 传值调用:拷贝整个结构体(4004字节)
void Print1(struct S t)
{
    for (int i = 0; i < 5; i++)
        printf("%d ", t.data[i]);
    printf("%d\n", t.num);
}

// 传址调用:仅拷贝地址(4/8字节)
void Print2(const struct S* ps)  // const保护,避免误修改
{
    for (int i = 0; i < 5; i++)
        printf("%d ", ps->data[i]);
    printf("%d\n", ps->num);
}

int main()
{
    struct S s = {{1,2,3,4,5}, 100};
    Print1(s);  // 效率低,浪费空间
    Print2(&s); // 效率高,推荐使用
    return 0;
}

💡 核心结论:结构体传参时,优先选择传址调用 ,既节省空间,又提升效率;若无需修改结构体,建议用const修饰指针,避免误操作。

五、结构体进阶:位段(节省空间的利器) 🪀

位段是结构体的"紧凑版",通过指定成员占用的二进制位数,实现内存的精准分配,常用于需要节省空间的场景(如网络协议、嵌入式开发)。

1. 位段的声明规则

位段声明和结构体类似,但有两个核心差异,用表格总结:

对比项 普通结构体 位段
成员类型限制 无限制 必须是int/unsigned int/signed int(C99支持char)
成员语法 类型 成员名; 类型 成员名 : 位数;
空间分配 按成员大小+对齐规则分配 按bit位分配,可共用字节
跨平台性 差(存在多种不确定因素)

2. 位段的空间优势

对比普通结构体和位段的大小:

c 复制代码
#include <stdio.h>

// 普通结构体:4个int,共16字节
struct A1
{
    int _a;
    int _b;
    int _c;
    int _d;
};

// 位段:总占用47位→8字节(按4字节/8字节开辟)
struct A2  // 位段
{
    int _a : 2;  // 2位
    int _b : 5;  // 5位
    int _c : 10; // 10位
    int _d : 30; // 30位
};

int main()
{
    printf("A1大小:%zd\n", sizeof(struct A1));  // 16
    printf("A2大小:%zd\n", sizeof(struct A2));  // 8
    return 0;
}

✅ 效果:位段仅用8字节,就实现了普通结构体16字节的功能,空间节省50%!

3. 位段的不确定性(跨平台问题)

不确定点 具体表现 影响
位段存储方向 从左向右或从右向左存储 不同编译器读取结果不同
剩余空间处理 成员超出剩余位时,是否舍弃剩余空间 影响内存利用率
int位段符号性 默认是有符号还是无符号 影响负数存储
最大位数限制 16位机器最大16位,32位机器最大32位 不同平台位数上限不同

4. 位段的使用注意事项

位段成员共用一个字节,没有独立地址,因此:

  • 不能用&取位段成员的地址;
  • 不能直接用scanf给位段成员输入,需先输入到普通变量再赋值。
c 复制代码
#include <stdio.h>

struct A
{
    int _a : 2;
    int _b : 5;
};

int main()
{
    struct A sa = {0};
    // scanf("%d", &sa._b);  // 错误:无法取位段成员地址
    int b = 0;
    scanf("%d", &b);
    sa._b = b;  // 正确:间接赋值
    return 0;
}

写在最后 📝

结构体是C语言描述复杂数据的核心工具,也是连接基础语法和数据结构的桥梁。掌握结构体的关键在于三点:

  1. 理解声明与自引用的逻辑,尤其是指针在自引用中的作用;
  2. 牢记内存对齐规则,能独立计算结构体大小(笔试高频);
  3. 分清传值与传址调用的差异,优先选择高效的传址调用;
  4. 位段是节省空间的利器,但要注意跨平台问题。

这些知识点不仅是日常开发的必备技能,更是笔面试的核心考点。建议大家把文中的案例(尤其是内存对齐和自引用)手动敲一遍,观察运行结果,加深对底层逻辑的理解。下一篇我们将讲解自定义类型的其他成员------枚举和联合,继续拓展C语言的编程边界!

相关推荐
世转神风-2 小时前
qt-文件自动按编号命名
开发语言·qt
lkbhua莱克瓦242 小时前
基础-MySQL概述
java·开发语言·数据库·笔记·mysql
龘龍龙2 小时前
Python基础学习(七)
开发语言·python·学习
wjs20242 小时前
Julia 基本语法
开发语言
MediaTea2 小时前
Python 库手册:wave WAV 音频读写工具
开发语言·python·音视频
写代码的【黑咖啡】2 小时前
python的小型实践项目
开发语言·python
Once_day2 小时前
CC++八股文之基础语法(2)
c语言·c++
lkbhua莱克瓦242 小时前
反射4-反射获取成员变量
java·开发语言·servlet·反射
dawnButterfly2 小时前
C 语言标准、编译器与操作系统的关系
c语言·开发语言·c++