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

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

2021-01-167.6k 阅读

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

在C语言的学习和实际编程应用中,结构体(struct)和联合体(union)是两个非常重要的数据类型构造方式。虽然它们在定义和使用上有一定的相似性,但在内存分配和使用方式上存在显著差异。理解这些差异对于优化内存使用、编写高效且正确的代码至关重要。

结构体的内存分配

  1. 结构体的基本定义 结构体是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起。例如,我们可以定义一个表示学生信息的结构体:
struct Student {
    char name[20];
    int age;
    float score;
};

在这个结构体中,name 是一个字符数组,用于存储学生的姓名;age 是一个整数,代表学生的年龄;score 是一个浮点数,记录学生的成绩。

  1. 结构体的内存布局 结构体的内存布局遵循顺序分配原则。编译器会按照结构体成员定义的顺序依次为每个成员分配内存空间。并且,为了保证数据的对齐,编译器可能会在成员之间插入一些填充字节(padding)。 以 struct Student 为例,假设在一个32位系统上,char 类型占1个字节,int 类型占4个字节,float 类型占4个字节。name 数组需要20个字节来存储字符,age 成员从下一个内存地址开始分配4个字节,score 再接着分配4个字节。但是由于内存对齐的原因,age 会从一个4字节对齐的地址开始分配(假设 name 数组后的地址不是4字节对齐的,编译器会在 name 数组后填充一些字节,使得 age 能从4字节对齐的地址开始)。这样,整个 struct Student 结构体占用的内存大小为 20 + 4 + 4 = 28 字节(这里不考虑结构体整体对齐的情况,如果考虑结构体整体对齐,可能会在 score 后再填充一些字节以满足结构体整体对齐要求)。

下面通过代码来验证结构体的内存大小:

#include <stdio.h>

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

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

运行上述代码,在32位系统上,会输出 Size of struct Student: 28

  1. 结构体成员的访问 结构体成员可以通过结构体变量名和点运算符(.)来访问。例如:
#include <stdio.h>

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

int main() {
    struct Student s = {"Alice", 20, 85.5};
    printf("Name: %s\n", s.name);
    printf("Age: %d\n", s.age);
    printf("Score: %.2f\n", s.score);
    return 0;
}

在这段代码中,我们创建了一个 struct Student 类型的变量 s,并初始化了它的成员,然后通过点运算符访问并打印出各个成员的值。

联合体的内存分配

  1. 联合体的基本定义 联合体也是一种用户自定义的数据类型,它允许不同类型的数据共享同一块内存空间。其定义方式与结构体类似,但使用 union 关键字。例如:
union Data {
    int i;
    float f;
    char c;
};

在这个联合体中,ifc 三个成员共享同一块内存空间。

  1. 联合体的内存布局 联合体的内存大小取决于其最大成员的大小。因为所有成员共享同一块内存,所以联合体的内存大小必须能够容纳其最大的成员。以 union Data 为例,假设在32位系统上,int 类型占4个字节,float 类型占4个字节,char 类型占1个字节。由于 intfloat 都是4字节,所以 union Data 的内存大小为4字节。 下面通过代码验证联合体的内存大小:
#include <stdio.h>

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

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

运行上述代码,在32位系统上,会输出 Size of union Data: 4

  1. 联合体成员的访问 由于联合体成员共享内存,同一时间只能有一个成员的值是有效的。当给一个成员赋值时,会覆盖其他成员的值。例如:
#include <stdio.h>

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

int main() {
    union Data d;
    d.i = 10;
    printf("Value of i: %d\n", d.i);
    d.f = 3.14f;
    printf("Value of f: %.2f\n", d.f);
    d.c = 'A';
    printf("Value of c: %c\n", d.c);
    printf("Value of i after setting c: %d\n", d.i);
    return 0;
}

在这段代码中,我们先给 d.i 赋值为10,然后打印 d.i 的值。接着给 d.f 赋值,此时 d.i 的值已经被覆盖。再给 d.c 赋值,d.f 的值也被覆盖。最后打印 d.i 的值,由于 d.c 只占用1个字节,覆盖了 d.i 的部分字节,所以 d.i 的值变成了不确定的(取决于具体的内存布局和字节序)。

内存对齐对结构体和联合体的影响

  1. 结构体的内存对齐 正如前面提到的,结构体的内存分配会考虑内存对齐。内存对齐的目的是为了提高CPU对数据的访问效率。不同的系统和编译器可能有不同的对齐规则,但通常遵循以下原则:
  • 每个成员的起始地址必须是该成员大小的整数倍。例如,int 类型成员的起始地址必须是4的倍数(在32位系统上)。
  • 结构体的整体大小必须是其最大成员大小的整数倍。 以如下结构体为例:
struct Example {
    char a;
    int b;
    char c;
};

在32位系统上,char 类型占1个字节,int 类型占4个字节。a 占用1个字节,为了使 b 从4字节对齐的地址开始,编译器会在 a 后填充3个字节。b 占用4个字节,c 占用1个字节。此时结构体的大小为 1 + 3 + 4 + 1 = 9 字节,但由于结构体整体大小必须是最大成员(int 类型,4字节)的整数倍,所以编译器会在 c 后再填充3个字节,最终结构体大小为12字节。

  1. 联合体的内存对齐 联合体同样遵循内存对齐原则,但其内存大小只取决于最大成员。例如:
union ExampleUnion {
    char a;
    int b;
    char c;
};

在32位系统上,最大成员 int 类型占4个字节,所以联合体的内存大小为4字节。由于 b 需要4字节对齐,所以整个联合体的内存布局也是以4字节对齐的方式分配。

结构体和联合体在实际应用中的差异

  1. 结构体的应用场景 结构体适用于需要同时存储和处理多个不同类型相关数据的场景。比如,在文件系统中,文件的元数据(文件名、文件大小、创建时间等)可以用结构体来表示;在图形处理中,一个点的坐标(x, y)以及颜色信息可以封装在一个结构体中。例如,定义一个表示文件信息的结构体:
struct FileInfo {
    char filename[50];
    long filesize;
    time_t creation_time;
};

这样可以方便地对文件的各种属性进行统一管理和操作。

  1. 联合体的应用场景 联合体主要用于节省内存空间,当同一时间只需要使用不同类型数据中的一种时,可以使用联合体。例如,在嵌入式系统中,一个寄存器可能根据不同的配置方式,存储不同类型的数据(如整数、标志位等),这时就可以使用联合体来表示这个寄存器。
union RegisterValue {
    int value;
    struct {
        unsigned int bit0 : 1;
        unsigned int bit1 : 1;
        unsigned int bit2 : 1;
        // 其他位定义
    } flags;
};

在这个例子中,value 可以表示整个寄存器的值,而 flags 结构体可以用来单独访问寄存器的各个标志位。

结构体和联合体的嵌套使用

  1. 结构体中嵌套结构体 结构体中可以嵌套其他结构体,这样可以构建更复杂的数据结构。例如,定义一个表示地址的结构体,并将其嵌套在表示人的结构体中:
struct Address {
    char street[50];
    char city[20];
    int zipcode;
};

struct Person {
    char name[20];
    int age;
    struct Address addr;
};

在这个例子中,struct Person 结构体包含了一个 struct Address 类型的成员 addr。访问嵌套结构体成员需要使用多个点运算符,如下:

#include <stdio.h>

struct Address {
    char street[50];
    char city[20];
    int zipcode;
};

struct Person {
    char name[20];
    int age;
    struct Address addr;
};

int main() {
    struct Person p = {"Bob", 30, {"123 Main St", "Anytown", 12345}};
    printf("Name: %s\n", p.name);
    printf("Age: %d\n", p.age);
    printf("Address: %s, %s, %d\n", p.addr.street, p.addr.city, p.addr.zipcode);
    return 0;
}
  1. 结构体中嵌套联合体 结构体中也可以嵌套联合体,这种方式在处理一些需要根据不同情况使用不同数据类型的场景中很有用。例如:
union Data {
    int i;
    float f;
};

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

在这个例子中,struct Container 结构体的 type 成员可以用来表示 value 联合体中存储的数据类型。如果 type'i',则 value.i 有效;如果 type'f',则 value.f 有效。

#include <stdio.h>

union Data {
    int i;
    float f;
};

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

int main() {
    struct Container c1 = {'i', {5}};
    struct Container c2 = {'f', {3.14f}};
    if (c1.type == 'i') {
        printf("Value in c1: %d\n", c1.value.i);
    }
    if (c2.type == 'f') {
        printf("Value in c2: %.2f\n", c2.value.f);
    }
    return 0;
}
  1. 联合体中嵌套结构体或联合体 联合体也可以嵌套结构体或其他联合体。例如:
struct InnerStruct {
    int a;
    float b;
};

union OuterUnion {
    struct InnerStruct s;
    double d;
};

在这个例子中,union OuterUnion 包含了一个 struct InnerStruct 类型的成员 s 和一个 double 类型的成员 d。由于 double 类型在大多数系统上占8个字节,而 struct InnerStruct 假设在32位系统上大小为 4 + 4 = 8 字节(不考虑对齐填充),所以 union OuterUnion 的内存大小为8字节。

