「指標」扮演「記憶體」和「物件」之間的橋樑 Copyright (慣C) 2015, 2016 宅色夫
stackoverflow 上的 頭腦體操
取自 C Traps and Pitfalls 的案例 [Understanding Declarations]
(*(void(*)())0)();
其實可改寫為:
typedef void (*funcptr)();
(* (funcptr) 0)();
int main() {
typedef void (*funcptr)();
(* (funcptr) (void*) 0)();
}
-Os
(空間最佳化)main:
pushq %rax
xorl %eax, %eax
call *%rax
xorl %eax, %eax
popq %rdx
ret
void **(*d) (int &, char **(*)(char *, char **));
上述宣告的解讀:
d is a pointer to a function that takes two parameters:
and returns a pointer to a pointer to void
signal 系統呼叫的宣告方式也很經典:
source: https://media.giphy.com/media/G10pb1bOz98oE/giphy.gif
1999 年 4 月 27 日,Ken Thompson 和 Dennis Ritchie 自美國總統柯林頓手中接過 1998 年 National Medal of Technology (國家科技獎章),隔年 12 月,時年 58 歲的 Ken Thompson 自貝爾實驗室退休
Ken Thompson 成為了一名飛行員。大概是整日翱翔天際,獲得頗多啟發,在 2006 年,他進入 Google 工作,隔年他和過去貝爾實驗室的同僚 Rob Pike 及 Robert Griesemer 等人在公司內部提出嶄新的 Go 程式語言,後者可用於雲端運算。
在實做層面,pointer 和 struct 往往是成雙成對存在 ( 下方會解釋 )
從一則笑話談起
葉秉哲博士的推文:「==溯源能力==是很重要的,才不會被狀似革新,實則舊瓶裝新酒或跨領域借用的『新觀念』所迷惑」
規格書 (PDF) 搜尋 "object",共出現 735 處
從第一手資料學習:大文豪寫作都不免要查字典,庸俗的軟體開發者如我們,難道不需要翻閱語言規格書嗎?難道不需要搞懂術語定義和規範嗎?
&
不要都念成 and,涉及指標操作的時候,要讀為 "address of"
C99 [3.14] object
C99 [6.2.4] Storage durations of objects
注意生命週期 (lifetime) 的概念,中文講「初始化」時,感覺像是「盤古開天」,很容易令人誤解。其實 initialize 的英文意義很狹隘: "to set (variables, counters, switches, etc.) to their starting values at the beginning of a program or subprogram."
在 object 的生命週期以內,其存在就意味著有對應的常數記憶體位址。注意,C 語言永遠只有 call-by-value
作為 object 操作的「代名詞」(alias) 的 pointer,倘若要在 object 生命週期以外的時機,去取出 pointer 所指向的 object 內含值,是未知的。考慮以下操作
ptr = malloc(size); free(ptr);
倘若之後做*ptr
,這個 allocated storage 已經超出原本的生命週期
C99 [6.2.5] Types
注意到術語!這是 C 語言只有 call-by-value 的實證,函式的傳遞都涉及到數值
這裡的 "incomplete type" 要注意看,稍後會解釋。要區分
char []
和char *
注意 "scalar type" 這個術語,日後我們看到
++
,--
,+=
,-=
等操作,都是對 scalar (純量)
[來源] 純量只有大小,它們可用數目及單位來表示(例如溫度=30oC)。純量遵守算數和普通的代數法則。注意:純量有「單位」(可用
sizeof
操作子得知單位的「大小」),假設ptr
是個 pointer type,對ptr++
來說,並不是單純ptr = ptr + 1
,而是遞增或遞移 1 個「單位」
這是 C/C++ 常見的 forward declaration 技巧的原理,比方說我們可以在標頭檔宣告
struct GraphicsObject;
(不用給細部定義) 然後struct GraphicsObject *initGraphics(int width, int height);
是合法的,但struct GraphicsObject obj;
不合法
這句話很重要,看起來三個不相關的術語「陣列」、「函式」,以及「指標」都稱為 derived declarator types,讀到此處會覺得驚訝的人,表示不夠理解 C 語言
(一個實值函數的圖像曲線。函數在一點的導數等於它的圖像上這一點處之切線的斜率)
回到 C 語言,你看到一個數值,是 scalar,但可能也是自某個型態衍生出的 declarator type derivation,實際對應到 array, function, pointer 等型態的 derivation
(練習題) 設定絕對地址為 0x67a9
的 32-bit 整數變數的值為 0xaa6
,該如何寫?
*(int32_t * const) (0x67a9) = 0xaa6;
/* Lvalue */
關鍵描述!規範
void *
和char *
彼此可互換的表示法
void *memcpy(void *dest, const void *src, size_t n);
float *
has type ""pointer to float’". Its type category is pointer, not a floating type. The const-qualified version of this type is designated as float - const
whereas the type designated as "const float *
is not a qualified type — its type is ""pointer to const qualified float’" and is a pointer to a qualified type.struct tag (*[5])(float)
has type "array of pointer to function returning struct tag’". The array has length five and the function has a single parameter of type float. Its type category is array.安裝 cdecl
程式,可以幫你產生 C 程式的宣告。
$ sudo apt-get install cdecl`
使用案例
$ cdecl
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char
會得到以下輸出:
char *(*(*a[])())()
把前述 C99 規格的描述帶入,可得:
cdecl> declare array of pointer to function returning struct tag
struct tag (*var[])()
如果你沒辦法用英文來解說 C 程式的宣告,通常表示你不理解!
cdecl
可以解釋 C 程式宣告的意義,比方說:
cdecl> explain char *(*fptab[])(int)
declare fptab as array of pointer to function (int) returning pointer to char
void *
之謎void
在最早的 C 語言是不存在的,直到 C89 才確立,為何要設計這樣的型態呢?
int
(伴隨著 0
作為返回值),但這導致無從驗證 function prototype 和實際使用的狀況void *
的設計,導致開發者必須透過 ==explict (顯式)== 或強制轉型,才能存取最終的 object,否則就會丟出編譯器的錯誤訊息,從而避免危險的指標操作
void *
做數值操作void- p = ...;
void *p2 = p + 1; /- what exactly is the size of void? */
C/C++ implicit conversions
C99 對 sign extension 的定義和解說
對某硬體架構,像是 ARM,我們需要額外的 ==alignment==。ARMv5 (含) 以前,若要操作 32-bit 整數 (uint32_t),該指標必須對齊 32-bit 邊界 (否則會在 dereference 時觸發 exception)。於是,當要從 void *
位址讀取 uint16_t 時,需要這麼做:
/* may receive wrong value if ptr is not 2-byte aligned */
uint16_t value = *(uint16_t*)ptr;/* portable way of reading a little-endian value */
uint16_t value = *(uint8_t*)ptr | ((*(uint8_t*)(ptr+1))<<8);
「雙馬尾」(左右「獨立」的個體) 和「馬尾的馬尾」(由單一個體關聯到另一個體的對應) 不同
C 語言中,萬物皆是數值 (everything is a value.),函式呼叫當然只有 call-by-value
「指標的指標」(英文就是 a pointer of a pointer) 是個常見用來改變「傳入變數原始數值」的技巧
考慮以下程式碼:
int B = 2;
void func(int **p) { *p = &B; }
int main()
{
int A = 1, C = 3;
int *ptrA = &A;
func(&ptrA);
printf("%d\n", *ptrA);
return 0;
}
*ptrA
的數值從 1 變成 2,而且 ptrA 指向的物件也改變了案例: oltk 是 Openmoko 為了工廠測試而開發出的精簡繪圖系統,支援觸碰螢幕,程式碼不到 1000 行 C 語言程式。執行畫面: (oltk 的開發者是 olv)
struct oltk; // 宣告 (incomplete type, void)
struct oltk_button;
typedef void oltk_button_cb_click(struct oltk_button *button, void *data);
typedef void oltk_button_cb_draw(struct oltk_button *button,
struct oltk_rectangle *rect, void *data);
struct oltk_button *oltk_button_add(struct oltk *oltk,
int x, int y,
int width, int height);
struct oltk
和 struct oltk_button
沒有具體的定義 (definition) 或實做 (implementation),僅有宣告 (declaration)
struct oltk {
struct gr *gr;
struct oltk_button **zbuttons;
...
struct oltk_rectangle invalid_rect;
};
軟體界面 (interface) 揭露於 oltk.h,不管 struct oltk 內容怎麼修改,已公開的函式如 oltk_button_add
都是透過 pointer 存取給定的位址,而不用顧慮具體 struct oltk 的實做,如此一來,不僅可以隱藏實做細節,還能兼顧二進位的相容性 (binary compatibility)。
同理,struct oltk_button 不管怎麼變更,在 struct oltk 裡面也是用給定的 pointer 去存取,保留未來實做的彈性。
printf()
觀察的話,永遠只看到你設定的框架 (format string) 以內的資料,但很容易就忽略資料是否合法、範圍是否正確,以及是否看對地方printf()
大概是最早被記下來的函式,也困擾很多人,有意思的是,1960 年代初期 MIT 開發的 CTSS 作業系統 中,終端機命令就包含了 printf,後者一路從 Multics 和 Unix 繼承至今STMTDC PRINTF,11,T,T25
,前一行註解寫 "The following tables are the dictionaries of statement types"source: NASA before PowerPoint, 1961
extern char x[];
=> 不能變更為 pointer 的形式char x[10]
=> 不能變更為 pointer 的形式func(char x[])
=> 可變更為 pointer 的形式 => func(char *x)
int main() {
int x[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
printf("%d %d %d %d\n", x[4], *(x + 4), *(4 + x), 4[x]);
}
As formal parameters in a function definition,
Page 100 則寫 char s[]; and char *s are equivalent.
x[i]
總是被編譯器改寫為 *(x + i)
<== in expressionC 提供操作多維陣列的機制 (C99 [6.5.2.1] Array subscripting),但實際上只有一維陣列的資料存取
int x[3][5];
Here x is a 3 × 5 array of ints; more precisely, x is an array of three element objects, each of which is an array of five ints. In the expression x[i]
, which is equivalent to (*((x)+(i)))
, x is first converted to a pointer to the initial array of five ints. Then i is adjusted according to the type of x, which conceptually entails multiplying i by the size of the object to which the pointer points, namely an array of five int objects. The results are added and indirection is applied to yield an array of five ints. When used in the expression x[i][j]
, that array is in turn converted to a pointer to the first of the ints, so x[i][j]
yields an int.array subscripting 在編譯時期只能作以下兩件事:
前兩者以外的操作,都透過 pointer
Array declaration:
int a[3];
struct { double v[3]; double length; } b[17];
int calendar[12][31];
那麼...
sizeof(calendar) = ? sizeof(b) = ?
善用 GDB,能省下沒必要的 printf()
,並可互動分析: (下方以 GNU/Linux x86_64 作為示範平臺)
printf()
的輸出格式沒用對,而誤以為自己程式沒寫好的狀況(gdb) p sizeof(calendar)
$1 = 1488
(gdb) print 1488 / 12 / 31
$2 = 4
(gdb) p sizeof(b)
$3 = 544
還可以分析型態:
(gdb) whatis calendar
type = int [12][31]
(gdb) whatis b[0]
type = struct {...}
(gdb) whatis &b[0]
type = struct {...} *
更可直接觀察和修改記憶體內容:
(gdb) x/4 b
0x601060 <b>: 0x00000000 0x00000000 0x00000000 0x00000000
(gdb) p b
$4 = {{
v = {0, 0, 0},
length = 0
} <repeats 17 times>}
終於可以來作實驗!
(gdb) p &b
$5 = (struct {...} (*)[17]) 0x601060 <b>
(gdb) p &b+1
$6 = (struct {...} (*)[17]) 0x601280 <a>
上一行 &b + 1
指向的位址,就是 int a[3]
的所在處?!確認一下:
(gdb) p &a[0]
$7 = (int *) 0x601280 <a>
那 &b[0] + 1
指向哪呢?
(gdb) p &b[0]+1
$8 = (struct {...} *) 0x601080 <b+32>
(gdb) p sizeof(b[0]()
$9 = 32
原來 &b[0] + 1
的 +1
就是遞移一個 b[0]
佔用的空間
提昇輸出的可讀性
(gdb) set print pretty
繼續觀察記憶體內容:
(gdb) p &b[0]
$10 = (struct {...} *) 0x601060 <b>
(gdb ) p (&b[0])->v
$11 = {0, 0, 0}
p
命令不僅能 print,可以拿來變更記憶體內容:
(gdb) p (&b[0])->v = {1, 2, 3}
$12 = {1, 2, 3}
(gdb) p b[0]
$13 = {
v = {1, 2, 3},
length = 0
}
還記得前面提到 (float) 7.0
和 (int) 7
的不同嗎?我們來觀察執行時期的表現:
(gdb) whatis (&b[0])->v[0]
type = double
(gdb) p sizeof (&b[0])->v[0]
$14 = 8
Linux x86_64 採用 LP64 data model,double 依據 C 語言規範,至少要 64-bit 長度。現在試著強制轉型:
(gdb) p &(&b[0])->v[0]
$15 = (double *) 0x601060 <b>
(gdb) p (int *) &(&b[0])->v[0]
$16 = (int *) 0x601060 <b>
(gdb) p *(int *) &(&b[0])->v[0]
$17 = 0
然後竟然變成 0
了?!
因為:
(gdb) p sizeof(int)
$18 = 4
我們只取出 v[0]
開頭的 4 bytes,轉型為 int
後,內容就是 0。印出記憶體來觀察:
(gdb) x/4 (int *) &(&b[0])->v[0]
0x601060 <b>: 0x00000000 0x3ff00000 0x00000000 0x40000000
GDB 強大之處不只如此,你甚至在動態時期可呼叫函式 (改變執行順序),比方說 memcpy
:
(gdb) p calendar
$19 = {{0 <repeats 31 times>} <repeats 12 times>}
(gdb) p memcpy(calendar, b, sizeof(b[0]))
$20 = 6296224
(gdb) p calendar
$21 = {{0, 1072693248, 0, 1073741824, 0, 1074266112, 0 <repeats 25 times>}, {0 <repeats 31 times>} <repeats 11 times>}
現在 calendar[][]
內容已變更。前述輸出有個數字 6296224
,到底是什麼呢?試著觀察:
(gdb) p (void *) 6296224
$22 = (void *) 0x6012a0 <calendar>
原來就是 memcpy
的目的位址,符合 man page memcpy(3) 描述。
從 calendar 把 {1, 2, 3}
內容取出該怎麼作呢?
(gdb) p *(double *) &calendar[0][0]
$23 = 1
(gdb) p *(double *) &calendar[0][2]
$24 = 2
(gdb) p *(double *) &calendar[0][4]
$25 = 3
int a[10];
...
int *p;
p = a; /* take the pointer to a[0] */
p++; /* next element */
p--; /* previous element */
int *p;
p = p + 1; /* this advances p's value (pointed-address) by sizeof(int) which is usually not 1 */
int a[3];
int *p;
p = a; /* p points to a[0] */
*a = 5; /* sets a[0] to 5 */
*(a+1) = 5; /* sets a[1] to 5 */
The only difference is in sizeof:
sizeof(a) /* returns the size of the entire array not just a[0] */
char *r;
strcpy(r, s);
strcat(r, t);
Doesn’t work cause r doesn’t point anywhere.
Lets make r an array – now it points to 100 chars
char r[100];
strcpy(r, s);
strcat(r, t);
This works as long as the strings pointed to by s and t aren’t too big.
char r[strlen(s) + strlen(t)];
strcpy(r, s); strcat(r, t);
However C requires us to state the size of the array as constant.
char *r = malloc(strlen(s) + strlen(t));
strcpy(r, s); strcat(r, t);
This fails for three reasons:
malloc()
might fail to allocate the required memoryThe correct code will be:
char *r = malloc(strlen(s) + strlen(t) + 1); // use sbrk; change progrram break
if (!r) exit(1); /* print some error and exit */
strcpy(r, s); strcat(r, t);
/* later */
free(r);
r = NULL; /* Try to reset free’d pointers to NULL */
`int main(int argc, char *argv[], char *envp[])` 的奧祕
#include <stdio.h>
int main(int argc, char (*argv)[0])
{
puts(((char **) argv)[0]);
return 0;
}
使用 gdb
(gdb) b main
(gdb) r
(gdb) print *((char **) argv)
$1 = 0x7fffffffe7c9 "/tmp/x"
這裡符合預期,但接下來:
(gdb) x/4s (char **) argv
0x7fffffffe558: "\311\347\377\377\377\177"
0x7fffffffe55f: ""
0x7fffffffe560: ""
0x7fffffffe561: ""
看不懂了,要換個方式:
(gdb) **x/4s ((char **) argv)[0]**
0x7fffffffe7c9: "/tmp/x"
0x7fffffffe7d0: "LC_PAPER=zh_TW"
0x7fffffffe7df: "XDG_SESSION_ID=91"
0x7fffffffe7f1: "LC_ADDRESS=zh_TW"
後三項是 envp (environment variable)
C array 大概是 C 語言中數一數二令人困惑的部分。根據你使用的地方不同,Array 會有不同的意義:
- 如果是用在 expression,array 永遠會被轉成一個 pointer
- 用在 function argument 以外的 declaration 中它還是一個 array,而且「不能」被改寫成 pointer
- function argument 中的 array 會被轉成 pointer
若現在這裡有一個 global
char a[10];
在另一個檔案中,我不能夠用
extern char *a
來操作原本的a
,因為實際上會對應到不同的指令 (instruction)。但若你的 >declaration 是在 function argument 中,那麼:
void function(char a[])
與void function(char * const a)
是等價的。且真實的type 會是 pointer。因此你不能用 sizeof 來抓它的大小!(array是unmodifiable l-value,所以除了被轉成pointer,它還會是一個不能再被賦值的pointer,因此需要加上const修飾。)最後,用在取值時,array的行為與pointer幾乎雷同,但array會是用兩步取值,而pointer是三步。(array的位址本身加上offset,共兩步,而使用pointer時,cpu需先載入pointer位址,再用pointer的值當作位址並加上offset取值)
[name=陳仁乾]
A common C pitfall is to confuse a pointer with the data to which it points
依據 man-page strdup(3) :
strdup()
實際上會呼叫 malloc()
,來配置足以放置給定字串的「新空間」。
int main() { return (********puts)("Hello"); }
為何可運作?
*
is Right associative operator(注意到 designator 的發音,KK 音標為 [͵dɛzɪgˋnetɚ])
根據 6.3.2.1
void (*fptr)();
void test();
fptr = test;
test: void (), 會被轉成 function pointer: void (*) ()
fptr is a pointer to function with returning type,
根據 6.5.3.2
(*fptr) is a function designator, a function designator will be converted to pointer.
type of (*fptr): void ()
我們可以利用 gdb 去查看這些數值,搭配 print 指令
(gdb) print test
$1 = {void ()} 0x400526 <test>
(gdb) print test
$2 = {void ()} 0x400526 <test>
(gdb) print fptr
$3 = (void (*)()) 0x400526 <test>
(gdb) print (*fptr)
$4 = {void ()} 0x400526 <test>
(gdb) print (**fptr)
$5 = {void ()} 0x400526 <test>
test 是一個 function designator ,因為不是搭配 &
, sizeof 使用,所以會被轉成為 pointer to function
(*fptr)
是一個 function pointer 搭配 * (dereference, indirection) operator 使用,則它的結果會被轉成為一個 function designator
所以 (**fptr)
要拆成兩個部份: (* ( *fptr) )
, 裡面的 *fptr
是個 function designator, 但是還是會被轉成一個 pointer to function,我們可註記為 fptr
。
又,外面又有一個 * operator, ( *fptr’
是個 function designator, 最後還是會被轉化為 pointer to function
但是,0x400526 這個數值是怎麼來的呢?
我們可以是用以下指令觀察:
$ gdb -o fp -g fp.c ` & ` objdump -d fp `
參考輸出裡頭的一段:
0000000000400526 <test>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: 90 nop
40052b: 5d pop %rbp
40052c: c3 retq
這個數值其實是執行檔反組譯後的函式進入點,但是這個數值並非是在實體記憶體的實際數值,而是虛擬記憶載入位址。
由於 puts
的 function prototype 是 int puts(const char *s)
,因此 每次經過 *
operator 運算後得到的結果是仍然是 int
。所以,*
的數目不會影響結果。最後 return 的值是根據 s 的長度加上 ’\n’。而這個例子 return 給 main
的值是 6
memcpy()
取代 strcpy()
,前者通常有平臺相依的最佳化,而後者因為不能事先知道長度,無法做更有效率的最佳化。但這樣的最佳化技巧要讓編譯器自動施行,導致額外的維護成本gets()
就有機會搞垮系統,對照看 Insecure coding in C:::info UTF-8 的發明人也是主導 UNIX 發展的 Rob Pike 與 Ken Thompson :::
取自 Wikipedia: "Support for Unicode literals such as
char foo[512] = "φωωβαρ";
(UTF-8) orwchar_t foo[512] = L"φωωβαρ";
(UTF-16 or UTF-32) is implementation defined, and may require that the source code be in the same encoding. Some compilers or editors will require entering all non-ASCII characters as\xNN
sequences for each byte of UTF-8, and/or\uNNNN
for each word of UTF-16."
出處 為了使用上方便,許多程式語言提供 string literals (在 C 就是 C99 [6.4.5]) ,讓開發者得以在程式碼內表示一些字串。由於 C 語言是個系統程式語言,所以大家對這些東西會身在何處是有高度興趣的。而 string literals 到底會身在何處,和編譯器的實作有關
C 語言規格中定義了 string literals 會被分配於 "static storage" 當中 (C99 [6.4.5]),並說明如果程式嘗試修改 string literals 的內容,將會造成未定義行為
\0
結尾)由於 C 語言提供了一些 syntax sugar 來初始化陣列,這使得 char *p = ”hello world”
和 char p[] = “hello world”
寫法相似,但底層的行為卻大相逕庭
char *p
來說,意思是 p
將會是指向 static storage 的一個指標。如此的寫法有潛在問題,因為當開發者嘗試修改 string literals 的內容,將會造成未定義行為,而編譯器並不會對存取 p 的元素提出警告char p[]
的語意則表示要把資料分配在 stack 內,所以這會造成編譯器 (gcc) 隱性地 (implicitly) 產生額外的程式碼,使得 C 程式在執行時期可把 string literals 從 static storage 複製到 stack 中。雖然字串本身並非存放於 stack 內,但 char p[]
卻是分配在 stack內,這也造成 return p
是未定義行為光有字串表示法是遠遠不夠的,我們需要在字串上進行若干操作。C 語言的規格書中定義了標準函式庫,在 [7.21] String handling <string.h> 提及,以避免製造重複的輪子。接下來問題來了,這些字串處理函式的原型大部分是使用 char *
或 void *
的型別來當參數,那這些參數到底能不能接受 null pointer (void *) 0
呢?如果不能,那函式要負責檢查嗎?null pointer 算是一個字串嗎 ?對一個 null pointer 使用這些函式 (如 strlen
) 會發生什麼事?規格書中有定義這些東西嗎?
strlen
一類的 "string handling function" 不可 接受 null pointer 作為參數,因為在絕大部分的狀況中,null pointer 並非合法字串 (why?),所以將 null pointer 代入 strlen()
也造成未定義行為回頭閱讀規格書: [7.21.1] 提到以下:
Various methods are used for determining the lengths of the arrays, but in all cases a char *
or void *
argument points to the initial (lowest addressed) character of the array. If an array is accessed beyond the end of an object, the behavior is undefined.
null pointer 顯然在 絕大部分 的狀況都不符合這個規定,既然不符合規定,函式庫的實作顯然也不需要浪費心力去做檢查。更不用說想要在一些物件導向的字串函式庫中使用 string 物件的 null pointer 來做字串運算。
int fn(int a[][10])
int fn(int (*a)[10])
and "a+1" is actually 40 bytes ahead of "a", so it does not act like an "int *". (And I might have screwed that up mightily - C multidimensional arrays and the conversions to pointers are really easy to get confused about. Which is why I hope we don’t have them)
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
`char *`
在規格書的 Rationale 將 lvalue 定義為 object locator (C99)
透過 Lvalue 解釋 compound literal 效果的案例:
#include <stdio.h>
struct s { char *a; };
void f(struct s *p) { printf("%s\n", p->a); }
int main() {
f(&(struct s){ "hello world" });
return 0;
}
[nʌl]
請不要發「怒偶」,他沒有怒。考慮以下程式 (null.c)
int main() { return (NULL == 0); }
用 gcc-6.2 編譯:
null.c:1:22: error: ‘NULL’ undeclared (first use in this function)
int main() { return (NULL == 0); }
^~~~
null.c:1:22: note: each undeclared identifier is reported only once for each function it appears in
表示 NULL 需要定義,我們加上 #include <stddef.h>
即可通過編譯。那 main
的回傳值是 0 還是 1 (或非零) 呢?
依據 C99 規格 6.3.2.3
C99 規格 6.3.2.3 - 3
依據 C99 / C11 規格 6.3.2.3 - 3
延伸 C99/C11 規格 6.3.2.3 - 4
C99 規格 6.7.8 Initialization
free()
傳入 NULL 是安全的
source (裡頭有非常精彩的 undefined behavior 討論)
<stddef.h> 定義了 offsetof 巨集,取得 struct 特定成員 (member) 的位移量。傳統的實做方式如下:
#define **offsetof**(st, m) **((size_t)&(((st *)0)->m))**
這對許多 C 編譯器 (像是早期的 gcc) 都可正確運作,但依據 C99 規格,這是 undefined behavior。後來的編譯器就不再透過巨集來實做,而改用 builtin functions,像是 gcc:
#define offsetof(st, m) __builtin_offsetof(st, m)
這對於 C++ 非常重要,否則原本的巨集連編譯都會失敗。
延伸閱讀: C99 的 offsetof macro
Linux 核心延伸 offsetof,提供 container_of 巨集,作為反向的操作,也就是給予成員位址和型態,反過來找 struct 開頭位址:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
應用方式請見: Linux Kernel: container_of 巨集