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

C++ struct和union的内在差异对比

2024-09-011.3k 阅读

C++ struct 和 union 的基本定义

struct 的定义

在 C++ 中,struct 是一种用户自定义的数据类型,它允许将不同类型的数据成员组合在一起,形成一个单一的实体。例如:

struct Point {
    int x;
    int y;
};

这里定义了一个名为 Pointstruct,它包含两个 int 类型的数据成员 xy。我们可以通过以下方式来使用这个 struct

int main() {
    Point p;
    p.x = 10;
    p.y = 20;
    return 0;
}

struct 可以包含成员函数,这使得它在功能上与 class 非常相似。例如:

struct Rectangle {
    int width;
    int height;
    int area() {
        return width * height;
    }
};

在上述代码中,Rectangle struct 不仅包含数据成员 widthheight,还包含一个成员函数 area,用于计算矩形的面积。

union 的定义

union 也是一种用户自定义的数据类型,它允许不同类型的数据成员共享同一块内存空间。union 的定义方式与 struct 类似,例如:

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

这里定义了一个名为 Dataunion,它包含一个 int 类型成员 i,一个 float 类型成员 f 和一个 char 类型成员 c。由于这些成员共享同一块内存,所以在某一时刻,union 只能存储其中一个成员的值。使用示例如下:

int main() {
    Data d;
    d.i = 10;
    // 此时 d.f 和 d.c 的值是未定义的
    return 0;
}

当我们给 d.i 赋值后,这块内存就被 i 使用了,如果此时访问 d.fd.c,得到的将是未定义的值。

内存布局差异

struct 的内存布局

struct 的内存布局是按照成员声明的顺序依次排列的。每个成员在内存中占据其自身所需的空间大小,并且为了满足内存对齐的要求,成员之间可能会存在一些填充字节。例如:

struct Example1 {
    char c;
    int i;
};

在一个以 4 字节对齐的系统中,char 类型占用 1 字节,int 类型通常占用 4 字节。为了满足 int 类型的 4 字节对齐要求,在 char 成员之后会填充 3 字节,所以 Example1 struct 的大小为 8 字节。我们可以通过 sizeof 运算符来验证:

int main() {
    Example1 e;
    std::cout << "Size of Example1: " << sizeof(e) << std::endl;
    return 0;
}

输出结果将是 8

如果 struct 中包含其他 struct 成员,同样遵循内存对齐规则。例如:

struct Inner {
    short s;
    char c;
};
struct Outer {
    Inner inner;
    int i;
};

在以 4 字节对齐的系统中,Inner structshort 占用 2 字节,char 占用 1 字节,为了满足 4 字节对齐,Inner struct 大小为 4 字节。Outer struct 中,Inner 成员占用 4 字节,int 成员占用 4 字节,所以 Outer struct 大小为 8 字节。

union 的内存布局

union 的内存布局比较特殊,它的大小是其最大成员的大小,因为所有成员共享同一块内存空间。例如:

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

在这个 union 中,char 占用 1 字节,int 通常占用 4 字节,double 占用 8 字节,所以 Example2 union 的大小为 8 字节。同样可以通过 sizeof 运算符验证:

int main() {
    Example2 e;
    std::cout << "Size of Example2: " << sizeof(e) << std::endl;
    return 0;
}

输出结果将是 8

由于 union 成员共享内存,对一个成员的赋值会覆盖其他成员的值。例如:

int main() {
    Example2 e;
    e.i = 10;
    // 此时 e.c 和 e.d 的值是被覆盖后的未定义值
    e.d = 3.14;
    // 此时 e.c 和 e.i 的值是被覆盖后的未定义值
    return 0;
}

数据存储和访问差异

struct 的数据存储和访问

struct 可以同时存储所有成员的值,每个成员都有自己独立的存储空间。我们可以根据需要分别对每个成员进行访问和修改。例如:

struct Student {
    std::string name;
    int age;
    float grade;
};
int main() {
    Student s;
    s.name = "Alice";
    s.age = 20;
    s.grade = 3.5;
    std::cout << "Name: " << s.name << ", Age: " << s.age << ", Grade: " << s.grade << std::endl;
    return 0;
}

上述代码可以正常存储和访问 Student struct 的各个成员。

union 的数据存储和访问

union 在某一时刻只能存储一个成员的值。当给一个成员赋值时,会覆盖其他成员的值。例如:

union Value {
    int intValue;
    float floatValue;
};
int main() {
    Value v;
    v.intValue = 100;
    // 此时 v.floatValue 是未定义的
    std::cout << "Int Value: " << v.intValue << std::endl;
    v.floatValue = 3.14f;
    // 此时 v.intValue 是未定义的
    std::cout << "Float Value: " << v.floatValue << std::endl;
    return 0;
}

在访问 union 成员时,必须确保访问的是当前有效的成员,否则会得到未定义的结果。

初始化差异

struct 的初始化

struct 可以通过多种方式进行初始化。在 C++11 之前,可以使用传统的初始化列表:

struct Point {
    int x;
    int y;
};
int main() {
    Point p = {10, 20};
    return 0;
}

C++11 引入了统一初始化语法,使得初始化更加灵活:

struct Rectangle {
    int width;
    int height;
};
int main() {
    Rectangle r{100, 200};
    Rectangle r2 = Rectangle{100, 200};
    return 0;
}

