最近,我無意中引發了一場大辯論,議題是當 P 是一個空指標時,在 C/C++ 中使用 &P->m_foo 表示式是否合法。程式設計師社群分成了兩大陣營。一方自信地宣稱這不合法,而另一方則同樣肯定地說這合法。雙方都給出了各種論據和連結,到某個時刻,我意識到我必須把事情說清楚。為此,我透過一個封閉的郵件列表聯絡了微軟 MVP 專家和 Visual C++ 微軟開發團隊。他們幫助我準備了這篇文章,現在歡迎所有感興趣的人閱讀。對於那些等不及想知道答案的人:那段程式碼是“不”正確的。
這一切都始於一篇關於 PVS-Studio 分析器對 Linux 核心進行檢查的文章。但問題與檢查本身無關。關鍵在於,在那篇文章中,我引用了 Linux 程式碼中的以下片段:
|
|
我稱這段程式碼是危險的,因為我認為它會導致未定義行為。
之後,我收到了大量的電子郵件和評論,讀者們反對我的這個觀點,我甚至差點就被他們有說服力的論點說服了。例如,為了證明那段程式碼是正確的,他們指出了 offsetof 宏的實現,通常是這樣的:
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
這裡我們處理了空指標解引用,但程式碼仍然工作得很好。還有一些其他的郵件論證說,既然沒有透過空指標進行訪問,那就沒有問題。
儘管我容易輕信,但我還是會努力複核任何我可能懷疑的資訊。我開始研究這個主題,最終寫了一篇小文章:“關於空指標解引用問題的反思”。
一切都表明我是對的:不能那樣寫程式碼。但我沒能為我的結論提供有說服力的證據,也沒能引用標準中的相關摘錄。
發表那篇文章後,我再次收到了大量抗議郵件的轟炸,所以我認為我應該一勞永逸地把這一切搞清楚。我向語言專家提出了一個問題,以瞭解他們的意見。這篇文章是他們回答的總結。
當 'podhd' 是一個空指標時,'&podhd->line6' 表示式在 C 語言中是未定義行為。
C99 標準關於 '&' 取地址運算子的規定如下 (6.5.3.2 "Address and indirection operators"):
一元 & 運算子的運算元應該是一個函式指示符,一個 [] 或一元 * 運算子的結果,或者是一個指定了非位域且未使用 register 儲存類說明符宣告的物件的左值。
表示式 'podhd->line6' 顯然不是函式指示符,也不是 [] 或 * 運算子的結果。它“是”一個左值表示式。然而,當 'podhd' 指標為 NULL 時,該表示式並不指定一個物件,因為 6.3.2.3 "Pointers" 中說:
如果一個空指標常量被轉換成指標型別,得到的指標,稱為空指標,保證與任何指向物件或函式的指標比較結果為不相等。
當“一個左值在求值時未指定一個物件,其行為是未定義的” (C99 6.3.2.1 "Lvalues, arrays, and function designators")
左值是具有物件型別或除 void 之外的不完整型別的表示式;如果一個左值在求值時未指定一個物件,其行為是未定義的。
所以,簡而言之,是同樣的想法:
當 -> 運算子作用於該指標時,它求值為一個不存在物件的左值,結果是未定義行為。
在 C++ 語言中,情況完全相同。當 'podhd' 是一個空指標時,'&podhd->line6' 表示式是未定義行為。
我在前一篇文章中提到的 WG21 的討論(232. Is indirection through a null pointer undefined behavior?)帶來了一些困惑。參與討論的程式設計師堅持認為這個表示式不是未定義行為。然而,沒有人能在 C++ 標準中找到任何條款允許在 "poldh" 為空指標時使用 "poldh->line6"。
"polhd" 指標未能滿足基本約束 (5.2.5/4, 第二點),即它必須指定一個物件。沒有任何 C++ 物件的地址是 nullptr。
struct usb_line6 *line6 = &podhd->line6;
當 podhd 指標等於 0 時,這段程式碼在 C 和 C++ 中都是不正確的。如果指標等於 0,就會發生未定義行為。
程式能正常執行純屬運氣。未定義行為可能以不同形式出現,包括程式完全按照程式設計師預期的方式執行。這只是未定義行為的一種特例,僅此而已。
你不能這樣寫程式碼。指標在解引用之前必須進行檢查。
這篇文章的問世,得益於那些我毫無理由懷疑其能力的專家們。我要感謝以下人士幫助我撰寫本文: