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

C语言联合体的定义与内存共享特性

2021-10-047.4k 阅读

联合体的定义

在 C 语言中,联合体(Union)是一种特殊的数据类型,它允许不同的数据类型共享同一块内存空间。联合体的定义与结构体(Struct)的定义有些相似,但在内存使用方式上有着本质的区别。

联合体的定义语法如下:

union 联合体名 {
    数据类型1 成员1;
    数据类型2 成员2;
    // 可以有更多不同类型的成员
    数据类型n 成员n;
};

例如,定义一个简单的联合体来存储不同类型的数据:

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

在上述例子中,union Data 定义了一个名为 Data 的联合体,它包含三个不同类型的成员:一个整数 i,一个浮点数 f,以及一个字符 c

联合体变量的声明方式与结构体变量类似。可以在定义联合体的同时声明变量,也可以先定义联合体类型,然后再声明变量。

// 在定义联合体时声明变量
union Data {
    int i;
    float f;
    char c;
} data1, data2;

// 先定义联合体类型,再声明变量
union Data {
    int i;
    float f;
    char c;
};
union Data data3;

联合体的内存共享特性

联合体的核心特性是内存共享。这意味着联合体的所有成员共享同一块内存空间,该内存空间的大小是其最大成员所需的内存大小。例如,在上述 union Data 中,如果 int 类型占 4 个字节,float 类型占 4 个字节,char 类型占 1 个字节,那么 union Data 所占用的内存空间就是 4 个字节(因为 intfloat 是最大的成员)。

下面通过一个代码示例来演示联合体的内存共享特性:

#include <stdio.h>

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

int main() {
    union Data data;

    data.i = 10;
    printf("data.i = %d\n", data.i);

    data.f = 3.14f;
    printf("data.f = %f\n", data.f);

    data.c = 'A';
    printf("data.c = %c\n", data.c);

    // 观察共享内存导致的数据变化
    printf("data.i (after setting c) = %d\n", data.i);
    printf("data.f (after setting c) = %f\n", data.f);

    return 0;
}

在上述代码中,首先给 data.i 赋值为 10,然后输出 data.i 的值。接着给 data.f 赋值为 3.14f,此时 data.i 的值已经被覆盖,因为 data.fdata.i 共享同一块内存。再给 data.c 赋值为 'A',同样,data.idata.f 的值也会因为内存共享而发生变化。

运行上述代码,输出结果可能如下:

data.i = 10
data.f = 3.140000
data.c = A
data.i (after setting c) = 65
data.f (after setting c) = 0.000000

从输出结果可以看出,给 data.c 赋值 'A'(其 ASCII 码值为 65)后,data.i 的值变为了 65,而 data.f 的值由于内存内容的改变变得毫无意义(0.000000)。

联合体的内存对齐

与结构体一样,联合体也存在内存对齐的问题。内存对齐是为了提高内存访问效率,确保数据在内存中的存储地址是特定数据类型大小的倍数。

联合体的内存对齐规则与结构体类似,但有一些区别。联合体的对齐方式是按照其最大成员的对齐要求来进行的。例如,对于以下联合体:

union Example {
    char c;
    int i;
    double d;
};

假设 char 类型占 1 个字节,int 类型占 4 个字节,double 类型占 8 个字节。由于 double 是最大的成员,其对齐要求可能是 8 字节对齐(具体取决于编译器和硬件平台)。所以整个 union Example 的大小是 8 个字节,而不是简单的 1 + 4 + 8 = 13 个字节。

下面通过代码来验证联合体的内存对齐:

#include <stdio.h>
#include <stddef.h>

union Example {
    char c;
    int i;
    double d;
};

int main() {
    printf("Size of union Example: %zu\n", sizeof(union Example));
    printf("Offset of c: %zu\n", offsetof(union Example, c));
    printf("Offset of i: %zu\n", offsetof(union Example, i));
    printf("Offset of d: %zu\n", offsetof(union Example, d));

    return 0;
}

在上述代码中,sizeof(union Example) 用于获取联合体的大小,offsetof(union Example, member) 用于获取联合体成员在内存中的偏移量。运行上述代码,输出结果可能如下:

Size of union Example: 8
Offset of c: 0
Offset of i: 0
Offset of d: 0

从输出结果可以看出,联合体的大小为 8 个字节,并且所有成员的偏移量都为 0,这表明它们共享同一块内存空间,并且按照 double 的对齐要求进行了内存对齐。

联合体的应用场景

  1. 节省内存空间:当程序需要在不同时刻使用不同类型的数据,但这些数据不会同时使用时,可以使用联合体来节省内存。例如,一个表示图形对象的结构体,它可能是圆形、矩形或三角形,不同类型的图形对象会有不同的属性。可以使用联合体来存储这些不同类型图形对象的属性,而不是为每个图形对象都分配完整的内存空间。
