前言
熟悉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資料類型提供一組訪問函數。