对union的进一步认识与一些深层应用

时间:2021-05-16 09:45:14
虽然平时在程序开发时较少使用 union ,虽然当初学C语言时 , union一章被老师略过没有介绍 ,虽然 ,自认为自己对 union的认识已经足够 ,但是 ,在写完上一篇文章 <(大卫的阅读笔记)C++中使用union的几点思考>之后 ,网上的讨论驱使我对这一基本的语言特性又投入了一些精力去关注 ,并写成了此文 .

下面以MSDN中关于 union的概述作为开头 ,这好像有些无聊 ,不过 ,有时候看specification的东西可以给我们很多提示 ,当我们从应用的角度去关注一样东西的时候 ,很多更深层次的考虑被我们忽略了 .所以 ,看下去 ,里面有些东西可能正是你忽略了的 .

union
union
 [tag ] { member -list  } [declarators ];

[
union ] tag declarators ;

The  union keyword declares a  union type  and / or a variable of a  union type .

A  union is a user -defined data type that can hold values of different types at different times . It is similar to a structure
except that all of its members start at the same location in memory . A  union variable can contain only one of its members at
a time . The size of the  union is at least the size of the largest member (大卫注 :我想不出来大于的情况 ).

For related information , see  class ,  struct ,  and Anonymous Union .

Declaring a Union

Begin the declaration of a  union with the  union keyword ,  and enclose the member list in curly braces :

union
 UNKNOWN     // Declare union type
{
   char
   ch ;
   int
    i ;
   long
   l ;
   float
  f ;
   double
 d ;
}
 var1 ;           // Optional declaration of union variable
Using a Union

A C ++  union is a limited form of the  class type . It can contain access specifiers  ( public ,  protected ,  private ), member data ,
and
 member functions , including constructors  and destructors . It cannot contain  virtual functions  or static data members . It
cannot be used as a base  class , nor can it have base classes . Default access of members in a  union is  public .

A C  union type can contain only data members .

In C , you must use the  union keyword to declare a  union variable . In C ++, the  union keyword is unnecessary :

Example  1

union
 UNKNOWN var2 ;    // C declaration of a union variable
UNKNOWN var3 ;          // C++ declaration of a union variable
Example  2

A variable of a  union type can hold one value of any type declared in the  union . Use the member -selection  operator  (.) to access a member of a  union :

var1 .i  =  6 ;            // Use variable as integer
var2 .d  =  5.327 ;        // Use variable as double

为了避免对上述文字有稍许的歪曲 ,我故意没有翻译它 ,但在此对其进行一些归纳 :
1.
union是一种特殊的 struct / class ,是一种可用于容纳多种类型的类型 ,但与 struct / class不同的是 ,所有的成员变量共享同一存储空间 (最大的那一个成员类型的大小 ),这使得它具有多变的特性 ,可以在不同成员中任意切换 ,而无需借助强制类型转换 ,但这也使得你不能把它当作一个成员变量进行修改而不影响到另一成员变量 ;
2.
union也可以有构造 /析构函数 ,也可以包含访问标识符 ,但不能包含虚函数或静态成员变量 /方法 .

关于使用 union时需要注意的一些问题 ,可以参考我的前一篇文章 :<(大卫的阅读笔记 )C ++中使用 union的几点思考 >.
下面谈谈一些比较有意思并且有意义的 union的应用 .
1.
in_addr

struct
 in_addr  {
  union
 {
          struct
 { u_char s_b1 ,s_b2 ,s_b3 ,s_b4 ; }   S_un_b ;
          struct
 { u_short s_w1 ,s_w2 ; }            S_un_w ;
          u_long                                   S_addr ;
  }
 S_un ;
};


对于上面的 struct ,写过socket应用的人 ,肯定都用过它 .不知你注意过没有 ,它包含了一个很有趣的 union ,union的各成员具有相同的大小 ,分别代表同一信息的不同表现形式 .你在进行程序设计的时候也可以利用这一特性来提供同一信息的不同表现形式 ,不过要注意 ,在进行跨平台应用时 ,字节顺序的影响可能给你造成一些不必要的麻烦 .

2.
匿名 union
匿名 union是没有名称和声明列表的 union ,这跟 '__unnamed'  union不是一回事 ,它的声明形式如下 :
union
 { member -list  } ;

匿名 union仅仅通知编译器它的成员变量共享一个地址 ,而变量本身是直接引用的 ,不使用通常的点号运算符语法 .也正因此 ,匿名 union与同一程序块内的其它变量具有相同的作用域级别 ,需注意命名冲突 .
请看下面的例子 :

