D17—C语言结构体详解:从声明、对齐到位段应用

文章目录

引言

[1. 结构体类型的声明](#1. 结构体类型的声明)

[1.1 结构体回顾](#1.1 结构体回顾)

[1.1.1 结构体的声明](#1.1.1 结构体的声明)

[1.2 结构体变量的创建和初始化](#1.2 结构体变量的创建和初始化)

[1.3 匿名结构体类型](#1.3 匿名结构体类型)

[1.4 结构体的自引用](#1.4 结构体的自引用)

[2. 结构体内存对齐](#2. 结构体内存对齐)

[2.1 对齐规则](#2.1 对齐规则)

[2.2 练习与分析](#2.2 练习与分析)

[2.3 为什么存在内存对齐?](#2.3 为什么存在内存对齐?)

[2.4 修改默认对齐数](#2.4 修改默认对齐数)

[3. 结构体传参](#3. 结构体传参)

[4. 结构体实现位段](#4. 结构体实现位段)

[4.1 什么是位段?](#4.1 什么是位段?)

[4.2 位段的内存分配](#4.2 位段的内存分配)

[4.3 位段的应用](#4.3 位段的应用)

[4.4 位段使用的注意事项](#4.4 位段使用的注意事项)

总结

使用建议:


引言

在C语言中,结构体是一种非常重要的自定义数据类型,它允许我们将不同类型的数据组合成一个整体,为复杂数据的组织和管理提供了极大的便利。本文将全面讲解结构体的声明、使用、内存对齐规则、传参方式以及位段的应用,帮助你深入理解结构体的底层原理和实际应用。

1. 结构体类型的声明

1.1 结构体回顾

结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.1.1 结构体的声明
cpp 复制代码
struct tag {
    member-list;
} variable-list;

示例:描述一个学生

cpp 复制代码
struct Stu {
    char name[20]; // 名字
    int age;       // 年龄
    char sex[5];   // 性别
    char id[20];   // 学号
}; // 分号不能丢

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

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

struct Stu {
    char name[20];
    int age;
    char sex[5];
    char id[20];
};

int main() {
    // 按照结构体成员的顺序初始化
    struct Stu s1 = {"张三", 20, "男", "20230818001"};
    
    // 按照指定的顺序初始化
    struct Stu s2 = {.age = 18, .name = "李四", .id = "20230818002", .sex = "女"};
    
    printf("s1: %s, %d, %s, %s\n", s1.name, s1.age, s1.sex, s1.id);
    printf("s2: %s, %d, %s, %s\n", s2.name, s2.age, s2.sex, s2.id);
    
    return 0;
}

1.3 匿名结构体类型

在声明结构体时可以省略标签(tag),这称为匿名结构体类型:

cpp 复制代码
// 匿名结构体类型
struct {
    int a;
    char b;
    float c;
} x;

struct {
    int a;
    char b;
    float c;
} a[20], *p;

注意 :编译器会将上面的两个声明当作完全不同的两个类型,因此 p = &x; 是非法的。匿名结构体类型如果没有重命名,基本上只能使用一次,同时只有在声明时才能创建变量,其他情况下无法创建变量。

1.4 结构体的自引用

在结构体中包含一个类型为该结构本身的成员(用于链表等数据结构):

cpp 复制代码
// 错误方式:会导致无限大小
struct Node {
    int data;
    struct Node next; // 错误!
};

// 正确方式:使用指针
struct Node {
    int data;
    struct Node* next; // 正确
};

第一种方式相当于无限的嵌套,会导致无限大小,我们选择创建一个对应的结构体指针,指向下一个节点,这样就使得两个节点之间产生了联系。

注意:使用typedef时要避免匿名结构体自引用的问题:

cpp 复制代码
// 错误方式
typedef struct {
    int data;
    Node* next; // 错误:Node尚未定义
} Node;

// 正确方式
typedef struct Node {
    int data;
    struct Node* next; // 使用完整的类型名
} Node;

2. 结构体内存对齐

结构体的大小计算是C语言中的一个重要考点,涉及内存对齐规则。

2.1 对齐规则

  1. 结构体的第一个成员对齐到结构体变量起始位置偏移量为0的地址处

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍地址处

    • 对齐数 = 编译器默认对齐数与该成员大小的较小值

    • VS中默认对齐数为8,Linux中gcc没有默认对齐数

  3. 结构体总大小为最大对齐数(所有成员中对齐数的最大值)的整数倍

  4. 如果嵌套结构体,嵌套的结构体成员对齐到自己的最大对齐数的整数倍处,结构体整体大小是所有最大对齐数(包括嵌套结构体成员的对齐数)的整数倍

2.2 练习与分析

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

// 练习1
struct S1 {
    char c1;
    int i;
    char c2;
};

// 练习2
struct S2 {
    char c1;
    char c2;
    int i;
};

// 练习3
struct S3 {
    double d;
    char c;
    int i;
};

// 练习4 - 结构体嵌套
struct S4 {
    char c1;
    struct S3 s3;
    double d;
};

int main() {
    printf("sizeof(struct S1) = %zu\n", sizeof(struct S1)); // 12
    printf("sizeof(struct S2) = %zu\n", sizeof(struct S2)); // 8
    printf("sizeof(struct S3) = %zu\n", sizeof(struct S3)); // 16
    printf("sizeof(struct S4) = %zu\n", sizeof(struct S4)); // 32
    return 0;
}

内存布局分析(以S1为例,假设int为4字节):

  • c1:偏移0,大小1字节

  • i:对齐数为4,需要对齐到4的倍数,偏移4,大小4字节

  • c2:对齐数为1,偏移8,大小1字节

  • 总大小需要是最大对齐数(4)的整数倍,9→12字节

2.3 为什么存在内存对齐?

  1. 平台原因(移植性):不是所有硬件都能访问任意地址的任意数据,某些硬件平台只能在特定地址访问特定类型数据

  2. 性能原因:对齐的内存访问只需一次内存操作,未对齐的内存可能需要两次访问

空间优化技巧:将占用空间小的成员尽量集中在一起,可以节省空间。

2.4 修改默认对齐数

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

#pragma pack(1) // 设置默认对齐数为1
struct S {
    char c1;
    int i;
    char c2;
};
#pragma pack() // 恢复默认对齐数

int main() {
    printf("%zu\n", sizeof(struct S)); // 6(1+4+1)
    return 0;
}

3. 结构体传参

结构体传参时,应优先传递指针而非结构体本身,以节省栈空间和时间开销。

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

struct S {
    int data[1000];
    int num;
};

// 传值:复制整个结构体,开销大
void print1(struct S s) {
    printf("%d\n", s.num);
}

// 传址:只传递指针,高效
void print2(struct S* ps) {
    printf("%d\n", ps->num);
}

int main() {
    struct S s = {{1, 2, 3, 4}, 1000};
    
    print1(s);  // 传值,不推荐
    print2(&s); // 传址,推荐
    
    return 0;
}

结论:结构体传参时,应传递结构体的地址。

4. 结构体实现位段

4.1 什么是位段?

位段的声明与结构体类似,但有两个不同:

  1. 位段成员必须是整型(int、unsigned int、signed int或char)

  2. 位段成员名后有一个冒号和一个数字,表示该成员占用的位数

cpp 复制代码
struct A {
    int _a:2;   // 使用2个bit
    int _b:5;   // 使用5个bit
    int _c:10;  // 使用10个bit
    int _d:30;  // 使用30个bit
};

4.2 位段的内存分配

cpp 复制代码
struct S {
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};

int main() {
    printf("sizeof(struct S) = %zu\n", sizeof(struct S)); // 3字节
    
    struct S s = {0};
    s.a = 10;  // 10的二进制1010,但只有3位,取010=2
    s.b = 12;  // 12的二进制1100,但只有4位,取1100=12
    s.c = 3;   // 3的二进制11,但只有5位,取00011=3
    s.d = 4;   // 4的二进制100,但只有4位,取0100=4
    
    return 0;
}

位段的特性

  1. 空间按需以4字节(int)或1字节(char)方式开辟

  2. 位段涉及很多不确定因素,不具可移植性

  3. 位段成员在内存中的分配顺序(从左到右或从右到左)标准未定义

  4. 当一个结构体包含两个位段,第二个位段较大无法容纳于第一个位段剩余位时,是否舍弃剩余位不确定

4.3 位段的应用

位段能够有效节省空间,在网络协议等场景中广泛应用。例如IP数据报格式中很多属性只需几个bit位就能描述,使用位段可以减少数据报大小,提高网络传输效率。

4.4 位段使用的注意事项

  1. 不能取地址:位段的几个成员可能共享一个字节,有些成员的起始位置不是字节起始位置,这些位置没有地址

  2. 不能直接使用scanf输入:需要先输入到变量,再赋值给位段成员

cpp 复制代码
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;
}

总结

使用建议:

  1. 结构体设计:考虑内存对齐,将小成员集中放置以节省空间

  2. 结构体传参:传递指针而非结构体本身

  3. 位段使用:仅在需要节省空间且不关心可移植性时使用

  4. 跨平台开发:避免使用位段,或明确处理平台差异

掌握结构体的原理和使用技巧,能够帮助你编写更高效、更健壮的C语言程序,特别是在系统编程、嵌入式开发和网络编程等领域。


欢迎在评论区交流讨论,如果觉得有帮助,请点赞收藏支持!

更多C语言技术文章,请访问我的博客主页:我能坚持多久-CSDN博客

相关推荐
进击的小头2 小时前
行为型模式:策略模式的C语言实战指南
c语言·开发语言·策略模式
天马37983 小时前
Canvas 倾斜矩形绘制波浪效果
开发语言·前端·javascript
Tansmjs3 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
qx093 小时前
esm模块与commonjs模块相互调用的方法
开发语言·前端·javascript
Suchadar3 小时前
if判断语句——Python
开发语言·python
爱编码的小八嘎3 小时前
C语言对话-5.通过任何其他名字
c语言
莫问前路漫漫4 小时前
WinMerge v2.16.41 中文绿色版深度解析:文件对比与合并的全能工具
java·开发语言·python·jdk·ai编程
九皇叔叔4 小时前
【03】SpringBoot3 MybatisPlus BaseMapper 源码分析
java·开发语言·mybatis·mybatis plus
00后程序员张5 小时前
对比 Ipa Guard 与 Swift Shield 在 iOS 应用安全处理中的使用差异
android·开发语言·ios·小程序·uni-app·iphone·swift
偷星星的贼115 小时前
C++中的对象池模式
开发语言·c++·算法