MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C语言结构体与联合体的内存布局差异

2022-04-283.3k 阅读

一、结构体的内存布局

1.1 结构体的基本概念

结构体(struct)是C语言中一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个有机的整体。例如,一个表示学生信息的结构体可以包含姓名(字符数组)、年龄(整数)和成绩(浮点数)等不同类型的成员。

struct Student {
    char name[20];
    int age;
    float score;
};

在上述代码中,struct Student定义了一个结构体类型,它包含三个成员:name(字符数组)、age(整数)和score(浮点数)。

1.2 结构体的内存分配原则

结构体变量在内存中是按照成员定义的顺序依次存储的,但为了提高内存访问效率,编译器会对结构体的成员进行内存对齐。内存对齐的基本原则如下:

  1. 对齐规则:每个成员的起始地址必须是该成员类型大小的整数倍。例如,char类型大小为1字节,其起始地址可以是任意地址;int类型通常大小为4字节,其起始地址必须是4的倍数。
  2. 结构体整体大小:结构体的大小必须是其最大成员类型大小的整数倍。

下面通过一个示例来详细说明:

#include <stdio.h>

struct Example1 {
    char a;
    int b;
    char c;
};

struct Example2 {
    char a;
    char c;
    int b;
};

int main() {
    printf("Size of struct Example1: %zu\n", sizeof(struct Example1));
    printf("Size of struct Example2: %zu\n", sizeof(struct Example2));
    return 0;
}

struct Example1中,char a占用1字节,由于int b要求起始地址是4的倍数,所以在a后面会填充3个字节,int b占用4字节,char c占用1字节,此时结构体总大小为1 + 3 + 4 + 1 = 9字节,但由于结构体大小必须是最大成员(int,4字节)的整数倍,所以会再填充3个字节,最终struct Example1的大小为12字节。

而在struct Example2中,char achar c共占用2字节,int b要求起始地址是4的倍数,所以在c后面填充2个字节,int b占用4字节,此时结构体总大小为1 + 1 + 2 + 4 = 8字节,刚好是4的倍数,所以struct Example2的大小为8字节。

1.3 嵌套结构体的内存布局

当结构体中包含其他结构体成员时,嵌套结构体的内存布局同样遵循上述内存对齐规则。嵌套结构体的起始地址必须是其自身最大成员类型大小的整数倍。

#include <stdio.h>

struct Inner {
    char a;
    int b;
};

struct Outer {
    struct Inner inner;
    char c;
};

int main() {
    printf("Size of struct Inner: %zu\n", sizeof(struct Inner));
    printf("Size of struct Outer: %zu\n", sizeof(struct Outer));
    return 0;
}

struct Inner中,char a占用1字节,填充3字节,int b占用4字节,所以struct Inner大小为8字节。在struct Outer中,struct Inner inner占用8字节,char c占用1字节,由于结构体大小必须是最大成员(struct Inner,8字节)的整数倍,所以会再填充7字节,最终struct Outer的大小为16字节。

1.4 结构体与指针

结构体指针在内存中的存储方式与普通指针相同,它存储的是结构体变量的起始地址。通过结构体指针可以方便地访问结构体的成员。

#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p = {10, 20};
    struct Point *ptr = &p;
    printf("x: %d, y: %d\n", ptr->x, ptr->y);
    return 0;
}

在上述代码中,ptr是一个指向struct Point结构体变量p的指针,通过ptr->xptr->y可以访问结构体的成员。

二、联合体的内存布局

2.1 联合体的基本概念

联合体(union)也是C语言中一种用户自定义的数据类型,它允许不同类型的数据共享同一块内存空间。联合体的所有成员都从同一个内存地址开始存储。

union Data {
    int i;
    float f;
    char c;
};

在上述代码中,union Data定义了一个联合体类型,它包含三个成员:i(整数)、f(浮点数)和c(字符)。

2.2 联合体的内存分配原则