#include <iostream.h>

struct
 DataForm
{

    enum
 DataType  { CharData  =  1 , IntData , StringData  };
    DataType type ;

    // Declare an anonymous union.
    union
    {

        char
  chCharMem ;
        char
 *szStrMem ;
        int
   iIntMem ;
    };

    void
 print ();
};


void
 DataForm ::print ()
{

    // Based on the type of the data, print the
    // appropriate data type.
    switch ( type  )
    {

    case
 CharData :
        cout  << chCharMem ;
        break
;
    case
 IntData :
        cout  << szStrMem ;
        break
;
    case
 StringData :
        cout  << iIntMem ;
        break
;
    }
}


此外 ,匿名 union还具有以下约束 :
1
).因为匿名联合不使用点运算符 ,所以包含在匿名联合内的元素必须是数据 ,不允许有成员函数 ,也不能包含私有或受保护的成员 ;
2
).全局匿名联合必须是静态 ( static )的,否则就必须放在匿名名字空间中 .

附注 :
对匿名 union的概念 ,你或许有些陌生 ,但对于Windows应用的开发人员 ,有一个经常用到的结构中就包含了匿名 union ,它就是VARIANT ,也许你没有注意它罢了 :

typedef struct
 FARSTRUCT tagVARIANT VARIANT ;
typedef struct
 FARSTRUCT tagVARIANT VARIANTARG ;

typedef struct
 tagVARIANT   {
   VARTYPE vt ;
   unsigned short
 wReserved1 ;
   unsigned short
 wReserved2 ;
   unsigned short
 wReserved3 ;
   union
 {
      Byte                    bVal ;                  // VT_UI1.
      Short                   iVal ;                  // VT_I2.
      long                    lVal ;                  // VT_I4.
      float                   fltVal ;                // VT_R4.
      // ...
   };
};


3.
利用 union进行类型转换
前面已经说过 , union具有多变的特性 ,可以在不同成员中任意切换 ,而无需借助强制类型转换 ,下面举例说明这一点 (其实 1已经很好地说明了这一点 ):

#include <iostream>
using namespace std ;

struct
 DATA
{

    char
 c1 ;
    char
 c2 ;
};


int
 main ()
{

    union
 {  
        int
 i ; 
        DATA data
    }
 _ut ;
    
    _ut .i  =  0x6162 ;

    cout  <<  "_ut.data.c1 = "  << _ut .data .c1  << endl
        <<
 "_ut.data.c2 = "  << _ut .data .c2  << endl ;
    
    return
 0 ;
}


需要提醒你的是 ,数据类型的转换 ,并非 union的专长 ,只是一个可资利用的特性而已 .因为 ,采用 union进行类型间转换极易受平台影响 ,如上面的程序采用Intel x86  + Windows  2000  + VC6时输出为 :
_ut .data .c1  = b
_ut .data .c2  = a
(
:因为Intel CPU的架构是Little Endian )
而在Sun的Sparc上 ,你得到的结果却是 :
_ut .data .c1  = 
_ut .data .c2  = 
(
:因为采用Big Endian时 ,前两个字节为 0x0000 )

而即便是在同一平台上 ,在integer类型与real类型间进行转换时也不要采用 union ,否则 ,你会得到令你莫名其妙的结论 (这是由于CPU对real类型的处理方式引起的 ,该方式在各平台上有极大区别 ,同时 ,根据C ++ Standard ,这种作法会引起 "undefined behavior" ).

关于利用引用进行类型转换 ,可参考 <引用在强制类型转化中的应用>.

/////////////////////////////////////////////////////////////////////////////////////////////

事实上,我在工作中很少使用union,今天偶尔用了一下,呵呵,问题还真不少,很多细节以前都没有注意到。我查了一下csdn,关于union方面的帖子很少,这篇文章就算是弥补一下空白吧。大家有什么看法,都可以补充一下。
btw:我认为,c++中union的使用应该仅局限于传统的用法,就是类似于下面的用法

typedef   unsigned   short   VARTYPE;
struct   _tagMyVariant
{
              VARTYPE     vt;
              union
              {
                      LONG     lVal;
                      BYTE     bVal;
                      ...
              };
};

正文:
C++中的union,很大程度上是为了兼容C语言才提出来的,看了下面的文章,你就会感觉到,C++中的union显得有些不伦不类。如果需要用到union,我们最好使用传统的union,也就是C中的union。至于下面提到的C++中的union独有的特点,在实际工作中,最好不要用;当然,了解一下还是有好处的,至少和朋友吹牛的时候可以用到,^_^

在C++中,union有以下特点:
1.   union可以定义自己的函数,包括   constructor   以及   destructor。
2.   union支持   public   ,   protected   以及   private   权限。
读者看到这可能会问,要是这样的话,union与class还有什么区别吗?区别当然还是有的
3.   union不支持继承。也就是说,union既不能有父类,也不能作为别人的父类。
4.   union中不能定义虚函数。
5.   不是所有类型的变量都能作为union的成员变量。POD(注1)类型可以作为union的成员变量。如果一个类,包括其父类,含有自定义的constructor,copy   constructor,destructor,copy   assignment   operator,   virtual   function中的任意一个,那么这种类型的变量不能作为union的成员变量(注3)。
6.   union中可以定义static以及引用类型的成员变量,不过C++标准不推荐这样做,包含这样代码的程序是ill-formed的

如果我们在定义union的时候没有定义名字,那么这个union被称为匿名union(anonymous   union)。匿名union的特点如下:

1.   匿名union中不能定义static变量。
2.   匿名union中不能定义函数。
3.   匿名union中不支持   protected   以及   private   权限。
4.   在全局域以及namespace中定义的匿名union只能是static的。

ok,写道最后,留一个问题给大家:
union   {  
      int   aa;  
      char*   p;  
}obj;
这里定义的union是不是匿名的union?  

注1:   什么是POD?
POD   的全称是   Plain   Old   Data,在C++标准中,POD是这样定义的:
Arithmetic   types   (3.9.1),   enumeration   types,   pointer   types,   and   pointer   to   member   types   (3.9.2),   and   cv-qualified(注2)   versions   of   these   types   (3.9.3)   are   collectively   called   scalar   types.   Scalar   types,   POD-struct   types,POD-union   types   (clause   9),   arrays   of   such   types   and   cv-qualified   versions   of   these   types   (3.9.3)   are   collectively   called   POD   types.
我们可以这样理解POD,与C兼容的数据类型可以被成为POD类型。

注2:什么是cv-qualified   ?
这里的CV分别是const   以及volatile的缩写。用const,volatile以及const   volatile之一作修饰的变量被成为cv-qualified   ,否则该变量是cv-unqualified  

注3:class是不是POD类型?
我觉得class不应算作POD类型,但是如果一个class没有non-trivial   的   constructor,   destructor,   以及   copy   assignmet   operator这些东西的话,这个class事实上已经退化成一个POD类型了,就相当于C中的struct。这样,union中就可以包含你这样的数据类型。也就是说,union可以包含特定的非POD类型变量。不知道大家对此有什么看法?

//////////////////////////////////////////////////////////////////////////////////////////////////

联合(union)在C/C++里面见得并不多,但是在一些对内存要求特别严格的地方,联合又是频繁出现,那么究竟什么是联合、怎么去用、有什么需要注意的地方呢?就这些问题,我试着做一些简单的回答,里面肯定还有不当的地方,欢迎指出!

1、什么是联合?
   “联合”是一种特殊的类,也是一种构造类型的数据结构。 在一个“联合”内可以定义多种不同的数据类型, 一个被说明为该“联合”类型的变量中,允许装入该“联合”所定义的任何一种数据,这些数据共享同一段内存,已达到节省空间的目的(还有一个节省空间的类型:位域)。 这是一个非常特殊的地方,也是联合的特征。另外,同struct一样,联合默认访问权限也是公有的,并且,也具有成员函数。

2、联合与结构的区别?
   “联合”与“结构”有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和(空结构除外,同时不考虑边界调整)。而在“联合”中,各成员共享一段内存空间, 一个联合变量的长度等于各成员中最长的长度。应该说明的是, 这里所谓的共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值。

3、如何定义?
   例如: 

    union test 
    { 
      test() { } 
      
int  office; 
      
char  teacher[ 5 ]; 
    }; 

    定义了一个名为test的联合类型,它含有两个成员,一个为整型,成员名为office;另一个为字符数组,数组名为teacher。联合定义之后,即可进行联合变量说明,被说明为test类型的变量,可以存放整型量office或存放字符数组teacher。

4、如何说明?
   联合变量的说明有三种形式:先定义再说明、定义同时说明和直接说明。
   以test类型为例,说明如下: 

 

     1 ) union test 
       { 
         
int  office; 
         
char  teacher[ 5 ]; 
       }; 
       union test a,b;    
