C语言自定义类型详解:结构体与位段

在C语言中,基础数据类型(如intcharfloat等)难以描述复杂对象(如学生、链表节点等)。结构体(Struct)作为自定义类型的核心,允许我们将不同类型的数据组合成一个整体,极大地扩展了C语言的描述能力。本文将从结构体的声明、初始化、内存对齐,到结构体传参和位段实现,全面解析这一重要知识点。

一、结构体类型的声明

结构体是“值的集合”,其成员可以是不同类型的变量。声明结构体的本质是定义一种新的“数据类型”,后续可通过该类型创建变量。

1.1 基本声明格式

结构体的声明语法如下:

struct tag {
    member-list;  // 成员列表(类型+变量名)
} variable-list;  // 可选:直接声明变量

其中:

  • tag是结构体标签(自定义名称),用于标识结构体类型;
  • member-list是结构体成员,由多个不同类型的变量组成;
  • variable-list是可选的,可在声明结构体时直接创建变量。

示例:描述学生信息

struct Stu {
    char name[20];  // 姓名
    int age;        // 年龄
    char sex[5];    // 性别
    char id[20];    // 学号
};  // 分号不能省略!

上述代码声明了一个名为struct Stu的结构体类型,后续可通过该类型创建变量(如struct Stu s;)。

1.2 特殊声明:匿名结构体

声明结构体时可省略标签(tag),称为“匿名结构体”。匿名结构体仅能在声明时创建变量,无法后续复用(除非通过typedef重命名)。

示例:匿名结构体的使用

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

// 另一个匿名结构体,成员与上面相同
struct {
    int a;
    char b;
    float c;
} a[20], *p;

⚠️ 注意:编译器会将上述两个匿名结构体视为完全不同的类型,因此以下代码是非法的:

p = &x;  // 警告:类型不兼容

1.3 结构体的自引用

结构体中可以包含指向自身类型的指针(用于实现链表、树等数据结构),但不能直接包含自身类型的变量(会导致无限递归,内存大小无法计算)。

错误示例:直接包含自身变量
struct Node {
    int data;
    struct Node next;  // 错误!结构体大小会无限增大
};
正确示例:通过指针自引用
struct Node {
    int data;          // 数据域
    struct Node* next; // 指针域:指向同类型结构体
};
结合typedef的自引用

使用typedef重命名结构体时,需避免在匿名结构体内部提前使用新名称:

// 错误写法:匿名结构体内部无法识别Node
typedef struct {
    int data;
    Node* next;  // 错误!Node尚未定义
} Node;

// 正确写法:先声明带标签的结构体
typedef struct Node {
    int data;
    struct Node* next;  // 正确:使用struct Node
} Node;  // 重命名为Node

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

声明结构体类型后,可通过该类型创建变量并初始化。初始化方式分为“按顺序初始化”和“指定成员初始化”。

2.1 按顺序初始化

按结构体成员的声明顺序依次赋值,适用于成员较少或顺序明确的场景。

示例代码

#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);  // name: 张三
    printf("age : %d\n", s.age);   // age : 20
    printf("sex : %s\n", s.sex);   // sex : 男
    printf("id : %s\n", s.id);     // id : 20230818001
    return 0;
}

2.2 指定成员初始化

通过.成员名指定初始化的成员,顺序可以任意,未指定的成员会被初始化为0(或空)。

示例代码

int main() {
    // 按指定成员初始化(顺序无关)
    struct Stu s2 = {
        .age = 18,
        .name = "lisi",
        .id = "20230818002",
        .sex = "女"
    };
    
    // 访问成员并打印
    printf("name: %s\n", s2.name);  // name: lisi
    printf("age : %d\n", s2.age);   // age : 18
    printf("sex : %s\n", s2.sex);   // sex : 女
    printf("id : %s\n", s2.id);     // id : 20230818002
    return 0;
}

三、结构成员访问操作符

访问结构体成员需使用以下两种操作符:

  • .:用于直接访问结构体变量的成员(结构体变量.成员名);
  • ->:用于通过指针访问结构体成员(结构体指针->成员名)。

示例代码

struct Stu s = {"张三", 20, "男", "20230818001"};
struct Stu* ps = &s;

// 直接访问
printf("name: %s\n", s.name);  // 使用.操作符

// 指针访问
printf("age: %d\n", ps->age);  // 使用->操作符

四、结构体内存对齐(核心考点)

计算结构体大小是C语言的高频考点,其核心是内存对齐规则。理解对齐规则需先明确“偏移量”(成员地址与结构体起始地址的差值)和“对齐数”(成员自身大小与编译器默认对齐数的较小值)。

4.1 对齐规则

  1. 结构体第一个成员的偏移量为0;
  2. 其他成员的偏移量必须是其“对齐数”的整数倍(对齐数 = min(编译器默认对齐数, 成员大小));
  3. 结构体总大小是所有成员“最大对齐数”的整数倍;
  4. 嵌套结构体时,嵌套成员的偏移量需是其内部最大对齐数的整数倍,总大小是所有最大对齐数(含嵌套)的整数倍。

⚠️ 编译器默认对齐数:

  • VS中默认是8;
  • Linux gcc中默认无对齐数(对齐数 = 成员自身大小)。

4.2 实战练习:计算结构体大小

通过以下练习理解对齐规则:

