C/C++ 刁鑽問題各個擊破之細說 sizeof

發表於2016-11-18

摘要:

Sizeof的作用非常簡單:求物件或者型別的大小。然而sizeof又非常複雜,它涉及到很多特殊情況,本篇把這些情況分門別類,總結出了sizeof的10個特性:

(0)sizeof是運算子,不是函式;

(1)sizeof不能求得void型別的長度;

(2)sizeof能求得void型別的指標的長度;

(3)sizeof能求得靜態分配記憶體的陣列的長度!

(4)sizeof不能求得動態分配的記憶體的大小!

(5)sizeof不能對不完整的陣列求長度;

(6)當表示式作為sizeof的運算元時,它返回表示式的計算結果的型別大小,但是它不對錶達式求值!

(7)sizeof可以對函式呼叫求大小,並且求得的大小等於返回型別的大小,但是不執行函式體!

(8)sizeof求得的結構體(及其物件)的大小並不等於各個資料成員物件的大小之和!

(9)sizeof不能用於求結構體的位域成員的大小,但是可以求得包含位域成員的結構體的大小!

概述:

Sizeof是C/C++中的關鍵字,它是一個運算子,其作用是取得一個物件(資料型別或者資料物件)的長度(即佔用記憶體的大小,以byte為單位)。其中型別包含基本資料型別(不包括void)、使用者自定義型別(結構體、類)、函式型別。資料物件是指用前面提到的型別定義的普通變數和指標變數(包含void指標)。不同型別的資料的大小在不同的平臺下有所區別,但是c標準規定所有編譯平臺都應該保證sizeof(char)等於1。關於sizeof的更多概述你可以在msdn總輸入sizeof進行查詢。

看了上面這些,或許你看了沒有多少感覺。沒關係,下面我將詳細列出sizeof的諸多特性,這些特性是造成sizeof是一個較刁鑽的關鍵字的原因:

十大特性:

特性0:sizeof是運算子,不是函式

這個特性是sizeof的最基本特性,後面的很多特性都是受到這個特性的影響,正因為sizeof不是函式,因此我們不把它所要求得長度的物件叫做引數,我本人習慣上叫做運算元(這不嚴謹,但是有助於我記住sizeof是個操作符)。

特性1:sizeof不能求得void型別的長度

是的,你不能用sizeof(void),這將導致編譯錯誤:illegalsizeof operand。事實上你根本就無法宣告void型別的變數,不信你就試試void a;這樣的語句,編譯器同樣會報錯:illegal use of type ‘void’。或許你要問為什麼,很好,學東西不能只知其然,還要知其所以然。我們知道宣告變數的一個重要作用就是告訴編譯器該變數需要多少儲存空間。然而,void是“空型別”,什麼是空型別呢,你可以理解成不知道儲存空間大小的型別。既然編譯器無法確定void型別的變數的儲存大小,那麼它自然不讓你宣告這樣的變數。當然了,宣告void型別的指標是可以的!這就是特性2的內容。

特性2:sizeof能求得void型別的指標的長度

在特性1中說過,可以申明void型別的指標,也就是說編譯器可以確定void型別的指標所佔用的儲存空間。事實上確實如此,目前,幾乎所有平臺上的所有版本的編譯器都把指標的大小看做4byte,不信你試試sizeof(int*);sizeof(void*);sizeof(double*);sizeof(Person*);等等,它們都等於4!為什麼呢?問得好,我將盡全力對此作出解釋:其實指標也是變數,只不過這個變數很特殊,它是存放其他變數的地址的變數。又由於目前32位計算機平臺上的程式段的定址範圍都是4GB,定址的最小單元是byte,4GB等於232Byte,這麼多的記憶體其地址如果編碼呢,只需要用32個bit就行了,而32bit = 32/8 = 4byte,也就是說只需要4byte就能儲存這些記憶體的地址了。因此對任何型別的指標變數進行sizeof運算其結果就是4!

特性3:sizeof能求得靜態分配記憶體的陣列的長度!

Int a[10];int n = sizeof(a);假設sizeof(int)等於4,則n= 10*4=40;特別要注意:charch[]=”abc”;sizeof(ch);結果為4,注意字串陣列末尾有’’!通常我們可以利用sizeof來計算陣列中包含的元素個數,其做法是:int n = sizeof(a)/sizeof(a[0]);

非常需要注意的是對函式的形引數組使用sizeof的情況。舉例來說,假設有如下的函式:

你會覺得在fun內,n的值為多少呢?如果你回答40的話,那麼我很遺憾的告訴你,你又錯了。這裡n等於4,事實上,不管形參是int的型陣列,還是float型陣列,或者其他任何使用者自定義型別的陣列,也不管陣列包含多少個元素,這裡的n都是4!為什麼呢?原因是在函式引數傳遞時,陣列被轉化成指標了,或許你要問為什麼要轉化成指標,原因可以在很多書上找到,我簡單說一下:假如直接傳遞整個陣列的話,那麼必然涉及到陣列元素的拷貝(實參到形參的拷貝),當陣列非常大時,這會導致函式執行效率極低!而只傳遞陣列的地址(即指標)那麼只需要拷貝4byte。

特性4:sizeof不能求得動態分配的記憶體的大小!

假如有如下語句:int*a = new int[10];int n = sizeof(a);那麼n的值是多少呢?是40嗎?答案是否定的!其實n等於4,因為a是指標,在特性2中講過:在32位平臺下,所有指標的大小都是4byte!切記,這裡的a與特性3中的a並不一樣!很多人(甚至一些老師)都認為陣列名就是指標,其實不然,二者有很多區別的,要知詳情,請看《c專家程式設計》。通過特性3和特性4,我們看到了陣列和指標有著千絲萬縷的關係,這些關係也是導致程式潛在錯誤的一大因素,關於指標與陣列的關係問題我將在《C/C++刁鑽問題各個擊破之指標與陣列的祕密》一文中進行詳細介紹。

特性3指出sizeof能求靜態分配的陣列的大小,而特性4說明sizeof不能求的動態分配的記憶體的大小。於是有人認為sizeof是編譯時進行求值的,並給出理由:語句int array[sizeof(int)*10];能編譯通過,而很多書上都說過陣列大小是編譯時就確定下來的,既然前面的語句能編譯通過,所以認為sizeof是編譯時進行求值的。經過進一步測試我發現這個結論有些武斷!至少是有些不嚴謹!因為在實現了c99標準的編譯器(如DEV C++)中可以定義動態陣列,即:語句:int num;cin>>num; int arrary[num];是對的(注意在vc6.0中是錯的)。因此我就在DEV C++中對剛才的array利用語句int n=sizeof(array);cout<<n<<endl來求大小,結果編譯通過,執行時輸入num的值10之後,輸出n等於40!在這裡很明顯num的值是執行時才輸入的,因此sizeof不可能在編譯時就求得array的大小!這樣一來sizeof又變成是執行時求值的了。

那麼到底sizeof是編譯時求值還是執行時求值呢?最開初c標準規定sizeof只能編譯時求值,後來c99又補充規定sizeof可以執行時求值。但值得注意的是,即便是在實現了c99標準的DEV C++中仍然不能用sizeof求得動態分配的記憶體的大小!

特性5:sizeof不能對不完整的陣列求長度!

在闡述該特性之前,我們假設有兩個原始檔:file1.cpp和file2.cpp,其中file1.cpp中有如下的定義:

file2.cpp包含如下幾個語句:

在file2.cpp中第三條語句編譯出錯,而第條語句正確,並且能輸出40!為什麼呢?原因就是sizeof(arrayA)試圖求不完整陣列的大小。這裡的不完整的陣列是指陣列大小沒有確定的陣列!sizeof運算子的功能就是求某種物件的大小,然而宣告:extern int arrayA[]只是告訴編譯器arrayA是一個整型陣列,但是並沒告訴編譯器它包含多少個元素,因此對file2.cpp中的sizeof來說它無法求出arrayA的大小,所以編譯器乾脆不讓你通過編譯。

那為什麼sizeof(arrayB)又可以得到arraryB的大小呢?關鍵就在於在file2.cpp中其宣告時使用externint arrayB[10]明確地告訴編譯器arrayB是一個包含10個元素的整型陣列,因此大小是確定的。

到此本特性講解差不多要結束了。其實本問題還能引申出連線和編譯等知識點,但是目前我暫時還沒自信對這兩個知識點進行詳細的,徹底的講解,因此不便在此班門弄斧,不久的將來我會在本系列中加上相關問題的闡述。

特性6:當表示式作為sizeof的運算元時,它返回表示式的計算結果的型別大小,但是它不對錶達式求值!

為了說明這個問題,我們來看如下的程式語句:

假設char佔用1byte,int佔用4byte,那麼執行上面的程式之後,n1,n2,ch的值是多少呢?我相信有不少人會認為n1與n2相等,也有不少人認為ch等於2,事實這些人都錯了。事實上n1等於4,n2等於1,ch等於1,為什麼呢?請看分析:

由於預設型別轉換的原因,表示式ch+num的計算結果的型別是int,因此n1的值為4!而表示式ch=ch+num;的結果的型別是char,記住雖然在計算ch+num時,結果為int,但是當把結果賦值給ch時又進行了型別轉換,因此表示式的最終型別還是char,所以n2等於1。n1,n2的值分別為4和1,其原因正是因為sizeof返回的是表示式計算結果的型別大小,而不是表示式中佔用最大記憶體的變數的型別大小!

對於n2=sizeof(ch=ch+num);乍一看該程式貌似實現了讓ch加上num並賦值給ch的功能,事實並非如此!由於sizeof只關心型別大小,所以它自然不應該對錶達式求值,否則有畫蛇添足之嫌了。正是因為這點,這裡告誡各位,儘量不要在sizeof中直接對錶達式求大小,以免出現錯誤,你可以將sizeof(ch = ch+num);改寫成 ch = ch +num;sizeof(ch);雖然多了一條語句,看似冗餘了,其實好處多多:首先更加清晰明瞭,其次不會出現ch等於1這樣的錯誤(假設程式的邏輯本身就是要執行ch = ch +num;)。

特性7:sizeof可以對函式呼叫求大小,並且求得的大小等於返回型別的大小,但是不執行函式體!

假設有如下函式(是一個寫得很不好的函式,但是能很好的說明需要闡述的問題):

那麼語句:

輸出多少呢?不同的人會給出不同的答案,我將對sizeof(fun(a,b))的值和a的值分別進行討論:

首先sizeof(fun(a,b))的值:其正確是4,因為用sizeof求函式呼叫的大小時,它得到的是函式返回型別的大小,而fun(a,b)的返回型別是int,sizeof(int)等於4。很多人把函式的返回型別返回值的型別弄混淆了,認為sizeof(fun(a,b))的值是8,因為函式返回值是ret,而ret被定義成double,sizeof(doube)等於8。注意,雖然函式返回值型別是double,但是在函式返回時,將該值進行了型別轉換(這裡的轉換不安全)。也有人錯誤的認為sizeof(fun(a,b))的值是12,它們的理由是:fun內部定義了兩個區域性變數,一個是float一個是double,而sizeof(float)+sizeof(doube)= 4+8=12。這樣的答案看似很合理,其實他們是錯誤地認為這裡的sizeof是在求函式內部的變數的大小了。這當然是錯誤的。