联合体的大小取决于其最大成员的大小。因为所有成员共享同一块内存,所以联合体的大小必须能够容纳其最大成员。

#include <stdio.h>

union Example {
    char a;
    int b;
    float c;
};

int main() {
    printf("Size of union Example: %zu\n", sizeof(union Example));
    return 0;
}

在上述代码中,union Example的最大成员是intfloat(通常大小为4字节),所以union Example的大小为4字节。

2.3 联合体的使用特点

由于联合体成员共享内存,同一时刻只能有一个成员有效。例如:

#include <stdio.h>

union Data {
    int i;
    float f;
    char c;
};

int main() {
    union Data data;
    data.i = 10;
    printf("Data as int: %d\n", data.i);
    data.f = 3.14f;
    printf("Data as float: %f\n", data.f);
    data.c = 'A';
    printf("Data as char: %c\n", data.c);
    return 0;
}

在上述代码中,先将data作为int类型赋值为10,然后又将其作为float类型赋值为3.14f,最后作为char类型赋值为'A'。每次赋值都会覆盖之前存储的数据,因为它们共享同一块内存。

2.4 联合体与结构体的结合使用

在实际应用中,联合体常常与结构体结合使用,以实现更灵活的数据存储和处理。例如:

#include <stdio.h>

union Data {
    int i;
    float f;
    char c;
};

struct Container {
    char type;
    union Data data;
};

int main() {
    struct Container container;
    container.type = 'i';
    container.data.i = 20;
    if (container.type == 'i') {
        printf("Data as int: %d\n", container.data.i);
    }
    container.type = 'f';
    container.data.f = 2.718f;
    if (container.type == 'f') {
        printf("Data as float: %f\n", container.data.f);
    }
    return 0;
}

在上述代码中,struct Container包含一个char类型的type成员用于标识union Data中存储的数据类型,这样可以根据type的值来正确解读union Data中的数据。

三、结构体与联合体内存布局差异的深入分析

3.1 内存占用差异

结构体的内存占用是其所有成员占用内存之和,再加上为满足内存对齐而填充的字节数。而联合体的内存占用仅为其最大成员的大小,因为所有成员共享同一块内存。

例如:

#include <stdio.h>

struct StructExample {
    char a;
    int b;
    char c;
};

union UnionExample {
    char a;
    int b;
    char c;
};

int main() {
    printf("Size of struct StructExample: %zu\n", sizeof(struct StructExample));
    printf("Size of union UnionExample: %zu\n", sizeof(union UnionExample));
    return 0;
}

struct StructExample的大小通常为12字节,而union UnionExample的大小为4字节(假设int大小为4字节)。

3.2 数据存储方式差异

结构体的各个成员在内存中是按照定义顺序依次存储的,每个成员有自己独立的内存空间。而联合体的所有成员共享同一块内存空间,同一时刻只有一个成员的值是有效的,对一个成员的赋值会覆盖其他成员的值。

3.3 应用场景差异

结构体适用于需要同时存储多种不同类型数据的场景,例如表示一个复杂对象的各种属性。例如,一个表示文件信息的结构体可以包含文件名(字符数组)、文件大小(整数)、文件创建时间(结构体)等成员。

#include <stdio.h>
#include <string.h>

struct FileInfo {
    char name[50];
    long size;
    struct {
        int year;
        int month;
        int day;
    } creationDate;
};

int main() {
    struct FileInfo file;
    strcpy(file.name, "example.txt");
    file.size = 1024;
    file.creationDate.year = 2023;
    file.creationDate.month = 10;
    file.creationDate.day = 15;
    printf("File name: %s, Size: %ld, Creation date: %d-%d-%d\n", file.name, file.size, file.creationDate.year, file.creationDate.month, file.creationDate.day);
    return 0;
}

联合体适用于在不同时刻需要使用不同类型数据,但不需要同时存储这些数据的场景。例如,在网络协议解析中,某些字段可能根据协议类型以不同的数据类型表示,但不会同时以多种类型存在。

#include <stdio.h>