练习1
struct S1 {
    char c1;  // 大小1,对齐数min(8,1)=1,偏移量0
    int i;    // 大小4,对齐数min(8,4)=4,偏移量4(前空3字节)
    char c2;  // 大小1,对齐数1,偏移量8
};
// 最大对齐数是4,总大小需为4的整数倍 → 12(8+1=9,向上取12)
printf("%d\n", sizeof(struct S1));  // 输出:12
练习2
struct S2 {
    char c1;  // 偏移量0
    char c2;  // 偏移量1(对齐数1)
    int i;    // 对齐数4,偏移量4(前空2字节)
};
// 最大对齐数4,总大小8(4+4=8)
printf("%d\n", sizeof(struct S2));  // 输出:8
练习3(含double类型)
struct S3 {
    double d;  // 大小8,对齐数8(VS中),偏移量0
    char c;    // 对齐数1,偏移量8
    int i;     // 对齐数4,偏移量12(8+1=9,空3字节到12)
};
// 最大对齐数8,总大小16(12+4=16)
printf("%d\n", sizeof(struct S3));  // 输出:16
练习4(嵌套结构体)
struct S4 {
    char c1;       // 偏移量0
    struct S3 s3;  // S3最大对齐数8,偏移量8(空7字节)
    double d;      // 对齐数8,偏移量8+16=24
};
// 最大对齐数8,总大小32(24+8=32)
printf("%d\n", sizeof(struct S4));  // 输出:32

4.3 为什么需要内存对齐?

内存对齐是“空间换时间”的设计:

  1. 平台兼容性:部分硬件仅能访问特定地址的数据(如只能从4的倍数地址读int);
  2. 性能优化:对齐数据可减少CPU访问次数(未对齐数据可能需要2次访问)。

4.4 优化结构体空间

为节省空间,应将占用空间小的成员集中存放。例如:

// 优化前:大小12
struct S1 { char c1; int i; char c2; };
// 优化后:大小8(成员c1和c2集中)
struct S2 { char c1; char c2; int i; };

4.5 修改默认对齐数

使用#pragma pack(n)可修改默认对齐数(n为1、2、4、8等),#pragma pack()还原默认。

示例代码

#include <stdio.h>
#pragma pack(1)  // 设置默认对齐数为1(无对齐)
struct S {
    char c1;  // 偏移量0
    int i;    // 偏移量1(对齐数1)
    char c2;  // 偏移量5
};
#pragma pack()  // 还原默认

// 总大小:1+4+1=6
printf("%d\n", sizeof(struct S));  // 输出:6

五、结构体传参

结构体传参有两种方式:传值(结构体变量)和传地址(结构体指针)。

5.1 两种传参方式对比

struct S {
    int data[1000];  // 大数组,占用较多空间
    int num;
};
struct S s = {{1,2,3,4}, 1000};

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

// 方式2:传地址(仅复制指针)
void print2(struct S* ps) {
    printf("%d\n", ps->num);
}

int main() {
    print1(s);  // 传值:开销大(复制1004*4字节)
    print2(&s); // 传地址:开销小(仅复制指针大小)
    return 0;
}

5.2 结论

优先选择传地址:函数传参时参数需压栈,结构体过大时传值会导致严重的时间和空间开销。

六、结构体实现位段

位段(Bit Field)是结构体的特殊形式,用于按位分配内存,可极大节省空间(适用于只需要少数bit的场景)。

6.1 位段的声明

位段声明与结构体类似,但成员后多了“:数字”(表示占用的bit数):

struct A {
    int _a:2;   // 占用2个bit
    int _b:5;   // 占用5个bit
    int _c:10;  // 占用10个bit
    int _d:30;  // 占用30个bit
};

6.2 位段的内存分配

  1. 成员类型通常为intunsigned intchar等;
  2. 空间按4字节(int)或1字节(char) 开辟;
  3. 未用完的bit可能被复用或舍弃(取决于编译器)。

示例:计算位段大小

// struct A的成员总bit数:2+5+10+30=47bit
// 按4字节(32bit)开辟:第1个32bit存_a(2)+_b(5)+_c(10)=17bit,剩余15bit不够存_d(30);
// 再开辟第2个32bit存_d(30),总大小为8字节。
printf("%d\n", sizeof(struct A));  // 输出:8

6.3 位段的跨平台问题

位段依赖编译器实现,存在以下跨平台风险:

  1. int位段的符号性不确定(有符号/无符号);
  2. 最大bit数不确定(16位机器最大16,32位机器最大32);
  3. 成员分配方向不确定(从左到右或从右到左);
  4. 剩余bit的复用规则不确定。

⚠️ 注重可移植性的程序应避免使用位段。

6.4 位段的应用场景

位段适合存储“只需少量bit”的信息,例如网络协议(如IP数据报):

  • IP数据报中“版本号”仅需4bit,“标志”仅需3bit;
  • 使用位段可减少数据报大小,降低网络传输开销。

6.5 位段使用注意事项

位段成员没有独立地址(内存以字节为单位分配地址),因此:

  • 不能用&取位段成员的地址;
  • 不能直接用scanf输入位段成员(需先存到变量再赋值)。

示例代码

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语言描述复杂数据的核心工具,掌握其声明、初始化和内存对齐是基础;结构体传参应优先传地址以优化性能;位段作为结构体的扩展,能高效节省空间,但需注意跨平台问题。理解这些知识点,不仅能应对面试考点,更能在实际开发中写出高效、规范的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值