#include <stdio.h>

// 定义图形类型枚举
typedef enum {
    CIRCLE,
    RECTANGLE,
    TRIANGLE
} ShapeType;

// 定义联合体存储不同图形的属性
union ShapeAttributes {
    struct {
        float radius;
    } circle;
    struct {
        float width;
        float height;
    } rectangle;
    struct {
        float base;
        float height;
    } triangle;
};

// 定义图形结构体
struct Shape {
    ShapeType type;
    union ShapeAttributes attributes;
};

void printShapeInfo(struct Shape shape) {
    switch (shape.type) {
        case CIRCLE:
            printf("Circle - Radius: %f\n", shape.attributes.circle.radius);
            break;
        case RECTANGLE:
            printf("Rectangle - Width: %f, Height: %f\n", shape.attributes.rectangle.width, shape.attributes.rectangle.height);
            break;
        case TRIANGLE:
            printf("Triangle - Base: %f, Height: %f\n", shape.attributes.triangle.base, shape.attributes.triangle.height);
            break;
    }
}

int main() {
    struct Shape circle = {CIRCLE, {.circle = {10.0f}}};
    struct Shape rectangle = {RECTANGLE, {.rectangle = {20.0f, 30.0f}}};
    struct Shape triangle = {TRIANGLE, {.triangle = {15.0f, 25.0f}}};

    printShapeInfo(circle);
    printShapeInfo(rectangle);
    printShapeInfo(triangle);

    return 0;
}

在上述代码中,union ShapeAttributes 用于存储不同图形的属性,struct Shape 则包含图形类型和对应的属性联合体。通过这种方式,不同类型的图形对象可以共享内存空间,节省了内存。

  1. 访问数据的不同表示形式:联合体可以用于以不同的数据类型视角来访问同一块内存。例如,在处理二进制数据时,可能需要以字节(char)、整数(int)或浮点数(float)等不同方式来解读数据。
#include <stdio.h>

union BinaryData {
    int i;
    char bytes[4];
};

void printBytes(union BinaryData data) {
    for (int i = 0; i < 4; i++) {
        printf("%02hhX ", data.bytes[i]);
    }
    printf("\n");
}

int main() {
    union BinaryData data;
    data.i = 0x12345678;

    printf("Integer value: %#x\n", data.i);
    printf("Byte representation: ");
    printBytes(data);

    return 0;
}

在上述代码中,union BinaryData 可以让我们以 int 类型和 char 数组两种方式来访问同一块内存。通过 printBytes 函数,我们可以将 int 值以字节的形式打印出来,从而了解数据在内存中的存储方式。

  1. 硬件编程:在硬件编程中,联合体常用于访问硬件寄存器。硬件寄存器可能需要以不同的数据类型进行读写操作,例如以字节、字(16 位)或双字(32 位)等方式。联合体可以方便地实现这种需求。
#include <stdio.h>

// 假设这是一个硬件寄存器的联合体表示
union HardwareRegister {
    unsigned int value32;
    unsigned short value16[2];
    unsigned char value8[4];
};

int main() {
    union HardwareRegister reg;

    // 以 32 位方式写入寄存器
    reg.value32 = 0x11223344;

    // 以 16 位方式读取寄存器
    printf("Value as two 16 - bit words: %#hx %#hx\n", reg.value16[0], reg.value16[1]);

    // 以 8 位方式读取寄存器
    printf("Value as four 8 - bit bytes: ");
    for (int i = 0; i < 4; i++) {
        printf("%02hhX ", reg.value8[i]);
    }
    printf("\n");

    return 0;
}

在上述代码中,union HardwareRegister 可以以 32 位、16 位和 8 位不同的方式来访问硬件寄存器的值,这在硬件编程中非常实用。

联合体与结构体的对比

  1. 内存使用:结构体的所有成员都有自己独立的内存空间,结构体的大小是其所有成员大小之和(考虑内存对齐)。而联合体的所有成员共享同一块内存空间,联合体的大小是其最大成员的大小(考虑内存对齐)。
  2. 数据存储方式:在结构体中,所有成员的数据可以同时存在并保持其值。而在联合体中,同一时间只能有一个成员的值是有效的,对一个成员的赋值会覆盖其他成员的值。
  3. 应用场景:结构体适用于需要同时存储多个不同类型数据的场景,例如表示一个人的信息(姓名、年龄、地址等)。联合体适用于在不同时刻使用不同类型数据且数据不会同时使用的场景,如上述提到的图形对象属性存储、硬件寄存器访问等。

下面通过一个代码示例来直观地对比结构体和联合体的内存使用和数据存储方式:

#include <stdio.h>

// 定义结构体
struct StructExample {
    int i;
    float f;
    char c;
};

// 定义联合体
union UnionExample {
    int i;
    float f;
    char c;
};

