问题

C++中 union 储存的 char 成员能否通过 int 成员读取?

回答
在 C++ 中,`union` 是一种特殊的复合数据类型,它允许你在同一块内存区域中存储不同类型的数据。但关键在于,同一时间只能有一个成员是活跃的,也就是当前正在被使用的。

对于你提出的问题:“`union` 中存储的 `char` 成员能否通过 `int` 成员读取?”,答案是:可以,但这样做存在潜在的风险和未定义的行为。

我们来详细拆解一下。

`union` 的工作原理

想象一下 `union` 就像一个共享的房间。这个房间可以根据需要容纳不同大小的家具(成员),但同一时间只能摆放一件家具。所有家具都占用房间的同一块地方。

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

在这个 `Data` union 中,`i`(一个 `int`)、`c`(一个 `char`)和 `f`(一个 `float`)都共享同一块内存。这块内存的大小通常由 union 中占用空间最大的成员决定。

存储 `char`,读取 `int`

假设我们向 `union` 的 `char` 成员写入了数据,然后尝试通过 `int` 成员读取。

```c++
include

union Data {
int i;
char c;
};

int main() {
Data myData;

// 1. 向 char 成员写入数据
myData.c = 'A'; // 假设 char 是 ASCII,'A' 的 ASCII 值是 65

// 2. 尝试通过 int 成员读取
std::cout << "Reading as int: " << myData.i << std::endl;

return 0;
}
```

会发生什么?

`char` 类型在 C++ 中通常是 1 个字节。`int` 类型通常是 4 个字节(也可能更多,取决于平台和编译器)。

当我们将 `'A'`(ASCII 值为 65)存储到 `myData.c` 时,`myData` 这块内存中的第一个字节(低位字节)会被设置为 `65`。

然后,当我们尝试通过 `myData.i` 读取时,我们实际上是在读取 `myData` 所占用的内存区域(假设是 4 个字节)的全部内容,并将这 4 个字节解释为一个 `int`。

内存的视角:

假设 `myData` 在内存中的样子(这是简化和抽象的表示,实际内存表示可能因字节序而异):

存储 `myData.c = 'A'`:
内存块: `[65] [??] [??] [??]` (假设 `int` 是 4 字节)
这里 `[??]` 表示未初始化或之前的值,我们只关心 `c` 写入的部分。

读取 `myData.i`:
程序会读取 `[65] [??] [??] [??]` 这整个 4 字节的内存,并尝试将其解释为一个 `int`。

结果将是什么?

1. 如果 `int` 是小端字节序 (Littleendian):
低位字节(`65`)会放在内存的起始位置。读取 `int` 时,就会将 `65` 作为 `int` 的最低有效字节。如果其他字节(`??`)是零,那么 `myData.i` 的值就是 `65`。

2. 如果 `int` 是大端字节序 (Bigendian):
高位字节会放在内存的起始位置。如果 `65` 被存储到 `c`,并且 `c` 只是 `int` 内存的一小部分,那么 `65` 可能只占 `int` 的一个字节,而其他字节将是未初始化的(`??`)。读取 `int` 时,结果会非常依赖于那些未初始化字节的值,这带来了未定义行为。

通常情况下,如果你只写入一个 `char`,并且其他字节未被初始化,然后将其解释为 `int`,结果很可能是 `char` 的值加上其他未初始化字节(可能为 0)构成的 `int` 值。

为什么这被称为“潜在风险”和“未定义行为”?

1. 未初始化的内存访问: 当你写入 `char` 成员时,你只初始化了 `union` 内存中的一部分。当你读取 `int` 成员时,你实际上是在读取整个 `union` 的内存,包括那些你从未显式写入过的部分。读取未初始化内存是 C++ 中的未定义行为 (Undefined Behavior, UB)。这意味着编译器可以自由地做任何它想做的事情:程序可能崩溃,产生奇怪的结果,或者在某些情况下“碰巧”工作正常。

2. 类型双关 (Type Punning): 这种通过一个类型的成员读取另一类型成员的行为,在 C++ 中被称为“类型双关”。C++ 标准对此有明确的规定:“当 union 的一个非活动成员被读取时,其行为是未定义的。” (ISO/IEC 14882:2020, Section 12.6.2 Unions)。