接下來看a的值:其正確答案是3!還記得特性6嗎?這裡很類似,sizeof的操作物件是函式呼叫時,它不執行函式體!為此,建議大家不要把函式體放在sizeof後面的括號裡,這樣容易讓人誤以為函式執行了,其實它根本沒執行。

既然對函式條用使用sizeof得到的是函式返回型別的大小,那麼很自然能得出這樣的結論:不能對返回型別為void的函式使用sizeof求其大小!原因請參考特性1。同理,對返回型別是任何型別的指標的函式呼叫使用sizeof求得的大小都為4,原因請參考特性2。

最後我們來看看這樣的語句:cout<<sizeof(fun);其答案是多少呢?其實它得不到答案,原因是編譯就通不過!最開始,我以為能輸出答案4,因為我認為fun是函式名,而我知道函式名就是函式的地址,地址就是指標,於是我認為sizeof(fun)其實就是對一個指標求大小,根據特性2,任何指標的大小都是4。可是當我去驗證時,編譯器根本不讓我通過!這個是為什麼呢?我一時半會想不到,所以還請朋友們補充!

特性8:sizeof求得的結構體(及其物件)的大小並不等於各個資料成員物件的大小之和!

結構體的大小跟結構體成員對齊有密切關係,而並非簡單地等於各個成員的大小之和!比如對如下結構體兩個結構體A、B使用sizeof的結果分別是:16,24。可以看出sizeof(B)並不等於sizeof(int)+sizeof(double)+sizeof(int)=16。

%e8%a1%a8

如果您不瞭解結構體的成員對齊,你會感到非常驚訝:結構體A和B中包含的成員都一樣,只不過順序不同而已,為什麼其大小不一樣呢?要解釋這個問題,就要了解結構體成員對齊的規則,由於結構體成員對齊非常複雜,我將用專題——C/C++刁鑽問題各個擊破之位域和成員對齊——進行講解,這裡我只簡單地介紹其規則:

1、  結構體的大小等於結構體內最大成員大小的整數倍

2、  結構體內的成員的首地址相對於結構體首地址的偏移量是其型別大小的整數倍,比如說double型成員相對於結構體的首地址的地址偏移量應該是8的倍數。

3、  為了滿足規則1和2編譯器會在結構體成員之後進行位元組填充!

基於上面三個規則我們來看看為什麼sizeof(B)等於24:首先假設結構體的首地址為0,第一個成員num1的首地址是0(滿足規則2,前面無須位元組填充,事實上結構體絕對不會在第一個資料成員前面進行位元組填充),它的型別是int,因此它佔用地址空間0——3。第二個成員num3是double型別,它佔用8個位元組,由於之前的num1只佔用了4個位元組,為了滿足規則2,需要使用規則3在num1後面填充4個位元組(4——7),使得num3的起始地址偏移量為8,因此num3佔用的地址空間是:8——15。第三個成員num2是int型,其大小為4,由於num1和num3一共佔用了16個位元組,此時無須任何填充就能滿足規則2。因此num2佔用的地址空間是16——19。那麼是不是結構體的總大小就是0——19共20個位元組呢?請注意,別忘了規則1!由於結構體內最大成員是double佔用8個位元組,因此最後還需要在num2後面填充4個位元組,使得結構體總體大小為24。

按照上面的三個規則和分析過程,你可以很容易地知道為什麼sizeof(A)等於16。特別需要說明的是,我這裡給出了三個結論性的規則,而沒有闡述為什麼要這樣。你或許有很多疑問:為什麼要結構體成員對齊,為什麼要定義規則1等。如果你有這樣的疑問,並嘗試去弄清楚的話,那麼我敢斷言,不久的將來你必定會有大成就,至少在學習c++上是這樣。前面說過,我會再寫一篇專題:C/C++刁鑽問題各個擊破之位域和成員對齊來詳細回答這些問題,如果你急於要弄明白,那麼你可以參考其他資料,比如說《高質量c++程式設計指南》。

