February 12, 2007

sizeof 在語言層面的陷阱

小弟與許多開發者一樣,在低階程式設計時使用 C 語言、開發應用程式時使用 C++ 或 C#,而這三種語言在某些關鍵字與運算子有雷同,但這也是造成潛在錯誤的原因。sizeof 就是 C / C++ / C# 等三種程式語言所共有的 operator (注意:不是 function),顧名思義就是用以取得數值型別的位元組大小,大量用在 macro 定義中,比方說計算陣列 A 的元素個數,就可以這麼寫:
#define getNumberInArray(a) \
	(sizeof(a) / sizeof(a[0]))
這是相當常見的 C/C++ 技巧,但越是簡單的東西,越容易令人忽略細節,我們就來看看在不同語言層面中,sizeof 會有什麼差異。在 ANSI C 規格中,sizeof(char) 被嚴格定義為 1 個 size_t,所以考慮以下程式碼:
#include <stdio.h>
int main()                                                                                          
{
    printf("%d, %d\n", sizeof('J'), sizeof(char));
    return 0;
}
輸出會是什麼呢?編譯並執行看看:
$ gcc -o sizeof sizeof.c
$ ./sizeof 
4, 1
第二項的輸出 "1" 乃是依據 ANSI C 規格,不過第一項就比較有意思,當我們用 C++ compiler 重新編譯並執行時:
$ g++ -o sizeof sizeof.c
$ ./sizeof
1, 1
所以同樣是 sizeof('J'),在 C 與 C++ 語言就有不同的表徵,C++ 設計的本意是與 ANSI C 相容,但實際上存在些微的差異,這裡就是一個好例子。我們可從以上輸出得知,C++ 中 sizeof('J') 等同於 sizeof(char),所以我們看到輸出 "1",但是對於 C 語言來說,有著 "default to int" 的語意,所以等同於計算 sizeof(int),又在 32-bit 硬體架構來說,大致會輸出 "4",而 C# 與 C++ 對此有類似的表現,不過還是得考慮 sizeof('x') 或 sizeof(L'x') 的不同行為 (多國語文的 multi-character)。

再來作一個實驗:
#include <stdio.h>
struct Empty { } empty;
int main()
{
        printf("sizeof(empty) = %d\n", sizeof(empty));                                              
        return 0;
}
先用 C compiler 編譯並執行:
$ gcc -o sizeof sizeof2.c
$ ./sizeof 
sizeof(empty) = 0
計算一個沒有內容的 struct,其 sizeof 為 "0",似乎很合理,那麼 C++ 呢?
$ g++ -o sizeof sizeof2.c
$ ./sizeof 
sizeof(empty) = 1
奇怪,GNU G++ 竟然輸出 "1",這是為什麼呢?依據 Standard C++ language definition 的說法:
    A class with an empty sequence of members and base class objects is an empty class. Complete objects and member subobjects of an empty class type shall have nonzero size.
這也是說,沒有任何一個 complete object 可有 zero size,任何空的 structure 空間至少為 "1",除非是 base class (pure virtual),因為那不是 complete object。這個定義是有實際需求的,考慮以下程式碼:
struct Bar { };
struct Foo {
    struct Bar a[2];
    struct Bar b;
};
Foo f;
我們可以發現,在 f 這個 object 的 member:a 與 b,如果 sizeof(Bar) = 0,那麼 f.a[] 與 f.b 是否就有相同的 address 呢?為了避免歧異性,C++ 標準審議委員決定禁止 zero-sized addressable objects 的存在,所以基於 C++ 技術上考量,freestanding objects 必須有 non-zero size (非零大小),這是 C 與 C++ 語言層面的差異之一。

當然 sizeof 既然是個 operator,可作的變化可多了,比方說觀察 object value assign 的變化,不過這裡就不列舉了,因為 sizeof 大多出現於與低階 I/O 或記憶體操作有關之處,不然就是在 header file 中的 macro,這都是隨著專案需求,很可能一下子用 C compiler,而轉眼改用 C++ compiler 編譯的程式碼,但倘若我們忽視語言層面的差異,將會造成許多難以偵測的錯誤,筆者恰好在日前的專案遇過,於是稍加紀錄,慎之。
由 jserv 發表於 February 12, 2007 04:02 PM
迴響

的確是個很難注意的陷阱...不過jserv會用c#啊!

k3 發表於 February 12, 2007 08:57 PM

To k3
哈哈,我猜跟 Mono Project 有關。

York 發表於 February 12, 2007 10:15 PM

喔喔! 原來有這樣的陷阱! 真是長見識 -.-

Eason 發表於 February 13, 2007 11:59 AM

考慮 typeof(Bar) == 0, 遇到這種狀況會怎樣?

struct Bar { };

...
Bar b[20];
Bar *i = b, *j = b+20; /* i == j ?? */
...

Palatis 發表於 February 13, 2007 01:43 PM

上了一課,不過想問 Jserv :「這篇文章是否可以給我轉貼?」

PingLunLiao 發表於 February 13, 2007 09:44 PM

To PingLunLiao,

歡迎轉貼,只要指明出處以作更正之參考即可,謝謝!

jserv 發表於 February 14, 2007 09:15 AM

@Palatis:
test.c:
--
#include
typedef struct { } Bar;

int main()
{
Bar b[20];
Bar *i = b, *j = b+20; /* i == j ?? */

printf("sizeof(b) = %d\ni = %d\nj = %d\n", sizeof(b), i, j);
return 0;
}
--
J:\>gcc test.c

J:\>a
sizeof(b) = 0
i = 2293596
j = 2293596

J:\>g++ test.c

J:\>a
sizeof(b) = 20
i = 2293576
j = 2293596

roytam1 發表於 February 25, 2007 10:07 PM
發表迴響









記住我的資訊?