结构体和联合体在内存管理上的注意事项

  1. 结构体的内存管理 当结构体包含动态分配的内存(如字符指针指向动态分配的字符串)时,需要特别注意内存的分配和释放。例如:
struct StringHolder {
    char *str;
};

int main() {
    struct StringHolder sh;
    sh.str = (char *)malloc(100);
    if (sh.str == NULL) {
        perror("malloc");
        return 1;
    }
    // 使用 sh.str
    free(sh.str);
    return 0;
}

在这个例子中,struct StringHolder 结构体包含一个字符指针 str,我们需要手动分配内存并在使用完毕后释放,否则会导致内存泄漏。

  1. 联合体的内存管理 由于联合体成员共享内存,在对联合体进行操作时,要确保不会因为错误的赋值导致数据丢失或错误的访问。例如,在前面的 union Data 例子中,如果先给 d.i 赋值,然后直接以 d.f 的方式访问,可能会得到错误的结果,因为 intfloat 的内存表示方式不同。

结构体和联合体在不同编译器和平台下的差异

  1. 编译器相关差异 不同的编译器对结构体和联合体的内存对齐规则可能略有不同。一些编译器提供了指令来控制内存对齐,例如GCC编译器可以使用 #pragma pack(n) 指令来指定结构体的对齐方式(n 表示对齐字节数)。例如:
#pragma pack(1)
struct NoPadding {
    char a;
    int b;
    char c;
};
#pragma pack()

在上述代码中,使用 #pragma pack(1) 指令后,struct NoPadding 结构体的成员将不再进行内存对齐填充,其大小为 1 + 4 + 1 = 6 字节。而默认情况下,在32位系统上,它的大小可能是12字节。

  1. 平台相关差异 不同的硬件平台(如32位和64位系统)对数据类型的大小和内存对齐要求也有所不同。例如,在32位系统上,int 类型通常占4个字节,而在64位系统上,int 类型依然可能占4个字节,但指针类型在32位系统上占4个字节,在64位系统上占8个字节。这会影响到结构体和联合体的内存大小和布局。比如,一个包含指针成员的结构体在32位和64位系统上的大小就会不同。

结构体和联合体在面向对象编程思想中的体现(C语言中的模拟)

虽然C语言不是面向对象的编程语言,但可以通过结构体和联合体来模拟一些面向对象的概念。

  1. 结构体与封装 结构体可以将相关的数据和操作函数指针封装在一起。例如,定义一个表示栈的结构体,并在结构体中包含操作栈的函数指针:
typedef struct Stack {
    int *data;
    int top;
    int capacity;
    void (*push)(struct Stack *, int);
    int (*pop)(struct Stack *);
} Stack;

void stackPush(Stack *s, int value) {
    if (s->top == s->capacity - 1) {
        // 处理栈满情况
        return;
    }
    s->data[++s->top] = value;
}

int stackPop(Stack *s) {
    if (s->top == -1) {
        // 处理栈空情况
        return -1;
    }
    return s->data[s->top--];
}

int main() {
    Stack s;
    s.data = (int *)malloc(10 * sizeof(int));
    s.top = -1;
    s.capacity = 10;
    s.push = stackPush;
    s.pop = stackPop;
    s.push(&s, 10);
    s.push(&s, 20);
    printf("Popped: %d\n", s.pop(&s));
    free(s.data);
    return 0;
}

在这个例子中,Stack 结构体封装了栈的数据(datatopcapacity)和操作(pushpop 函数指针),类似于面向对象编程中的类。

  1. 联合体与多态(有限模拟) 联合体可以在一定程度上模拟多态的概念。例如,通过联合体和一个类型标志来实现不同类型数据的统一处理:
union Shape {
    struct {
        float radius;
    } circle;
    struct {
        float length;
        float width;
    } rectangle;
};

typedef struct {
    char type;
    union Shape shape;
} ShapeHolder;

float calculateArea(ShapeHolder sh) {
    if (sh.type == 'c') {
        return 3.14 * sh.shape.circle.radius * sh.shape.circle.radius;
    } else if (sh.type == 'r') {
        return sh.shape.rectangle.length * sh.shape.rectangle.width;
    }
    return 0;
}

int main() {
    ShapeHolder sh1 = {'c', {{10.0}}};
    ShapeHolder sh2 = {'r', {{5.0, 3.0}}};
    printf("Area of circle: %.2f\n", calculateArea(sh1));
    printf("Area of rectangle: %.2f\n", calculateArea(sh2));
    return 0;
}

在这个例子中,ShapeHolder 结构体中的联合体 Shape 可以存储圆形或矩形的相关数据,通过 type 标志来区分不同的形状类型,并在 calculateArea 函数中根据不同类型进行相应的面积计算,模拟了面向对象编程中的多态行为。