union NetworkData {
    int intValue;
    float floatValue;
    char charValue;
};

int main() {
    union NetworkData data;
    // 假设根据协议解析,此处该字段为整数
    data.intValue = 1234;
    printf("Network data as int: %d\n", data.intValue);
    // 假设后续协议解析,该字段变为浮点数
    data.floatValue = 3.14f;
    printf("Network data as float: %f\n", data.floatValue);
    return 0;
}

3.4 内存对齐对两者的影响差异

对于结构体,内存对齐会增加结构体的总体大小,以满足成员对内存地址对齐的要求。而联合体的内存大小仅取决于最大成员的大小,内存对齐规则主要影响其内部成员的存储起始地址,以保证每个成员都能正确访问,但不会增加联合体的总体大小。

例如:

#include <stdio.h>

struct StructWithAlignment {
    char a;
    double b;
    char c;
};

union UnionWithAlignment {
    char a;
    double b;
    char c;
};

int main() {
    printf("Size of struct StructWithAlignment: %zu\n", sizeof(struct StructWithAlignment));
    printf("Size of union UnionWithAlignment: %zu\n", sizeof(union UnionWithAlignment));
    return 0;
}

struct StructWithAlignment中,char a占用1字节,填充7字节,double b占用8字节,char c占用1字节,再填充7字节,结构体大小为24字节。而union UnionWithAlignment大小为8字节,因为double是最大成员。

四、结构体和联合体内存布局在实际编程中的注意事项

4.1 结构体内存布局注意事项

  1. 合理安排成员顺序:为了减少内存浪费,应尽量将占用字节数小的成员放在前面,占用字节数大的成员放在后面。例如,将char类型成员放在int类型成员之前。
  2. 避免不必要的嵌套:复杂的嵌套结构体可能会使内存布局变得难以理解和管理,增加维护成本。尽量保持结构体的简洁性。
  3. 了解编译器对齐规则:不同的编译器可能对内存对齐有略微不同的实现,在跨平台编程时,要确保结构体的内存布局在不同编译器下的一致性。可以通过编译器特定的指令(如#pragma pack)来调整对齐方式。

4.2 联合体内存布局注意事项

  1. 明确当前有效成员:由于联合体成员共享内存,在使用联合体时,必须清楚当前哪个成员是有效的,避免错误地访问无效数据。
  2. 类型转换风险:因为联合体可以存储不同类型的数据,在进行类型转换时要特别小心,确保数据的正确性。例如,将一个整数存储在联合体中,然后以浮点数的形式读取,可能会得到错误的结果,除非这是经过精心设计的特定应用场景。
  3. 初始化注意事项:联合体的初始化只能对第一个成员进行初始化。例如:
union Data {
    int i;
    float f;
    char c;
};

union Data data = {10}; // 正确,初始化i为10
// union Data data = {3.14f}; // 错误,只能初始化第一个成员

4.3 两者混合使用的注意事项

当结构体和联合体混合使用时,要确保结构体中的联合体成员的使用逻辑清晰。例如,在前面提到的struct Containerunion Data结合使用的例子中,要正确设置和检查type成员,以保证正确访问union Data中的数据。

同时,要注意在不同的编译环境下,结构体和联合体的内存布局和行为可能会有所不同,进行跨平台开发时要进行充分的测试。

在实际编程中,深入理解结构体和联合体的内存布局差异,能够帮助我们更有效地使用内存,提高程序的性能和可维护性。无论是编写系统软件、嵌入式程序还是应用程序,合理运用结构体和联合体都能使代码更加简洁、高效。例如,在嵌入式系统中,内存资源有限,对结构体和联合体内存布局的优化可以节省宝贵的内存空间;在网络编程中,正确处理结构体和联合体的数据存储和解析,能够保证数据的准确传输和处理。通过不断实践和总结,我们可以更好地掌握这两种重要的数据类型及其内存布局特点,为编写高质量的C语言程序奠定坚实的基础。