前言
熟悉C的程序员都知道union(联合体)的用法,利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。当访问其内成员时可用"."和"->"来直接访问。在C++出现后,它继承了union并保留了其在C中的特性。但是在C++中的union又有了新的扩展,这需要大家了解,要不然你会感到费解和迷惑。下面我讲两点。
一、在union中存储对象
在C中union中可以存储任意类型的内置数据类型,那么在C++中union是否可以存储对象呢?还是让我们看一个例子吧,这比任何言语都能说明问题,不是吗?
#pragma warning(disable : 4786)
#include
using namespace std;
class TestUnionClass
{
public:
TestUnionClass(long l):data_(l)
{
};
int data_;
};
typedef union _tagUtype_
{
TestUnionClass obj;
}UT;
int main (void)
{
return 0;
}
这样不行,union中不可以存储TestUnionClass类的对象,但在C中union可以存储struct呀,为什么不能存储类的对象呢?很简单,请问,在C中union可以存储带有构造函数的struct吗?对了,在C中的struct是没有构造函数的。所以如果C++中union可以存储有构造函数的类的对象就不太符合逻辑,那不是说C++和C完全兼容吗?不错,正因为这一点,C++中union不可以存储有构造函数的类的对象,但是可以存储不带构造函数的类的对象,这样就和C保持一致了,不想信你试试。对TestUnionClass类的声明进行如下修改:
class TestUnionClass
{
public:
int data_;
};
再进行编译,一切OK!。但是这样却失去了C++的构造初始化特性,这样做是没有任何意义的,我只是在说其在C++中的语义,并不是推荐大家使用(绝对不推荐)。但是我们可以在union中存储对象的指针,从而引用不同的对象类型。不用我再多说了吧,大家还是试试吧!
二、类中union的初始化
由于union的共享内存特点,我们可以使我们的类存储不同的型别而不浪费内存空间,在类中我们可以声明一个union存储不同型别的指针,示例如下:
#pragma warning(disable : 4786)
#include
using namespace std;
class TestUnionClass
{
enum StoreType{Long,Const_CharP};
union
{
const char* ch_;
long l_;
} data_;
StoreType stype_;
TestUnionClass(TestUnionClass&);
TestUnionClass& operator=(const TestUnionClass&);
public:
TestUnionClass(const char* ch);
TestUnionClass(long l);
operator const char*() const {return data_.ch_;}
operator long() const {return data_.l_;}
};
TestUnionClass::TestUnionClass(const char* ch):data_.ch_(ch),stype_(Const_CharP)
{
}
TestUnionClass::TestUnionClass(long l):data_.l_(l),stype_(Long)
{
}
int main (void)
{
TestUnionClass pszobj("yuankai");
TestUnionClass lobj(1234);
cout<(pszobj)<<lobj<<endl;
return 0;
}
真是不幸,编译都通不过,好象没有什么问题呀,为什么呢?data_.ch_(ch)和data_.l_(l)有问题吗?如果你问一个C程序员他会告诉你,绝对没问题。你不会去怀疑编译器有问题吧!不好意思!我一开始就是这么想的,真是惭愧。费解,迷惑。让我们来看看构造TestUnionClass对象时发生了什么,这样你就会明白了。当创建TestUnionClass对象时,自然要调用其相应的构造函数,在构造函数中当然要调用其成员的构造函数,所以其要去调用union成员的构造函数,但是其为匿名的,没有构造函数可调用,所以出错。很明显在C++中union和class一样它可以有构造函数,不能如此直接引用其成员。struct同样有这限制。只要我们给其定义一个构造函数什么问题都解决了。示例如下:
class TestUnionClass
{
enum StoreType{Long,Const_CharP};
union DataUnion //不能匿名
{
DataUnion(const char*); //声明const char*构造函数
DataUnion(long); //声明long构造函数
const char* ch_;
long l_;
} data_;
StoreType stype_;
TestUnionClass(TestUnionClass&);
TestUnionClass& operator=(const TestUnionClass&);
public:
TestUnionClass(const char* ch);
TestUnionClass(long l);
operator const char*() const {return data_.ch_;}
operator long() const {return data_.l_;}
};
TestUnionClass::TestUnionClass(const char* ch):data_(ch),stype_(Const_CharP)
{//注意data_(ch),这里直接引用data_
}
TestUnionClass::TestUnionClass(long l):data_(l),stype_(Long)
{//注意data_(l),这里直接引用data_
}
TestUnionClass::DataUnion::DataUnion(const char* ch):ch_(ch)
{
}
TestUnionClass::DataUnion::DataUnion(long l):l_(l)
{
}
现在再编译,如果还不行,你怀疑编译器有问题是有理由的。
C由于没有类的概念,所有类型其实都可以看作是基本类型的组合,因此在union中包含struct也就是一件很自然的事情了,到了C++之后,既然普遍认为C++中的struct与class基本等价,那么union中是否可以有类成员呢?先来看看如下的代码:
struct TestStruct
{
TestStruct() {}
};
typedef union
{
TestStruct obj;
} UT;
int main (void)
{
return 0;
}
编译该程序,我们将被告知:
error C2620: union ‘__unnamed‘ : member ‘obj‘ has user-defined constructor or non-trivial default constructor
而如果去掉那个什么也没干的构造函数,则一切OK。
为什么编译器不允许我们的union成员有构造函数呢?我无法找到关于这个问题的比较权威的解释,对这个问题,我的解释是:
如果C++标准允许我们的union有构造函数,那么,在进行空间分配的时候要不要执行这个构造函数呢?如果答案是yes,那么如果TestStruct的构造函数中包含了一些内存分配操作,或者其它对整个application状态的修改,那么,如果我今后要用到obj的话,事情可能还比较合理,但是如果我根本就不使用obj这个成员呢?由于obj的引入造成的对系统状态的修改显然是不合理的;反之,如果答案是no,那么一旦我们今后选中了obj来进行操作,则所有信息都没有初始化(如果是普通的struct,没什么问题,但是,如果有虚函数呢?)。更进一步,假设现在我们的union不是只有一个TestStruct obj,还有一个TestStruct2 obj2,二者均有构造函数,并且都在构造函数中执行了一些内存分配的工作(甚至干了很多其它事情),那么,如果先构造obj,后构造obj2,则执行的结果几乎可以肯定会造成内存的泄漏。
鉴于以上诸多麻烦(可能还有更多麻烦),在构造union时,编译器只负责分配空间,而不负责去执行附加的初始化工作,为了简化工作,只要我们提供了构造函数,就会收到上面的error。
同理,除了不能加构造函数,析构函数/拷贝构造函数/赋值运算符也是不可以加。
此外,如果我们的类中包含了任何virtual函数,编译时,我们将收到如下的错误信息:
error C2621: union ‘__unnamed‘ : member ‘obj‘ has copy constructor
所以,打消在union中包含有构造函数/析构函数/拷贝构造函数/赋值运算符/虚函数的类成员变量的念头,老老实实用你的C风格struct吧!
不过,定义普通的成员函数是OK的,因为这不会使得class与C风格的struct有任何本质区别,你完全可以将这样的class理解为一个C风格的struct + n个全局函数。
现在,再看看在类中包含内部union时会有什么不同。看看下面的程序,并请注意阅读程序提示:
class TestStruct
{
union DataUnion
{
DataUnion(const char*);
DataUnion(long);
const char* ch_;
long l_;
} data_;
public:
TestStruct(const char* ch);
TestStruct(long l);
};
TestStruct::TestStruct(const char* ch) : data_(ch) // if you want to use initialzing list to initiate a nested-union member, the union must not be anonymous and must have a constructor。
{
}
TestStruct::TestStruct(long l) : data_(l)
{
}
TestStruct::DataUnion::DataUnion(const char* ch) : ch_(ch)
{
}
TestStruct::DataUnion::DataUnion(long l) : l_(l)
{
}
int main (void)
{
return 0;
}
正如上面程序所示,C++中的union也可以包含构造函数,但是,这虽然被语言所支持,但实在是一种不佳的编程习惯,因此,我不打算对上面的程序进行过多的说明。我更推荐如下的编程风格:
class TestStruct
{
union DataUnion
{
const char* ch_;
long l_;
} data_;
public:
TestStruct(const char* ch);
TestStruct(long l);
};
TestStruct::TestStruct(const char* ch)
{
data_.ch_ = ch;
}
TestStruct::TestStruct(long l)
{
data_.l_ = l;
}
int main (void)
{
return 0;
}
它完全是C风格的。
三、union可以避免c++的类型检查
在阅读到fastdelegate.h中,看到这么一段代码
template <class OutputClass, class InputClass>
union horrible_union
{
OutputClass out;
InputClass in;
};
template<class OutputClass, class InputClass>
inline OutputClass horrible_cast(const InputClass input)
{
horrible_uion<OutputClass, InputClass> u;
typedef int ERROR_CantUserHorrible_cast[sizeof(InputClass) == zizeof(u) && sizeof(InputClass) == sizeof(OutputClass) ? 1 : -1];
u.in = input;
return u.out;
};
这里(强制把input转换成output类型),在函数中使用了一个union来避免C++的类型检查
四、匿名union在C和C++中使用方法
main()
{
union
{ /*定义一个联合*/
int i;
struct
{ /*在联合中定义一个结构*/
char first;
char second;
}half;
};
i=0x4241; /*联合成员赋值*/
half.first='a'; /*联合中结构成员赋值*/
half.second='b';
printf("%x\n", i);
getchar();
}
上面的例子中,如果是c程序,则无法通过,提示的error: i undeclared identifier;half undeclared identifier。
但如果把此例子保存成c++文件,则可一编译通过,正常运行。
匿名联合仅仅通知编译器它的成员变量共同享一个地址,而变量本身是直接引用的,不使用通常的点号运算符语法.例如:
#include <iostream>
void main()
{
union
{
int test;
char c;
};
test=5;
c='a';
std::cout<<i<<" "<<c;
}
正如所见到的,联合成分象声明的普通局部变量那样被引用,事实上对于程序而言,这也正是使用这些变量的方式.另外,尽管被定义在一个联合声明中,他们与同一个程序快那的任何其他局部变量具有相同的作用域级别.这意味这匿名联合内的成员的名称不能与同一个作用域内的其他一直标志符冲突.
对匿名联合还存在如下限制:
因为匿名联合不使用点运算符,所以包含在匿名联合内的元素必须是数据,不允许有成员函数,也不能包含私有或受保护的成员。还有,全局匿名联合必须是静态(static)的,否则就必须放在匿名名字空间中。
五、c/c++中的union结构的占用空间问题
很多书说union所占用的内存空间大小为其成员中 占用空间最大的成员 所占用的空间大小。这是错误的! 怎么能这么忽悠人呢?为什么我们的技术人员都不把事情说清楚呢?
看一个例子:
struct block
{
struct block * next;
char * avail;
char * limit;
};
union align
{
struct block b;
double d;
};
sizeof(align)==?
如果按照书上说的,最大的成员为 struct block b; 它的大小为12个字节(在windows,vc编译器环境下). 但是事实是这样吗? 错!
sizeof(align) == 16.
为什么呢?看下面的分析就知道了。
在上面的例子中, b所占用的字节数是12, d 所占用的为8 , 如果只给align 分配12个字节的空间,那么当存储 double 型的d时,只能存储一个半,这是不行的,(半个算怎么回事?)
所以,必须扩展,再扩展4个字节,这样,即可以储存的下 b,又可以存储 整数倍的 d;
所以,16 就为最后所需要的空间。
六、几点需要讨论的地方:
1、联合里面那些东西不能存放?
我们知道,联合里面的东西共享内存,所以静态、引用都不能用,因为他们不可能共享内存。
2、类可以放入联合吗?
我们先看一个例子:
class Test
{
public:
Test():data(0) { }
private:
int data;
};
typedef union _test
{
Test test; file://??????/
}UI;
编译通不过,为什么呢?
因为联合里不允许存放带有构造函数、析够函数、复制拷贝操作符等的类,因为他们共享内存,编译器无法保证这些对象不被破坏,也无法保证离开时调用析够函数。
3、又是匿名惹的祸??
我们先看下一段代码:
class test
{
public:
test(const char* p);
test(int in);
const operator char*() const
{
return data.ch;
}
operator long() const
{
return data.l;
}
private:
enum type
{
Int, String
};
union
{
const char* ch;
int i;
} datatype;
type stype;
test(test&);
test& operator=(const test&);
};
test::test(const char *p):stype(String),datatype.ch(p)
{}
test::test(int in):stype(Int),datatype.l(i)
{}
看出什么问题了吗?呵呵,编译通不过。为什么呢?难道datatype.ch(p)和datatype.l(i)有问题吗?
哈哈,问题在哪呢?让我们来看看构造test对象时发生了什么,当创建test对象时,自然要调用其相应的构造函数,在构造函数中当然要调用其成员的构造函数,所以其要去调用datatype成员的构造函数,但是他没有构造函数可调用,所以出错。
注意了,这里可并不是匿名联合!因为它后面紧跟了个data!
4、如何有效的防止访问出错?
使用联合可以节省内存空间,但是也有一定的风险:通过一个不适当的数据成员获取当前对象的值!例如上面的ch、i交错访问。
为了防止这样的错误,我们必须定义一个额外的对象,来跟踪当前被存储在联合中的值得类型,我们称这个额外的对象为:union的判别式。
一个比较好的经验是,在处理作为类成员的union对象时,为所有union数据类型提供一组访问函数。