int main() {
    struct StructExample structData;
    union UnionExample unionData;

    // 结构体赋值和输出
    structData.i = 10;
    structData.f = 3.14f;
    structData.c = 'A';
    printf("Struct - i: %d, f: %f, c: %c\n", structData.i, structData.f, structData.c);
    printf("Size of struct StructExample: %zu\n", sizeof(struct StructExample));

    // 联合体赋值和输出
    unionData.i = 10;
    printf("Union - i: %d\n", unionData.i);
    unionData.f = 3.14f;
    printf("Union - f: %f\n", unionData.f);
    unionData.c = 'A';
    printf("Union - c: %c\n", unionData.c);
    printf("Size of union UnionExample: %zu\n", sizeof(union UnionExample));

    return 0;
}

在上述代码中,首先定义了一个结构体 StructExample 和一个联合体 UnionExample,它们包含相同类型的成员。然后分别对结构体和联合体进行赋值和输出操作,并打印它们的大小。运行代码,输出结果如下:

Struct - i: 10, f: 3.140000, c: A
Size of struct StructExample: 12
Union - i: 10
Union - f: 3.140000
Union - c: A
Size of union UnionExample: 4

从输出结果可以明显看出,结构体的大小是其成员大小之和(考虑内存对齐),并且所有成员的值可以同时存在。而联合体的大小是其最大成员的大小,对一个成员的赋值会覆盖其他成员的值。

联合体使用的注意事项

  1. 数据一致性:由于联合体的成员共享内存,对一个成员的修改会影响其他成员的值。在使用联合体时,需要特别注意数据的一致性,确保在访问某个成员时,该成员的值是有效的。例如,在上述的 union Data 示例中,如果先给 data.i 赋值,然后直接访问 data.f,得到的 data.f 值可能是无意义的,因为 data.i 的赋值改变了共享内存的内容。
  2. 类型转换:当以不同的数据类型访问联合体的共享内存时,需要注意类型转换的正确性。例如,将一个整数赋值给联合体的浮点数成员,再以浮点数类型访问时,可能会得到意外的结果,因为整数和浮点数在内存中的表示方式不同。
  3. 初始化:联合体的初始化方式与结构体有所不同。只能初始化联合体的第一个成员。例如:
union Data {
    int i;
    float f;
    char c;
};

union Data data = {10}; // 初始化第一个成员 i 为 10

如果要初始化其他成员,需要在声明变量后再进行赋值操作。 4. 与指针的结合使用:当使用联合体指针时,需要特别小心。由于联合体成员共享内存,通过指针访问不同成员时,要确保指针的类型与当前有效的成员类型一致,否则可能导致未定义行为。

#include <stdio.h>

union Data {
    int i;
    float f;
};

int main() {
    union Data data;
    union Data *ptr = &data;

    data.i = 10;
    printf("Value as int: %d\n", ptr->i);

    data.f = 3.14f;
    printf("Value as float: %f\n", ptr->f);

    // 错误的访问,可能导致未定义行为
    // printf("Value as int (after setting f): %d\n", ptr->i);

    return 0;
}

在上述代码中,如果在给 data.f 赋值后,尝试通过 ptr->i 访问数据,可能会得到意外的结果,因为此时内存中的数据是以浮点数的格式存储的,而不是整数格式。

  1. 嵌套联合体和结构体:在联合体中可以嵌套结构体,在结构体中也可以嵌套联合体。但在使用时需要注意内存对齐和数据访问的正确性。
#include <stdio.h>

// 定义一个结构体
struct InnerStruct {
    int a;
    char b;
};

// 定义一个联合体,包含嵌套的结构体
union OuterUnion {
    struct InnerStruct inner;
    float f;
};

int main() {
    union OuterUnion unionData;

    unionData.inner.a = 10;
    unionData.inner.b = 'A';
    printf("Inner struct - a: %d, b: %c\n", unionData.inner.a, unionData.inner.b);

    unionData.f = 3.14f;
    // 此时 unionData.inner 的值已被覆盖,访问 unionData.inner 可能得到无意义的结果

    return 0;
}

在上述代码中,union OuterUnion 包含一个嵌套的结构体 InnerStruct。在给 unionData.inner 赋值后,再给 unionData.f 赋值,unionData.inner 的值会被覆盖,此时访问 unionData.inner 可能会得到无意义的结果。

通过深入理解联合体的定义、内存共享特性、应用场景以及与结构体的对比和使用注意事项,开发者可以在 C 语言编程中有效地利用联合体来优化内存使用和实现特定的功能需求。联合体虽然在使用上需要更加谨慎,但在合适的场景下,它可以成为非常强大的编程工具。在实际编程中,应根据具体的需求和数据特点,合理选择使用结构体或联合体,以达到最佳的编程效果。同时,要时刻注意联合体的内存共享带来的潜在问题,确保程序的正确性和稳定性。