3. 字节序问题: 即使在同一平台上,不同的数据类型(如 `char` 和 `int`)的字节如何在内存中存储(字节序)可能会影响结果,尤其是在处理多字节类型时。

4. 填充和对齐: 编译器可能会在 `union` 成员之间插入填充字节以满足对齐要求,这会进一步复杂化内存布局。

总结:能读,但绝对不推荐

技术上讲,你可以通过 `int` 成员读取 `union` 中存储的 `char` 成员。 `char` 在 `union` 中占用的内存字节会被 `int` 读取时所覆盖。

但是,这样做是 C++ 标准所禁止的,属于未定义行为。 依赖这种行为的程序是不可移植的,并且在不同编译器、不同编译选项或不同硬件平台上可能会表现出完全不同的行为,甚至可能无法按预期工作。

何时可以这样做(但依然不推荐,因为存在更好的做法):

你知道 union 的内存布局,并且你确切地知道 `char` 占用了 `int` 的哪一个字节(取决于字节序)。
你只需要 `char` 的 ASCII 值,并且确保 `int` 的其他字节是零(这通常需要手动处理或依赖特定 ABI)。

推荐的做法:

如果你需要将一个 `char` 的值转换为 `int`,请使用 C++ 提供的类型转换方式:

```c++
include

int main() {
char myChar = 'A';
int myInt = static_cast(myChar); // 推荐的、明确的类型转换

std::cout << "Converted int: " << myInt << std::endl; // 输出 65
return 0;
}
```

或者,如果你是在 `union` 的上下文中使用,并且你确实需要在 `union` 中存储和读取不同类型的值,那么务必确保你总是读取那个你最后写入的成员。

```c++
include

union Data {
int i;
char c;
};

int main() {
Data myData;

// 写入 char
myData.c = 'A';
// 此时 c 是活跃成员

// 读取 char (安全)
std::cout << "Reading back as char: " << myData.c << std::endl;

// 写入 int
myData.i = 12345;
// 此时 i 是活跃成员

// 读取 int (安全)
std::cout << "Reading back as int: " << myData.i << std::endl;

// !!! 尝试读取已非活跃成员 (未定义行为,不应这样做) !!!
// myData.c = 'B';
// std::cout << "Reading as int after writing char: " << myData.i << std::endl;
// std::cout << "Reading as char after writing int: " << myData.c << std::endl; // UB

return 0;
}
```

总而言之,虽然从内存操作的角度看,你“可以”通过 `int` 读取 `union` 中存储的 `char`,但这种做法是 C++ 的禁区,会带来不可预测的后果,并且有明确的标准禁止。为了编写健壮、可移植的代码,请避免此类操作。

网友意见

user avatar

(邀请的名单有点长,我就默认选择谢邀第一个了)。这个问题是比较典型的大小端问题。我们从两个方面来说:

(补充:有小伙伴指出C++对union有特殊的生存周期的处理,我看了一下C++还确实有member lifetime这个说法,既然如此,那下面的程序我就只能说适用于C语言了。因为C++从编译器层面对union成员的行为有了特别的“规定”,这种处理几乎可以说从语法上限制了问题题目的这种使用方式,将这种行为直接视为未定义【具体的参考评论区和invalid s的讨论】。C语言则粗糙或者说开放很多,只要你程序员处理的好大小端的问题,这事我就不管~):

首先说一下大小端对

       union {   char c;   int n; } un;     

的影响。

我觉得这个很多人基本都应该是清楚的,比如下面的程序:

       #include <stdio.h> union mytest {   char c;   int n; };  int main() {         union mytest un = {0};          un.n = 0xFF0055AA;         printf("un.n = 0x%x
", un.n);         printf("un.c = 0x%hhx
", un.c);         return 0; }     

你觉得打印出来的结果会是什么?特别是un.c的结果,你觉得是FF还是AA?

……

实际上在抛开大小端不谈的情况下是没有明确答案的,在大端机器上(我使用的s390x)结果是:

       un.n = 0xff0055aa un.c = 0xff     

而在小端机器上(我使用的x86_64)结果是:‘

       un.n = 0xff0055aa un.c = 0xaa     

