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;
上面之所以要加括號是因為數組操作符“[]”的優先級較“*”高,但為什麼