最後再提醒一點,在進行設計時,最好仔細安排結構體中各個成員的順序,因為你已經看到了上面的結構體B與結構體A包含的成員相同,只不過順序略有差異,最終就導致了B比A多消耗了50%的空間,假如在工程中需要定義該結構體的陣列,多消耗的空降將是巨大的。即使將來記憶體降價為白菜價格,你也不要忽視這個問題,勤儉節約是中國人民的優良傳統,我們應該繼承和保持!

特性9:sizeof不能用於求結構體的位域成員的大小,但是可以求得包含位域成員的結構體的大小!

首先解釋一下什麼是位域:型別的大小都是以位元組(byte)為基本單位的,比如sizeof(char)為1byte,sizeof(int)為4byte等。我們知道某個型別的大小確定了該型別所能定義的變數的範圍,比如sizeof(char)為1byte,而1byte等於8bit,所以char型別的變數範圍是-128——127,或者0——255(unsigned char),總之它只能定義28=256個數!然而,要命的是bool型別只取值true和false,按理所只用1bit(即1/8byte)就夠了,但事實上sizeof(bool)等於1。因此我們可以認為bool變數浪費了87.5%的儲存空間!這在某些儲存空間有限的裝置(比如嵌入式裝置)上是不合適的,為此需要提供一種能對變數的儲存空間精打細算的機制,這就是位域。簡單來說,在結構體的成員變數後面跟上的一個冒號+一個整數,就代表位域,請看如下的結構體:

其中b,ch1,ch2都是位域成員,而i是普通成員。該結構體的試圖讓bool型別的變數b只佔用1個bit,讓ch1和ch2分別只佔用4個bit,以此來達到對記憶體精打細算的功能(事實上使用位域對記憶體精打細算有時候能成功,有時候卻未必,我將《C/C++刁鑽問題各個擊破之位域和成員對齊》進行論述)。另外需要特別注意的是:c語言規定位域只能用於int,signed int或者unsigned int型別,C++又補充了char和long型別!你不能這樣使用位域:floatf:8;這是不能通過編譯的。並且位域變數不能在函式或者全域性區定義,只能在結構體,自定義類,聯合(union)中使用!

基於上面的結構體,語句sizeof(item.b)和sizeof(item.ch1)等對位域成員求大小的語句均不能通過編譯。其原因能再本篇的概論中找到:sizeof以byte為單位返回運算元的大小!

那麼愛學好問的你可能要問,sizeof(A)能否通過編譯呢?如何能,其結果又是多少呢?這是兩給非常好的問題,事實上我之前沒有看到任何關於這方面的論述(可能是我看的資料不足),我正是在看到sizeof(item.b)不能通過編譯時想到了這兩個問題,然後通過驗證得出了後面的結論:對包含位域的結構體是可以使用sizeof求其大小的,但其求值規則比較複雜,不僅涉及到成員對齊,還與具體編譯環境有關!在這裡你只需要知道可以對包含位域的結構體使用sizeof求其大小,對於sizeof是根據什麼規則來求這個大小的問題,我將會在專題:《C/C++刁鑽問題各個擊破之位域和成員對齊》中進行詳細闡述

後記:

至此,本專題差不多該結束了,需要說明的是,這裡並沒有包含所有關於sizeof的知識點,但是也幾乎包含了所有的容易出錯的特性。為了完成該文,我花了斷斷續續3天半時間,想想效率實在是底下。由於是本系列的第一個專題,我格外慎重,深怕講錯了誤導大家。即便如此,也難免錯誤或不妥之處,還請各位朋友指正!

另外,我有幾句話要對大學生朋友們說:教科書通常只是教授很基礎的知識,要想深入學習,還需要翻閱其他資料,比如論文、網路資料、論壇博文,最重要的一點是要在學習時經常總結、記錄、歸納,積少成多,這樣堅持下來一定受益匪淺。

相關文章