如果你不能理解,那就要回答什么是大小端这个最原始的问题上来了。可能很多人都听过大小端的定义,但是实际使用时总是会有一些模糊的地方。关于大小端的问题我写过下面的一个回答,做了比较详细的解释:

如果你懒得翻,我就用一小段说一下:

关于Endianness有这样一段描述(来源wikipedia,非决定性定义)

In its most common usage, endianness indicates the ordering of bytes within a multi-byte number. A big-endian ordering places the most significant byte first and the least significant byte last, while a little-endian ordering does the opposite.

这段描述可以清楚的说明我们“通常”所说的大小端的意思。首先我们所使用的现代计算机系统都是以字节为一个最小存储单元,你向内存中读写数据一次最少是一个字节,换句话说一个字节内是不能拆分的(取bit是先取字节再得到的bit)。而多个字节就涉及了如何拆分/排布的问题,比如一个两个字节的数据类型,我们定义为:

       u16 data = 0x1122;     

那么

       高地址 +------+             +------+ | 0x11 |             | 0x22 | +------+     或      +------+ | 0x22 |             | 0x11 | +------+             +------+ 低地址     

的存储方式就成了人们争论的问题。很遗憾生产厂商没有就此达成共识,于是就有了针对一个多字节的数据类型有“从高地址往低地址存”和“从低地址往高地址”两种方案。关于大小端问题不只存在于内存存储数据的方式上,比如硬盘存储、网络数据传输都方面都涉及大小端问题,这里先不引申了。所以大小端是针对一个多字节数据类型产生的,比如int, long等。而对于单字节,如char本身,是没有大小端的问题(除非在按位分大小端的系统上,不过我们目前没有这样系统的机器)。

所以上面程序在大端机器上的表现形式就是:

       高地址 +------+ | 0xaa | +------+ | 0x55 | +------+ | 0x00 | +------+ | 0xff | +------+     <--- un.n / un.c 低地址     

而在小端机器上的表现形式就是:

       高地址 +------+ | 0xff | +------+ | 0x00 | +------+ | 0x55 | +------+ | 0xaa | +------+     <--- un.n / un.c 低地址     

然后我们再说一下:

       union {   char c;   int n; } un;     

这种用法合不合适,我不能说绝对不合适,因为这里没有写明它的应用场景。但是就一般的使用习惯上来说,除非你明确知道自己在操作的内存块在不同平台的表现都符合你的预期,否则就别这么玩。

现实中常见的是保持大小一致的用法,比如用两个不同的名字:

       union {         unsigned long   sllp;   /* SLB L||LP (exact mask to use in slbmte) */         unsigned long ap;       /* Ap encoding used by PowerISA 3.0 */ };     

比如用数组:

       union {         __u8 cdata[4];         __u32 idata; } result, last_result;     

比如用位域:

       union ia64_isr {         __u64  val;         struct {                 __u64 code : 16;                 __u64 vector : 8;                 __u64 reserved1 : 8;                 __u64 x : 1;                 __u64 w : 1;                 __u64 r : 1;                 __u64 na : 1;                 __u64 sp : 1;                 __u64 rs : 1;                 __u64 ir : 1;                 __u64 ni : 1;                 __u64 so : 1;                 __u64 ei : 2;                 __u64 ed : 1;                 __u64 reserved2 : 20;         }; }     

等等……

那有没有问题中那种用法呢,肯定是有的,我一开始就没有否定这种用法,我否定的是在“不知道自己在干什么”的胡乱使用。比如下面这个例子(摘自Linux内核):

       void kvm_mmio_write_buf(void *buf, unsigned int len, unsigned long data) {         void *datap = NULL;         union {                 u8      byte;                 u16     hword;                 u32     word;                 u64     dword;         } tmp;          switch (len) {         case 1:                 tmp.byte        = data;                 datap           = &tmp.byte;                 break;         case 2:                 tmp.hword       = data;                 datap           = &tmp.hword;                 break;         case 4:                 tmp.word        = data;                 datap           = &tmp.word;                 break;         case 8:                 tmp.dword       = data;                 datap           = &tmp.dword;                 break;         }          memcpy(buf, datap, len); }     