结构体和联合体在嵌入式系统中的应用

  1. 结构体在嵌入式系统中的应用 在嵌入式系统中,结构体常用于表示硬件寄存器的配置和状态。例如,一个微控制器的GPIO寄存器可以用结构体来表示:
struct GPIO_REG {
    volatile unsigned int GPIO_DATA;
    volatile unsigned int GPIO_DIR;
    volatile unsigned int GPIO_INT;
};

#define GPIO_BASE_ADDR 0x40000000
struct GPIO_REG *gpio = (struct GPIO_REG *)GPIO_BASE_ADDR;

int main() {
    // 设置GPIO方向为输出
    gpio->GPIO_DIR |= 0x01;
    // 设置GPIO数据为高电平
    gpio->GPIO_DATA |= 0x01;
    return 0;
}

通过结构体可以方便地访问和操作硬件寄存器,提高代码的可读性和可维护性。

  1. 联合体在嵌入式系统中的应用 联合体在嵌入式系统中常用于处理不同格式的数据转换。例如,在通信协议中,一个数据包可能根据不同的协议版本以不同的格式解析。可以使用联合体来表示这个数据包:
union Packet {
    struct {
        unsigned int version : 4;
        unsigned int length : 12;
        unsigned int data[16];
    } v1;
    struct {
        unsigned int version : 4;
        unsigned int flag : 1;
        unsigned int length : 11;
        unsigned int data[32];
    } v2;
};

这样可以根据数据包中的版本信息,选择合适的结构体成员来解析数据。

结构体和联合体在文件操作中的应用

  1. 结构体在文件操作中的应用 结构体可以方便地用于文件的读写操作。例如,将一个结构体的数据写入文件,并从文件中读取结构体数据:
#include <stdio.h>

struct Person {
    char name[20];
    int age;
};

int main() {
    struct Person p = {"Alice", 20};
    FILE *file = fopen("person.dat", "wb");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }
    fwrite(&p, sizeof(struct Person), 1, file);
    fclose(file);

    struct Person read_p;
    file = fopen("person.dat", "rb");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }
    fread(&read_p, sizeof(struct Person), 1, file);
    fclose(file);

    printf("Name: %s, Age: %d\n", read_p.name, read_p.age);
    return 0;
}

在这个例子中,我们使用 fwrite 函数将 struct Person 结构体的数据写入文件,然后使用 fread 函数从文件中读取数据并恢复结构体。

  1. 联合体在文件操作中的应用 联合体在文件操作中可用于处理不同格式的数据存储。例如,在一个包含多种数据类型的文件中,可以使用联合体来灵活地读取和解析数据:
#include <stdio.h>

union Data {
    int i;
    float f;
};

int main() {
    union Data d;
    FILE *file = fopen("data.dat", "rb");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }
    // 假设文件中第一个字节表示数据类型,0表示int,1表示float
    char type;
    fread(&type, 1, 1, file);
    if (type == 0) {
        fread(&d.i, sizeof(int), 1, file);
        printf("Read int: %d\n", d.i);
    } else if (type == 1) {
        fread(&d.f, sizeof(float), 1, file);
        printf("Read float: %.2f\n", d.f);
    }
    fclose(file);
    return 0;
}

在这个例子中,通过联合体可以根据文件中存储的类型标志,灵活地读取和处理不同类型的数据。

结构体和联合体的内存差异总结与实际编程建议

  1. 内存差异总结
  • 结构体的内存布局是顺序分配,各成员有自己独立的内存空间,其大小是所有成员大小之和(加上可能的填充字节)。
  • 联合体的所有成员共享同一块内存空间,其大小取决于最大成员的大小。
  • 内存对齐对结构体和联合体都有影响,结构体需要满足成员和整体的对齐要求,联合体主要根据最大成员进行对齐。
  1. 实际编程建议
  • 在选择使用结构体还是联合体时,要根据实际需求。如果需要同时存储和处理多个不同类型相关数据,应选择结构体;如果同一时间只需要使用不同类型数据中的一种,且希望节省内存空间,则选择联合体。
  • 在处理结构体和联合体时,要注意内存对齐的影响,避免因不同编译器和平台的差异导致程序出现兼容性问题。
  • 当结构体或联合体包含动态分配的内存时,要确保正确地分配和释放内存,防止内存泄漏。

通过深入理解结构体和联合体的内存差异,我们能够在C语言编程中更合理地使用这两种数据类型构造方式,编写出高效、稳定且内存优化的程序。无论是在系统开发、嵌入式系统、文件操作还是其他领域,对这些概念的掌握都是至关重要的。