C语言union大小如何计算?

99ANYc3cd6
预计阅读时长 22 分钟
位置: 首页 C语言 正文

一个 union 的大小是其所有成员中最大成员的大小

c语言 union size
(图片来源网络,侵删)

这背后有两个关键原因:

  1. 内存共享union 的所有成员共享同一段内存空间。
  2. 对齐:为了提高内存访问效率,编译器会对内存进行对齐。union 的起始地址必须是其最大成员大小的整数倍,以确保其最大成员能够被高效访问。

详细解释

什么是 union

union(联合体)是一种特殊的用户自定义数据类型,它允许你在同一个内存位置存储不同类型的数据

struct(结构体)不同,struct 会为每个成员分配独立的内存空间,而 union 的所有成员都从同一个内存地址开始。union 在任何时候只存储其中一个成员的值。

union 大小规则

union 的大小由以下两个因素决定,最终取其最大值:

c语言 union size
(图片来源网络,侵删)

最大成员的大小 union 必须足够大,以容纳它最大的那个成员,这是最基本的要求。

最大成员的对齐要求 union 的整体大小还必须满足其内部最大成员的对齐要求,也就是说,union 的大小必须是最大成员大小的整数倍,这通常也等于最大成员的对齐数。

为什么需要考虑对齐?

内存对齐是现代计算机体系结构为了提高数据访问速度而引入的机制,CPU 读取内存时,通常从某个特定地址(如 4 字节、8 字节的倍数)开始,一次性读取一个“字”(word),如果数据没有对齐,CPU 可能需要执行两次内存访问才能读取完整的数据,这会严重影响性能。

union 作为一个整体,它的内存布局也必须遵循对齐规则,以确保当它内部的最大成员被使用时,能够被 CPU 高效地访问。

c语言 union size
(图片来源网络,侵删)

示例分析

让我们通过几个具体的例子来理解 union 大小的计算。

示例 1:简单的 union

#include <stdio.h>
// 假设 sizeof(char) = 1, sizeof(int) = 4, sizeof(double) = 8
// 对齐数通常等于其大小
union SimpleU {
    char c;    // 大小 1, 对齐数 1
    int i;     // 大小 4, 对齐数 4
    double d;  // 大小 8, 对齐数 8
};
int main() {
    printf("Size of SimpleU: %zu bytes\n", sizeof(SimpleU));
    // 预期输出: Size of SimpleU: 8 bytes
    return 0;
}

分析:

  1. 成员大小char (1), int (4), double (8),最大成员是 double,大小为 8。
  2. 成员对齐数char (1), int (4), double (8),最大成员的对齐数是 8。
  3. 计算 union 大小
    • union 必须至少为 8 字节以容纳 double
    • union 的大小必须是最大对齐数(8)的整数倍,8 本身就是 8 的 1 倍。
    • sizeof(SimpleU) 的结果是 8

内存布局: SimpleU 的对象占用 8 字节的连续内存,当你给 c 赋值时,只使用了第一个字节;当你给 i 赋值时,使用了前 4 个字节;当你给 d 赋值时,使用了全部 8 个字节。

+---+---+---+---+---+---+---+---+
| c |   |   |   |   |   |   |   |  (char c)
+---+---+---+---+---+---+---+---+
| i | i | i | i |   |   |   |   |  (int i)
+---+---+---+---+---+---+---+---+
| d | d | d | d | d | d | d | d |  (double d)
+---+---+---+---+---+---+---+---+

示例 2:带有填充的 union

#include <stdio.h>
// 假设 sizeof(char) = 1, sizeof(int) = 4, sizeof(double) = 8
union PaddedU {
    char c;    // 大小 1, 对齐数 1
    double d;  // 大小 8, 对齐数 8
    int i;     // 大小 4, 对齐数 4
};
int main() {
    printf("Size of PaddedU: %zu bytes\n", sizeof(PaddedU));
    // 预期输出: Size of PaddedU: 8 bytes
    return 0;
}

分析:

  1. 成员大小char (1), double (8), int (4),最大成员是 double,大小为 8。
  2. 成员对齐数char (1), double (8), int (4),最大成员的对齐数是 8。
  3. 计算 union 大小
    • union 必须至少为 8 字节。
    • union 的大小必须是 8 的整数倍。
    • 结果仍然是 8

成员的顺序不影响 union 的大小,因为所有成员都从地址 0 开始。union 的大小只取决于最大的那个成员。


示例 3:嵌套 structunion

这个例子能更好地说明对齐的重要性。

#include <stdio.h>
// 假设 sizeof(char) = 1, sizeof(int) = 4, sizeof(double) = 8
// 假设结构体对齐规则:成员按对齐数排列,整体大小是最大对齐数的整数倍
struct MyStruct {
    char c;      // 大小 1, 对齐数 1
    int i;       // 大小 4, 对齐数 4
    // 为了对齐,编译器在 char c 后面会填充 3 个字节
    // struct MyStruct 的大小是 8 (最大对齐数是 4, 1+4+3=8)
};
union ComplexU {
    int a;               // 大小 4, 对齐数 4
    struct MyStruct s;   // 大小 8, 对齐数 4 (struct 的对齐数是其最大成员的对齐数)
    double d;            // 大小 8, 对齐数 8
};
int main() {
    printf("Size of MyStruct: %zu bytes\n", sizeof(struct MyStruct)); // 预期 8
    printf("Size of ComplexU: %zu bytes\n", sizeof(union ComplexU));  // 预期 16
    return 0;
}