到此,如果你觉得你已经清楚了,那么留几个思考题(设int是四个字节,char是一个字节):

       union mytest {         char c;         struct {                 char a[4];         }sta;         struct {                 char w;                 char x;                 char y;                 char z;         }stw;         int n; }un;     

这种情况下如果赋值:

       un.sta.a[0] = 0xAA; un.sta.a[1] = 0x55; un.sta.a[2] = 0x00; un.sta.a[3] = 0xFF;     

那么c, w, x, y, z和n(在32或64位大小端机器上,系统、编译器和库也是32位以上的)分别是多少?

如果赋值:

       un.stw.w = 0xAA; un.stw.x = 0x55; un.stw.y = 0x00; un.stw.z = 0xFF;     

那么c, a[0~3]和n分别是多少?

类似的话题

  • 回答
    在 C++ 中,`union` 是一种特殊的复合数据类型,它允许你在同一块内存区域中存储不同类型的数据。但关键在于,同一时间只能有一个成员是活跃的,也就是当前正在被使用的。对于你提出的问题:“`union` 中存储的 `char` 成员能否通过 `int` 成员读取?”,答案是:可以,但这样做存在潜.............
  • 回答
    在 C++ 中,为基类添加 `virtual` 关键字到析构函数是一个非常重要且普遍的实践,尤其是在涉及多态(polymorphism)的场景下。这背后有着深刻的内存管理和对象生命周期管理的原理。核心问题:为什么需要虚析构函数?当你在 C++ 中使用指针指向一个派生类对象,而这个指针的类型是基类指针.............
  • 回答
    结构体变量的读写速度 并不比普通变量快。这是一个常见的误解。事实上,在很多情况下,访问结构体成员的开销会比直接访问普通变量稍微 大一些,而不是更小。要详细解释这一点,我们需要深入理解 C++ 中的变量、内存模型以及编译器的工作方式。 1. 普通变量的读写首先,我们来看看一个简单的普通变量,例如:``.............
  • 回答
    在C++中,表达式 `unsigned t = 2147483647 + 1 + 1;` 的求值过程,既不是UB(Undefined Behavior),也不是ID(ImplementationDefined Behavior),而是一个有明确定义的整数溢出(Integer Overflow)行为。.............
  • 回答
    关于C++自定义函数写在 `main` 函数之前还是之后的问题,这涉及到C++的编译和链接过程,以及我们编写代码时的可读性和维护性。理解这一点,对你写出更健壮、更易于理解的代码非常有帮助。总的来说, 将自定义函数写在 `main` 函数之前通常是更推荐的做法,尤其是对于项目中主要的、被 `main`.............
  • 回答
    在 C++ 中讨论 `std::atomic` 是否是“真正的原子”时,我们需要拨开表面的术语,深入理解其底层含义和实际应用。答案并非一个简单的“是”或“否”,而是取决于你对“原子”的理解以及在什么上下文中去考量。首先,让我们明确一下在并发编程领域,“原子性”(Atomicity)通常指的是一个操作.............
  • 回答
    在C++中,函数返回并不是一个简单地“跳出去”的操作,它涉及到多个步骤,并且与值的传递方式、调用栈以及编译器优化等因素紧密相关。我们来详细拆解一下这个过程,力求还原真实的执行场景。核心概念:调用栈 (Call Stack)要理解函数返回,就必须先理解调用栈。当你调用一个函数时,程序会在调用栈上为这个.............
  • 回答
    在 C++ 中,将 `std::string` 类型转换为 `int` 类型有几种常见且强大的方法。理解它们的原理和适用场景对于编写健壮的代码至关重要。下面我将详细介绍几种常用的方法,并分析它们的优缺点: 方法一:使用 `std::stoi` (C++11 及以后版本)这是 最推荐 的方法,因为它提.............
  • 回答
    vector 和 stack 在 C++ 中都有各自的用处,它们虽然都属于序列容器,但设计目标和侧重点不同。可以这么理解:vector 就像一个可以随意伸缩的储物空间,你可以按照任何顺序往里面放东西,也可以随时拿出任何一个东西。而 stack 就像一个堆叠的盘子,你只能在最上面放盘子,也只能从最上面.............
  • 回答
    在C++中,区分 `char` 和数值(如 `int`, `float`, `double` 等)是编程中的基本概念,但理解其背后的机制能帮助你写出更健壮的代码。首先,我们需要明确一点:在C++底层,`char` 类型本质上也是一种整数类型。它通常用来存储单个字符的ASCII码值或其他编码标准下的数.............
  • 回答
    在C++中,我们不能直接“判断”一个指针指向的是栈(stack)还是堆(heap)。这种判断本身在很多情况下是不明确的,而且C++标准并没有提供直接的运行时机制来做到这一点。不过,我们可以通过一些间接的思考和观察来理解这个问题,并解释为什么直接判断很困难,以及我们通常是如何“知道”一个指针指向哪里。.............
  • 回答
    在 C++ 中,对整数进行除以 2 和右移 1 看起来很相似,它们都能将数字“减半”。但实际上,它们在底层执行机制、对负数和浮点数的影响,以及一些细微之处存在显著差异。我们来深入剖析一下。 除以 2 (`/ 2`):标准的算术运算在 C++ 中,`a / 2` 是一个标准的算术除法运算。它遵循正常的.............
  • 回答
    在 C 中,`async` 和 `await` 关键字提供了一种优雅的方式来编写异步代码,但它们并非直接等同于多线程。理解这一点至关重要。异步并非强制多线程,但常常借助它首先,我们要明确一个核心概念:异步编程的本质是为了提高程序的响应性和吞吐量,而不是简单地将任务并行执行。 异步的目的是让程序在等待.............
  • 回答
    如果 C 真的引入了类似 F 那样的管道运算符 “|>”,这无疑会是一场不小的革新,尤其是在函数式编程风格日益受到重视的今天。那么,它会带来什么变化?我们的代码会变成什么样?首先,我们得理解 F 中的管道运算符 `|>` 是做什么的。简单来说,它就是将一个表达式的结果作为另一个函数调用的第一个参数传.............
  • 回答
    在C中确实不存在Java或C++那样的“友元类”(friend class)机制。这常常让习惯了这种特性的开发者感到不适应,甚至认为这种设计“不太合理”。但实际上,C的设计哲学侧重于封装和明确的接口,友元类这种打破封装的特性并非是其追求的目标。那么,这种设计真的“不合理”吗?或者说,我们是否可以找到.............
  • 回答
    在C++中,当你在一个对象的成员函数内部执行 `delete this;` 时,对象的析构函数会先被调用,然后 `delete` 操作才会完成,并将内存释放。让我们来详细拆解一下这个过程,避免任何可能引起误解的地方。 核心机制:`delete this;` 的工作原理`delete this;` 这.............
  • 回答
    在 C++ 中处理超出标准 `char`、`int` 等基本数据类型表示范围的整数,其实并不是一个“存储”的问题,而是一个选择更合适数据类型的问题。C++ 为我们提供了多种整数类型,每种类型都有其固定的存储大小和取值范围。当我们需要处理的数值超出了某个类型的默认范围时,我们就需要选用更大的类型来容纳.............
  • 回答
    在C++中,当你使用指针作为 `std::map` 或 `std::set` 的键时,是否能改变键指向的对象,这涉及到指针的拷贝语义和容器内部的工作机制。理解这一点,我们需要深入分析以下几个方面:1. C++ 中的拷贝语义与指针首先,需要明确C++中拷贝一个指针时发生了什么。当你将一个指针赋值给另一.............
  • 回答
    在 C++ 编程中,指针和引用都是用来间接访问内存中数据的强大工具,但它们扮演的角色以及使用方式却各有侧重。很多人会疑惑,既然有了引用,为什么还需要指针呢?我们来深入聊聊这个问题。 指针:内存地址的直接操纵者简单来说,指针是一个变量,它存储的是另一个变量的内存地址。你可以想象一个房间的门牌号,这个门.............
  • 回答
    在C语言中,`struct`(结构体)之所以能成为构建复杂数据结构的基石,在于它提供了将不同类型的数据成员组合成一个单一逻辑单元的能力。这就像我们在现实生活中将不同零散的物品(姓名、年龄、学号等)打包成一个“学生”的概念一样。让我们一层层剥开,看看`struct`是如何做到这一点的,以及它在数据结构.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有