如果 struct 定义了构造函数,也可以使用构造函数进行初始化:

struct Circle {
    int radius;
    Circle(int r) : radius(r) {}
};
int main() {
    Circle c(5);
    return 0;
}

union 的初始化

union 的初始化只能初始化其第一个成员。例如:

union Data {
    int i;
    float f;
    char c;
};
int main() {
    Data d = {10}; // 初始化 i 为 10
    return 0;
}

如果要初始化其他成员,需要在初始化后单独赋值。例如:

int main() {
    Data d;
    d.f = 3.14f;
    return 0;
}

成员函数和继承差异

struct 的成员函数和继承

struct 可以包含成员函数,并且在 C++ 中,structclass 在继承和成员访问控制方面非常相似,唯一的区别在于默认的访问控制权限。struct 默认的成员访问权限是 public,而 class 默认是 private。例如:

struct Base {
    int value;
    void printValue() {
        std::cout << "Value: " << value << std::endl;
    }
};
struct Derived : Base {
    void incrementValue() {
        value++;
    }
};
int main() {
    Derived d;
    d.value = 10;
    d.printValue();
    d.incrementValue();
    d.printValue();
    return 0;
}

在上述代码中,Derived struct 继承自 Base struct,并且可以访问 Base structpublic 成员 valueprintValue 函数。

union 的成员函数和继承

union 也可以包含成员函数,但由于其内存共享的特性,成员函数的编写需要特别小心。union 不能作为基类被继承,因为继承涉及到对象内存布局的扩展,而 union 的内存布局是固定的且所有成员共享内存,这与继承的机制不兼容。例如:

union MyUnion {
    int i;
    float f;
    void setValue(int val) {
        i = val;
    }
    void setValue(float val) {
        f = val;
    }
};

在这个 union 中定义了两个重载的 setValue 函数,分别用于设置 intfloat 成员的值。

应用场景差异

struct 的应用场景

  1. 数据聚合:当需要将多个相关的数据项组合在一起时,struct 是很好的选择。例如,一个表示日期的 struct 可以包含年、月、日等成员。
struct Date {
    int year;
    int month;
    int day;
};
  1. 面向对象编程:在一些简单的面向对象场景中,struct 可以作为轻量级的类使用,尤其是当成员主要是数据且默认访问权限为 public 更合适时。例如,一个简单的图形库中表示点和矩形的 struct
struct Point {
    int x;
    int y;
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};
struct Rectangle {
    Point topLeft;
    Point bottomRight;
    int area() {
        int width = bottomRight.x - topLeft.x;
        int height = bottomRight.y - topLeft.y;
        return width * height;
    }
};

union 的应用场景

  1. 节省内存:当需要在不同时刻存储不同类型的数据,并且这些数据不会同时使用时,union 可以节省内存。例如,在一个通信协议中,可能会根据消息类型,使用同一个内存区域来存储不同类型的消息数据。
union MessageData {
    int commandId;
    float sensorValue;
    char text[100];
};
  1. 位操作union 可以方便地进行位操作。通过将不同类型的成员组合在一起,可以利用不同类型的访问方式来操作同一块内存的不同位。例如,通过 union 可以将一个 int 类型数据看作是由多个 char 类型数据组成,从而对 int 的各个字节进行单独操作。
union ByteManipulation {
    int num;
    char bytes[4];
};
int main() {
    ByteManipulation bm;
    bm.num = 0x12345678;
    std::cout << "Byte 0: " << std::hex << static_cast<int>(bm.bytes[0]) << std::endl;
    std::cout << "Byte 1: " << std::hex << static_cast<int>(bm.bytes[1]) << std::endl;
    std::cout << "Byte 2: " << std::hex << static_cast<int>(bm.bytes[2]) << std::endl;
    std::cout << "Byte 3: " << std::hex << static_cast<int>(bm.bytes[3]) << std::endl;
    return 0;
}

在上述代码中,通过 union 可以方便地访问 int 类型数据的各个字节。

注意事项

struct 的注意事项

  1. 内存对齐:由于 struct 的内存布局受内存对齐影响,在不同的系统和编译器设置下,struct 的大小可能会有所不同。在进行跨平台开发或对内存布局有严格要求的场景中,需要特别注意。
  2. 访问控制:虽然 struct 默认成员访问权限为 public,但在设计复杂的类层次结构时,需要谨慎考虑是否需要将某些成员设置为 privateprotected,以提高代码的安全性和封装性。

union 的注意事项

  1. 未定义行为:由于 union 成员共享内存,访问未当前有效的成员会导致未定义行为。在编写代码时,必须确保在访问某个成员之前,该成员是最近被赋值的。
  2. 构造函数和析构函数:如果 union 包含具有非平凡构造函数或析构函数的成员,在使用 union 时需要特别小心。例如,如果 union 包含一个 std::string 成员,由于 std::string 有自己的资源管理机制,直接使用 union 可能会导致内存泄漏或其他未定义行为。此时,需要手动管理构造和析构过程。

通过以上对 C++ 中 structunion 的详细对比,我们可以更清晰地了解它们的内在差异,从而在实际编程中根据具体需求选择合适的数据类型,提高代码的效率和正确性。无论是数据聚合、节省内存还是面向对象编程,正确使用 structunion 都能为我们的程序开发带来很大的便利。