(邀请的名单有点长,我就默认选择谢邀第一个了)。这个问题是比较典型的大小端问题。我们从两个方面来说:
(补充:有小伙伴指出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分别是多少?