C++從零開始(九)——何謂結構

    前篇已經說明編程時,拿到算法後該乾的第一件事就是把資源映射成數字,而前面也說過“類型就是人爲制訂的如何解釋內存中的二進制數的協議”,也就是說一個數字對應着一塊內存(可能4字節,也可能20字節),而這個數字的類型則是附加信息,以告訴編譯器當發現有對那塊內存的操作語句(即某種操作符)時,要如何編寫機器指令以實現那個操作。比如兩個char類型的數字進行加法操作符操作,編譯器編譯出來的機器指令就和兩個long類型的數字進行加法操作的不一樣,也就是所謂的“如何解釋內存中的二進制數的協議”。由於解釋協議的不同,導致每個類型必須有一個唯一的標識符以示區別,這正好可以提供強烈的語義。

    typedef

    提供語義就是要儘可能地在代碼上體現出這句或這段代碼在人類世界中的意義,比如前篇定義的過河方案,使用一char類型來表示,然後定義了一數組char sln[5]以期從變量名上體現出這是方案。但很明顯,看代碼的人不一定就能看出sln是solution的縮寫並進而瞭解這個變量的意義。但更重要的是這裏有點本末倒置,就好像這個東西是紅蘋果,然後知道這個東西是蘋果,但它也可能是玩具、CD或其它,即需要體現的語義是應該由類型來體現的,而不是變量名。即char無法體現需要的語義。

    對此,C++提供了很有意義的一個語句——類型定義語句。其格式爲typedef <源類型名> <標識符>;。其中的<源類型名>表示已存在的類型名稱,如char、unsigned long等。而<標識符>就是程序員隨便起的一個名字,符合標識符規則,用以體現語義。對於上面的過河方案,則可以如下:

    typedef char Solution; Solution sln[5];

    上面其實是給類型char起了一個別名Solution,然後使用Solution來定義sln以更好地體現語義來增加代碼的可讀性。而前篇將兩岸的人數分佈映射成char[4],爲了增強語義,則可以如下:

    typedef char PersonLayout[4]; PersonLayout oldLayout[200];

    注意上面是typedef char PersonLayout[4];而不是typedef char[4] PersonLayout;,因爲數組修飾符“[]”是接在被定義或被聲明的標識符的後面的,而指針修飾符“*”是接在前面的,所以可以typedef char *ABC[4];但不能typedef char [4]ABC*;,因爲類型修飾符在定義或聲明語句中是有固定位置的。

    上面就比char oldLayout[200][4];有更好的語義體現,不過由於爲了體現語義而將類型名或變量名增長,是否會降低編程速度?如果編多了,將會發現編程的大量時間不是花在敲代碼上,而是調試上。因此不要忌諱書寫長的變量名或類型名,比如在Win32的Security SDK中,就提供了下面的一個函數名:

    BOOL ConvertSecurityDescriptorToStringSecurityDescriptor(…);

    很明顯,此函數用於將安全描述符這種類型轉換成文字形式以方便人們查看安全描述符中的信息。

    應注意typedef不僅僅只是給類型起了個別名,還創建了一個原類型。當書寫char* a, b;時,a的類型爲char*,b爲char,而不是想象的char*.因爲“*”在這裏是類型修飾符,其是獨立於聲明或定義的標識符的,否則對於char a[4], b;,難道說b是char[4]?那嚴重不符合人們的習慣。上面的char就被稱作原類型。爲了讓char*爲原類型,則可以:typedef char *PCHAR; PCHAR a, b, *c[4];。其中的a和b都是char*,而c是char**[4],所以這樣也就沒有問題:char **pA = &a;。

    結構

    再次考慮前篇爲什麼要將人數佈局映射成char[4],因爲一個人數可以用一個char就表示,而人數佈局有四個人數,所以使用char[4].即使用char[4]是希望只定義一個變量就代表了一個人數分佈,編譯器就一次性在棧上分配4個字節的空間,並且每個字節都各自代表一個人數。所以爲了表現河岸左側的商人數,就必須寫a[0],而左側的僕人數就必須a[1].壞處很明顯,從a[0]無法看出它表示的是左岸的商人數,即這個映射意義(左岸的商人數映射爲內存塊中第一個字節的內容以補碼格式解釋)無法從代碼上體現出來,降低了代碼的可讀性。

    上面其實是對內存佈局的需要,即內存塊中的各字節二進制數如何解釋。爲此,C++提出了類型定義符“{}”。它就是一對大括號,專用在定義或聲明語句中,以定義出一種類型,稱作自定義類型。即C++原始缺省提供的類型不能滿足要求時,可自定義內存佈局。其格式爲:<類型關鍵字> <名字> { <聲明語句> …}.<類型關鍵字>只有三個:struct、class和union.而所謂的結構就是在<類型關鍵字>爲struct時用類型定義符定義的原類型,它的類型名爲<名字>,其表示後面大括號中寫的多條聲明語句,所定義的變量之間是串行關係(後面說明),如下:

    struct ABC { long a, *b; double c[2], d; } a, *b = &a;

    上面是一個變量定義語句,對於a,表示要求編譯器在棧上分配一塊4+4+8*2+8=32字節長的連續內存塊,然後將首地址和a綁定,其類型爲結構型的自定義類型(簡稱結構)ABC.對於b,要求編譯器分配一塊4字節長的內存塊,將首地址和b綁定,其類型爲結構ABC的指針。

    上面定義變量a和b時,在定義語句中通過書寫類型定義符“{}”定義了結構ABC,則以後就可以如下使用類型名ABC來定義變量,而無需每次都那樣,即:

    ABC &c = a, d[2];

    現在來具體看清上面的意思。首先,前面語句定義了6個映射元素,其中a和b分別映射着兩個內存地址。而大括號中的四個變量聲明也生成了四個變量,各自的名字分別爲ABC::a、ABC::b、ABC::c、ABC::d;各自映射的是0、4、8和24;各自的類型分別爲long ABC::、long* ABC::、double (ABC::) [2]、double ABC::,表示是偏移。其中的ABC::表示一種層次關係,表示“ABC的”,即ABC::a表示結構ABC中定義的變量a.應注意,由於C++是強類型語言,它將ABC::也定義爲類型修飾符,進而導致出現long* ABC::這樣的類型,表示它所修飾的標識符是自定義類型ABC的成員,稱作偏移類型,而這種類型的數字不能被單獨使用(後面說明)。由於這裏出現的類型不是函數,故其映射的不是內存的地址,而是一偏移值(下篇說明)。與之前不同了,類型爲偏移類型的(即如上的類型)數字是不能計算的,因爲偏移是一相對概念,沒有給出基準是無法產生任何意義的,即不能:ABC::a; ABC::c[1];。其中後者更是嚴重的錯誤,因爲數組操作符“[]”要求前面接的是數組或指針類型,而這裏的ABC::c是double的數組類型的結構ABC中的偏移,並不是數組類型。

    注意上面的偏移0、4、8、24正好等同於a、b、c、d順次安放在內存中所形成的偏移,這也正是struct這個關鍵字的修飾作用,也就是前面所謂的各定義的變量之間是串行關係。

    爲什麼要給偏移制訂映射?即爲什麼將a映射成偏移0字節,b映射成偏移4字節?因爲可以給偏移添加語義。前面的“左岸的商人數映射爲內存塊中第一個字節的內容以補碼格式解釋”其實就是給定內存塊的首地址偏移0字節。而現在給出一個標識符和其綁定,則可以將這個標識符起名爲LeftTrader來表現其語義。

    由於上面定義的變量都是偏移類型,根本沒有分配內存以和它們建立映射,它們也就很正常地不能是引用類型,即struct AB{ long a, &b; };將是錯誤的。還應注意上面的類型double (ABC::)[2],類型修飾符“ABC::”被用括號括起來,因爲按照從左到右來解讀類型操作符的規則,“ABC::”實際應該最後被解讀,但其必須放在標識符的左邊,就和指針修飾符“*”一樣,所以必須使用括號將其括住,以表示其最後才起修飾作用。故也就有:double (*ABCD::)[2]、double (**ABCD::)[2],各如下定義:

    struct ABCD { double ( *pD )[2]; double ( **ppD )[2]; };

    但應注意,“ABCD::”並不能直接使用,即double ( *ABCD:: pD )[2];是錯誤的,要定義偏移類型的變量,必須通過類型定義符“{}”來自定義類型。還應注意C++也允許這樣的類型double ( *ABCD::* )[2],其被稱作成員指針,即類型爲double ( *ABCD:: )[2]的指針,也就是可以如下:

    double ( **ABCD::*pPPD )[2] = &ABC::ppD, ( **ABCD::**ppPPD )[2] = &pPPD;

    上面很奇怪,回想什麼叫指針類型。只有地址類型的數字纔能有指針類型,表示不計算那個地址類型的數字,而直接返回其二進制表示,也就是地址。對於變量,地址就是它映射的數字,而指針就表示直接返回其映射的數字,因此&ABCD::ppD返回的數字其實就是偏移值,也就是4.

    爲了應用上面的偏移類型,C++給出了一對操作符——成員操作符“。”和“->”。前者兩邊接數字,左邊接自定義類型的地址類型的數字,而右邊接相應自定義類型的偏移類型的數字,返回偏移類型中給出的類型的地址類型的數字,比如:a.ABC::d;。左邊的a的類型是ABC,右邊的ABC::d的類型是double ABC::,則a.ABC::d返回的數字是double的地址類型的數字,因此可以這樣:a.ABC::d = 10.0;。假設a對應的地址是3000,則a.ABC::d返回的地址爲3000+24=3024,類型爲double,這也就是爲什麼ABC::d被叫做偏移類型。由於“。”左邊接的結構類型應和右邊的結構類型相同,因此上面的ABC::可以省略,即a.d = 10.0;。而對於“->”,和“。”一樣,只不過左邊接的數字是指針類型罷了,即b->c[1] = 10.0;。應注意b->c[1]實際是( b->c )[1],而不是b->( c[1] ),因爲後者是對偏移類型運用“[]”,是錯誤的。

    還應注意由於右邊接偏移類型的數字,所以可以如下:

    double ( ABC::*pA )[2] = &ABC::c, ( ABC::**ppA )[2] = &pA;

    ( b->**ppA )[1] = 10.0; ( a.*pA )[0] = 1.0;

    上面之所以要加括號是因爲數組操作符“[]”的優先級較“*”高,但爲什麼不是b->( **ppA )[1]而是( b->**ppA )[1]?前者是錯誤的。應注意括號操作符“()”並不是改變計算優先級,而是它也作爲一個操作符,其優先級被定得很高罷了,而它的計算就是計算括號內的數字。之前也說明了偏移類型是不能計算的,即ABC::c;將錯誤,而剛纔的前者由於“()”的加入而導致要求計算偏移類型的數字,故編譯器將報錯。

    還應該注意,成員指針是偏移類型的指針,即裝的是偏移,則可以程序運行時期得到偏移,而前面通過ABC::a這種形式得到的是編譯時期,由編譯器幫忙映射的偏移,只能實現靜態的偏移,而利用成員指針則可以實現動態的偏移。不過其實只需將成員定義成數組或指針類型照樣可以實現動態偏移,不過就和前篇沒有使用結構照樣映射了人數佈局一樣,欠缺語義而代碼可讀性較低。成員指針的提出,通過變量名,就可以表現出豐富的語義,以增強代碼的可讀性。現在,可以將最開始說的人數佈局定義如下:

    struct PersonLayout{ char LeftTrader, LeftServitor, RightTrader, RightServitor; };

    PersonLayout oldLayout[200], b;

    因此,爲了表示b這個人數分佈中的左側商人數,只需b.LeftTrader;,右側的僕人數,只需b.RightServitor;。因爲PersonLayout::LeftTrader記錄了偏移值和偏移後應以什麼樣的類型來解釋內存,故上面就可以實現原來的b[0]和b[3].很明顯,前者的可讀性遠遠地高於後者,因爲前者通過變量名(b和PersonLayout::LeftTrader)和成員操作符“。”表現了大量的語義——b的左邊的商人數。

    注意PersonLayout::LeftTrader被稱作結構PersonLayout的成員變量,而前面的ABC::d則是ABC的成員變量,這種叫法說明結構定義了一種層次關係,也纔有所謂的成員操作符。既然有成員變量,那也有成員函數,這在下篇介紹。

    前篇在映射過河方案時將其映射爲char,其中的前4位表示僕人數,後4位表示商人數。對於這種使用長度小於1個字節的用法,C++專門提供了一種語法以支持這種情況,如下:

    struct Solution { ServitorCount : 4; unsigned TraderCount : 4; } sln[5];

    由於是基於二進制數的位(Bit)來進行操作,只准使用兩種類型來表示數字,原碼解釋數字或補碼解釋數字。對於上面,ServitorCount就是補碼解釋,而TraderCount就是原碼解釋,各自的長度都爲4位,而此時Solution::ServitorCount中依舊記錄的是偏移,不過不再以字節爲單位,而是位爲單位。並且由於其沒有類型,故也就沒有成員指針了。即前篇的( sln[ cur[ curSln ] ] & 0xF0 ) >> 4等效於sln[ cur[ curSln] ].TraderCount,而sln[ cur[ curSln ] ] & 0xF0等效於sln[ cur[ curSln] ].ServitorCount,較之前具有了更好的可讀性。

    應該注意,由於struct AB { long a, b; };也是一條語句,並且是一條聲明語句(因爲不生成代碼),但就其意義上來看,更通常的叫法把它稱爲定義語句,表示是類型定義語句,但按照不生成代碼的規則來判斷,其依舊是聲明語句,並進而可以放在類型定義符“{}”中,即:

    struct ABC{ struct DB { long a, *b[2]; }; long c; DB a; };

    上面的結構DB就定義在結構ABC的聲明語句中,則上面就定義了四個變量,類型均爲偏移類型,變量名依次爲:ABC::DB::a、ABC::DB::b、ABC::c、ABC::a;類型依次爲long ABC::DB::、long* (ABC::DB::)[2]、long ABC::、ABC::DB;映射的數值依次爲0、4、0、4.這裏稱結構DB嵌套在結構ABC中,其體現出一種層次關係,實際中這經常被使用以表現特定的語義。欲用結構DB定義一個變量,則ABC::DB a;。同樣也就有long* ( ABC::DB::*pB )[2] = &ABC::DB::b; ABC c; c.a.a = 10; *( c.a.b[0] ) = 20;。應注意ABC::DB::表示“ABC的DB的”而不是“DB的ABC的”,因爲這裏是重複的類型修飾符,是從右到左進行修飾的。

    前面在定義結構時,都指明瞭一個類型名,如前面的ABC、ABCD等,但應該注意類型名不是必須的,即可以struct { long a; double b; } a; a.a = 10; a.b = 34.32;。這裏就定義了一個變量,其類型是一結構類型,不過這個結構類型沒有標識符和其關聯,以至於無法對其運用類型匹配等比較,如下:

    struct { long a; double b; } a, &b = a, *c = &a; struct { long a; double b; } *d = &a;

    上面的a、b、c都沒有問題,因爲使用同一個類型來定義的,即使這個類型沒有標識符和其映射,但d將會報錯,即使後寫的結構的定義式和前面的一模一樣,但仍然不是同一個,只是長得像罷了。那這有什麼用?後面說明。

    最後還應該注意,當在複合語句中書寫前面的聲明語句以定義結構時,之前所說的變量作用域也同樣適用,即在某複合語句中定義的結構,出了這個複合語句,它就被刪除,等於沒定義。如下:

 void ABC()
{
    struct AB { long a, b; };
    AB d; d.b = 10;
}
void main()
{
    {
        struct AB{ long a, b, e; };
        AB c; c.e = 23;
    }
    AB a;  // 將報錯,說AB未定義,但其他沒有任何問題
}

    初始化

    初始化就是之前在定義變量的同時,就給在棧上分配的內存賦值,如:long a = 10;。當定義的變量的類型有表示多個元素時,如數組類型、上面的結構類型時,就需要給出多個數字。對此,C++專門給出了一種語法,使用一對大括號將欲賦的值括起來後,整體作爲一個數字賦給數組或結構,如下:

    struct ABC { long a, b; float c, d[3]; };

    ABC a = { 1, 2, 43.4f, { 213.0f, 3.4f, 12.4f } };

    上面就給出了爲變量a初始化的語法,大括號將各元素括起來,而各元素之間用“,”隔開。應注意ABC::d是數組類型,其對應的初始化用的數字也必須用大括號括起來,因此出現上面的嵌套大括號。現在應該瞭解到“{}”只是用來構造一個具有多個元素的數字而已,因此也可以有long a = { 34 };,這裏“{}”就等同於沒有。還應注意,C++同意給出的大括號中的數字個數少於相應自定義類型或數組的元素個數,即:ABC a = { 1, 2, 34 }, b = { 23, { 34 }, 65, { 23, 43 } }, c = { 1, 2, { 3, { 4, 5, 6 } } };

    上面的a.d[0]、a.d[1]、a.d[2]都爲0,而只有b.d[2]才爲0,但c將會報錯,因爲嵌套的第一個大括號將{ 4, 5, 6 }也括了起來,表示c.c將被一個具有兩個元素的數字賦值,但c.c的類型是float,只對應一個元素,編譯器將說初始化項目過多。而之前的a和b未賦值的元素都將被賦值爲0,但應注意並不是數值上的0,而是簡單地將未賦值的內存的值用0填充,再通過那些補碼原碼之類的格式解釋成數值後恰好爲0而已,並不是賦值0這個數字。

    應注意,C++同意這樣的語法:long a[] = { 34, 34, 23 };。這裏在定義a時並沒有給出元素個數,而是由編譯器檢查賦值用的大括號包的元素個數,由其來決定數組的個數,因此上面的a的類型爲long[3].當多維數組時,如:long a[3][2] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };。因爲每個元素又是需要多個元素的數字,就和前面的ABC::d一樣。再回想類型修飾符的修飾順序,是從左到右,但當是重複類型修飾符時,就倒過來從右到左,因此上面就應該是三個long[2],而不是兩個long[3],因此這樣將錯誤:long a[3][2] = { { 1, 2, 3 }, { 4, 5, 6 } };。

    還應注意,C++不止提供了上面的“{}”這一種初始化方式,對於字符串,其專門提供如:char a[] = "ABC";。這裏a的類型就爲char[4],因爲字符串"ABC"需要佔4個字節的內存空間。除了這兩種初始化方式外,C++還提供了一種函數式的初始化函數,下篇介紹。

    類型的運用

    char a = -34; unsigned char b = ( unsigned char )a;

    上面的b等於222,將-34按照補碼格式寫成二進制數11011110,然後將這個二進制數用原碼格式解釋,得數值222.繼續:

    float a = 5.6f; unsigned long b = ( unsigned long )a;

    這回b等於5.爲什麼?不是應該將5.6按照IEEE的real*4的格式寫成二進制數0X40B33333(這裏用十六進制表示),然後將這個二進制數用原碼格式解釋而得數值1085485875嗎?因爲類型轉換是語義上的類型轉換,而不是類型變換。

    兩個類型是否能夠轉換,要視編譯器是否定義了這兩個類型之間的轉換規則。如char和unsigned char,之所以前面那樣轉換是因爲編譯器把char轉unsigned char定義成了那樣,同樣float轉unsigned long被編譯器定義成了取整而不是四捨五入。

    爲什麼要有類型轉換?有什麼意義?的確,像上面那樣的轉換,毫無意義,僅僅只是爲了滿足語法的嚴密性而已,不過由於C++定義了指針類型的轉換,而且定義得非常地好,以至於有非常重要的意義。

    char a = -34; unsigned char b = *( unsigned char* )( &a );

    上面的結果和之前的一樣,b爲222,不過是通過將char*轉成unsigned char*,然後再用unsigned char來解釋對應的內存而得到222,而不是按照編譯器的規定來轉換的,即使結果一樣。因此:

    float a = 5.6f; unsigned long b = *( unsigned long* )( &a );

    上面的b爲1085485875,也就是之前以爲的結果。這裏將a的地址所對應的內存用unsigned long定義的規則來解釋,得到的結果放在b中,這體現了類型就是如何解釋內存中的內容。上面之所以能實現,是因爲C++規定所有的指針類型之間的轉換,數字的數值沒有變化,只有類型變化(但由於類的繼承關係也是可能會改變,下篇說明),因此上面才說b的值是用unsigned long來解釋a對應的內存的內容所得的結果。因此,前篇在比較oldLayout[ curSln ][0~3]和oldLayout[ i ][0~3]時寫了四個“==”以比較了四次char的數字,由於這四個char數字是連續存放的,因此也可如下只比較一次long數字即可,將節約多餘的三次比較時間。

    *( long* )&oldLayout[ curSln ] == *( long* )&oldLayout[ i ]

    上面只是一種優化手段而已,對於語義還是沒有多大意義,不過由於有了自定義類型,因此:

 struct AB { long a1; long a2; }; struct ABC { char a, b; short c; long d; };
    AB a = { 53213, 32542 }; ABC *pA = ( ABC* )&a;
    char aa = pA->a, bb = pA->b, cc = pA->c; long dd = pA->d;
    pA->a = 1; pA->b = 2; pA->c = 3; pA->d = 4;
    long aa1 = a.a1, aa2 = a.a2;

    上面執行後,aa、bb、cc、dd的值依次爲-35、-49、0、32542,而aa1和aa2的值分別爲197121和4.相信只要稍微想下就應該能理解爲什麼沒有修改a.a1和a.a2,結果它們的值卻變了,因爲變量只不過是個映射而已,而前面就是利用指針pA以結構ABC來解釋並操作a所對應的內存的內容。

    因此,利用自定義類型和指針轉換,就可以實現以什麼樣的規則來看待某塊內存的內容。有什麼用?傳遞給某函數一塊內存的引用(利用指針類型或引用類型),此函數還另有一個參數,比如是long類型。當此long類型的參數爲1時,表示傳過去的是一張定單;爲2時,表示傳過去的是一張發貨單;爲3時表示是一張收款單。如果再配上下面說明的枚舉類型,則可以編寫出語義非常完善的代碼。

    應注意由於指針是可以隨便轉換的,也就有如下的代碼,實際並沒什麼意義,在這隻爲加深對成員指針的理解:

    long AB::*p = ( long AB::* )( &ABC::b ); a.a1 = a.a2 = 0; a.*p = 0XAB1234CD;

    上面執行後,a.a1爲305450240,a.a2爲171,轉成十六進制分別爲0X1234CD00和0X000000AB.

    枚舉

    上面欲說明1時爲定單,2時爲發貨單而3時爲收款單,則可以利用switch或if語句來進行判斷,但是語句從代碼上將看見類似type == 1或type == 2之類,無法表現出語義。C++專門爲此提供了枚舉類型。

    枚舉類型的格式和前面的自定義類型很像,但意義完全不同,如下:

 enum AB { LEFT, RIGHT = 2, UP = 4, DOWN = 3 }; AB a = LEFT;
    switch( a )
    {
        case LEFT:;  // 做與左相應的事
        case UP:;    // 做與上相應的事
    }

    枚舉也要用“{}”括住一些標識符,不過這些標識符即不映射內存地址也不映射偏移,而是映射整數,而爲什麼是整數,那是因爲沒有映射浮點數的必要,後面說明。上面的RIGHT就等同於2,注意是等同於2,相當於給2起了個名字,因此可以long b = LEFT; double c = UP; char d = RIGHT;。但注意上面的變量a,它的類型爲AB,即枚舉類型,其解釋規則等同於int,即編譯成在16位操作系統上運行時,長度爲2個字節,編譯成在32位操作系統上運行時爲4個字節,但和int是屬於不同的類型,而前面的賦值操作之所以能沒有問題,可以認爲編譯器會將枚舉類型隱式轉換成int類型,進而上面沒有錯誤。但倒過來就不行了,因爲變量a的類型是AB,則它的值必須是上面列出的四個標識符中的一個,而a = b;則由於b爲long類型,如果爲10,那麼將無法映射上面的四個標識符中的一個,所以不行。

    注意上面的LEFT沒有寫“=”,此時將會從其前面的一個標識符的值自增一,由於它是第一個,而C++規定爲0,故LEFT的值爲0.還應注意上面映射的數字可以重複,即:

    enum AB { LEFT, RIGHT, UP = 5, DOWN, TOP = 5, BOTTOM };

    上面的各標識符依次映射的數值爲0、1、5、6、5、6.因此,最開始說的問題就可以如下處理:

    enum OperationType { ORDER = 1, INVOICE, CHECKOUT };

    而那個參數的類型就可以爲OperationType,這樣所表現的語義就遠遠地超出原來的代碼,可讀性高了許多。因此,當將某些人類世界的概念映射成數字時,發現它們的區別不表現在數字上,比如吃飯、睡覺、玩表示一個人的狀態,現在爲了映射人這個概念爲數字,也需要將人的狀態這個概念映射成數字,但很明顯地沒有什麼方便的映射規則。這時就強行說1代表吃飯,2代表睡覺,3代表玩,此時就可以使用將1、2、3定義成枚舉以表現語義,這也就是爲什麼枚舉只定義爲整數,因爲沒有定義成浮點數的必要性。

    聯合

    前面說過類型定義符的前面可以接struct、class和union,當接union時就表示是聯合型自定義類型(簡稱聯合),它和struct的區別就是後者是串行分佈來定義成員變量,而前者是並行分佈。如下:

    union AB { long a1, a2, a3; float b1, b2, b3; }; AB a;

    變量a的長度爲4個字節,而不是想象的6*4=24個字節,而聯合AB中定義的6個變量映射的偏移都爲0.因此a.a1 = 10;執行後,a.a1、a.a2、a.a3的值都爲10,而a.b1的值爲多少,就用IEEE的real*4格式來解釋相應內存的內容,該多少是多少。

    也就是說,最開始的利用指針來解釋不同內存的內容,現在可以利用聯合就完成了,因此上面的代碼搬到下面,變爲:

  union AB
    {
        struct { long a1; long a2; };
        struct { char a, b; short c; long d; };
    };
    AB a = { 53213, 32542 };
    char aa = a.a, bb = a.b, cc = a.c; long dd = a.d;
    a.a = 1; a.b = 2; a.c = 3; a.d = 4;
    long aa1 = a.a1, aa2 = a.a2;

    結果不變,但代碼要簡單,只用定義一個自定義類型了,而且沒有指針變量的運用,代碼的語義變得明顯多了。

    注意上面定義聯合AB時在其中又定義了兩個結構,但都沒有賦名字,這是C++的特殊用法。當在類型定義符的中間使用類型定義符時,如果沒有給類型定義符定義的類型綁定標識符,則依舊定義那些偏移類型的變量,不過這些變量就變成上層自定義類型的成員變量,因此這時“{}”等同於沒有,唯一的意義就是通過前面的struct或class或union來指明變量的分佈方式。因此可以如下:

 struct AB
    {
        struct { long a1, a2; };
        char a, b;
        union { float b1; double b2; struct { long b3; float b4; char b5; }; };
        short c;
    };

    上面的自定義類型AB的成員變量就有a1、a2、a、b、b1、b2、b3、b4、b5、c,各自對應的偏移值依次爲0、4、8、9、10、10、10、14、18、19,類型AB的總長度爲21字節。某類型的長度表示如果用這個類型定義了一個變量,則編譯器應該在棧上分配多大的連續空間,C++爲此專門提供了一個操作符sizeof,其右側接數字或類型名,當接數字時,就返回那個數字的類型需要佔的內存空間的大小,而接類型名時,就返回那個類型名所標識的類型需要佔的內存空間的大小。

    因此long a = sizeof( AB ); AB d; long b = sizeof d;執行後,a和b的值都爲40.怎麼是40?不應該爲21嗎?而之前的各成員變量對應的偏移也依次實際爲0、4、8、9、16、16、16、20、24、32.爲什麼?這就是所謂的數據對齊。

    CPU有某些指令,需要處理多個數據,則各數據間的間隔必須是4字節或8字節或16字節(視不同的指令而有不同的間隔),這被稱作數據對齊。當各個數據間的間隔不符合要求時,CPU就必須做附加的工作以對齊數據,效率將下降。並且CPU並不直接從內存中讀取東西,而要經一個高速緩衝(CPU內建的一個存取速度比內存更快的硬件)緩衝一下,而此緩衝的大小肯定是2的次方,但又比較小,因此自定義類型的大小最好能是2的次方的倍數,以便高效率的利用高速緩衝。

    在自定義類型時,一個成員變量的偏移值一定是它所屬的類型的長度的倍數,即上面的a和b的偏移必須是1的倍數,而c的偏移必須是2的倍數,b1的偏移必須是4的倍數。但b2的偏移必須是8的倍數,而b1和b2由於前面的union而導致是並行佈局,因此b1的偏移必須和b2及b3的相同,因此上面的b1、b2、b3的偏移變成了8的倍數16,而不是想象的10.

    而一個自定義類型的長度必須是其成員變量中長度最長的那個成員變量的長度的倍數,因此struct { long b3; float b4; char b5; };的長度是4的倍數,也就是12.而上面的無名聯合的成員變量中,只有double b2;的長度最長,爲8個字節,所以它的長度爲16,並進而導致c的偏移爲b1的偏移加16,故爲32.由於結構AB中的成員變量只有b2的長度最長,爲8,故AB的長度必須是8的倍數40.因此在定義結構時應儘量將成員和其長度對應起來,如下:

    struct ABC1 { char a, b; char d; long c; };

    struct ABC2 { char a, b; long c; char d; };

    ABC1的長度爲8個字節,而ABC2的長度爲12個字節,其中ABC1::c和ABC2::c映射的偏移都爲4.

    應注意上面說的規則一般都可以通過編譯選項而進行一定的改變,不同的編譯器將給出不同的修改方式,在此不表。

    本篇說明了如何使用類型定義符“{}”來定義自定義類型,說明了兩種自定義類型,實際還有許多自定義類型的內容未說明,將在下篇介紹,即後面介紹的類及類相關的內容都可應用在聯合和結構上,因爲它們都是自定義類型。

   

 

發佈了3 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章