分析:

  1. 计算 struct MyStruct 的大小

    • char c (1字节) + int i (4字节)。
    • 为了让 int i 从 4 字节对齐的地址开始,需要在 char c 后面填充 3 个字节。
    • struct MyStruct 的大小 = 1 + 3 (padding) + 4 = 8 字节,它的对齐数是其最大成员 int 的对齐数,即 4。
  2. 计算 union ComplexU 的大小

    • 成员大小int a (4), struct MyStruct s (8), double d (8),最大成员是 struct sdouble d,大小都是 8。
    • 成员对齐数int a (4), struct s (4), double d (8)。最大成员的对齐数是 double d 的 8
    • 计算 union 大小
      • union 必须至少为 8 字节以容纳最大成员。
      • union 的大小必须是最大对齐数(8)的整数倍。
      • 8 是 8 的 1 倍,所以大小是 8?不,这里有一个陷阱!
  3. 深入分析 ComplexU 的内存布局

    • ComplexU 的起始地址必须是 8 的倍数(因为 double d 的对齐要求是 8)。
    • ComplexU 的大小是 8,那么它的内存布局如下:
      +---+---+---+---+---+---+---+---+
      | a | a | a | a | s | s | s | s |  (假设先放 a, 再放 s)
      +---+---+---+---+---+---+---+---+
      | d | d | d | d | d | d | d | d |  (d)
      +---+---+---+---+---+---+---+---+
    • 这看起来没问题。struct MyStruct 本身有它自己的对齐要求,它的起始地址也应该是 4 的倍数。
    • 在一个 8 字节的 union 中,地址 0, 1, 2, 3, 4, 5, 6, 7。
      • s 从地址 0 开始,没问题(0是4的倍数)。
      • s 从地址 4 开始,没问题(4是4的倍数)。
    • 似乎 8 字节就够了。为什么很多编译器会给 16 字节?

    更严谨的解释: C 标准规定,union 的每个成员都必须被正确对齐,这意味着 union 的整体大小必须满足其所有成员的对齐要求。

    • int a 的对齐要求是 4。
    • struct s 的对齐要求是 4。
    • double d 的对齐要求是 8。

    ComplexU 的对齐要求是这些成员对齐要求中的最大值,即 max(4, 4, 8) = 8ComplexU 的大小必须是其对齐要求(8)的整数倍。 它必须能容纳最大的成员(struct sdouble d,大小为 8)。 sizeof(ComplexU) 应该是 max(8, 8) 的整数倍,即 8。

    在实践中,为了确保 union 内部任何成员的任何子成员都能被正确对齐,编译器可能会采取更保守的策略。struct MyStruct 内部有填充,当它被放入 union 时,union 的大小不是 struct 内部最大成员对齐数的倍数,可能会出问题,虽然在这个简单例子中 8 字节足够,但在更复杂的嵌套情况下,确保 union 的大小是所有成员(包括其嵌套成员)对齐数的最大值的整数倍是更安全的做法。

    对于这个例子,sizeof(ComplexU) 在大多数现代编译器(如 GCC, Clang on x86-64)上是 16,这是因为编译器确保 union 的大小是其所有成员(包括嵌套成员)对齐要求的最小公倍数或最大值的整数倍,以避免任何潜在的未定义行为和对齐问题,最可靠的规则是:union 的大小是其最大成员的大小,并且是最大成员对齐数的整数倍,这个整数倍要足够大以容纳所有可能的布局。

    简化版规则(适用于99%的场景)sizeof(union) = max(sizeof(member)),然后向上取整到该成员对齐数的整数倍。

    ComplexU 中,max(sizeof(member)) 是 8。double 的对齐数是 8,8 已经是 8 的整数倍,所以大小是 8。这再次说明了 C 语言内存布局的复杂性,具体实现可能略有不同。

    让我们再看一个更清晰的例子:

#include <stdio.h>
struct S1 {
    char c;
};
struct S2 {
    double d;
};
union U1 {
    struct S1 s1; // sizeof(s1)=1, alignof(s1)=1
    struct S2 s2; // sizeof(s2)=8, alignof(s2)=8
};
int main() {
    printf("Size of S1: %zu\n", sizeof(struct S1)); // 1
    printf("Align of S1: %zu\n", _Alignof(struct S1)); // 1
    printf("Size of S2: %zu\n", sizeof(struct S2)); // 8
    printf("Align of S2: %zu\n", _Alignof(struct S2)); // 8
    printf("Size of U1: %zu\n", sizeof(union U1));  // 8
    printf("Align of U1: %zu\n", _Alignof(union U1)); // 8
    return 0;
}

这个例子清晰地表明,U1 的大小由 S2 决定,为 8 字节。


特性 描述
核心原则 union 的大小是其所有成员中最大成员的大小
对齐规则 union 的整体大小也必须是其最大成员的对齐数的整数倍,以保证高效访问。
最终大小 sizeof(union) = max(sizeof(member_1), sizeof(member_2), ...),然后确保这个值是 max(alignof(member_1), alignof(member_2), ...) 的整数倍,如果还不是,则继续向上取整。
内存布局 所有成员共享从同一个起始地址开始的内存空间,一次只能安全地使用一个成员。
用途 当你需要用同一段内存来存储不同类型的数据,并且知道这些数据不会同时使用时(解析网络协议包、实现类型安全的联合等)。

简单记:union 的大小 = 最大的那个成员的大小,并满足其对齐要求。 在绝大多数情况下,就是那个最大成员的大小。

-- 展开阅读全文 --
头像
PID在MATLAB与C语言中如何实现?
« 上一篇 昨天
dede模板首页左侧横向菜单如何制作?
下一篇 » 昨天

相关文章

取消
微信二维码
支付宝二维码

目录[+]