sizeof,一個其貌不揚的傢伙,引無數菜鳥竟折腰,小蝦我當初也沒少犯迷糊,秉著“
辛苦我一個,幸福千萬人”的偉大思想,我決定將其儘可能詳細的總結一下。
但當我總結的時候才發現,這個問題既可以簡單,又可以複雜,所以本文有的地方並不
適合初學者,甚至都沒有必要大作文章。但如果你想“知其然,更知其所以然”的話,
那麼這篇文章對你或許有所協助。
菜鳥我對C++的掌握尚未深入,其中不乏錯誤,歡迎各位指正啊
1. 定義:
sizeof是何方神聖sizeof乃C/C++中的一個操作符(operator)是也,簡單的說其作
用就是返回一個對象或者類型所佔的記憶體位元組數。
MSDN上的解釋為:
The sizeof keyword gives the amount of storage, in bytes, associated with a
variable or a type (including aggregate types).
This keyword returns a value of type size_t.
其傳回值類型為size_t,在標頭檔stddef.h中定義。這是一個依賴於編譯系統的值,一
般定義為
typedef unsigned int size_t;
世上編譯器林林總總,但作為一個規範,它們都會保證char、signed char和unsigned
char的sizeof值為1,畢竟char是我們編程能用的最小資料類型。
2. 文法:
sizeof有三種文法形式,如下:
1) sizeof( object ); // sizeof( 對象 );
2) sizeof( type_name ); // sizeof( 類型 );
3) sizeof object; // sizeof 對象;
所以,
int i;
sizeof( i ); // ok
sizeof i; // ok
sizeof( int ); // ok
sizeof int; // error
既然寫法3可以用寫法1代替,為求形式統一以及減少我們大腦的負擔,第3種寫法,忘
掉它吧!
實際上,sizeof計算對象的大小也是轉換成對物件類型的計算,也就是說,同種類型的
不同對象其sizeof值都是一致的。這裡,對象可以進一步延伸至運算式,即sizeof可以
對一個運算式求值,編譯器根據運算式的最終結果類型來確定大小,一般不會對錶達式
進行計算。如:
sizeof( 2 );// 2的類型為int,所以等價於 sizeof( int );
sizeof( 2 + 3.14 ); // 3.14的類型為double,2也會被提升成double類型,所以等價
於 sizeof( double );
sizeof也可以對一個函數調用求值,其結果是函數傳回型別的大小,函數並不會被調用
,我們來看一個完整的例子:
char foo()
{
printf("foo() has been called./n");
return 'a';
}
int main()
{
size_t sz = sizeof( foo() ); // foo() 的傳回值類型為char,所以sz = sizeof(
char ),foo()並不會被調用
printf("sizeof( foo() ) = %d/n", sz);
}
C99標準規定,函數、不能確定類型的運算式以及位域(bit-field)成員不能被計算s
izeof值,即下面這些寫法都是錯誤的:
sizeof( foo );// error
void foo2() { }
sizeof( foo2() );// error
struct S
{
unsigned int f1 : 1;
unsigned int f2 : 5;
unsigned int f3 : 12;
};
sizeof( S.f1 );// error
3. sizeof的常量性
sizeof的計算髮生在編譯時間刻,所以它可以被當作常量運算式使用,如:
char ary[ sizeof( int ) * 10 ]; // ok
最新的C99標準規定sizeof也可以在運行時刻進行計算,如下面的程式在Dev-C++中可以
正確執行:
int n;
n = 10; // n動態賦值
char ary[n]; // C99也支援數組的動態定義
printf("%d/n", sizeof(ary)); // ok. 輸出10
但在沒有完全實現C99標準的編譯器中就行不通了,上面的代碼在VC6中就通不過編譯。
所以我們最好還是認為sizeof是在編譯期執行的,這樣不會帶來錯誤,讓程式的可移植
性強些。
4. 基礎資料型別 (Elementary Data Type)的sizeof
這裡的基礎資料型別 (Elementary Data Type)指short、int、long、float、double這樣的簡單內建資料類型,
由於它們都是和系統相關的,所以在不同的系統下取值可能不同,這務必引起我們的注
意,盡量不要在這方面給自己程式的移植造成麻煩。
一般的,在32位編譯環境中,sizeof(int)的取值為4。
5. 指標變數的sizeof
學過資料結構的你應該知道指標是一個很重要的概念,它記錄了另一個對象的地址。既
然是來存放地址的,那麼它當然等於電腦內部地址匯流排的寬度。所以在32位電腦中
,一個指標變數的傳回值必定是4(注意結果是以位元組為單位),可以預計,在將來的6
4位系統中指標變數的sizeof結果為8。
char* pc = "abc";
int* pi;
string* ps;
char** ppc = &pc;
void (*pf)();// 函數指標
sizeof( pc ); // 結果為4
sizeof( pi ); // 結果為4
sizeof( ps ); // 結果為4
sizeof( ppc ); // 結果為4
sizeof( pf );// 結果為4
指標變數的sizeof值與指標所指的對象沒有任何關係,正是由於所有的指標變數所佔內
存大小相等,所以MFC訊息處理函數使用兩個參數WPARAM、LPARAM就能傳遞各種複雜的消
息結構(使用指向結構體的指標)。
6. 數組的sizeof
數組的sizeof值等於數組所佔用的記憶體位元組數,如:
char a1[] = "abc";
int a2[3];
sizeof( a1 ); // 結果為4,字元 末尾還存在一個NULL終止符
sizeof( a2 ); // 結果為3*4=12(依賴於int)
一些朋友剛開始時把sizeof當作了求數組元素的個數,現在,你應該知道這是不對的,
那麼應該怎麼求數組元素的個數呢Easy,通常有下面兩種寫法:
int c1 = sizeof( a1 ) / sizeof( char ); // 總長度/單個元素的長度
int c2 = sizeof( a1 ) / sizeof( a1[0] ); // 總長度/第一個元素的長度
寫到這裡,提一問,下面的c3,c4值應該是多少呢
void foo3(char a3[3])
{
int c3 = sizeof( a3 ); // c3 ==
}
void foo4(char a4[])
{
int c4 = sizeof( a4 ); // c4 ==
}
也許當你試圖回答c4的值時已經意識到c3答錯了,是的,c3!=3。這裡函數參數a3已不
再是數群組類型,而是蛻變成指標,相當於char* a3,為什麼仔細想想就不難明白,我
們調用函數foo1時,程式會在棧上分配一個大小為3的數組嗎不會!數組是“傳址”的
,調用者只需將實參的地址傳遞過去,所以a3自然為指標類型(char*),c3的值也就為
4。
7. 結構體的sizeof
這是初學者問得最多的一個問題,所以這裡有必要多費點筆墨。讓我們先看一個結構體
:
struct S1
{
char c;
int i;
};
問sizeof(s1)等於多少聰明的你開始思考了,char佔1個位元組,int佔4個位元組,那麼
加起來就應該是5。是這樣嗎你在你機器上試過了嗎也許你是對的,但很可能你是錯
的!VC6中按預設設定得到的結果為8。
Why為什麼受傷的總是我
請不要沮喪,我們來好好琢磨一下sizeof的定義——sizeof的結果等於對象或者類型所
占的記憶體位元組數,好吧,那就讓我們來看看S1的記憶體配置情況:
S1 s1 = { 'a', 0xFFFFFFFF };
定義上面的變數後,加上斷點,運行程式,觀察s1所在的記憶體,你發現了什麼
以我的VC6.0為例,s1的地址為0x0012FF78,其資料內容如下:
0012FF78: 61 CC CC CC FF FF FF FF
發現了什麼怎麼中間夾雜了3個位元組的CC看看MSDN上的說明:
When applied to a structure type or variable, sizeof returns the actual siz
e, which may include padding bytes inserted for alignment.
原來如此,這就是傳說中的位元組對齊啊!一個重要的話題出現了。
為什麼需要位元組對齊電腦群組成原理教導我們這樣有助於加快電腦的取數速度,否
則就得多花指令周期了。為此,編譯器預設會對結構體進行處理(實際上其它地方的數
據變數也是如此),讓寬度為2的基礎資料型別 (Elementary Data Type)(short等)都位於能被2整除的地址上,
讓寬度為4的基礎資料型別 (Elementary Data Type)(int等)都位於能被4整除的地址上,以此類推。這樣,兩個
數中間就可能需要加入填充位元組,所以整個結構體的sizeof值就增長了。
讓我們交換一下S1中char與int的位置:
struct S2
{
int i;
char c;
};
看看sizeof(S2)的結果為多少,怎麼還是8再看看記憶體,原來成員c後面仍然有3個填
充位元組,這又是為什麼啊別著急,下面總結規律。
位元組對齊的細節和編譯器實現相關,但一般而言,滿足三個準則:
1) 結構體變數的首地址能夠被其最寬基本類型成員的大小所整除;
2) 結構體每個成員相對於結構體首地址的位移量(offset)都是成員大小的整數倍,
如有需要編譯器會在成員之間加上填充位元組(internal adding);
3) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最
末一個成員之後加上填充位元組(trailing padding)。
對於上面的準則,有幾點需要說明:
1) 前面不是說結構體成員的地址是其大小的整數倍,怎麼又說到位移量了呢因為有
了第1點存在,所以我們就可以只考慮成員的位移量,這樣思考起來簡單。想想為什麼。
結構體某個成員相對於結構體首地址的位移量可以通過宏offsetof()來獲得,這個宏也
在stddef.h中定義,如下:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
例如,想要獲得S2中c的位移量,方法為
size_t pos = offsetof(S2, c);// pos等於4
2) 基本類型是指前面提到的像char、short、int、float、double這樣的內建資料類型
,這裡所說的“資料寬度”就是指其sizeof的大小。由於結構體的成員可以是複合類型
,比如另外一個結構體,所以在尋找最寬基本類型成員時,應當包括複合類型成員的子
成員,而不是把複合成員看成是一個整體。但在確定複合類型成員的位移位置時則是將
複合類型作為整體看待。
這裡敘述起來有點拗口,思考起來也有點撓頭,還是讓我們看看例子吧(具體數值仍以
VC6為例,以後不再說明):
struct S3
{
char c1;
S1 s;
char c2
};
S1的最寬簡單成員的類型為int,S3在考慮最寬簡單類型成員時是將S1“打散”看的,
所以S3的最寬簡單類型為int,這樣,通過S3定義的變數,其儲存空間首地址需要被4整
除,整個sizeof(S3)的值也應該被4整除。
c1的位移量為0,s的位移量呢這時s是一個整體,它作為結構體變數也滿足前面三個
準則,所以其大小為8,位移量為4,c1與s之間便需要3個填充位元組,而c2與s之間就不需
要了,所以c2的位移量為12,算上c2的大小為13,13是不能被4整除的,這樣末尾還得補
上3個填充位元組。最後得到sizeof(S3)的值為16。
通過上面的敘述,我們可以得到一個公式:
結構體的大小等於最後一個成員的位移量加上其大小再加上末尾的填充位元組數目,即:
sizeof( struct ) = offsetof( last item ) + sizeof( last item ) + sizeof( tr
ailing padding )
到這裡,朋友們應該對結構體的sizeof有了一個全新的認識,但不要高興得太早,有
一個影響sizeof的重要參量還未被提及,那便是編譯器的pack指令。它是用來調整結構
體對齊的,不同編譯器名稱和用法略有不同,VC6中通過#pragma pack實現,也可以
直接修改/Zp編譯開關。#pragma pack的基本用法為:#pragma pack( n ),n為位元組對齊
數,其取值為1、2、4、8、16,預設是8,如果這個值比結構體成員的sizeof值小,那麼
該成員的位移量應該以此值為準,即是說,結構體成員的位移量應該取二者的最小值,
公式如下:
offsetof( item ) = min( n, sizeof( item ) )
再看樣本:
#pragma pack(push) // 將當前pack設定壓棧儲存
#pragma pack(2)// 必須在結構體定義之前使用
struct S1
{
char c;
int i;
};
struct S3
{
char c1;
S1 s;
char c2
};
#pragma pack(pop) // 恢複先前的pack設定
計算sizeof(S1)時,min(2, sizeof(i))的值為2,所以i的位移量為2,加上sizeof(i)
等於6,能夠被2整除,所以整個S1的大小為6。
同樣,對於sizeof(S3),s的位移量為2,c2的位移量為8,加上sizeof(c2)等於9,不能
被2整除,添加一個填充位元組,所以sizeof(S3)等於10。
現在,朋友們可以輕鬆的出一口氣了,:)
還有一點要注意,“空結構體”(不含資料成員)的大小不為0,而是1。試想一個“不
占空間”的變數如何被取地址、兩個不同的“空結構體”變數又如何得以區分呢於是
,“空結構體”變數也得被儲存,這樣編譯器也就只能為其分配一個位元組的空間用於占
位了。如下:
struct S5 { };
sizeof( S5 ); // 結果為1
8. 含位域結構體的sizeof
前面已經說過,位域成員不能單獨被取sizeof值,我們這裡要討論的是含有位域的結構
體的sizeof,只是考慮到其特殊性而將其專門列了出來。
C99規定int、unsigned int和bool可以作為位域類型,但編譯器幾乎都對此作了擴充,
允許其它類型類型的存在。
使用位域的主要目的是壓縮儲存,其大致規則為:
1) 如果相鄰位域欄位的類型相同,且其位寬之和小於類型的sizeof大小,則後面的字
段將緊鄰前一個欄位儲存,直到不能容納為止;
2) 如果相鄰位域欄位的類型相同,但其位寬之和大於類型的sizeof大小,則後面的字
段將從新的儲存單元開始,其位移量為其類型大小的整數倍;
3) 如果相鄰的位域欄位的類型不同,則各編譯器的具體實現有差異,VC6採取不壓縮方
式,Dev-C++採取壓縮方式;
4) 如果位域欄位之間穿插著非位域欄位,則不進行壓縮;
5) 整個結構體的總大小為最寬基本類型成員大小的整數倍。
還是讓我們來看看例子。
樣本1:
struct BF1
{
char f1 : 3;
char f2 : 4;
char f3 : 5;
};
其記憶體布局為:
|_f1__|__f2__|_|____f3___|____|
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
0 3 7 8 1316
位域類型為char,第1個位元組僅能容納下f1和f2,所以f2被壓縮到第1個位元組中,而f3隻
能從下一個位元組開始。因此sizeof(BF1)的結果為2。
樣本2:
struct BF2
{
char f1 : 3;
short f2 : 4;
char f3 : 5;
};
由於相鄰位域類型不同,在VC6中其sizeof為6,在Dev-C++中為2。
樣本3:
struct BF3
{
char f1 : 3;
char f2;
char f3 : 5;
};
非位域欄位穿插在其中,不會產生壓縮,在VC6和Dev-C++中得到的大小均為3。
9. 聯合體的sizeof
結構體在記憶體組織上是順序式的,聯合體則是重疊式,各成員共用一段記憶體,所以整個
聯合體的sizeof也就是每個成員sizeof的最大值。結構體的成員也可以是複合類型,這
裡,複合類型成員是被作為整體考慮的。
所以,下面例子中,U的sizeof值等於sizeof(s)。
union U
{
int i;
char c;
S1 s;
};