/* 说明a,b为test类型 */  
    
2 ) union test 
       { 
         
int  office; 
         
char  teacher[ 5 ]; 
       } a,b; 
    
3 ) union 
       { 
         
int  office; 
         
char  teacher[ 5 ]; 
       } a,b; 


       经说明后的a,b变量均为test类型。
    a,b变量的长度应等于test的成员中最长的长度,即等于teacher数组的长度,共5个字节。a,b变量如赋予整型值时,只使用了4个字节,而赋予字符数组时,可用5个字节。

5、如何使用?
   对联合变量的赋值,使用都只能是对变量的成员进行。
   联合变量的成员表示为:联合变量名.成员名
   例如,a被说明为test类型的变量之后,可使用a.class、a.office
   不允许只用联合变量名作赋值或其它操作,也不允许对联合变量作初始化赋值,赋值只能在程序中进行。
   还要再强调说明的是,一个联合变量,每次只能赋予一个成员值。换句话说,一个联合变量的值就是联合变员的某一个成员值。

6、匿名联合
   匿名联合仅仅通知编译器它的成员变量共同享一个地址,而变量本身是直接引用的,不使用通常的点号运算符语法.例如: 

 

     #include  < iostream >  
     
void  main() 
     { 
         union{ 
                
int  test; 
                
char  c; 
               };          
        test
= 5
        c
= '' a ''
        std::cout
<< i << "   " << c; 
     } 


    正如所见到的,联合成分象声明的普通局部变量那样被引用,事实上对于程序而言,这也正是使用这些变量的方式.另外,尽管被定义在一个联合声明中,他们与同一个程序快那的任何其他局部变量具有相同的作用域级别.这意味这匿名联合内的成员的名称不能与同一个作用域内的其他一直标志符冲突.
    对匿名联合还存在如下限制:
    因为匿名联合不使用点运算符,所以包含在匿名联合内的元素必须是数据,不允许有成员函数,也不能包含私有或受保护的成员。还有,全局匿名联合必须是静态(static)的,否则就必须放在匿名名字空间中。

7、几点需要讨论的地方:
   1、联合里面那些东西不能存放?
      我们知道,联合里面的东西共享内存,所以静态、引用都不能用,因为他们不可能共享内存。
   2、类可以放入联合吗?
      我们先看一个例子: 

       class  Test 
      { 
      
public
          Test():data(
0 ) { } 
      
private
          
int  data; 
      }; 

     typedef union _test 
     { 
              Test test; 
     }UI;

   
     编译通不过,为什么呢?
     因为联合里不允许存放带有构造函数、析够函数、复制拷贝操作符等的类,因为他们共享内存,编译器无法保证这些对象不被破坏,也无法保证离开时调用析够函数。


    8、又是匿名惹的祸??
       我们先看下一段代码: 

 1  class  test 
 2 
 3           public
 4               test( const   char *  p); 
 5               test( int   in ); 
 6                const   operator   char * ()  const  { return  data.ch;} 
 7                operator   long ()  const  { return  data.l;} 
 8           private
 9           enum  type {Int, String }; 
10           union 
11           { 
12                 const   char *  ch; 
13                 int  i; 
14           } datatype; 
15          type stype; 
16          test(test & ); 
17          test &   operator = ( const  test & ); 
18  }; 
19     test::test( const   char   * p):stype(String),datatype.ch(p)     { } 
20     test::test( int   in ):stype(Int),datatype.l(i)     { } 
21 

     看出什么问题了吗?呵呵,编译通不过。为什么呢?难道datatype.ch(p)和datatype.l(i)有问题吗?
     哈哈,问题在哪呢?让我们来看看构造test对象时发生了什么,当创建test对象时,自然要调用其相应的构造函数,在构造函数中当然要调用其成员的构造函数,所以其要去调用datatype成员的构造函数,但是他没有构造函数可调用,所以出错。
     注意了,这里可并不是匿名联合!因为它后面紧跟了个data!


    9、如何有效的防止访问出错?
       使用联合可以节省内存空间,但是也有一定的风险:通过一个不适当的数据成员获取当前对象的值!例如上面的ch、i交错访问。
       为了防止这样的错误,我们必须定义一个额外的对象,来跟踪当前被存储在联合中的值得类型,我们称这个额外的对象为:union的判别式。
       一个比较好的经验是,在处理作为类成员的union对象时,为所有union数据类型提供一组访问函数。