跳轉到內容

可移植性和C語言

100% developed
來自華夏公益教科書,開放書籍,開放世界

Linux商標歸Linus Torvalds所有。

UNIX® 是The Open Group的註冊商標。

各版本前言

[編輯 | 編輯原始碼]

1989年第1版(稍作編輯)

[霍華德·W·薩姆斯,海登圖書,ISBN 0-672-48428-5,1989。]

早在1986年初,我被邀請在全美幾個主要城市教授為期三天的關於可移植性的研討會,該研討會與C語言有關。碰巧的是,這系列研討會被取消了,但我已經編寫了一份70頁的手稿,打算用作講義。

自從我進入C的領域以來,我一直被C語言的明顯矛盾所吸引:它既是一種低階系統實現語言,又是一種可移植語言。每次我聽到有人熱情洋溢地談論C的“固有”可移植性時,我都會更加不安,因為我注意到,要麼是我,要麼是C社群中相當一部分人遺漏了“C畫面”中的一些重要部分。事實證明,我認為不是我,儘管看起來確實有一部分編寫良好的C程式碼可以相對輕鬆地移植。

鑑於我有一份基本的可移植性文件,而且對C現象普遍感興趣,特別是對C標準和可移植性感興趣,我開始正式詳細地研究C和可移植性。由於我的主要收入來源是C諮詢和教授有關C的入門級和高階研討會,因此我更加堅定地決定為為期三天的可移植性研討會編寫一份嚴肅的手稿。在此過程中,我決定最終結果值得成為一本書。

起初,我預計會出版大約200頁的書籍。然後變成了300頁和400頁,最後我定稿為425頁,但這只是在我決定刪除一些附錄之後,純粹是出於篇幅的考慮。由於留在“編輯室地板上”的材料數量和實用性很大,我正在尋找分發這些材料的方法,也許透過未來的修訂版或配套書籍。無論如何,這本書並沒有包含我所有的發現。

這本書試圖記錄在移植現有程式碼或編寫要移植到多個目標環境的程式碼時可能會遇到的C特異性問題。我使用“試圖”這個詞,因為我不認為這本書提供了所有答案,而且在很多情況下,它甚至不打算做到這一點。例如,如果您要從一種UNIX版本移植到另一種UNIX版本,本書不會討論該作業系統的任何角落。儘管如此,我相信這是一份可信的起點,未來的作品可以以此為基礎。據我所知,這是第一本專門討論與C相關的可移植性的作品,其篇幅超過20-30頁。由於我並不精通3-4種作業系統和硬體環境,因此我可能忽略了一些相關問題。或者,我可能過度沉迷於可能只在理論上出現的各種深奧方面。

無論您對可移植性的興趣如何,我希望這本書都能為您提供一些思考的素材,即使只是為了幫助您相信可移植性不適合您。如果這本書只實現了這一點,那它就已經取得了巨大的成功。另一方面,如果它能幫助您制定移植策略,或者避免您走上幾條錯誤的道路,那麼我也很高興。無論您對這本書有什麼看法,請告訴我,因為只有透過獲得建設性的批評、外部意見和更多個人經驗,我才能在未來的修訂版或配套書籍中改進它。

任何曾經編寫過一篇要被很多人閱讀的長篇文件的人都明白,在閱讀前兩三次之後,您實際上不再閱讀所寫的內容。您只是閱讀應該出現的內容。因此,您需要技術精通的審閱者,他們能夠提供建設性的批評。在這方面,以下人員透過校對全部或大部分手稿做出了重大貢獻:史蒂夫·巴特爾斯、唐·比克斯勒、唐·考特尼、丹尼斯·德洛里亞、約翰·豪斯曼、布萊恩·希格斯、加里·杰特、湯姆·麥克唐納和蘇·梅洛伊。雖然我採納了許多他們的建議,但空間和時間限制不允許我充分利用他們的組織和其他建議。但是,正如軟體供應商所說,“我們必須為下一個版本保留一些東西。”

對我相對短暫而密集的C生涯產生過重大影響的其他人還有:P.J. 普勞格,C標準秘書,ISO C召集人,以及Whitesmiths Ltd的總裁,一家國際C和Pascal開發工具供應商;湯姆·普拉姆,C標準副主席,Plum Hall的主席,以及領先的C作者;拉里·拉斯勒,以前是C標準草案文件的編輯,以及AT&T在C標準委員會的主要成員(現在屬於惠普);以及吉姆·布羅迪,獨立顧問(前摩托羅拉員工),他在1983年中期召集了C標準委員會,並出色地領導該委員會直到(希望)在1988年末或前後成功完成。此外,我要感謝我在C標準X3J11標準委員會的同事們,感謝你們讓我有機會與你們一起工作——沒有你們的論文、簡報,以及有時在委員會內外激烈的討論(雙關語),這本書中的材料質量和數量將會大大減少,可能不足以出版。

雷克斯·傑施克

2021年第2版

快進32年,C的世界發生了很多變化。特別是:

  • C95、C99、C11和C17已經發布。

  • C++已經標準化,並且該標準已經多次修訂。

  • 16位系統已經很少見,甚至32位系統也不再常見。主流世界已經轉向64位。

  • 只支援C89之前版本的C編譯器不太可能普遍,儘管最初透過它們編譯的程式碼可能仍在使用。

這次修訂是我在遺產規劃期間的結果,當時我問自己:“如果我不採取任何行動,我死後我的智慧財產權會怎樣?” 估計會丟失!因此,我四處尋找一個公開場所,在那裡我可以釋出它,讓它可以被閱讀,並且(希望負責任地)保持最新。

一旦我決定需要修訂,我就變得相當無情。(我非常相信斯特朗和懷特的建議,“越少越好!”)我刪除了所有與可移植性無關的材料。因此,庫章節的很多內容都被刪除了。早在1988年,第一個C標準即將問世,關於庫幾乎沒有明確的文字。因此,我在第一版中包含了這一點。但是,現在不再需要了。此外,人們可以購買C(和C++)以及相關標準的可搜尋電子版。

關於潛在的移植目標,我做出了兩個重要的決定

  • 承認即使程式碼不是,而且永遠不會是,標準C相容的,想要移植程式碼也是可以的!
  • 提及C++:C++被廣泛使用,許多程式設計師從C++呼叫C函式,或者將C程式碼透過C++編譯器編譯。

當然,這個版本將會過時;當我寫這些的時候,C標準委員會正在完成C23的最終版本!

第一版包含一個附錄,主要由各種順序的保留識別符號列表組成。我選擇不包括這個附錄,原因有幾個:自C89以來,各種標準修訂版添加了大量名稱,因此更新這些列表需要大量工作,而且隨著C23即將釋出,還需要更多工作來再次修訂該列表。無論如何,審閱者無法就這些列表應該以什麼形式才能易於閱讀且仍然有用達成一致。

最後,要特別感謝本版審閱者:拉詹·巴克塔、吉姆·布羅迪、道格·格溫、大衛·基頓、湯姆·麥克唐納、羅伯特·西科德、弗雷德·泰德曼和維萊姆·瓦克爾。

雷克斯·傑施克

本檔案的未來修訂

[編輯 | 編輯原始碼]

會有更新本文件的理由,例如,為了做到以下幾點

  • 修復排版或事實性錯誤

  • 擴充套件某個主題

  • 新增特定移植場景以及目標硬體和作業系統的詳細資訊

  • 新增標準C和標準C++之間的不相容性

  • 涵蓋C和C++標準的未來版本

  • 擴充套件與C99及更高版本新增的標頭檔案相關的問題,尤其是與浮點數相關的那些

  • 新增有關可選的IEC 60559(IEEE 754)浮點數和複數支援的問題

  • 新增有關可選的擴充套件庫的問題

  • 新增尚未提到的未指定、未定義、實現定義和區域特定行為的例項

  • 完善“目標受眾”部分。

  • 考慮提供可下載的保留識別符號列表,可能按標頭檔案和標準版本組織。

關於特定庫函式,只存在與可移植性相關的註釋的條目。如果要為未列出的函式新增此類註釋,則必須首先為其建立條目。

如果您要新增內容到這本書,請準確無誤並使用正確的術語,如C標準所定義。只說一次,在適當的地方說,然後根據需要指向其他地方的權威宣告。

目標受眾

[編輯 | 編輯原始碼]

審閱者維萊姆·瓦克爾寫道:我瀏覽了這份文件,我認為它(可能)是一份非常有用的文件,雖然我不太確定目標受眾。您的引言沒有提及目標受眾,而經驗豐富的C程式設計師可能會認為自己不需要這些資訊(“我已經知道這些細節,因為我是一名經驗豐富的程式設計師”)。

可移植性,如同安全性,需要從專案的開始就考慮到,在這個早期階段,對可移植性的整體考慮比你書中描述的所有(雖然有用!)細節和陷阱都要多。這可能意味著這本書需要讓專案中更多管理型別的人員注意到,然後他們可以“強迫”程式設計師將好的建議考慮在內。對於那些管理人員來說,這本書看起來太像技術指南了,而不是他們需要關注的東西。因此,也許在書的開頭,為非技術管理人員編寫關於可移植性概念和需求的幾段引言可能是一個有用的補充。

我的回覆:目前,我正在新增這一部分作為佔位符。但是,我決定不自己編寫內容,而是把它留給讀者,讓他們在出版後根據自己的情況進行充實。

讀者假設和建議

[編輯 | 編輯原始碼]

本書並不試圖教授入門甚至高階的 C 語言結構。它也不是關於標準 C 的教程。有時,一些段落可能看起來和 C 本身一樣簡短。雖然我試圖使這些段落變得柔和,但我對那些仍然存在的段落並不道歉。可移植性不是第一次或受訓的 C 程式設計師開始做的事情——恰恰相反。

本文專門針對移植 C 原始碼的語言相關方面。然而,它並沒有提供在任何給定目標環境集中成功移植系統的方案——它只是詳細說明了您可能會遇到或可能需要調查的許多問題和情況。本書假設您熟悉 C 語言的基本結構,例如所有運算子、語句和預處理器指令,並且您熟練使用資料和函式指標,以及與標準執行時庫的介面。

由於 C 標準、附帶的理由文件和本文具有相同的基本組織,因此擁有每份文件的副本都是有利的,雖然並非完全必要,因為標準有時可能難以閱讀。但是,理由的節奏更為輕鬆,非語言學家更容易閱讀。不過請注意,由於參與了標準委員會 15 年(1984-1999)的審議,我的詞彙反映了 C 標準的詞彙。因此,該文件的副本將特別有用。

在整本書中,“K&R”的使用是指 Kernighan 和 Ritchie 的書 的第一版(1978 年),《C 程式語言》。

對《標準 C》的引用包括所有版本,用於從第一個標準 C89 開始存在的核心功能。對於在特定版本中新增的功能,使用該版本號。 C90 沒有被這樣使用,因為它只是 ANSI 標準 C89 的 ISO 重打包。

C 標準化的歷史如下

  • C89 – 第一個 C 標準,ANSI X3.159-1989,由美國委員會 X3J11 於 1989 年制定。
  • C90 – 第一個 ISO C 標準,ISO/IEC 9899:1990,由委員會 ISO/IEC JTC 1/SC 22/WG 14 於 1990 年制定,與美國委員會 X3J11 合作。C90 在技術上等同於 C89。
  • C95 – 對 C90 的修訂版由委員會 WG 14 於 1995 年制定,與美國委員會 X3J11 合作。術語 C95 表示“C90 加上該修訂版”。
  • C99 – ISO C 標準的第二版,ISO/IEC 9899:1999,由委員會 WG14 制定,與美國委員會 INCITS/J11(以前為 X3J11)合作。
  • C11 – ISO C 標準的第三版,ISO/IEC 9899:2011,由委員會 WG14 制定,與美國委員會 INCITS/PL22.11(以前為 INCITS/J11)合作。
  • C17 – ISO C 標準的第四版,於次年作為 ISO/IEC 9899:2018 出版,由委員會 WG14 制定,與美國委員會 INCITS/PL22.11 合作。這是一個維護版本,包括基於缺陷報告對標準的修正。沒有新增新的功能。
  • C23 – 規劃釋出 ISO C 標準的第五版。

一些段落被標記為“C++ 注意事項”。C++ 被廣泛使用,許多程式設計師從 C++ 呼叫 C 函式,或將 C 程式碼透過 C++ 編譯器。但是,C++ 不是 C 的超集,因此瞭解不相容性是值得的。C++ 標準社群中常說的一句話是“儘可能接近標準 C,但不要更近!”

在整本書中,對許多縮略詞、縮寫和專業術語進行了引用。大多數在當今 C 社群中使用廣泛;但是,這裡列出了與可移植性直接相關的幾個(它們的定義摘自 C17)

  • 未指定的行為 – 使用未指定的值,或使用本檔案提供兩個或多個可能性,並且對任何例項中選擇哪個可能性沒有進一步要求的行為。

  • 未定義的行為 – 使用不可移植或錯誤的程式結構或錯誤的資料的行為,本檔案對此沒有規定任何要求。

  • 實現定義的行為 – 未指定的行為,其中每個實現都記錄瞭如何進行選擇。

  • 特定於區域設定的行為 – 取決於每個實現記錄的國家、文化和語言的本地慣例的行為。

C 標準包含更完整的定義列表,特別是討論了程式和實現符合性的標準。

雖然本書包含上述四種行為的許多例項,但它包含所有行為。完整列表包含在 C 標準的“可移植性問題”附錄中。

雖然符合的實現需要記錄實現定義的行為,但在本書中使用“實現相關的”一詞來指代實現的某些特徵,而這些特徵不需要標準 C 記錄。

C89 宣告,“某些特性是過時的,這意味著它們可能被認為將在標準的未來修訂版中被撤銷。它們被保留在標準中是因為它們被廣泛使用,但鼓勵在新實現(對於實現特性)或新程式(對於語言或庫特性)中避免使用它們。”標準 C 的一些版本透過棄用某些特性來宣告它們已過時。根據 維基詞典,棄用意味著“宣佈某事物已過時;建議不再使用仍然有效但已被取代的功能、技術、命令等”。

從 C 標準化委員會成立之初,它就一直遵循一項章程(並隨著時間的推移進行了修訂)。以下幾項來自原始章程值得一提:

第 2 項。C 程式碼可以移植。雖然 C 語言最初誕生於 DEC PDP-11 上的 UNIX 作業系統,但它後來在各種計算機和作業系統上得到了實現。它還在嵌入式系統的跨編譯中得到了廣泛應用,以在獨立的環境中執行。委員會試圖儘可能廣泛地指定語言和庫,同時認識到系統必須滿足某些最低標準才能被視為語言的有效主機或目標。

第 3 項。C 程式碼可以不可移植。雖然委員會努力為程式設計師提供編寫真正可移植程式的機會,但它不希望強迫程式設計師編寫可移植程式,以阻止 C 作為“高階彙編程式”的使用;編寫特定於機器的程式碼的能力是 C 的優勢之一。正是這一原則在很大程度上促使區分嚴格符合的程式和符合的程式。

定義可移植性

[編輯 | 編輯原始碼]

根據羅伯特·A·埃德蒙茲的《普倫蒂斯·霍爾標準計算機術語詞彙表》,可移植性定義如下:“可移植性:與相容性相關的術語。可移植性決定了程式或其他軟體在不同計算機系統之間遷移的程度。” 這裡的關鍵短語是“程式遷移的程度”。

來自 維基百科,“在軟體工程中,移植是指調整軟體以使其在與原始設計軟體的計算環境不同的環境中執行(例如,不同的 CPU、作業系統或第三方庫)。當軟體/硬體發生變化以使其在不同的環境中可用時,也使用該術語。當移植軟體到新平臺的成本遠低於從頭開始編寫軟體的成本時,該軟體就被認為是可移植的。與實現成本相比,移植軟體的成本越低,它就被認為越可移植。”

我們可以從兩個角度談論可移植性:通用和特定。一般來說,可移植性意味著在一個或多個環境中執行程式,這些環境在某種程度上與程式設計時所處的環境不同。由於生產和維護軟體的成本遠遠超過生產硬體的成本,因此我們有極大的動力將軟體的使用壽命延長到當前硬體的版本之外。從經濟角度來說,這樣做是有道理的。

特定可移植性涉及識別給定程式必須執行的各個目標環境,並明確說明這些環境之間的差異。移植場景的示例包括

  • 從一臺機器上的一個作業系統遷移到同一機器上的另一個作業系統。

  • 從一臺機器上的一個作業系統版本遷移到另一臺具有不同架構的機器上的同一作業系統版本。

  • 在不同機器上不同版本的同一作業系統之間遷移(例如,UNIX 和 Linux 的各種版本)。

  • 在兩種完全不同的硬體和作業系統環境之間遷移。

  • 在使用不同浮點硬體或模擬軟體的系統之間遷移。

  • 在同一系統上的不同編譯器之間遷移。

  • 在符合標準 C 的實現和不符合的實現之間遷移,反之亦然。

  • 在同一編譯器上重新編譯程式碼,但使用不同的編譯器選項。

  • 在同一系統上從一個版本的編譯器遷移到另一個版本的同一編譯器。

最後兩種情況可能並不明顯。但是,在使用新版本的相同編譯器或只是使用不同的編譯時選項執行時,可能會遇到問題,這些問題是在使用沒有錯誤編譯、執行並完成工作的現有程式碼時出現的。潛在的意外行為的原因之一是實現定義的行為發生變化(例如,普通char的符號性)。另一個可能是先前依賴於未定義的行為,而該行為碰巧按程式設計師預期的那樣工作(例如,某些表示式的求值順序)。

請注意,在不符合標準 C 的系統之間移植程式碼是可以的!例如,早期的數字訊號處理 (DSP) 晶片僅支援 32 位浮點資料和操作,在這種情況下,型別floatdoublelong double(如果後兩種型別甚至受編譯器支援)將對映到 32 位。在這種情況下,有意義的應用程式仍然可以在 DSP 晶片系列成員之間移植。

移植不僅僅是讓軟體在多個目標上執行。它還涉及以合理(且可負擔得起)的資源量、及時的方式以及以使生成的程式碼能夠充分執行的方式進行。將系統移植到目標上,以至於移植完成後,執行速度非常慢或使用太多系統資源而變得不可用,這是沒有意義的。

需要問自己的重要問題是

  • 我是在移植到標準 C 實現還是從標準 C 實現移植?如果是這樣,支援哪些標準版本?

  • 我是在移植設計和編寫時考慮了可移植性的程式碼嗎?

  • 我是否事先知道所有環境以及我實際上可以用於測試的環境數量?

  • 我對速度、記憶體和磁碟效率的效能要求是什麼?

還有一個重要的移植場景,即使用 C++ 編譯器進行編譯。即使此類移植的程式碼沒有利用 C++ 的功能,也會進行額外的檢查。例如,C++ 要求使用 C 的原型風格的函式宣告和定義。隨著時間的推移,可以使用 C++ 功能的新程式碼可以新增,或者 C 程式碼可以由現有的 C++ 函式呼叫。請注意,並非只有一個 C++ 標準;到目前為止,我們已經有了 C++99、C++03、C++11、C++14、C++17 和 C++20。

可移植性並非新鮮事物

[edit | edit source]

隨著 1980 年代初高質量且廉價的 C 編譯器和開發工具的廣泛可用,軟體可移植性的理念開始流行起來。以至於,從一些人的說法來看,可移植性之所以成為可能,是因為 C 的出現。

可移植性的概念遠比 C 出現得早,而且在 C 成為丹尼斯·裡奇腦海中的想法之前很久,軟體就被成功地移植了。1959 年,一小群人定義了一種稱為 COBOL 的標準商業語言,1960 年,兩家供應商(雷明頓·蘭德和 RCA)實現了該語言的編譯器。在那年 12 月,他們進行了一項實驗,交換了 COBOL 程式,根據 COBOL 設計團隊成員讓·薩默特的說法,“… 僅進行了最少的修改,主要原因是實現上的差異,程式在這兩臺機器上都運行了”。關於 COBOL 對邏輯上獨立於機器的資料描述的開發,薩默特在 1969 年寫道,“[COBOL] 不會同時保留效率和跨機器的相容性”。

Fortran 也是可移植性領域中的早期參與者。根據維基百科,“… FORTRAN 的日益普及促使競爭的計算機制造商為他們的機器提供 FORTRAN 編譯器,因此到 1963 年,已經存在超過 40 個 FORTRAN 編譯器。由於這些原因,FORTRAN 被認為是第一個廣泛使用的跨平臺程式語言”。

給定一個用 C 編寫的程式並不能提供任何關於移植它所需的努力的指示。這項任務可能是微不足道的、困難的、不可能的或不經濟的。鑑於一個程式是用一種語言編寫的,而沒有考慮將其移植到某個不同的環境中,它實際移植到該環境的難易程度可能與其作者的紀律和怪癖一樣依賴於語言本身。

設計一個可以在一系列環境中移植的程式,其中一些環境可能尚未定義,這可能很困難,但並非不可能。它只需要相當的紀律和計劃。它需要理解和控制(以及在合理的情況下消除)在預期的不同環境中可能會產生不可接受的不同結果的功能的使用。這種理解可以幫助你避免有意地(或者更可能是無意地)依賴你正在編寫的程式的不可移植特性或特徵。此外,在這樣的專案中,一個關鍵目標通常不是編寫一個可以在任何系統上執行而無需修改的程式,而是將特定於環境的函式隔離起來,以便可以為新系統重新編寫它們。主要的移植考慮因素對於任何語言都是一樣的。只有特定的實現細節是由使用的語言決定的。

可移植性的經濟學

[edit | edit source]

成功移植的兩個主要要求是:擁有完成這項工作所需的必要的技術專長和工具,以及獲得管理層的支援和批准。也就是說,必須承認,許多專案是由個人或一個沒有任何管理的小組實施的,但仍然需要可移植性。

顯然,一個人需要擁有,或者能夠獲得並保留優秀的 C 程式設計師。“優秀”這個詞並不意味著僅僅是或完全是大師級,因為這種員工通常會有難以管理的自我。也許成功移植專案中最重要的一項屬性是紀律,無論是在個人層面還是在小組層面。

管理層支援的問題通常更為重要,但它卻被開發人員和管理層本身所忽視。請考慮以下場景。為所有(或指定目標環境的代表性子集)提供足夠的硬體和軟體,開發團隊定期(至少每週)透過所有目標執行其所有程式碼。通常,它每天晚上都會提交一個批處理作業中的測試流。

專案進行到六個月後,管理層評估了進度,發現該專案比預期消耗了更多資源(難道不是這樣嗎?),並決定縮小目標範圍,至少暫時這樣做。也就是說,“我們需要有一些實實在在的東西來在貿易展覽會上展示,因為我們已經宣佈了該產品”,或者“風險投資家希望在下次董事會會議上看到一個原型”。無論原因是什麼,對某些目標的測試和專門針對這些目標的開發都將暫停,通常是永久性地暫停。

從那時起,開發團隊必須忽略已刪除機器的特性,因為它們不再是專案的一部分,而且公司無法承擔額外的資源來認真考慮它們。當然,管理層的建議通常是,“雖然我們不想讓你費心去支援已刪除的環境,但如果我們以後能夠重新啟動這些環境,不要做任何可能使我們無法或效率低下地重新啟動這些環境的事情,那就太好了”。

當然,隨著專案進一步拖延,競爭對手宣佈和/或釋出了替代產品,或者公司遇到了經濟困難,其他目標也可能被刪除,最終可能只剩下一個目標,因為這是開發和營銷能夠支援的全部。每次刪除一個目標時,開發團隊就開始偷工減料,因為它不再需要擔心其他硬體和/或作業系統目標。最終,這降低了將來某個時間點重新啟動被刪除目標的可能性,因為所有由於支援這些目標而被刪除的設計和編寫的程式碼都需要檢查(假設當然,可以識別這些程式碼),以確定所需的努力以及對恢復支援該目標的影響。你可能會發現,某些設計決策要麼禁止要麼負面影響重新啟用已放棄的專案。

最終結果通常是該產品最初只針對一個目標交付,並且從未在任何其他環境中釋出。另一種情況是針對一個目標交付,然後返回並“儘可能地”為一個或多個其他目標進行補救。在這種情況下,這項任務可能與你移植從未考慮過可移植性的程式碼的任務沒有區別。

衡量可移植性

[edit | edit source]

你如何知道系統何時或是否已成功移植?是否是在程式碼在沒有錯誤的情況下編譯和連結時?結果必須完全相同嗎?如果不是,什麼才算是足夠接近?哪些測試用例足以證明成功?在除最簡單的情況之外的所有情況下,你都無法對所有可能的輸入/情況進行詳盡/完全的測試。

當然,程式碼必須在沒有錯誤的情況下編譯和連結,但由於實現定義的行為,從不同的目標獲得不同的結果是完全可能的。合法的結果甚至可能相差足夠大,以至於使它們變得無用。例如,浮點範圍和精度可能在不同的目標之間有很大差異,以至於由浮點環境限制最嚴格的浮點環境產生的結果不夠精確。當然,這是一個設計問題,應該在移植系統之前就考慮好。

一個普遍的誤解是,必須在所有目標上使用完全相同的原始碼檔案,這樣檔案就充滿了條件編譯的行。這根本沒有必要。當然,你可能需要為某些目標定製標頭檔案。你可能還需要用 C 語言編寫的特定於系統的程式碼,以及可能用匯編語言或其他語言編寫的程式碼。只要這樣的程式碼被隔離在單獨的模組中,並且對這些模組的內容和介面有很好的文件,這種方法就不應該成為問題。

如果你在多個目標上使用相同的資料檔案,你需要確保資料被正確移植,特別是如果資料以二進位制而不是文字格式儲存,並且涉及位元組序差異。如果你不這樣做,你可能會浪費大量資源尋找不存在的程式碼錯誤。

除非你已經充分定義了你的特定可移植性場景和需求,否則你無法判斷何時實現了可移植性。並且根據定義,如果你實現了它,你就必須滿意。如果你不滿意,要麼你的需求發生了變化,要麼你的設計有缺陷。最重要的是,成功地將一個程式移植到某些環境中,不能可靠地說明將它移植到另一個目標環境所需要的工作量。

環境問題

[編輯 | 編輯原始碼]

正如在其他章節中指出的那樣,一些可移植性問題與實現語言幾乎沒有關係。相反,這些問題與程式必須執行的硬體和作業系統環境相關。一些問題在本書的正文中有所提及;這裡對它們進行總結,如下所示

  • 混合語言環境。對於要呼叫或被其他語言處理器呼叫的 C 程式碼,可能會提出一些要求。

  • 命令列處理。不同的命令列處理器在行為上差異很大,而且對於某些目標,甚至可能不存在與命令列處理器等效的東西。

  • 資料表示。這當然完全是實現定義的,並且可能差異很大。不僅int的大小在你的目標之間可能不同,而且你甚至不能保證分配給一個物件的全部位都被用來表示該物件的數值。另一個重要的問題是字內的位元組順序以及長字內的字順序。這種編碼方案被稱為大端小端

  • CPU 速度。一種常見的做法是假設在給定環境中執行一個空迴圈n次會導致暫停 5 秒,例如。但是,在更快或更慢的機器上執行相同的程式將使這種方法失效。(在執行相同處理器但具有不同時鐘頻率的版本時也是如此。)或者,當在同一個系統上執行更多(或更少)程式時,時間可能略有不同。相關問題包括處理計時器中斷(包括硬體和軟體)的頻率和效率。

  • 作業系統。即使存在(獨立的 C 不需要作業系統),主要問題是單任務與多工以及固定記憶體組織與虛擬記憶體組織。其他問題包括處理同步和非同步中斷的能力,是否存在可重入程式碼以及共享記憶體。看似簡單的任務,例如獲取系統日期和時間,在某些系統上可能無法實現。當然,系統時間測量的粒度差異很大。

  • 檔案系統。同一個檔案的多個版本是否可以共存,或者建立或最後修改的日期和時間是否被儲存,都是實現相關的。同樣,檔名的字元集、名稱的長度以及名稱是否區分大小寫也都是實現相關的。至於裝置和目錄命名約定,其變化範圍與發明者的想象力一樣廣。因此,C 標準對檔案系統沒有任何說明,除了順序檔案由單個使用者訪問。

  • 開發支援工具。這些工具可能會對你在給定系統上編寫程式碼或需要編寫程式碼的方式產生重大影響。它們包括 C 編譯器、連結器、物件和原始碼庫、彙編器、原始碼管理系統、宏預處理器和實用程式庫。限制示例包括外識別符號的大小寫、意義和數量,甚至可能包括每個目標模組的大小或源模組的數量和大小。也許覆蓋連結器對覆蓋方案的複雜性有很大的限制。

  • 交叉編譯。在目標不是開發軟體的系統所在的環境中,字元集、算術表示和位元組序的差異變得很重要。

  • 螢幕和鍵盤裝置。這些裝置使用的協議差異很大。雖然許多實現了部分或全部的各種 ANSI 標準,但同樣多的是沒有實現,或者包含不相容的擴充套件。從標準輸入獲取字元而不回顯,或者不需要同時按下回車鍵或 Enter 鍵,可能並非普遍適用。直接游標定址、圖形顯示以及光筆、軌跡球和滑鼠等輸入裝置也是如此。

  • 其他外設介面。你的設計可能要求與印表機、繪圖儀、掃描器和調變解調器以及其他裝置進行互動。雖然每個裝置可能存在一些事實上的標準,但你可能出於某種原因被迫採用“略微”不相容的裝置。

程式設計師的可移植性

[編輯 | 編輯原始碼]

在所有關於可移植性的討論中,我們一直在提到將程式碼從一個環境遷移到另一個環境的方面。雖然這是一個重要的考慮因素,但 C 程式設計師比他們編寫的軟體更容易遷移到不同的環境中。出於這個原因,作者創造了程式設計師可移植性這個術語。

程式設計師的可移植性可以定義為 C 程式設計師從一個環境遷移到另一個環境的難易程度。對於任何 C 專案來說,這都是一個重要問題,而不僅僅是涉及程式碼可移植性的專案。如果你採用某些程式設計策略和風格,你可以更容易、更快地將新的團隊成員整合到專案中。需要注意的是,雖然你可能已經制定了一個強大的方法,但如果它與主流 C 實踐相差太遠,那麼教導其他 C 程式設計師使用它或讓他們相信它的優點將非常困難和/或昂貴。

編寫 C 程式時,請考慮兩個主要環境:編譯(即翻譯)環境和執行環境。對於絕大多數 C 程式來說,這兩個環境很可能是一樣的。然而,C 被越來越多地用於執行環境具有不同於翻譯環境的屬性的情況。

概念模型

[編輯 | 編輯原始碼]

翻譯環境

[編輯 | 編輯原始碼]

翻譯階段

[編輯 | 編輯原始碼]

在 C89 之前,C 編譯器在識別和處理標記的方式上有所不同。為了確定源標記應該被處理的順序,標準 C 明確地識別了一組規則,這些規則被稱為翻譯階段。這些規則破壞了以前依賴於不同翻譯順序的程式。

建議:閱讀並理解標準 C 的翻譯階段,這樣你就可以看到你的實現是否遵循了這些階段。

標準 C 不要求預處理器是一個獨立的程式,儘管它允許這樣做。在大多數情況下,預處理器被允許在不知道目標實現的具體屬性的情況下工作。(一個例外是,標準 C 要求對預處理的算術表示式使用給定的型別進行計算;參見#if算術。)

診斷資訊

[編輯 | 編輯原始碼]

標準 C 定義了符合實現需要發出診斷資訊的幾種情況。診斷資訊的形式是實現定義的。標準沒有關於諸如“變數x在被初始化之前就被使用了”和“不可到達的程式碼”之類的資訊或警告訊息的說明。這些被認為是實現質量問題,最好由市場決定。

標準 C 允許擴充套件,只要它們不使嚴格符合的程式失效。符合的編譯器必須能夠停用(或診斷)擴充套件。對符合編譯器的擴充套件僅限於對標準 C 沒有賦予語義的語法進行賦值,或者定義未定義或未指定行為的含義。

執行環境

[編輯 | 編輯原始碼]

標準 C 定義了兩種執行環境:獨立環境和宿主環境。在這兩種情況下,程式啟動 都發生在執行環境呼叫指定的 C 函式時。

靜態初始化的方式和時間是未定義的。但是,所有靜態儲存中的物件都必須在程式啟動之前初始化。對於宿主環境,指定的 C 函式通常稱為main,但它不一定是main。對於標準 C,函式main在程式啟動時被呼叫。如果使用除了main之外的入口點,則程式不符合標準。

建議:對於宿主應用程式,始終使用main作為程式的入口點,除非您有充分的理由不這樣做,並且您確保對其進行了充分的文件記錄。

程式終止 是將控制權返回給執行環境。

獨立環境

[編輯 | 編輯原始碼]

獨立環境 在沒有作業系統的情況下執行,因此,程式執行可以以任何所需的方式開始。儘管此類應用程式環境本質上是不可移植的,但如果設計得當,它們的大部分程式碼通常可以移植(例如,移植到向上相容的一系列裝置控制器)。即使是嵌入式系統編寫者也需要移植到新的和不同的環境。

在程式啟動時呼叫的函式的名稱和型別是實現定義的,程式終止的方法也是實現定義的

獨立程式可用的庫設施(如果有)是實現定義的。但是,標準 C 需要標頭檔案<float.h><iso646.h><limits.h><stdalign.h><stdarg.h><stdbool.h><stddef.h><stdint.h><stdnoreturn.h>

宿主環境

[編輯 | 編輯原始碼]

標準 C 允許main具有零個或兩個引數,如下所示

int main(void) { /* ... */ }
int main(int argc, char *argv[]) { /* ... */ }

(當然,argv 可以改為宣告為 char **,引數名 argcargv 是任意的。)

一個常見的擴充套件是函式main接收第三個引數char *envp[],其中envp指向一個以空指標結尾的指向char的指標陣列,每個指標都指向一個字串,該字串提供了有關此程序執行的環境的某些資訊。任何定義了兩個以上引數的程式都不符合標準。也就是說,它不是最大程度的可移植的。

建議:使用庫函式getenv而不是main中的envp引數來訪問環境變數。但是請注意,getenv返回的字串的格式以及環境變數集是實現定義的

一些使用者手冊和書籍錯誤地建議將main定義為具有void(或其他型別)型別而不是int型別,因為許多程式很少(如果有的話)明確地從main(有或沒有返回值)返回。

建議:始終將函式main定義為具有int型別並返回適當的退出程式碼。

標準 C 要求argc為非負數。傳統上,即使argv[0]被設定為指向空字串,argc也至少為 1。

建議:不要假設argc始終大於零。標準 C 允許它為零。

標準 C 要求argv[argc]包含空指標。這意味著argv陣列包含argc + 1個元素,而不是argc。這允許在不考慮argc的值的情況下處理argv指標陣列。

標準 C 並沒有對命令列中引號字面量的處理發表任何評論。因此,是否能夠處理引號字串,或者包含嵌入式空格的引號字串,是實現相關的。如果宿主環境無法處理包含字母大小寫混合的命令列引數,則它必須以小寫形式提供文字引數。

建議:不要對命令列處理中引號字面量的特殊處理做任何假設。這些引號可能用於分隔字串,或者它們可能被視為字串的一部分,在這種情況下,"abc def" 將導致兩個引數 "abcdef"。字母的大小寫可能不會被保留,即使在存在引號的情況下也是如此。(在將命令列引數與有效字串列表進行比較之前,請使用tolower(或toupper)。即使引號被識別,用於轉義引號(以便它可以作為引數傳遞)的方法也可能不同。標準 C 甚至不要求存在命令列環境。

命令列引數的主要用途是指定開關,這些開關決定呼叫程式執行的處理型別。例如,在文字處理實用程式中,您可能希望使用多字開關。在這種情況下,使用下劃線將這些詞連線起來,如下所示

textpro /left_margin=10 /page_length=55

並在開關處理期間忽略大小寫。注意,您可以設計一個非常可移植的命令列引數語法。但是請注意,您不需要比系統支援的更大的命令列緩衝區。如果程式可能具有許多和/或很長的引數,則應將它們放在配置檔案中並將它的名稱作為命令列引數傳遞。例如,

textpro /command_file=commands.txt

允許處理無限數量的引數,而不管命令列緩衝區的大小。

根據標準 C,argv[0] 代表“程式名稱”(對於給定的實現,這可能被翻譯成什麼)。如果它不可用,則argv[0]必須指向空字串。(一些無法確定程式名稱的系統將argv[0]指向字串,例如"c""C"。)

建議:不要假設程式名稱可用。即使argv[0]指向程式名稱,該名稱也可能是它在命令列上指定的樣子(可能帶大小寫轉換),也可能是作業系統用來實際定位和載入程式的已轉換名稱。(如果您希望解析argv[0]指向的字串以確定某些磁碟和目錄資訊,則完整的名稱轉換通常很有用。)

標準 C 要求引數argcargv 以及 argv 指向的字串可以由使用者程式修改,並且在使用者程式執行期間,這些引數可能不會被實現更改。

許多環境支援命令列運算子<>>>。在這樣的系統中,這些字元(以及與它們相關的檔名)由命令列處理器處理(並刪除),然後它將剩餘的命令列傳遞給執行環境。沒有以這種方式處理這些運算子的系統會將它們作為命令列的一部分傳遞給執行環境,在那裡它們可以被處理或傳遞給應用程式程式。這些運算子超出了標準 C 的範圍。

上述運算子通常允許重定向stdinstdout。一些系統允許重定向stderr。一些系統認為stderrstdout相同。

建議:不要假設普遍支援命令列重定向運算子<>>>。可以使用庫函式freopen從程式內部重定向標準檔案。

建議:將錯誤訊息寫入stderr而不是stdout,即使這兩個檔案指標都被視為相同。這樣,您就可以利用允許stdoutstderr 被獨立重定向的系統。

在程式啟動期間呼叫main的方法可能會有所不同。標準 C 要求這樣做,就好像使用以下程式碼一樣

exit(main(argc, argv));

在這種情況下,從main返回的任何值都將作為程式的退出程式碼傳遞。

main 的結束花括號中掉出來會導致退出程式碼為零。

一些實現可能會將退出程式碼限制為無符號整數值,或者限制為適合位元組的值。有關更多詳細資訊,請參閱庫函式exit。此外,雖然一些系統將退出程式碼 0 解釋為成功,但其他系統可能不會。標準 C 要求 0 表示“成功”。它還在<stdlib.h> 中提供了實現定義的EXIT_SUCCESSEXIT_FAILURE

建議:退出程式碼的值範圍、含義和格式是實現定義的。即使exit 返回一個int引數,該引數也可能在被傳遞給宿主系統之前被終止程式碼修改、截斷等。使用EXIT_SUCCESS 而不是 0 來表示成功退出程式碼。

如果您使用退出程式碼從一個使用者程式返回資訊給它的父使用者程式,那麼您通常可以自由地採用自己的值約定,因為宿主環境可能不會直接處理退出程式碼。

程式執行

[編輯 | 編輯原始碼]

標準 C 為了定義一個抽象機器,在一定程度上進行了努力。在執行序列中稱為 *序列點* 的特定指定點,所有先前計算的副作用應已完成,並且後續計算的任何副作用都不應發生。

一個特殊的問題是處理終端輸入和輸出,其中一些實現使用緩衝 I/O,而另一些使用非緩衝 I/O。

最佳化編譯器允許在序列點之間進行最佳化,只要它可以保證與嚴格遵循序列點相同的結果。

C11 添加了對多個執行執行緒的支援。以前,多執行緒程式使用庫函式和/或編譯器擴充套件。

環境因素

[編輯 | 編輯原始碼]

字元集

[編輯 | 編輯原始碼]

C 程式涉及兩個可能的字元集:原始碼和執行。原始碼字元集用於表示原始碼程式,執行字元集在執行時可用。大多數程式在與它們被翻譯的同一臺機器上執行,在這種情況下,它們的原始碼和執行字元集是相同的。交叉編譯的程式通常在與開發它們所使用的機器不同的機器上執行,在這種情況下,原始碼和執行字元集可能不同。

原始碼字元集中的字元(除非由標準 C 明確指定)是 *實現定義的*。執行字元集中的字元(除了 `'\0'` 字元)及其值是 *實現定義的*。執行字元 `'\0'` 必須由全零位表示。

原始碼文字中未指定字元的含義(除了在字元常量、字串文字或註釋中)是 *實現定義的*。

雖然許多 C 程式是在 ASCII(現在是 Unicode)環境中翻譯和執行的,但其他字元集也在使用。由於大小寫字母集可能不連續(例如在 EBCDIC 中),因此在編寫處理多個字元集的例程時必須小心。在處理非英語字母時,它們也可能沒有相應的上或下大小寫等效項。當使用庫函式 `qsort` 時,字元集的排序順序也很重要。

**建議:**如果您編寫的程式碼特定於某個字元集,請根據主機字元集有條件地編譯程式碼,或將其記錄為實現特定模組。使用 `ctype.h` 函式,而不是將字元與特定集合或整數範圍進行比較。

三字母組序列

[編輯 | 編輯原始碼]

在某些環境中,一些必需的原始碼字元對程式設計師不可用。這通常是因為他們使用的是字元集不包括所有必要標點符號的機器。(這也可能是因為他們使用的是鍵盤沒有所有必要標點符號鍵的鍵盤。)

為了能夠輸入 ISO 646-1983 不變程式碼集(它是七位 ASCII 程式碼集的子集)中未定義的字元,C89 引入了以下三字母組序列。

三字母組 含義
??= #
??( [
??/ \
??) ]
??' ^
??< {
??! |
??> }
??- ~

*三字母組* 是一個由三個字元組成的標記,前兩個字元是 `??`。三個字元共同代表上表中對應的字元。

在編譯器中新增對三字母組的支援可能會改變現有字元常量或字串文字的解釋方式。例如,

printf("??(error at line %d)\n", msgno);

將被視為已寫入為

printf("[error at line %d)\n", msgno);

而 `sizeof("??(") ` 將為 2,而不是 4。

如果此類文字字串打算顯示,那麼從不支援三字母組的系統遷移到支援三字母組的系統的影響將很小且明顯——使用者將看到稍微不同的輸出。但是,如果程式解析字串以期望找到特定字元,例如 `?`,那麼如果它以前被解釋為三字母組序列的一部分,它將不再找到它。

儘管絕大多數 C 程式設計師可能不會使用三字母組,但符合標準的實現需要支援它們。因此,您需要了解它們的存在,以便理解為什麼看似無害的字串會被“誤解”。

**建議:**使用搜索程式檢查現有原始碼中是否出現 `??` 序列。如果它們在超過幾個地方出現,您可能希望專門搜尋三字母組序列。

**建議:**為了保留看起來像三字母組但並非打算成為三字母組的序列,請使用標準 C 轉義序列 `\?` 在文字字串或單字元常量中強制使用文字 `?` 字元。例如,`sizeof("\??(") ` 為 4,與 `sizeof("\?\?(") ` 相同。

**建議:**如果您的實現不支援三字母組,您可以透過現在使用 `\?` 序列來防範它們,因為如果反斜槓不是以識別的轉義序列開頭,則反斜槓應該被忽略。

雖然一些編譯器識別三字母組,但其他實現需要使用獨立工具將包含三字母組的程式碼轉換為不包含三字母組的程式碼。

C95 添加了 *雙字母組* 作為一種機制,允許 *有時不可用* 的原始碼標記具有替代拼寫(參見 原始碼標記)。與三字母組不同,雙字母組是標記,因此它們不能在另一個標記(例如字元常量或字串文字)中 *識別*。

多位元組字元

[編輯 | 編輯原始碼]

C89 引入了 *多位元組字元* 的概念。處理此類字元的某些方面是 *區域設定特定的*。在此之前,一些實現使用雙位元組和其他方法來處理擴充套件字元。

字元顯示語義

[編輯 | 編輯原始碼]

處理標準 C 中某些轉義序列涉及 *區域設定特定* 或 *未指定的行為*。

C89 定義了轉義序列 `\a` 和 `\v`。

某些系統將 `\n` 視為回車和換行,而另一些系統將其視為僅換行。

訊號和中斷

[編輯 | 編輯原始碼]

標準 C 對訊號處理程式可以修改的物件型別施加了一些限制。除了 `signal` 函式之外,標準 C 庫函式不能保證可重入,並且允許它們修改靜態資料物件。

環境限制

[編輯 | 編輯原始碼]

對符合標準的實現有一些環境限制,如下所述。

翻譯限制

[編輯 | 編輯原始碼]

截至 C17,標準 C 要求“實現能夠翻譯和執行至少 *一個* 程式,該程式包含至少 *一個* 例項,以下所有限制:

  • 127 個塊巢狀級別

  • 63 個條件包含巢狀級別

  • 12 個指標、陣列和函式宣告符(以任何組合)在宣告中修改算術、結構、聯合或 void 型別

  • 63 個完全宣告符中括號內宣告符的巢狀級別

  • 63 個完整表示式中括號內表示式的巢狀級別

  • 63 個內部識別符號或宏名稱中的有效初始字元(每個通用字元名稱或擴充套件源字元被視為單個字元)

  • 31 個外部識別符號中的有效初始字元(每個指定為 0000FFFF 或更小的短識別符號的通用字元名稱被視為 6 個字元,每個指定為 00010000 或更大的短識別符號的通用字元名稱被視為 10 個字元,並且每個擴充套件源字元被視為與對應的通用字元名稱相同數量的字元(如果有))

  • 一個翻譯單元中的 4095 個外部識別符號

  • 一個塊中宣告的 511 個具有塊範圍的識別符號

  • 一個預處理翻譯單元中同時定義的 4095 個宏識別符號

  • 一個函式定義中的 127 個引數

  • 一個函式呼叫中的 127 個引數

  • 一個宏定義中的 127 個引數

  • 一個宏呼叫中的 127 個引數

  • 邏輯原始碼行中的 4095 個字元

  • 字串文字中的 4095 個字元(連線後)

  • 物件中的 65535 個位元組(僅在託管環境中)

  • `#include` 檔案的 15 個巢狀級別

  • `switch` 語句的 1023 個情況標籤(不包括任何巢狀 `switch` 語句的情況標籤)

  • 單個結構或聯合中的 1023 個成員

  • 單個列舉中的 1023 個列舉常量

  • 單個 `struct-declaration-list` 中結構或聯合定義的 63 個巢狀級別”

這些數字有些誤導。實際上,標準 C *不* 保證對所有限制組合的任何特定支援。

數值限制

[編輯 | 編輯原始碼]

符合標準的實現必須透過一系列在 <limits.h><float.h> 標頭檔案中定義的宏來記錄這些限制。C99 新增的 <stdint.h> 中還指定了其他限制。

從 C99 開始,可選預定義宏 __STDC_IEC_559__ 的存在表示支援 IEC 60559 浮點標準,如 C 標準附錄中所述。

從 C99 開始,可選預定義宏 __STDC_NO_COMPLEX__ 的不存在表示支援複數型別及其相關的算術運算。此外,可選預定義宏 __STDC_IEC_559_COMPLEX__ 的存在表示複數支援符合 IEC 60559,如 C 標準附錄中所述。

另請參見 <complex.h><fenv.h>

詞法元素

[編輯 | 編輯原始碼]

原始碼標記

[編輯 | 編輯原始碼]

標準 C 要求,當原始碼輸入被解析成標記時,必須形成最長的有效標記序列。對特定構造的含義必須沒有歧義。例如,文字 a+++++b 必須生成語法錯誤,因為找到的標記為 a+++++b,而(字尾)第二個 ++ 運算子的運算元不是左值。請注意,a++ + ++b 是有效的,因為空格會導致標記被解析為 a+++++b。同樣,對於 a+++ ++b 也是如此。

過時:在 C89 之前,一些預處理器允許從其他標記建立標記。例如

#define PASTE(a,b) a/**/b

PASTE(total, cost))

這裡的目的是讓宏擴充套件為單個標記 totalcost 而不是兩個標記 totalcost。它依賴於非標準 C 的方法,即用空字元替換宏定義中的註釋,而不是用單個空格。標準 C 添加了預處理器標記貼上運算子 ##,作為實現所需行為的行動式解決方案。

在 C89 之前,一些預處理器允許在預處理期間建立字串文字標記。標準 C 添加了預處理器字串化運算子 #,作為實現所需行為的行動式解決方案。

建議:避免利用遵循與標準 C 定義的標記化規則不同的標記化規則的預處理器的特性。

關鍵字

[編輯 | 編輯原始碼]

以下標記由標準 C 定義為關鍵字

auto break case char constC89
continue default do double else
enum extern float for goto
if inlineC99 int long register
restrictC99 return short signedC89 sizeof
static struct switch typedef union
unsigned void volatileC89 while _AlignasC11
_AlignofC11 _AtomicC11 _BoolC99 _ComplexC99 _GenericC11
_ImaginaryC99 _NoreturnC11 _Static_assertC11 _Thread_localC11

過時:雖然 enumvoid 在 K&R 中沒有定義,但在 C89 之前,它們得到了各種編譯器的支援。

標準 C 沒有定義或保留以前在 K&R 和一些舊的 C 編譯器中保留的關鍵字 entry

C++ 注意事項:標準 C++ 沒有定義或保留關鍵字 restrict。它也沒有定義那些以下劃線和一個大寫字母開頭的關鍵字。(但是,對於其中一些,它提供了替代拼寫,例如 alignasalignofboolthread_local。)

許多編譯器支援擴充套件關鍵字,其中一些以一個或兩個下劃線開頭,或者使用程式設計師識別符號空間中的名稱。

識別符號

[編輯 | 編輯原始碼]

K&R 和 C89 允許下劃線、英文大小寫字母和十進位制數字。

(較舊的) 環境允許的外部名稱集可能不包括下劃線,並且可能不區分大小寫,在這種情況下,外部名稱中的一些字元可能會被對映到其他字元。

C99 添加了預定義識別符號 __func__。C99 還增加了對識別符號中通用字元名稱的支援(參見 通用字元名稱),以及任意數量的 實現定義的擴充套件字元。

C++ 注意事項:以下標記由標準 C++ 定義為關鍵字

alignas alignof and and_eq asm
bitand bitor bool catch char8_t
char16_t char32_t class compl concept
consteval constexpr constinit const_cast co_await
co_return co_yield decltype delete dynamic_cast
explicit export false friend mutable
namespace new noexcept not not_eq
nullptr operator or or_eq private
protected public reinterpret_cast requires static_assert
static_cast template this thread_local throw
true try typeid typename using
virtual wchar_t xor xor_eq

其中一些名稱在標準 C 中被定義為宏(例如 <stdalign.h> 中的 alignas)。這些將在其他地方討論。

C++ 注意事項:標準 C++ 對以下識別符號賦予特殊含義:finalimportmoduleoverride

建議:如果 C 程式碼有可能透過 C++ 編譯器執行,請避免使用標準 C++ 定義為關鍵字或具有特殊含義的識別符號。

C++ 注意事項:根據標準 C++:“每個包含雙下劃線 __ 或以一個下劃線後跟一個大寫字母開頭的識別符號都為實現保留,用於任何用途”,“每個以一個下劃線開頭的識別符號都為實現保留,用於在全域性名稱空間中使用。”

長度和有效字元限制

[編輯 | 編輯原始碼]

雖然標準 C 對識別符號的長度沒有最大限制,但處理的有效字元數量可能是有限的。具體來說,外部名稱的長度限制可能比內部名稱的長度限制更嚴格(通常是由於連結器方面的考慮)。識別符號中有效字元的數量是 實現定義的。標準 C 要求實現至少區分外部識別符號的前 31 個字元,以及內部識別符號的前 63 個字元。

名稱空間

[編輯 | 編輯原始碼]

K&R 定義了兩個不相交的識別符號類別:與普通變數相關的識別符號,以及結構和聯合成員和標記。

標準 C 添加了幾個新的識別符號名稱空間類別。完整集合是標籤;結構、聯合和列舉標記;結構和聯合的成員(每個結構和聯合都有自己的名稱空間);以及所有其他識別符號,稱為 普通識別符號

標準 C 中可選允許的函式原型識別符號具有自己的名稱空間。它們的範圍從它們的名稱一直到該原型宣告的末尾。因此,可以在不同的原型中使用相同的識別符號,但不能在同一個原型中使用兩次。

K&R 中包含以下語句:“兩個結構體可以共享一個共同的初始成員序列;也就是說,如果同一個成員在兩個不同的結構體中具有相同的型別,並且如果兩個結構體中所有先前的成員都相同,則該成員可以出現在兩個不同的結構體中。(實際上,編譯器只檢查兩個不同結構體中的名稱在兩個結構體中是否具有相同的型別和偏移量,但如果前面的成員不同,則該構造是非可移植的。) ” 標準 C 透過支援每個結構體單獨的成員名稱空間消除了此限制。

通用字元名稱

[edit | edit source]

C99 添加了對通用字元名稱的支援。它們的格式為 \uXXXX 和 \UXXXXXXXX,其中 X 是十六進位制數字。它們可以出現在識別符號、字元常量和字串文字中。

常量

[edit | edit source]

標準 C 要求實現處理常量表達式時使用至少與目標執行環境中可用的精度相同的精度。它可以使用更高的精度。

整數常量

[edit | edit source]

C89 提供了字尾 U(和 u)來支援無符號常量。這些字尾可以與十進位制、八進位制和十六進位制常量一起使用。 long int 常量可以用 L(或 l)作為字尾。

C99 添加了型別 long long int,並且這種型別的常量可以用 ll(或 LL)作為字尾。C99 還添加了型別 unsigned long long int,並且這種型別的常量可以用 ull(或 ULL)作為字尾。

K&R 允許八進位制常量包含數字 8 和 9(它們分別具有八進位制值 10 和 11)。標準 C 不允許這些數字出現在八進位制常量中。

整數常量的型別取決於其大小、基數和可選後綴字元的存在。這會導致問題。例如,考慮一臺 int 為 16 位且使用二進位制補碼錶示的機器。最小的 int 值為 -32768。但是,表示式 -32768 的型別是 long,而不是 int!不存在負整數常量;相反,我們有兩個標記:整數常量 32768 和一元減運算子。由於 32768 太大而無法放入 16 位中,因此它的型別為 long,並且該值被取反。因此,在沒有作用域內的函式原型的情況下進行函式呼叫 f(-32768) 可能會導致引數/引數不匹配。如果您檢視這種機器上的實現中 <limits.h>INT_MIN 的定義,您可能會發現以下內容

#define INT_MIN (-32767 - 1)

這滿足了該宏必須具有型別 int 的要求。

關於基數,在這臺 16 位機器上,0xFFFF 的型別為 unsigned int,而 -32768 的型別為 long

類似的情況發生在 32 位二進位制補碼整數的最小值 -2147483648 上,該值可能具有型別 longlong long 而不是 int,具體取決於型別對映。

建議: 當整數常量的型別很重要時(例如,作為函式呼叫引數和使用 sizeof 運算子),明確地指定整數常量的型別(或將其強制轉換)。

當將零常量傳遞給期望指標的函式(意圖將其表示為“空指標”)但作用域中沒有函式原型時,也會出現類似的問題,例如 f(0)。零的型別是 int,其大小/格式可能與引數的指標型別不匹配。此外,對於指標不像整數的機器,不會進行隱式轉換以彌補這一點。

正確的方法是使用 NULL 庫宏,該宏通常使用以下方法之一定義

#define NULL 0
#define NULL 0L
#define NULL ((void *)0)

過時: 在 C89 之前,不同的編譯器使用不同的規則對整數常量進行型別化。K&R 要求以下內容:“一個十進位制常量,其值超過最大的帶符號機器整數,被認為是 long;一個八進位制或十六進位制常量,其值超過最大的無符號機器整數,也被認為是 long。”

標準 C 要求以下規則對整數常量進行型別化:“整數常量的型別是對應列表中其值可以表示的第一個型別。無後綴十進位制:intlong intunsigned long int;無後綴八進位制或十六進位制:intunsigned intlong intunsigned long int;用字母 U(或 u)作為字尾:unsigned intunsigned long int;用字母 L(或 l)作為字尾:long intunsigned long int;用 U(或 u)和 L(或 l)作為字尾:unsigned long int。” C99 添加了 long longunsigned long long 的步驟。

一些編譯器支援用二進位制(即二進位制)表示的整數常量;其他編譯器允許在所有基數的數字之間使用分隔符(例如下劃線)。這些功能不是標準 C 的一部分。

0 開頭的整數常量被認為是八進位制的。#line 預處理指令具有以下形式

# line digit-sequence new-line

請注意,語法不涉及 integer-constant。相反,digit-sequence 被解釋為十進位制整數,即使它有一個或多個前導零!

浮點常量

[edit | edit source]

浮點常量的預設型別是 double。C89 添加了對型別 long double 的支援,以及用於 float 常量的浮點常量字尾 F(或 f),以及用於 long double 常量的 L(或 l)。

建議: 當浮點常量的型別很重要時(例如,作為函式呼叫引數和使用 sizeof 運算子),明確地指定浮點常量的型別(或將其強制轉換)。

C99 添加了對使用十六進位制表示法編寫浮點常量的支援。

C99 還添加了宏 FLT_EVAL_METHOD(在 <float.h> 中),其值可能允許浮點常量被評估為其範圍和精度大於要求的格式。例如,編譯器有權(靜默地)將 3.14f 視為 3.14 甚至 3.14L

列舉常量

[edit | edit source]

為列舉定義的值的名稱是整數常量,標準 C 將其定義為 int

K&R 沒有包含列舉。

C++ 注意事項: 列舉常量具有其父列舉的型別,該型別是某種整數型別,可以表示在列舉中定義的所有列舉常量值。

字元常量

[edit | edit source]

源字元集中字元到執行字元集中字元的對映是實現定義的

包含執行字元集中未表示的字元或轉義序列的字元常量的值是實現定義的

字元常量或字串文字中未指定的轉義序列(除了反斜槓後跟小寫字母)的含義是實現定義的。請注意,具有小寫字母的未指定序列保留供標準 C 將來使用。這意味著符合標準的實現可以自由地為 '\E'(例如,用於 ASCII 跳脫字元)提供語義,但它不應為 '\e' 提供語義。

建議: 避免在字元常量中使用非標準轉義序列。

包含多個字元的字元常量的值是實現定義的。在 32 位機器上,可以使用 int i = 'abcd'; 將四個字元打包到一個字中。在 16 位機器上,可能會允許類似 int i = 'ab'; 的操作。

建議: 避免使用多字元常量,因為它們的內部表示是實現定義的。

標準 C 支援十六進位制形式字元常量的早期流行擴充套件。這通常具有 '\xh''\xhh' 的形式,其中 h 是十六進位制數字。

K&R 宣告,如果反斜槓後的字元不是指定的字元之一,則反斜槓將被忽略。標準 C 說行為是未定義的

與一些舊的實現不同,由於標準 C 不允許在八進位制常量中使用數字 8 和 9,因此以前支援的字元(如 '\078')將具有新的含義。

為了避免與三元組(其形式為 ??x)混淆,C89 定義了字元常量 '\?'。現有的 '\?' 形式的常量現在將具有不同的含義。

建議: 由於字元集不同,請使用字元的圖形表示而不是其內部表示。例如,在 ASCII 環境中,請使用 'A' 而不是 '\101'

一些實現可能允許 '' 表示空字元——標準 C 不允許。

K&R 沒有定義常量 '\"',儘管它在字串文字中顯然是必需的。在標準 C 中,字元 '"''\"' 是等效的。

C89 添加了 寬字元常量 的概念,它與字元常量寫法相同,但以 L 開頭。

C99 添加了對字元常量中通用字元名稱的支援。請參閱通用字元名稱

C11 添加了對以 u(或 U)為字首的寬字元常量的支援。

標準 C 要求整數字符常量具有型別 int

C++ 注意事項: 標準 C++ 要求整數字符常量具有型別 char

字串文字

[edit | edit source]

標準 C 允許具有相同拼寫形式的字串文字共享,但不要求這樣做。

在某些系統中,字串字面量儲存在讀寫記憶體中,而在其他系統中,儲存在只讀記憶體中。標準 C 規定,如果程式試圖修改字串字面量,其行為是未定義的

建議:即使您的實現允許這樣做,也不要修改字面量字串,因為這與程式設計師的直覺相悖。此外,不要依賴於類似字串的共享。如果您有修改字串字面量的程式碼,請將其更改為初始化為該字串的字元陣列,然後修改該陣列。這樣做不僅不需要修改字面量,而且允許您透過在其他地方使用同一個陣列來顯式共享類似字串。

建議:通常會編寫類似以下內容:char *pMessage = "some text";。假設您使用的是 C89 或更高版本的編譯器,請改用 const char * 宣告指標,這樣任何嘗試修改底層字串的操作都會被診斷出來。

C++ 注意事項:標準 C++ 要求字串字面量隱式地具有 const 限定符,這意味著以下常用的 C 習語在 C++ 中無效

char *message = "…";

這必須改為以下方式

const char *message = "…";

字面量字串的最大長度是實現定義的,但標準 C 要求它至少為 509 個字元。

與某些舊實現不同,由於標準 C 不允許八進位制常量中出現數字 8 和 9,因此以前支援的字串字面量(如 "\078")將具有新的含義。

K&R 和標準 C 允許使用反斜槓/換行符約定將字串字面量跨多行原始碼繼續,如下所示

static char text[] = "a string \
of text";

但是,這要求續行必須正好在第一列開始。另一種方法是使用 C89(以及在此之前的一些編譯器提供的)字串連線功能,如下所示

static char text[] = "a string "
"of text";

C89 添加了寬字串字面量的概念,它與字串字面量寫法相同,但前面帶有 L(例如,L"abc")。

C99 在字串字面量中添加了對通用字元名的支援。參見 通用字元名

C11 添加了對帶有字首 u(或 U)的寬字元字串字面量的支援,以及對透過字首 u8 實現的UTF-8 字串字面量的支援。

標點符號

[edit | edit source]

過時:在 K&R 之前,複合賦值運算子寫成 =op。但是,K&R 和標準 C 將它們寫成 op=。例如,s =* 10 變成了 s *= 10

C89 添加了省略號標點符號 ...,作為增強函式宣告和定義語法的組成部分。它還添加了標點符號 ###,它們代表預處理器專用運算子。

C95 添加了以下二進位制標點符號:<::><%%>%:%:%:

標頭檔案名稱

[edit | edit source]

標準 C 定義了標頭檔案名稱的語法。如果字元 '\"/* 出現在 <…> 形式的 #include 指令中,其行為是未定義的。當使用 "…" 形式的 #include 指令時,'\/* 也是如此。

在標準 C 中,當使用 #include "…" 形式時,文字 "…" 被視為字串字面量。在使用分層檔案系統的環境中,需要使用 \ 來表示不同的資料夾/目錄級別,這個反斜槓不是轉義序列的開始,因此本身不需要轉義。

註釋

[edit | edit source]

C99 添加了對以 // 開頭的行註釋的支援。在 C99 之前,一些實現支援它作為擴充套件。

K&R 和標準 C 都不支援巢狀註釋,儘管許多現有實現支援它。對巢狀註釋的需求主要在於允許包含註釋的程式碼塊被停用,如下所示

/*
int i = 10; /* ... */
*/

可以使用以下方法達到相同的效果

#if 0
int i = 10; /* ... */
#endif

標準 C 要求在標記化期間,註釋應被替換為一個空格。一些實現將它們替換為空,因此允許一些巧妙的標記貼上。有關示例,請參見 原始碼標記

轉換

[edit | edit source]

算術運算子

[edit | edit source]

布林值、字元和整數

[edit | edit source]

普通 char 是否被視為有符號或無符號是實現定義的

在 C89 開發期間,目前使用著兩種不同的算術轉換規則集:無符號保留 (UP) 和值保留 (VP)。在 UP 中,如果表示式中存在兩個較小的無符號型別(例如,unsigned charunsigned short),它們將被擴充套件為 unsigned int。也就是說,擴充套件後的值也是無符號的。VP 方法將這些值擴充套件為 signed int(前提是它們可以容納),否則將它們擴充套件為 unsigned int

雖然這兩種方法在幾乎所有情況下都產生相同的結果,但在以下情況下可能會出現問題。這裡,我們有一個二元運算子,一個運算元的型別為 unsigned short(或 unsigned char),另一個運算元的型別為 int(或某些更窄的型別)。假設程式正在 16 位補碼機器上執行。

#include <stdio.h>

int main()
{
    unsigned char uc = 10;
    int i = 32767;
    int j;
    unsigned int uj;

    j = uc + i;
    uj = uc + i;

    printf("j = %d (%x), uj = %u (%x)\n", j, j, uj, uj);
    printf("expr shifted right = %x\n", (uc + i) >> 4);
    return 0;
}

使用 UP 規則,uc 將被提升為 unsigned inti 也會被提升為 unsigned int,從而導致 uc + i 的結果為 unsigned int。使用 VP 規則,uc 將被提升為 inti 的型別),兩者相加後的結果將為 int 型別。這本身並不構成問題,但如果 (uc + i) 被用作右移運算子的物件(如所示),或用作 /%<<=>>= 的運算元,則可能會產生不同的結果。例如

UP 規則產生

j = -32759 (8009), uj = 32777 (8009)
expr shifted right = 800

VP 規則產生

j = -32759 (8009), uj = 32777 (8009)
expr shifted right = f800

如果表示式為無符號型別,UP 規則將使用零位替換高位位,而如果物件為有符號型別,則結果是實現定義的(由於算術移位和邏輯移位可能性)。在上面的第二個輸出示例中,VP 在移位期間產生了符號位傳播,產生了完全不同的結果。

請注意,上述示例只會引起對 uci 的某些值的關注,並非所有情況下都如此。例如,如果 uc10i30,000,則輸出將為

UP 規則產生

j = 30010 (753a), uj = 30010 (753a)
expr shifted right = 753

VP 規則產生

j = 30010 (753a), uj = 30010 (753a)
expr shifted right = 753

在這種情況下,(uc + i) 的高位(符號位)未設定,因此 UP 和 VP 都會產生相同的結果。

可以在這種混合模式算術中使用強制轉換,以確保無論使用何種規則都能獲得所需的結果。例如,

#include <stdio.h>

int main()
{
    unsigned char uc = 10;
    int i = 32767;
    int expr1, expr2, expr3;

    expr1 = ((int) uc + i) >> 4;
    expr2 = (uc + (unsigned) i) >> 4;
    expr3 = (uc + i) >> 4;

    printf("expr1 = %x\n", expr1);
    printf("expr2 = %x\n", expr2);
    printf("expr3 = %x\n", expr3);
    return 0;
}

UP 規則產生

expr1 = f800
expr2 = 800
expr3 = 800

VP 規則產生

expr1 = f800
expr2 = 800
expr3 = f800

如上所示,即使在沒有顯式強制轉換的情況下結果不同,包含顯式強制轉換的兩個表示式的結果相同。

儘管標準 C 使用 VP 規則,但在 C89 之前的一些廣泛使用的編譯器使用 UP 規則。依賴於 UP 規則的程式碼現在可能會產生不同的結果。具體來說,charshortint 位域(所有這些都是有符號或無符號的)或列舉型別可以在任何可以使用 int 的地方使用。如果 int 可以表示原始型別的所有值,則該值將轉換為 int;否則,它將轉換為 unsigned int

請注意,在標準 C 中,“正常”整數擴充套件規則也適用於位域,位域可以是有符號的,也可以是無符號的。

C99 添加了 _Bool 型別。C99 還允許新增擴充套件整數型別。

浮點數和整數

[edit | edit source]

浮點型別

[edit | edit source]

標準 C 規定,“當實浮點型別的有限值轉換為 _Bool 以外的整數型別時,小數部分將被丟棄(即,該值將向零截斷)。如果整數部分的值不能用整數型別表示,則行為是未定義的。”

標準 C 規定,“當整數型別的值轉換為實浮點型別時,如果被轉換的值可以在新型別中精確表示,則它將保持不變。如果被轉換的值在可以表示的範圍之內,但不能精確表示,則結果是可表示的最近較高值或最近較低值,以實現定義的方式選擇。如果被轉換的值超出了可表示的範圍,則行為是未定義的。”

標準 C 要求,當 double 被截斷為 float,或 long double 被截斷為 doublefloat 時,如果被轉換的值不能表示,則行為是未定義的。如果該值在範圍內,但不能精確表示,則截斷後的結果是兩個最近的可表示值之一——哪一個被選擇是實現定義的

請注意,透過使用函式原型,實現可以允許將 float 按值傳遞給函式,而無需先將其擴充套件為 double。但是,即使標準 C 允許這種窄型別保留,它也不是必需的。

複數型別

[edit | edit source]

C99 添加了 _Complex 型別及其相應的轉換規則,以及標頭檔案 <complex.h>

通常的算術轉換

[編輯 | 編輯原始碼]

這些在標準 C 中被更改以適應 布林值、字元和整數 中描述的 VP 規則。表示式也可以在比實際需要的“更寬”模式下進行評估,以允許更有效地使用硬體。表示式也可以在“更窄”型別中進行評估,前提是它們給出的結果與在“更寬”模式下進行評估時相同。

如果二元運算子的運算元具有不同的算術型別,則會導致一個或兩個運算元的提升。標準 C 定義的轉換規則類似於 K&R 定義的規則,不同之處在於適應了 VP 規則,添加了一些新的型別,並且允許在不擴充套件的情況下使用窄型別算術。

其他運算元

[編輯 | 編輯原始碼]

C89 引入了指向 void 的指標的概念,寫成 void *。這樣的指標可以轉換為指向任何型別物件的指標,而無需使用強制型別轉換。物件指標可以轉換為指向 void 的指標,然後再轉換回來,不會丟失資訊。

C++ 注意事項:標準 C++ 在將 void 指標賦值給指向物件型別的指標時需要強制型別轉換。

物件指標不必都具有相同的大小。例如,指向 char 的指標不必與指向 int 的指標大小相同。

雖然可以將指向一種型別物件的指標轉換為指向不同型別物件的指標,但如果生成的指標未針對指向的型別正確對齊,則行為是 未定義的

雖然 int 和資料指標通常佔用相同大小的儲存空間,但兩種型別完全不同,並且關於兩種型別之間的互換,沒有任何可移植的內容可以說明,除了可以將零分配給指標或與指標進行比較。請注意,此空指標概念不要求空指標值為“全零位”,儘管它可能以這種方式實現。標準 C 唯一要求的是 (void *) 0 代表一個地址,該地址永遠不會等於物件或函式的地址。在表示式 p == 0 中,零在與 p 進行比較之前會提升為 p 的型別。

將整數轉換為任何指標型別會導致 實現定義的 行為。同樣,對於另一個方向的轉換,但如果結果不能在整數型別中表示,則行為是 未定義的

函式指標與資料指標完全不同,並且不應對兩者的大小進行任何假設。函式指標的格式和大小可能與資料指標的格式和大小大不相同。

標準 C 在將指向返回一種型別的函式的指標賦值給指向返回不同型別的函式的指標時,要求顯式強制型別轉換。標準 C 在複製函式指標方面更加嚴格,因為存在函式原型。現在,指向函式的指標的屬性不僅包括該函式的返回型別,還包括其引數列表。雖然可以將指向一種型別函式的指標轉換為指向另一種型別函式的指標,然後再轉換回來,但如果使用轉換後的指標呼叫型別與引用型別不相容的函式,則行為是 未定義的

表示式

[編輯 | 編輯原始碼]

求值順序和順序點

[編輯 | 編輯原始碼]

根據標準 C,表示式求值的順序是 未指定的,除了函式呼叫運算子 ()、邏輯或運算子 ||、邏輯與運算子 &&、逗號運算子和條件運算子 ?:。雖然優先順序表定義了運算子的優先順序和結合性,但這些可以透過分組括號來覆蓋。但是,根據 K&R,可交換和結合二元運算子 (*+&|^) 可以任意重新排列,即使存在分組括號。(請注意,對於 &|^,順序無關緊要,因為始終會獲得相同的結果。)但是,標準 C 要求在所有表示式中尊重分組括號。

使用 K&R (但不是標準 C) 規則,即使您可能寫下以下內容

i = a + (b + c);

表示式可以評估為

i = (a + b) + c;

甚至

i = (a + c) + b;

如果表示式以一種方式而不是另一種方式進行評估,這會導致中間值的溢位。為了強制執行特定的求值順序,將表示式分解為多個語句並使用中間變數,如下所示

i = (b + c);
i += a;

這些示例僅在整數型別的“邊界”條件下才會出現問題,即使那樣,也只在某些機器上才會出現問題。例如,二進位制補碼機器上的整數算術通常是“行為良好的”。(但是,某些機器在發生整數溢位時會引發中斷,並且可以推測應該避免這種情況。)

建議:如果您擔心與交換和結合相關的表示式的求值順序,請將它們分解為單獨的表示式,以便您可以控制順序。瞭解目標系統的整數算術溢位屬性,並檢視它們是否會影響此類表示式。

對於浮點運算元,溢位和精度丟失錯誤的可能性要高得多,因為在有限的空間內無法準確地表示某些實數。一些在使用有限表示時並不總是成立的數學定律是

(x + y) + z == x + (y + z)
(x * y) * z == x * (y * z)
(x / y) * y == x /* for non-zero y */
(x + y) - y == x

當表示式涉及副作用時,求值順序可能很重要。例如,

void test()
{
    int i, f(void), g(void);

    i = f() + g();
}

這裡,f() 可以在 g() 之前或之後進行評估。雖然 i 的值在任一情況下都可能相同,但如果 f()g() 產生副作用,則可能不為真。

副作用發生的順序是 未指定的。例如,以下是具有不可預測結果的表示式

j = (i + 1)/i++
dest[i] = source[i++]
dest[i++] = source[i]
i & i++
i++ | i
i * --i

在上面的每一行中,包含 i 的哪個表示式先進行評估是 未指定的

建議:即使您可以確定編譯器如何評估包含副作用的表示式,也不要依賴於這種行為在相同產品的未來版本中仍然成立。即使對於相同的編譯器,在不同的情況下,它也可能有所不同。例如,透過更改原始碼中的其他可能不相關的部分,您可能會改變最佳化器對世界觀的看法,從而導致它為相同的表示式生成不同的程式碼。編譯器編寫者沒有義務以任何方式支援可預測的行為,因為允許行為是未定義的。

C89 引入了完整表示式和順序點的概念,如下所示:“完整表示式 是一個表示式,它不是另一個表示式的一部分,也不是宣告符或抽象宣告符的一部分。在為可變修改型別評估非常量大小表示式時,還有一個隱式完整表示式;在該完整表示式中,不同大小表示式的評估彼此之間是無序的。在完整表示式的評估和下一個要評估的完整表示式的評估之間有一個順序點。”

建議:確保您可以識別程式碼中的所有順序點。

對有符號型別進行按位運算 (使用 ~<<>>&^|) 的結果本質上是 實現定義的

建議:由於按位運算的結果取決於整數型別的表示形式,因此您應該確定移位和按位掩碼運算的性質,特別是對於有符號型別。

浮點算術的屬性是 實現定義的。還要記住,軟體模擬獲得的結果與硬體執行獲得的結果之間可能存在差異。此外,機器可能具有多種不同的浮點格式,其中任何一種都可能透過編譯時開關進行選擇。

建議:在表示式中使用浮點資料型別時,確定每種此類型別的大小、範圍和表示形式。此外,確定軟體中的浮點模擬與浮點硬體產生的結果之間是否存在差異。檢視您是否可以確定浮點硬體在執行時是否可用。

關於浮點表示式求值,C99 添加了以下內容:“浮點表示式可以收縮,即,評估為好像它是一個單一操作,從而省略原始碼和表示式求值方法所暗示的舍入誤差。<math.h> 中的 FP_CONTRACT 編譯指示提供了一種禁止收縮表示式的方法。否則,是否收縮表示式以及如何收縮表示式是 實現定義的。”

如果算術運算無效 (例如,除以零) 或產生無法在提供的空間中表示的結果 (例如,溢位或下溢),則結果是 未定義的

主表示式

[編輯 | 編輯原始碼]

帶括號的表示式是基本表示式。C89 要求支援至少 32 個巢狀級別的帶括號表示式,這些表示式位於一個完整的表示式中。C99 將此提高到 63 個。

通用選擇操作是基本表示式。此運算子由 C11 引入,並涉及關鍵字 _Generic

字尾運算子

[編輯 | 編輯原始碼]

陣列下標

[編輯 | 編輯原始碼]

陣列引用的格式為 a[b],其中 ab 是表示式。其中一個表示式必須具有指向某種型別的指標型別(不包括 void),而另一個表示式必須具有整型型別。K&R 和標準 C 都不要求 a 是指標表示式,而 b 是整數表示式,即使這幾乎總是編寫下標表達式的寫法。具體來說,a[b] 也可以寫成 b[a],這對許多人來說可能令人驚訝,包括 C 老手。

C 不要求下標中的整型表示式具有無符號值 - 它可以是有符號的。例如:

#include <stdio.h>

int main()
{
    int i[] = {0, 1, 2, 3, 4};
    int *pi = &i[2];
    int j;

    for (j = -2; j <= 2; ++j)
    {
        printf("x[%2d] = %d\n", j, pi[j]);
    }
    return 0;
}

x[-2] = 0
x[-1] = 1
x[ 0] = 2
x[ 1] = 3
x[ 2] = 4

建議:對於任何定義為陣列的給定物件 A,永遠不要使用除 0 到 n-1 之外的值對 A 進行下標,其中 n 是定義為位於 A 中的最大元素數。

建議:使用負下標和指標表示式是可以的,前提是表示式對映到可預測的位置。

以下示例演示了讓陣列從任意下標開始的技術。(請注意,此技術不受標準 C 支援,並且可能無法在某些實現上執行 - 在分段記憶體體系結構上執行的那些實現可能會導致它失敗,因為並非所有指標運算都以“迴圈”的方式執行。)

#include <stdio.h>

int main()
{
    int k[] = {1, 2, 3, 4, 5};
    int *p4 = &k[-1];
    int *yr = &k[-1983];

    printf("array p4 = %d %d %d %d %d\n",
        p4[1], p4[2], p4[3], p4[4], p4[5]);
    printf("array yr = %d %d %d %d %d\n",
        yr[1983], yr[1984], yr[1985], yr[1986], yr[1987]);
    return 0;
}

array p4 = 1 2 3 4 5
array yr = 1 2 3 4 5

透過使 p4 指向 &k[-1]p4 具有下標 1 到 5。我們從未嘗試訪問元素 k[-1],因此為元素 k[-1] 分配了多少空間無關緊要。我們所做的只是建立了一個指向 k[-1] 所在位置的指標表示式(如果存在)。然後,當我們有表示式 p4[1] 時,它等於 *(p4 + 1)*(&k[-1] + 1),它給出 *(&*(k - 1) + 1)*(k - 1 + 1),最後給出 *k,這與 k[0] 相同。也就是說,p4[1]k[0] 可以互換,p4[1]p4[5] 對映到陣列 k 中。

指標 yr 的使用將同樣的想法推得更遠,並允許 yr 像陣列一樣使用,其下標範圍從 1983 到 1987。同樣的想法可以使陣列具有下標 -1004 到 -1000,只需將指標初始化為 &k[1004] 即可。

這在某些具有線性地址空間的“行為良好”的機器上有效。在這裡,地址運算為無符號,因此將地址 6 減去 10 將得到一個大的無符號地址,而不是 -4。也就是說,地址運算在高階和低端“迴圈”。雖然這可能不是每臺機器都一樣,但它肯定在許多常見的機器上有效。

標準 C 指出,如果指標上的算術運算的結果指向陣列內部或指向超出陣列末尾的(不存在)元素,則該運算結果是可以的。否則,行為將是 未定義的;也就是說,p - i + i 不一定等於 p

可以透過僅知道維數來便攜地計算陣列中每個維的大小。

#include <stdio.h>

int main()
{
    int i[2][3][4];
    unsigned int dim1, dim2, dim3;

    dim3 = sizeof(i[0][0])/sizeof(i[0][0][0]);
    printf("dim3 = %u\n", dim3);

    dim2 = sizeof(i[0])/(dim3 * sizeof(i[0][0][0]));
    printf("dim2 = %u\n", dim2);

    dim1 = sizeof(i)/(dim2 * dim3 * sizeof(i[0][0][0]));
    printf("dim1 = %u\n", dim1);
    return 0;
}

dim3 = 4
dim2 = 3
dim1 = 2

i[0][0] 是一個包含四個元素的陣列,因此 sizeof(i[0][0]) 除以 sizeof(i[0][0][0]) 為 4。請注意,i[0][0] 的型別不是 int *,而是 int (*p)[4]。也就是說,p 是指向包含四個 int 的陣列的指標,而 sizeof(*p)4 * sizeof(int)

類似地,i[0] 是一個包含三個元素的陣列,每個元素都是一個包含四個 int 的陣列。最後,i 是一個包含兩個元素的陣列,每個元素都是一個包含三個元素的陣列,每個元素都是一個包含四個 int 的陣列。

函式呼叫

[編輯 | 編輯原始碼]

C99 要求每個對該函式的呼叫都必須在作用域內具有函式宣告。

如果函式呼叫在作用域內沒有函式原型宣告,並且在進行預設轉換後,引數的數量或型別與形式引數不匹配,則行為是 未定義的

如果呼叫接受可變數量引數的函式,並且在作用域內沒有帶省略號表示法的原型宣告,則行為是 未定義的

建議:當在函式中使用可變長度引數列表時,請對其進行全面記錄,並根據需要使用 stdarg(或 varargs)標頭檔案。在呼叫這些函式之前,始終使用具有適當省略號表示法的原型對其進行宣告。

函式引數的求值順序未指定。例如:

f(i, i++);

包含不安全的引數列表;i++ 可能會在 i 之前求值。

現在考慮該示例的擴充套件,它涉及函式指標陣列

(*table[i])(i, i++);

不僅引數的求值順序未指定,指定函式的表示式的呼叫順序也未指定。具體來說,我們無法確定使用了陣列的哪個元素!標準 C 保證 的是,在函式呼叫點有一個順序點;也就是說,在所有三個表示式都已求值後。

建議:永遠不要依賴函式呼叫中引數的求值順序或指定被呼叫函式的表示式的求值順序。

沒有顯式宣告的函式將被視為以類 extern 宣告並返回型別 int

建議:在移植時,請注意目標環境中的標頭檔案是否包含必要的函式宣告;否則,函式呼叫將被解釋為返回 int,而如果函式原型在作用域內,則不會返回 int。例如,標準 C 在 stdlib.h 中宣告 atofatoi(以及 malloc 家族)。

在移植使用整型常量作為函式引數的程式碼時,可能會出現問題。例如:

/* no prototype for g is in scope of the following call */

g(60000);

void g(int i) { /* … */ }

此程式在具有 32 位 int 的機器上可以正常執行。但在 16 位機器上,傳遞給 g 的實際引數將是 long int,而 g 將期望 int,這兩個型別完全不同。

建議:在將整型常量作為函式引數傳遞時要小心,因為這種常量的型別取決於其大小和當前實現的限制。如果該常量隱藏在宏(例如 NULL)中,則這種問題可能難以找到。使用強制轉換確保引數型別匹配,或者在存在原型的情況下呼叫函式。

標準 C 允許結構和聯合按值傳遞。但是,可以按值傳遞的最大結構或聯合大小是 實現定義的

C89 要求實現允許函式呼叫中至少有 31 個引數。C99 將此提高到 127 個。K&R 沒有設定最小限制。

標準 C 允許使用指向函式的指標使用 (*pfunct)()pfunct() 呼叫函式。後一種格式使呼叫看起來像正常的函式呼叫,儘管它可能會導致不太複雜的源交叉引用實用程式將 pfunct 假設為函式名,而不是函式指標。

建議:透過指標呼叫函式時,使用格式 (*fp)() 而不是 fp(),因為後者是標準 C 的發明。

建議:函式原型可用於更改呼叫函式時使用的引數擴充套件和傳遞機制。確保在所有呼叫 以及定義中,作用域內都具有相同的原型。

建議:標準 C 要求嚴格符合的程式在呼叫具有可變數量引數的函式時,始終在作用域內具有原型(帶尾隨 ...)。因此,在使用 printfscanf 函式系列時,始終 #include <stdio.h>。如果不這樣做,則行為將是 未定義的

雖然 C 支援遞迴,但它沒有指定在耗盡堆疊(或其他)資源之前,任何函式可以遞迴多少層。

結構和聯合成員

[編輯 | 編輯原始碼]

由於在 C89 中添加了按值傳遞和返回結構(和聯合)以及結構(和聯合)賦值,因此結構(和聯合)表示式可以存在。

K&R 指出,在 x->y 中,x 可以是指向結構(或聯合)的指標,也可以是絕對機器地址。標準 C 要求每個結構和聯合都有自己的成員名稱空間。這要求 .-> 運算子的第一個運算元分別具有結構(或聯合)型別或指向結構(或聯合)的指標型別。

在某些機器上,硬體 I/O 頁面被對映到物理記憶體中,因此裝置暫存器對於可以對映到該區域的任何任務而言,看起來就像普通的記憶體一樣。要從特定物理地址訪問偏移量(例如,名為 status 的結構成員),以前可以使用以下形式的表示式:

0xFF010->status

由於現在每個結構和聯合都有自己的成員名稱空間,因此無法再以這種方式訪問 status 成員。相反,必須將物理地址轉換為結構指標,這樣偏移量引用就是明確的,如下所示

((struct tag1 *) 0xFF010)->status
((union tag2 *) 0xFF010)->status

當使用除用於儲存立即前一個值的成員以外的成員訪問聯合時,結果是 實現定義的。除非聯合包含多個結構,並且每個結構具有相同的初始成員序列,否則不能對聯合中成員的重疊程度進行任何假設。在這種特殊情況下,可以在聯合當前包含其中一個結構的情況下檢查任何結構中公共序列中的成員。例如:

struct rectype1 {
    int rectype;
    int var1a;
};

struct rectype2 {
    int rectype;
    float var2a;
};

union record {
    struct rectype1 rt1;
    struct rectype2 rt2;
} inrec;

如果聯合當前包含型別為 rectype1rectype2 的結構,則可以透過檢查 inrec.rt1.rectypeinrec.rt2.rectype 來可靠地確定正在儲存的特定型別。這兩個成員都保證對映到同一個區域。

標準 C 規定,“訪問原子結構或聯合物件成員會導致 未定義的行為”。

字尾自增和自減運算子

[編輯 | 編輯原始碼]

一些(非常老的)實現認為字尾自增和自減運算子表示式的結果是可修改的左值。這在標準 C 中不被認可。因此,(i++)++ 應該會產生錯誤。

複合字面量

[編輯 | 編輯原始碼]

C99 添加了對複合字面量的支援。

C++ 注意事項:標準 C++ 不支援複合字面量。

一元運算子

[編輯 | 編輯原始碼]

字首自增和自減運算子

[編輯 | 編輯原始碼]

一些(非常老的)實現認為字首自增和自減運算子表示式的結果是可修改的左值。這在標準 C 中不被認可。因此,++(++i) 應該會產生錯誤。

地址和間接運算子

[編輯 | 編輯原始碼]

如果出現無效的陣列引用(下標“越界”)、空指標解引用或解引用已在終止的塊中以自動儲存持續時間宣告的物件,或者訪問已釋放的已分配空間,則行為是 未定義的。請注意,根據空指標的實現方式,解引用它可能會導致災難性的結果。例如,在一個實現中,嘗試訪問影像前 512 位元組內的某個位置會導致致命的“訪問衝突”。

在標準 C 中,使用 & 運算子與函式名稱一起使用是多餘的。

當按值傳遞結構和聯合體被新增到語言中時,使用 & 與結構或聯合體名稱一起使用不再是多餘的——它的缺失表示“值”,它的存在表示“指向”。

一些實現接受 &位域 並返回包含位域的物件的地址。這在 K&R 中不允許,也不受標準 C 支援。

一些實現允許 &暫存器變數,在這種情況下,register 類別將被忽略。這在 K&R 中不允許,也不受標準 C 支援。

一些實現允許您在特殊情況下(例如在函式引數列表中)獲取常量表達式的地址。這在 K&R 中不允許,也不受標準 C 支援。

解引用指標可能會導致致命的執行時錯誤,如果指標是從其他一些指標型別轉換而來,並且違反了對齊標準。例如,考慮一臺要求所有標量物件(除了 char 之外)都對齊在字(int)邊界上的 16 位機器。因此,如果您將包含奇數地址的 char 指標轉換為 int 指標,並且您解引用了 int 指標,則會導致致命的“奇數地址”陷阱錯誤。

一元算術運算子

[編輯 | 編輯原始碼]

一元加號運算子是 C89 的發明。

請仔細注意,當使用二進位制補碼錶示負整數時,對 INT_MIN 求反會靜默地導致相同的值 INT_MIN;該值根本沒有正的等效值!(同樣,對於 LONG_MINLLONG_MIN 也是如此。)

sizeof 運算子

[編輯 | 編輯原始碼]

在 C99 之前,sizeof 的結果是一個編譯時常量。但是,從 C99 開始,如果運算元是可變長度陣列,則運算元將在執行時進行評估。

sizeof 生成的結果的型別是什麼?似乎可以使用 sizeof 來查詢實現支援的最大物件的大小,這可能是一個非常大的 char 陣列,或者可能是一個具有非常大量的大結構的陣列,例如。當然,似乎 sizeof 生成一個無符號整數結果是合理的,但哪一個呢?

在非常早期的實現中,sizeof 的型別為 int(帶符號)。C89 指出,“它的型別(一個無符號整型)為 size_t,在 <stddef.h> 標頭檔案中定義。”(有關更多資訊,請參閱 常見定義。)

那麼如何使用 printf 來顯示結果呢?考慮以下情況,其中 type 是某種任意資料型別

/*1*/ printf("sizeof(''type'') = %u\n", (unsigned int)sizeof(''type''));
/*2*/ printf("sizeof(''type'') = %lu\n", (unsigned long)sizeof(''type''));
/*3*/ printf("sizeof(''type'') = %llu\n", (unsigned long long)sizeof(''type''));
/*4*/ printf("sizeof(''type'') = %zu\n", sizeof(''type''));

情況 1 在最大為 UINT_MAX(值為 65535)的大小情況下是可移植的;情況 2 在最大為 ULONG_MAX(值為 4294967295)的大小情況下是可移植的;情況 3 在最大為 ULLONG_MAX(值為 18446744073709551615)的大小情況下是可移植的;情況 4 是最大程度地可移植的,前提是您的實現支援長度修飾符 z(在 C99 中引入)。

建議:在呼叫期望 size_t 型別引數的函式時,始終使用原型,這樣您提供給函式的引數就可以在必要時透過原型進行隱式轉換。但是,由於 printf 的函式原型在尾隨引數中包含省略號,因此無法指定隱式轉換。

_Alignof 運算子

[編輯 | 編輯原始碼]

這是由 C11 新增的,C11 指出,“它的型別(一個無符號整型)為 size_t,在 <stddef.h> 標頭檔案中定義。”

標頭檔案 <stdalign.h>) 包含一個名為 alignof 的宏,它擴充套件到 _Alignof

C++ 注意事項:在 C++11 中新增的等效(但不同)關鍵字是 alignof,標準 C 在 <stdalign.h> 中將其定義為宏。

強制轉換運算子

[編輯 | 編輯原始碼]

將指標轉換為整數或反之(除了值零)的結果是 實現定義的,就像將一種指標型別轉換為對齊方式更嚴格的指標型別的結果一樣。

有關允許在不同資料指標和不同函式指標之間進行的轉換的詳細討論,請參閱 指標

C11 添加了限制,即指標型別不能轉換為任何浮點型別,反之亦然。

可能需要顯式強制轉換才能獲得正確的結果,因為“無符號保留”與“值保留”轉換規則(請參閱 布林值、字元和整數)。

許多標準 C 庫函式返回 void * 值,這反映在它們相應的原型中。由於 void * 與所有其他資料指標型別相容,因此您不需要顯式強制轉換返回的值。

C++ 注意事項:void * 轉換為資料指標型別需要顯式強制轉換。

使用一系列複雜的強制轉換,可以編寫一個“相當”可移植的表示式,該表示式將生成結構的特定成員的偏移量(以位元組為單位)。但是,這可能在某些實現上不起作用,特別是那些在字架構上執行的實現。例如

#define OFFSET(struct_type, member) \
((size_t)(char *) &((struct_type *)0)->member)

建議:標準 C 提供宏 offsetof(在 <stddef.h> 中)來可移植地查詢結構中成員的偏移量。在可能的情況下,應使用此宏,而不是任何自制機制。

不要假設將零強制轉換為指標型別會生成一個全位為零的值。但是,值為 0 的指標(透過賦值或強制轉換產生)必須與零相等。

乘法運算子

[編輯 | 編輯原始碼]

根據標準 C,整數和浮點除法會導致 未定義的行為。C89 在整數除法情況下引入了一些 實現定義的 行為,但在 C99 中已將其刪除。

加法運算子

[edit | edit source]

如果對一個不指向陣列物件成員的指標(或指向陣列末尾不存在的元素)進行加減運算,結果是未定義的。標準 C 允許從指向陣列最後一個元素之後的元素的指標中減去一個整數,前提是結果地址對映到同一個陣列中。

儲存同一個陣列中的兩個成員的指標之間的差值所需的整數長度是實現定義的。標準 C 提供型別同義詞 ptrdiff_t 來表示這種值的型別。這個帶符號整數型別在 <stddef.h> 中定義。

位移運算子

[edit | edit source]

負數或大於或等於被移位表示式的位寬的移位量進行移位的結果是未定義的

如果左運算元有符號並且具有非負值,並且左運算元 × 2右運算元 在結果型別中不可表示,則行為是未定義的

如果左運算元有符號並且具有負值,則結果值是實現定義的

無符號保留和值保留的擴充套件規則會導致 >> 運算子產生不同的結果。使用 UP 規則,(unsigned short + int) >> 1 等同於除以 2,而在 VP 中則不然,因為要移位的表示式的型別是有符號的。

關係運算符

[edit | edit source]

如果比較的指標不指向同一個聚合體,結果是未定義的。“同一個聚合體”是指同一個結構體中的成員或同一個陣列中的元素。儘管如此,標準 C 仍然認可普遍做法,即允許指標增加到一個物件之外的一個位置。

無符號保留和值保留的擴充套件規則會導致 >>=<<= 運算子產生不同的結果。

相等運算子

[edit | edit source]

指標可以與 0 進行比較。但是,當非零整數值與指標進行比較時,行為是實現定義的

結構體和聯合體不能進行比較,除非按成員比較。根據是否存在空洞以及空洞的內容,可以使用庫函式 memcmp 來比較結構體是否相等。

在對浮點運算元使用這些運算子時要小心,因為大多數浮點值只能近似儲存。

按位與運算子

[edit | edit source]

按其本質,位掩碼的值可能取決於整數的大小/表示。

按位異或運算子

[edit | edit source]

按其本質,位掩碼的值可能取決於整數的大小/表示。

按位或運算子

[edit | edit source]

按其本質,位掩碼的值可能取決於整數的大小/表示。

邏輯與運算子

[edit | edit source]

標準 C 在第一個和第二個運算元的計算之間定義了一個順序點。

邏輯或運算子

[edit | edit source]

標準 C 在第一個和第二個運算元的計算之間定義了一個順序點。

條件運算子

[edit | edit source]

標準 C 在第一個運算元的計算與第二個或第三個運算元的計算(無論哪個被計算)之間定義了一個順序點。

賦值運算子

[edit | edit source]

簡單賦值

[edit | edit source]

將零值的整數常量表達式賦值給任何指標型別都是可移植的,但是將任何其他算術值賦值則不可移植。

將一種指標型別賦值給對齊要求更嚴格的指標型別的影響是實現定義的

標準 C 要求顯式強制轉換才能將一種物件型別的指標賦值給另一種物件型別的指標。(在向或從 void 指標賦值時不需要強制轉換。)

標準 C 允許將結構體(或聯合體)只賦值給型別相同的結構體(或聯合體)。

如果將一個物件賦值給一個重疊的物件,結果是未定義的。(例如,這可能在聯合體的不同成員之間發生。)要將聯合體的一個成員賦值給另一個成員,需要透過一個臨時變數。

複合賦值

[edit | edit source]

標準 C 不支援 =op 形式(非常舊)的賦值運算子。(K&R 在 1978 年就暗示它們已經過時了。)

以下表達式的結果是不可預測的,因為運算元的計算順序是未定義的

x[i] = x[i++] + 10;

可以使用複合賦值運算子解決此問題,如下所示

x[i++] += 10;

因為可以保證左運算元只計算一次。

逗號運算子

[edit | edit source]

標準 C 在第一個和第二個運算元的計算之間定義了一個順序點。

常量表達式

[edit | edit source]

允許在程式啟動時而不是在編譯時計算靜態初始化表示式。

翻譯環境必須使用與執行環境至少相同的精度。如果它使用更高的精度,則在編譯時初始化的靜態值可能與在目標機器上啟動時初始化的值不同。

C89 引入了 floatlong double 和無符號整型常量。C99 引入了帶符號/無符號 long long 整型常量。

C99 添加了對具有二進位制指數的浮點常量的支援。

標準 C 允許實現支援超出標準定義的常量表達式形式(以適應其他/擴充套件型別)。但是,編譯器在處理這些常量表達式方面存在差異:有些被視為整數常量表達式。

宣告

[edit | edit source]

也許 C89 對 C 語言影響最大的是宣告方面。添加了新的與型別相關的關鍵字以及用於對其分類的術語。從程式設計師的角度來看,最顯著的方面是採用函式原型(即新式函式宣告)來自 C++。

宣告元素的順序

[edit | edit source]

宣告可以包含一個或多個以下元素:儲存類說明符、型別說明符、型別限定符、函式說明符和對齊說明符。標準 C 允許這些元素以任何順序出現;但是,它確實要求任何識別符號列表都出現在最右邊。因此,

static const unsigned long int x = 123;

可以改寫為

int long unsigned const static x = 123;

或者以任何其他組合,只要x及其初始化器位於末尾。類似地。

typedef unsigned long int uType;

可以改寫為

int long unsigned typedef uType;

一些較舊的編譯器可能需要特定的順序。K&R是否允許型別說明符的任意排序是有爭議的。K&R 第 192 頁上的語法表明它們是受支援的,但第 193 頁上卻說,“以下[型別說明符]組合是可接受的:short intlong intunsigned intlong float。”目前尚不清楚這是否應被理解為明確禁止int shortint long 等。

塊內位置

[編輯 | 編輯原始碼]

在 C99 之前,在塊級範圍內,所有宣告都必須位於所有語句之前。但是,該限制在 C99 中被解除,允許它們交織在一起。C++ 也允許這樣做。

儲存類說明符

[編輯 | 編輯原始碼]

auto 儲存類

[編輯 | 編輯原始碼]

很少在程式碼中看到實際使用的 auto,因為標準 C 中沒有顯式儲存類的區域性變數預設使用 auto 儲存類。

用於分配自動變數的記憶體方法以及為自動變數提供的儲存量取決於實現。使用堆疊(或其他)方法的實現可能會對 auto 物件可用的空間量設定限制。例如,16 位機器可能會將堆疊限制為 64 KB,或者,如果整個地址空間為 64 KB,則程式碼、靜態資料和堆疊的總和可能為 64 KB。在這種情況下,隨著程式碼或靜態資料大小的增長,堆疊大小會減小,也許會減小到無法分配足夠 auto 空間的地步。

一些實現會在每個函式進入時檢查堆疊溢位的可能性。也就是說,他們在為函式分配所需的空間之前檢查可用的堆疊空間量。如果可用空間不足,他們將終止程式。一些實現實際上會呼叫一個函式來執行檢查,在這種情況下,每次呼叫一個具有自動類變數的函式時,您也會隱式呼叫另一個函式。

建議:在每次呼叫函式時都會“探測堆疊”的實現中,可能會有一個編譯時開關允許停用此類檢查。雖然停用此類檢查可以增加可用的堆疊量,可能允許程式在原本無法執行的情況下執行,但強烈建議您在測試期間不要這樣做。

考慮以下 auto 宣告

int i, values[10];
int j, k;

這四個變數在記憶體中的位置相對於彼此是未指定的,並且可能在同一個系統或不同系統上的編譯之間發生變化。但是,我們保證陣列 values 中的 10 個元素是連續的,並且地址按升序排列。

建議:不要依賴實現具有特定的 auto 空間分配方案。特別是,不要依賴於 auto 變數被分配空間的順序與宣告的順序完全相同。

C++ 注意事項:C++11 停止支援 auto 作為儲存類說明符,並賦予它新的語義,作為型別說明符。

register 儲存類

[編輯 | 編輯原始碼]

register 儲存類是向實現發出的一個提示,要求它將物件放置在可以“儘可能快”訪問的位置。這樣的位置通常是機器暫存器。可以實際放置在暫存器中的 register 物件的數量以及支援的型別集是實現定義的。無論出於何種原因,無法儲存在暫存器中的儲存類為 register 的物件,將被視為儲存類為 auto 的物件。標準 C 允許任何資料宣告具有此儲存類。它還允許將此儲存類應用於函式引數。

K&R 說:“... 只有某些型別的變數將儲存在暫存器中;在 PDP-11 上,它們是 intchar 和指標。”

建議:鑑於編譯器最佳化技術的進步,register 儲存類在託管實現中的價值已基本消失。(事實上,K&R 中就預測了這一點,它說:“... 程式碼生成的未來改進可能使[暫存器宣告]變得不必要。”)因此,建議您根本不要使用它們,除非您能證明它們對一個或多個目標實現提供了某些價值。

標準 C 不允許實現擴大具有 register 儲存類的變數的分配空間。也就是說,register char 不能被視為 register int。它在所有方面都必須表現得像 char,即使它儲存在大小大於 char 的暫存器中。(一些實現實際上可以將多個 register char 物件儲存在同一個暫存器中。)

C++ 注意事項:雖然 C++14 中還支援此儲存類,但其使用已不推薦。在 C++17 中,關鍵字 register 未被使用,但它為將來使用保留(可能具有不同的語義)。

static 儲存類

[編輯 | 編輯原始碼]

當嘗試向前引用 static 函式時,可能會出現問題,如下所示

void test()
{
    static void g(void);
    void (*pfi)(void) = &g;

    (*pfi)();
}

static void g()
{
    /* … */
}

函式 test 具有塊級範圍宣告,其中 g 被宣告為 static 函式。這允許 test 呼叫 static 函式 g 而不是任何同名 extern 函式。標準 C 不允許這種宣告。但是,它允許具有檔案範圍的函式宣告具有 static 儲存類,如下所示

static void g(void);

void test()
{
    void (*pfi)(void) = &g;

    (*pfi)();
}

建議:即使您的編譯器允許,也不要對塊級範圍函式宣告使用 static 儲存類。

_Thread_local 儲存類

[編輯 | 編輯原始碼]

這是由 C11 新增的。(請參閱<threads.h>。)

C++ 注意事項:C++11 中新增的等效(但不同)關鍵字是 thread_local,標準 C 將其定義為 <threads.h> (<threads.h>) 中的宏。

以下是如何確定您的編譯器是否支援執行緒區域性儲存持續時間

#ifdef __cplusplus /* Are we using a C++? compiler */
    #if __cplusplus >= 201103L
        /* we have a C++ compiler that supports thread_local storage duration */
    #else
        #error "This C++ compiler does not support thread_local storage duration"
    #endif
#else /* we're using a C compiler */
    #ifdef __STDC_NO_THREADS__
        /* we have a C compiler that supports thread_local storage duration */
    #else
        #error "This C compiler does not support thread_local storage duration"
    #endif
#endif

型別說明符

[編輯 | 編輯原始碼]

C89 添加了以下關鍵字用於型別說明符:enumsignedvoid。這些導致了以下基本型別宣告

void /* function returning, only */
signed char
signed short
signed short int
signed
signed int
signed long
signed long int
enum [tag] /* … */

C89 還添加了對以下新的型別宣告的支援(一些實現已經支援 unsigned charunsigned long

unsigned char
unsigned short
unsigned short int
unsigned long
unsigned long int
long double

C99 添加了對以下的支援

signed long long
signed long long int
unsigned long long
unsigned long long int

標準 C 規定,普通字元(沒有 signedunsigned 修飾符的字元)被視為有符號還是無符號是實現定義的

雖然 K&R 允許 long float 作為 double 的同義詞,但這種做法不受標準 C 支援。

在 C99 之前,可以省略型別說明符,並假設為 int;例如,在檔案級宣告中

i = 1;
extern j = 1;

C99 禁止了這一點。

C99 透過型別說明符 _Bool 添加了對布林型別的支援。(請參閱<stdbool.h>,其中包含如果此標頭檔案不可用時的解決方法。)

C++ 注意事項:標準 C 中的等效(但不同)關鍵字是 bool,標準 C 將其定義為 <stdbool.h> (<stdbool.h>) 中的宏。

C99 添加了型別說明符 _Complex,從而產生了 float _Complexdouble _Complexlong double _Complex 型別。(請參閱<complex.h>。)

C11 添加了型別說明符 _Atomic,但將其設定為可選;請參閱條件定義的標準宏 中的條件定義的宏 __STDC_NO_ATOMICS__。(請參閱<stdatomic.h>。)

表示、大小和對齊方式

[編輯 | 編輯原始碼]

<limits.h><float.h> 中的宏定義了算術型別的最小範圍和精度。標準 C 要求以下內容

  • _Bool - 足夠大以儲存值 0 和 1

  • char - 至少 8 位

  • short int - 至少 16 位

  • int - 至少 16 位

  • long int - 至少 32 位

  • long long int - 至少 64 位

  • float 的範圍和精度必須小於或等於 double 的範圍和精度,而 double 的範圍和精度必須小於或等於 long double 的範圍和精度。這三種類型可以有相同的尺寸和表示方式,也可以完全不同,或者部分重疊。

對於整數值,符合標準的實現允許使用一補碼、二補碼或符號大小表示。帶符號整數型別的最小限制允許一補碼。雖然一個具有 32 位 long 且使用二補碼的實現可以透過將 LONG_MIN 定義為 -2147483647 來符合標準,但如果它使用 -2147483648 的值來準確反映該型別的二補碼特性,也是合理的。

float 型別通常使用 32 位單精度表示,double 型別使用 64 位雙精度表示,long double 型別也使用 64 位雙精度表示。但是,在具有獨立擴充套件精度的系統上,long double 可能對映到 80 位或 128 位。

請注意,即使程式在具有相同大小和浮點型別表示方式的多個處理器上執行(例如,在多個基於 IEEE 的系統上),也不能指望從浮點計算中獲得相同的結果。例如,在早期的英特爾浮點處理器上,所有計算都在 80 位擴充套件模式下完成,這可能導致與使用嚴格(64 位)double 模式新增兩個 double 的結果不同的值。舍入模式也會影響結果。

建議:關於浮點計算,對不同浮點硬體和軟體庫之間結果的可重複性要有合理的預期。

標準 C 並不要求在預處理器 #if 算術表示式中將 sizeof 識別為運算子。

根據機器字長進行條件編譯很常見。這裡,示例假設,如果它不在 16 位系統上執行,它就在 32 位機器上執行。為了實現相同的結果,現在必須使用類似的方法

#include <limits.h>

#if INT_MAX < LONG_MAX
    long total;
#else
    int total;
#endif

sizeof 編譯時運算子報告某個給定資料型別的物件的記憶體佔用位元組數。如果將此值乘以 <limits.h> 中的宏 CHAR_BIT,就可以得到分配的位數。但是,並非為物件分配的所有位都用於表示該物件的值!以下是一些演示這一點的示例

案例 1:克雷研究公司的早期機器採用 64 位、字定址架構。當宣告 short int 時,儘管分配了 64 位(sizeof(short) 的結果為 8),但實際上只使用 24 位或 32 位來表示 short 的值。

案例 2:英特爾浮點處理器支援 32 位單精度、64 位雙精度和 80 位擴充套件精度。因此,針對該機器的編譯器可能會將 floatdoublelong double 分別對映到這三種表示方式。如果是這樣,人們可能會認為 sizeof(long double) 將為 10,這可能是真的。但是,出於效能原因,編譯器可能會選擇將此類物件對齊到 32 位邊界,從而導致分配 12 個位元組,其中兩個位元組未使用。

案例 3:在 C89 的討論過程中,出現了關於整數型別是否需要二進位制表示的問題,委員會決定它們確實需要。因此,描述中寫道“……每個物理相鄰位表示下一個最高位的二的冪”。但是,一位委員會成員報告說,他的公司有一個 16 位處理器,當使用兩個 16 位字來表示 long int 時,低字的高位沒有使用。本質上,中間有一個 1 位的空洞,並且向左或向右移位會考慮到這一點!(即使 31 位不足以在標準 C 中表示 long int,但所討論的實現對於針對其目標市場的應用程式來說是一個可行的實現。)

案例 4:出於對齊目的,結構中的欄位之間或最後一個欄位之後,以及位欄位的容器內部可能存在空洞(即未使用位)。

建議:不要假設或硬編碼任何物件型別的尺寸;使用 sizeof 獲取該尺寸,並根據需要使用 <limits.h><float.h><stdint.h> 中的宏。

建議:不要假設分配給物件的未使用位具有特定/可預測的值。

雖然所有資料和函式指標型別可能具有相同的尺寸和表示方式,這可能也是整數型別的尺寸和表示方式,但這並不是標準 C 的要求。有些機器使用看起來像帶符號整數的地址,在這種情況下,地址零位於地址空間的中間。(在這樣的機器上,空指標可能不會具有“所有位都為零”的值。在一些分段記憶體架構上,可能支援(16 位)和(32 位)指標。標準 C 所要求的是所有資料和函式指標值都可以由 void * 型別表示。

建議:除非你有非常特殊的應用程式,否則假設每種指標型別都有唯一的表示方式,這與任何整數型別不同,並且不要假設任何指標型別的空值具有“所有位都為零”的值。

有些程式透過建立一個與某個整數型別聯合的程式來檢查並可能操縱物件中的位。顯然,這依賴於實現定義的行為。根據標準 C,關於這種型別穿透,“為了簡化聯合的使用,做了一個特別的保證:如果一個聯合包含幾個共享共同初始序列的結構,並且如果聯合物件當前包含其中一個結構,則允許檢查其中任何一個的共同初始部分,只要聯合的完整型別的宣告可見”。

結構和聯合說明符

[edit | edit source]

雖然 K&R 沒有限制可以與位欄位一起使用的型別,但 C89 只允許 intunsigned intsigned int,並指出,““普通”int 位欄位的高位是否被視為符號位是實現定義的。”

C99 指出,“位欄位的型別必須是 _Boolsigned intunsigned int 或其他實現定義的型別的限定或非限定版本。” C11 添加了,“是否允許原子型別是實現定義的。”

K&R 要求連續的位欄位被打包到機器整數中,並且它們不能跨越字邊界。標準 C 宣告包含位欄位的容器物件是實現定義的。對於位欄位是否跨越容器邊界,也是如此。標準 C 允許位欄位在容器中的分配順序為實現定義的

標準 C 允許位欄位存在於聯合中,而無需首先將它們宣告為結構的一部分,如下所示

union tag {
    int i;
    unsigned int bf1 : 6;
    unsigned int bf2 : 4;
};

K&R 要求聯合中的所有成員“從偏移量 0 開始”。標準 C 更精確地說明了這一點,它說指向聯合的指標,經過適當的強制型別轉換,指向每個成員,反之亦然。(如果任何成員是位欄位,則指標指向該位欄位所在的容器。)

C11 添加了對匿名結構、匿名聯合和靈活陣列成員的支援。

列舉說明符

[edit | edit source]

列舉型別不是 K&R 的一部分;但是,一些編譯器在 C89 出現之前就很好地實現了它們。

根據 C 標準,“每個列舉型別應與 char、帶符號整數型別或無符號整數型別相容。型別的選擇是實現定義的,但應能夠表示列舉所有成員的值。”

建議:不要假設列舉型別用 int 表示——它可以是任何整型。

請注意,標準 C 要求列舉常量具有 int 型別。因此,列舉資料物件的型別不必與它的成員型別相同。

C99 添加了對列舉器列表後面的尾隨逗號的支援,如

enum Color { red, white, blue, }

C++ 考慮:標準 C++ 透過允許列舉型別指定特定的基本型別(即表示方式),以及透過將列舉常量的作用域限制為僅列舉型別本身,擴充套件了列舉型別。

原子型別說明符

[edit | edit source]

除非實現支援原子型別,否則不允許使用這些型別說明符,這可以透過測試條件定義的宏 __STDC_NO_ATOMICS__ 是否為整數常量 1 來確定。(參見 <stdatomic.h>。)

_Atomic 型別說明符的形式為 _Atomic ( type-name ),不要與 _Atomic 型別限定符(型別限定符)混淆,後者只涉及該名稱本身。

C++ 考慮:C++不支援_Atomic。但是,它確實定義了標頭檔案 <atomic>,它提供了對各種原子相關支援的訪問。

型別限定符

[edit | edit source]

C89 添加了const型別限定符,借鑑自 C++。C89 還添加了volatile型別限定符。

嘗試透過指向沒有const限定符型別的指標來修改const物件,會導致未定義行為

建議:不要嘗試透過指向沒有const限定符型別的指標來修改const物件。

嘗試透過指向沒有volatile限定符型別的指標來引用volatile物件會導致未定義行為。

建議:不要透過指向沒有volatile限定符型別的指標來訪問volatile物件。

C99 添加了restrict型別限定符,並根據需要將其應用於各種庫函式。

C++ 考慮:C++ *不支援*restrict

C11 添加了型別限定符_Atomic,不要將其與_Atomic型別說明符混淆 (原子型別說明符).

函式說明符

[edit | edit source]

C99 添加了函式說明符inline。這是一個對編譯器的建議,該提示的遵循程度是實現定義的。在 C99 之前,一些編譯器透過關鍵字__inline__支援此功能。

標準 C 允許對函式進行內聯定義和外部定義,在這種情況下,未指定對函式的呼叫是使用內聯定義還是外部定義。

C11 添加了函式說明符_Noreturn。它還提供了標頭檔案 stdnoreturn.h>,其中包含一個名為noreturn的宏,該宏展開為_Noreturn

C++ 考慮:C++11 中新增的_Noreturn的等效(但不同)方法是屬性noreturn

對齊說明符

[edit | edit source]

C11 添加了對使用關鍵字_Alignas的對齊說明符的支援。

標頭檔案 對齊 包含一個名為alignas的宏,該宏展開為_Alignas

C++ 考慮:C++11 中新增的等效(但不同)關鍵字是alignas

標準 C 指出,“如果不同翻譯單元中物件的宣告具有不同的對齊說明符,則行為是未定義的。”

宣告符

[edit | edit source]

一般資訊

[edit | edit source]

K&R 和標準 C 都將括號中的宣告符視為等效於沒有括號的宣告符。例如,以下是語法正確的。

void f()
{
    int (i);
    int (g)();
    
}

第二個宣告可用於隱藏函式宣告,該函式宣告來自與該函式同名的帶引數的宏。

標準 C 要求宣告支援至少 12 個指標、陣列和函式派生宣告符,修改基本型別。例如,***p[4] 有四個修飾符。K&R 沒有給出限制,只是說可以存在多個型別修飾符。(原始 Ritchie 編譯器在宣告符中僅支援六個型別修飾符。)

標準 C 要求陣列維具有正的非零值。也就是說,陣列不能為零大小,而某些實現允許為零大小。

陣列宣告符

[edit | edit source]

標準 C 允許陣列宣告透過省略大小資訊而變得不完整,如下所示

extern int i[];
int (*pi)[];

但是,在提供大小資訊之前,這些物件的用法是有限制的。例如,sizeof(i)sizeof(*pi) 是未知的,應該生成錯誤。

C99 添加了在宣告具有陣列型別的函式引數時使用型別限定符和關鍵字static的功能。

C++ 考慮:標準 C++ 不支援陣列型別宣告符中的這些內容。

C99 添加了對可變長度陣列 (VLA) 的支援,並要求這種支援。但是,C11 使 VLA 成為可選的;請參見 條件定義的標準宏 中的條件定義宏__STDC_NO_VLA__

C++ 考慮:標準 C++ *不支援* 可變長度陣列。

函式宣告符

[edit | edit source]

呼叫非 C 函式

[edit | edit source]

某些實現允許在函式宣告中使用fortran型別說明符(擴充套件),以指示要生成適合 Fortran(按引用呼叫)的函式連結,或者要生成外部名稱的不同表示形式。其他實現提供pascalcdecl關鍵字分別用於呼叫 Pascal 和 C 過程。標準 C 沒有提供任何外部連結機制。

C++ 考慮:標準 C++ 定義了extern"C"連結。

函式原型

[edit | edit source]

借鑑自 C++,C89 引入了一種宣告和定義函式的新方法,該方法將引數資訊放在引數列表中。這種方法使用俗稱的 *函式原型*。例如,以前寫成

int CountThings(); /* declaration with no parameter information */

int CountThings(table, tableSize, value) /* definition */
char table[][20];
int tableSize;
char* value;
{
    /* … */
}

現在可以寫成

/* function prototype – function declaration with parameter information */

int CountThings2(char table[][20], int tableSize, char* value);

int CountThings2(char table[][20], int tableSize, char *value) /* definition */
{
    /* … */
}

標準 C 繼續支援舊風格。

C++ 考慮:標準 C++ *要求* 函式原型表示法。

儘管您可能擁有使用舊式函式定義的生產原始碼,但這些程式碼可以與新式函式原型共存。唯一潛在的陷阱是窄型別。例如,舊式定義具有型別為charshortfloat的引數,它將期望以其較寬形式傳遞引數,即分別為intintdouble,如果作用域記憶體在包含窄型別的原型,則可能並非如此。

建議:儘可能使用函式原型,因為它們可以確保函式使用正確的引數型別進行呼叫。原型還可以執行引數轉換。例如,在沒有原型作用域的情況下呼叫f(int *p),並將 0 傳遞給它,不會導致該零轉換為int *,這在某些系統上可能會導致問題。

標準 C 要求所有對具有可變引數列表的函式的呼叫僅在存在原型的情況下進行。具體來說,以下來自 K&R 的著名程式不是符合標準的程式

main()
{
    printf("hello, world\n");
}

這樣做的原因是,在沒有原型的情況下,編譯器可以假定引數的數量是固定的。因此,它可能使用暫存器或其他(可能是)更有效的方法來傳遞引數,而不是其他方法。顯然,printf 函式期望可變引數列表。通常,如果呼叫程式碼是用固定列表假設編譯的,它將無法與呼叫printf的程式碼正常通訊。要更正上面的示例,您必須要麼#include <stdio.h>(首選方法),要麼在示例中在函式使用之前顯式編寫printf的原型(包括尾隨省略號)。[該函式還應被賦予顯式返回型別int。]

建議:在呼叫具有可變引數列表的函式時,始終讓原型在作用域內。確保原型包含省略號表示法。

允許在原型宣告符中使用虛擬識別符號名稱;但是,使用它們可能會導致問題,如下面的程式所示

#define test 10

int f(int test);

儘管原型中識別符號test的作用域從其宣告開始到原型結束,但預處理器會看到該名稱。因此,它被替換為常量 10,從而生成語法錯誤。更糟糕的是,如果宏test定義為*,則原型將從具有int型別引數悄悄更改為具有指向int型別的指標的引數。如果您的實現的標準標頭檔案使用虛擬名稱,而這些名稱是程式設計師名稱空間的一部分(即沒有前導下劃線),則也會出現類似的問題。

建議:如果必須在原型中放置識別符號,請為它們命名,以避免與宏名稱衝突。如果始終以大寫字母拼寫宏,而以小寫字母拼寫其他所有識別符號,則可以避免這種情況。

宣告int f(); 告訴編譯器 f 是一個返回 int 的函式,但不知道其引數的數量和型別。另一方面,int f(void); 指示沒有引數。

C++ 考慮:宣告int f();int f(void); 是等效的。

初始化

[edit | edit source]

如果在為具有自動儲存持續時間的未初始化物件賦值之前使用該物件的值,則行為是未定義的。

未顯式初始化的外部變數和static變數將被賦值為0,並轉換為其型別。(這可能與calloc (aligned_alloc)分配的區域不同,該區域初始化為全零位。)

K&R不允許自動陣列、結構體和聯合體進行初始化。但是,標準C允許這樣做,前提是任何初始化列表中的初始化表示式都是常量表達式,並且不涉及變長陣列。自動結構體或聯合體也可以用相同型別的(非常量)表示式初始化。

標準C允許顯式地初始化聯合體。該值透過將其轉換為第一個指定成員的型別儲存在聯合體中,因此成員宣告順序可能很重要!根據此規則,我們看到,如果static或外部聯合體未顯式初始化,它將包含轉換為第一個成員的0(這可能不會導致所有位都為零,如上所述)。

標準C允許自動結構體和聯合體具有作為結構體或聯合體值表示式的初始化器。

標準C允許位域進行初始化。例如,

struct {
    unsigned int bf1 : 5;
    unsigned int bf2 : 5;
    unsigned int bf3 : 5;
    unsigned int bf4 : 1;
} bf = {1, 2, 3, 0};

K&R和標準C要求初始化器中的表示式數量小於或等於預期數量,但絕不能超過預期數量。但是,在一種情況下,可以隱式地指定一個太多,但不會產生編譯錯誤。例如,

char text[5] = "hello";

在這裡,陣列text用字元hello初始化,不包含尾隨的'\0'

一些實現允許在初始化列表中使用尾隨逗號。標準C支援這種做法,並且K&R也允許這種做法。

C99添加了對指定初始化器的支援。

C++ 注意事項:標準C++不支援指定初始化器。

靜態斷言

[edit | edit source]

C11添加了對靜態斷言的支援。它還在標頭檔案<assert.h> (<assert.h>)中添加了一個名為static_assert的宏,它擴充套件為_Static_assert

外部定義

[edit | edit source]

匹配外部定義及其宣告

[edit | edit source]

雖然K&R定義了一個模型來定義和引用外部物件,但也使用了許多其他模型,這導致了一些混亂。這些模型在下面的子節中描述。

標準C採用了一個模型,該模型結合了嚴格的ref/def模型和初始化模型。採用這種方法是為了儘可能適應各種環境和現有的實現。

標準C指出,如果具有外部連結的識別符號在兩個原始檔中具有不相容的宣告,則行為是未定義的。

某些實現會導致將目標模組載入到可執行映像中,只要在其中定義的一個或多個外部識別符號在使用者程式碼中宣告但實際上沒有使用。標準C指出,如果具有外部連結的識別符號未在表示式中使用,則不需要為其提供外部定義。也就是說,您不能僅透過宣告一個物件來強制載入該物件!

嚴格的ref/def模型

[edit | edit source]
/* source file 1   source file 2 */

int i;             extern int i;
int main()         void sub()
{                  {
    i = 10;            /* … */
    /* … */        }
}

使用此模型,i的宣告只能出現一次,並且不能包含關鍵字extern。對該外部的所有其他引用必須包含關鍵字extern。這是K&R指定的模型。

寬鬆的ref/def模型

[edit | edit source]
/* source file 1   source file 2 */

int i;             int i;
int main()         void sub()
{                  {
    i = 10;            /* … */;
    /* … */        }
}

在這種情況下,i的任何宣告都不包含關鍵字extern。如果識別符號用extern類宣告(在某處),則必須在程式中的其他位置出現定義例項。如果識別符號用初始化器宣告,則在程式中必須出現一個且僅出現一個包含初始化器的宣告。此模型廣泛用於類Unix環境。採用此模型的程式符合標準C,但不具有最大的可移植性。

通用模型

[edit | edit source]
/* source file 1   source file 2 */

extern int i;      extern int i;

int main()         void sub()
{                  {
    i = 10;            /* … */;
    /* … */        }
}

在此模型中,外部變數i的所有宣告可以選擇性地包含關鍵字extern。此模型旨在模仿Fortran的COMMON塊的模型。

初始化器模型

[edit | edit source]
/* source file 1   source file 2 */

int i = 0;         int i;

int main()         void sub()
{                  {
    i = 10;            /* … */;
    /* … */        }
}

這裡,定義例項是包含顯式初始化器的例項(即使該初始化器是預設值)。

臨時物件定義

[edit | edit source]

標準C引入了臨時物件定義的概念。也就是說,宣告可能是定義,這取決於它後面的內容。例如,

/* tentative definition, external */
int ei1;

/* definition, external */
int ei1 = 10;

/* tentative definition, internal */
static int si1;

/* definition, internal */
static int si1 = 20;

這裡,ei1si1的第一個引用是臨時定義。如果它們後面沒有包含初始化列表的相同識別符號的宣告,那麼這些臨時定義將被視為定義。但是,如所示,它們後面有這樣的宣告,因此它們被視為宣告。這樣做的目的是允許兩個相互引用的變數被初始化為指向彼此。

語句

[edit | edit source]

帶標籤的語句

[edit | edit source]

K&R使用標籤和“普通”識別符號共享相同的名稱空間。也就是說,如果在從屬塊中聲明瞭與標籤名稱相同的識別符號,則該標籤名稱將被隱藏。例如,

void f()
{
    label: ;
    {
        int label;
        
        goto label;
    }
}

將產生編譯錯誤,因為goto語句的目標是宣告為int變數的識別符號,而不是標籤。

在標準C中,標籤有它們自己的名稱空間,允許上面的示例在沒有錯誤的情況下進行編譯。

K&R指定內部識別符號(如標籤)的有效長度為八個字元。

標準C要求內部識別符號(如標籤)的有效長度至少為63個字元。

複合語句(塊)

[edit | edit source]

在C99之前,塊中的所有宣告都必須位於所有語句之前。但是,從C99開始,兩者可以交錯。

C++ 注意事項:C++允許宣告和語句交錯。

gotoswitch可用於跳入塊。雖然這樣做是可移植的,但塊中任何“跳過的”自動變數是否能夠以可預測的方式初始化尚不清楚。

K&R允許塊巢狀,但沒有說明巢狀的深度。

C89要求複合語句至少巢狀15層。C99將此值提高到127。

表示式和空語句

[編輯 | 編輯原始碼]

考慮以下使用 volatile 型別限定符(由 C89 新增)的示例

extern volatile int vi[5];
void test2()
{
    volatile int *pvi = &vi[2];

    vi[0];
    pvi;
    *pvi;
    *pvi++;
}

最佳化器在處理具有 volatile 限定符的物件時必須非常小心,因為它們無法對這種物件的當前狀態做出任何假設。在最簡單的情況下,實現可能會評估包含 volatile 表示式的每個表示式,僅僅因為這樣做可能會生成在環境中可見的動作。例如,語句 *pvi; 可能會生成訪問 vi[2] 的程式碼。也就是說,它可能會將 vi[2] 的地址放到總線上,以便硬體可以“看到”它,並等待此類訪問同步。請注意,即使實現執行此操作,它也不應為語句 pvi; 生成程式碼,因為 pvi 本身不是 volatile,並且評估表示式 pvi 不涉及訪問 volatile 物件。

建議:不要依賴像 i[0];pi;*pi; 這樣的表示式語句來生成程式碼。即使 i 是一個 volatile 物件,也不能保證 volatile 物件會因此而被訪問。

選擇語句

[編輯 | 編輯原始碼]

關於選擇語句中巢狀限制的說明,請參閱 複合語句

if 語句

[編輯 | 編輯原始碼]

由於控制表示式是一個完整的表示式,因此在它之後立即有一個序列點。

switch 語句

[編輯 | 編輯原始碼]

K&R 要求控制表示式和每個 case 常量表達式都具有 int 型別。

標準 C 要求控制表示式具有一些整型。每個 case 表示式也必須是整型,並且每個表示式的值如果需要,將被轉換為控制表示式的型別。

由於標準 C 支援列舉資料型別(由整型表示),因此它允許在 switch 表示式和 case 常量表達式中使用它們。(列舉型別在 K&R 中沒有定義。)一些實現具有一種表示法,用於為 case 常量表達式指定一系列值。請注意,由於使用了多種不同的且不相容的語法,因此標準 C 不支援此功能。

標準 C 允許字元常量包含多個字元,如 'ab''abcd'。字元常量允許在 case 常量表達式中使用。

建議:由於多字元字元常量的內部表示是實現定義的,因此不應在 case 常量表達式中使用它們。

K&R 沒有指定 switch 語句中允許的最大 case 值數量。C89 要求每個 switch 語句至少支援 257 個 case。C99 將其增加到 1023。

由於控制表示式是一個完整的表示式,因此在它之後立即有一個序列點。

請參閱 複合語句,瞭解關於將轉移到 switch 語句中複合語句的討論。

迭代語句

[編輯 | 編輯原始碼]

whiledofor 語句中的控制表示式可能包含 expr1 == expr2 形式的表示式。如果 expr1expr2< 是浮點表示式,則由於浮點表示、舍入等的實現定義性質,可能難以或不可能實現相等性。

建議:如果 whiledofor 語句中的控制表示式包含浮點表示式,請注意浮點相等性測試的結果是實現定義的。例如,最好使用類似 fabs(expr1 - expr2) < 0.1e-5 的東西,而不是 expr1 == expr2

一些程式包含“空閒”迴圈;也就是說,這些迴圈僅僅是為了消磨時間,可能是對實際掛鐘時間的粗略近似。例如

for (i = 0; i <= 1000000; ++i) { }

為了解決此類構造的效用,C11 添加了以下內容:“控制表示式不是常量表達式的迭代語句,不執行輸入/輸出操作,不訪問 volatile 物件,並且在其主體、控制表示式或(對於 for 語句)其 expression-3 [每次迭代後評估的表示式] 中不執行同步或原子操作,可以假定由實現終止。”通俗地說,這意味著編譯器可以丟棄整個迴圈,前提是它實現了迴圈中包含的任何其他副作用(在本例中,確保 i 最終以值 1000001 結束)。

建議:不要使用“空閒”迴圈來簡單地消磨時間。即使此類迴圈沒有被最佳化掉,它們的執行時間也極大地依賴於任務/執行緒優先順序和處理器速度等因素。

標準 C 保證至少 15 級選擇控制結構、迭代控制結構和複合語句的巢狀。C99 將其增加到 127。K&R 沒有指定最低限制。

while 語句

[編輯 | 編輯原始碼]

由於控制表示式是一個完整的表示式,因此在它之後立即有一個序列點。

do 語句

[編輯 | 編輯原始碼]

由於控制表示式是一個完整的表示式,因此在它之後立即有一個序列點。

for 語句

[編輯 | 編輯原始碼]

由於三個表示式都是完整的表示式,因此在每個表示式之後立即有一個序列點。

C99 添加了對 for 的第一部分是宣告的支援,如 int i = 0,而不是要求 i 已經定義。

C++ 注意事項:標準 C++ 也支援此 C99 功能。C++ 在 for 語句中宣告的任何變數的範圍方面與 C 不同。

跳轉語句

[編輯 | 編輯原始碼]

goto 語句

[編輯 | 編輯原始碼]

請參閱 帶標籤的語句,瞭解有關單獨標籤名稱空間含義的討論。請參閱 複合語句,瞭解有關跳入複合語句的含義的討論。

return 語句

[編輯 | 編輯原始碼]

當使用 return expression; 形式時,由於 expression 是一個完整的表示式,因此在它之後立即有一個序列點。

如果使用了函式呼叫的值,但沒有返回任何值,則結果是 未定義的,但自 C99 起,main 除外,它有一個隱式的 return 0;

標準 C 支援 void 函式型別,它允許編譯器確保 void 函式沒有返回值。K&R 沒有包含 void 型別。

標準 C 支援透過值返回結構體和聯合體。它沒有對正在返回的物件的大小施加任何限制,儘管透過值傳遞給函式的此類物件的大小可能受到限制。K&R 沒有包含透過值返回結構體和聯合體。

K&R (第 68 和 70 頁) 顯示了 `return` 語句的一般形式為 `return(表示式);`,而第 203 頁的正式定義則顯示為 `return 表示式;`。這看起來可能是一個矛盾。第 203 頁是正確的——圓括號不是語法的一部分;它們只是多餘的組合圓括號,是表示式的一部分。混淆來自 K&R 中大多數(如果不是全部)使用 `return` 的示例將返回值放在圓括號內。從風格的角度來看,圓括號可能很有用,因為它們有助於將 `return` 關鍵字與表示式分開,並且如果表示式比較複雜,它們可以清楚地分隔表示式。但是,它們永遠不需要。(請注意,在 K&R 的第二版中,示例中的圓括號已被刪除,並且 `main` 通常使用 `return 0;` 終止。)

C99 添加了以下約束:“沒有表示式的 `return` 語句只能出現在返回型別為 `void` 的函式中。”

預處理器

[編輯 | 編輯原始碼]

根據最初的 C 標準原理文件(該文件是在開發 C89 時編寫的),“也許現有的 C 實現中最不可取的多樣性可以在預處理中找到。不可否認,一種獨特的、原始的語言疊加在 C 之上,預處理命令隨著時間的推移而積累,幾乎沒有中央指導,而且它們的文件中甚至缺乏精度。”

一般資訊

[編輯 | 編輯原始碼]

預處理器與編譯器

[編輯 | 編輯原始碼]

許多 C 編譯器包含多個遍,第一個遍通常包含預處理器。利用此知識,編譯器通常可以透過安排資訊在預處理器和編譯器本身的各個階段之間共享來走捷徑。雖然這對於特定實現來說可能是一個有用的功能,但您應該記住,其他實現可能使用完全獨立且不合作的程式來執行預處理器和編譯器。

建議:將預處理和編譯的概念分開。當您未能做到這一點時,一個可能的問題將在稍後討論的 `sizeof` 運算子中得到證明。

雖然 C 是一種自由格式語言,但預處理器不必是自由格式的,因為嚴格來說,它不是 C 語言的一部分。語言和預處理器各有其語法、約束和語義。兩者都由標準 C 定義。

指令名稱格式

[編輯 | 編輯原始碼]

預處理指令總是以 `#` 字元開頭。但是,並非所有預處理器都要求 `#` 和指令名稱是單個標記。也就是說,`#` 字首可以與指令名稱透過空格和/或水平製表符分隔。

K&R 將 `#` 顯示為指令名稱的一部分,沒有中間的空白。關於是否允許此類空白,沒有說明。

標準 C 允許 `#` 和指令名稱之間出現任意數量的水平製表符和空格,它們被認為是獨立的預處理標記。

指令的起始位置

[編輯 | 編輯原始碼]

許多預處理器允許指令前面有空白,允許縮排巢狀指令。不太靈活的預處理器要求 `#` 字元是原始碼行的第一個字元。

K&R 指出,“以 `#` 開頭的行與該預處理器通訊”。沒有給出“以…開頭”的定義。

標準 C 允許在 `#` 字元之前出現任意數量的空白。此空白不限於水平製表符和空格——允許任何空白。

指令中的空白

[編輯 | 編輯原始碼]

標準 C 要求在指令名稱和指令的終止換行符之間出現的空白必須是水平製表符和/或空格。

K&R 沒有說明此類嵌入式空白的有效性或性質。

如果您使用至少一個空白字元來分隔指令中的標記,則此類字元的實際數量(以及製表符和空格的組合)幾乎總是對預處理器無關緊要。一個例外與使用 `#define` 指令對宏進行良性重新定義有關。這將在本章後面討論。

指令中的宏擴充套件

[編輯 | 編輯原始碼]

根據標準 C,“預處理指令中的預處理標記不會進行宏擴充套件,除非另有說明。[例如],在

#define EMPTY
EMPTY # include <file.h>

第二行的預處理標記序列不是預處理指令,因為它在翻譯階段 4(參見 翻譯階段)的開始沒有以 `#` 開頭,即使它在宏 `EMPTY` 被替換後會這樣。”

指令延續行

[編輯 | 編輯原始碼]

K&R 宣告宏定義(帶引數和不帶引數)可以跨多行原始碼繼續,如果所有要繼續的行都在終止換行符之前包含一個反斜槓。

標準 C 將此概念推廣,並允許任何標記(不僅是預處理器看到的那些,還有語言本身看到的那些)使用反斜槓/換行符序列進行拆分/繼續。

在以下情況下,第二行原始碼以 `#define` 開頭,它不是宏定義指令的開頭,因為它是一個延續行,因此,`#` 前面不是空格和/或水平製表符。

#define NAME … \
#define …

尾隨標記

[編輯 | 編輯原始碼]

嚴格來說,預處理器應該診斷任何超出預期數量的標記。但是,一些實現只處理它們預期的標記,然後忽略指令行中剩下的任何標記。如果是這種情況,原始碼行

#include <header.h> #define MAX 23

(這似乎表明換行符不知何故被省略了,可能在移植轉換過程中丟失了)將導致包含標頭檔案。但是,宏定義將被忽略。另一個例子是

#ifdef DEBUG fp = fopen(name, "r");

在這種情況下,無論 `DEBUG` 是否定義,檔案都永遠不會開啟。

K&R 沒有給出關於這些情況下應該發生什麼的指示。

標準 C 要求如果存在多餘的標記,則進行診斷。

指令中的註釋

[編輯 | 編輯原始碼]

分隔註釋被視為單個空格,因此它們可以出現在任何空白可以出現的地方。由於所有空白或各種空白都可能出現在預處理指令中,因此分隔註釋也可能出現在預處理指令中。例如,在指令中

#define NUM_ELEMS 50 /* … */
#include "global.h" /* … */
/* … */ #define FALSE 0
/* … */ #ifdef SMALL
/* … */ #define TRUE 1 /* … */

在預處理期間,每個分隔註釋都將被替換為單個空格。雖然前兩個指令應該能夠無錯誤地移植,但後三個指令有前導水平空白,正如前面提到的,這不是普遍接受的。

當然,此類分隔註釋可以出現在指令標記之間。

請注意,分隔註釋可以無限期地跨多行原始碼繼續,而不需要反斜槓/換行符終止符。

行註釋也可以與指令一起使用。

翻譯階段

[編輯 | 編輯原始碼]

標準 C 包含對原始碼轉換為標記以供編譯器處理的方式和順序的詳細討論。在 C89 之前,沒有嚴格的規則來管理這個領域,允許以下程式碼被不同的預處理器以不同的方式解釋

#ifdef DEBUG
    #define T
#else
    #define T /\
    *
#endif

T printf(...); /* … */

這裡的意圖可能是透過在未定義 `DEBUG` 時使 `T` 成為註釋的開頭來停用 `printf` 函式呼叫。正如一位程式設計師所說,“為了將 `T` 定義為 `/*`,我們需要欺騙預處理器,因為它在執行任何其他操作之前都會檢測註釋。為此,我們將星號放在延續行上。由於預處理器沒有看到標記 `/*`,所以一切按預期工作。它在 UNIX 環境中的 C 編譯器中執行良好。”

但是,預處理器是否在執行任何其他操作之前都會檢測註釋?由於這個問題的答案因實現而異,讓我們看看標準 C 說了什麼。翻譯階段,因為它們影響預處理器,依次是

  • 反斜槓/換行符對被刪除,以便延續行被拼接在一起。

  • 原始碼被分解為預處理標記和空白字元序列(包括註釋)。

  • 每個註釋都將被替換為空格字元。但是,是否將連續的空白字元壓縮為一個這樣的字元是實現定義的。

  • 預處理指令被執行,宏呼叫被擴充套件。對於此處包含的每個標頭檔案,都會重複執行概述的步驟。

因此,標準 C 編譯器在遇到上述程式碼時,必須診斷出錯誤,因為 #endif 指令將包含在宏定義行開始的註釋中。

一些實現會在查詢預處理器命令之前擴充套件宏,因此會接受以下程式碼

#define d define
#d MAX 43

這在標準 C 中是不允許的。

檢查預處理器輸出

[編輯 | 編輯原始碼]

一些實現擁有獨立於編譯器的預處理器,在這種情況下,會生成一箇中間文字檔案。其他將預處理器和編譯器結合在一起的實現則具有列表選項,允許所有指令的最終效果出現在編譯列表檔案中。它們還可能允許列出定義中包含其他宏的宏的中間擴充套件。請注意,某些實現可能無法在儲存中間程式碼時保留註釋或空白,因為在處理預處理指令之前,註釋可能已被縮減為空格。這與標準 C 的翻譯階段一致。

建議:檢視您的實現中哪些允許儲存預處理器的輸出。一個特別有用的質量保證步驟是比較每個預處理器產生的輸出文字檔案。這使您能夠檢查它們是否以正確的方式擴充套件宏並有條件地包含程式碼。因此,當您將原始檔移植到新的環境時,您可能也希望移植該檔案的預處理版本。

原始檔包含

[編輯 | 編輯原始碼]

#include 指令用於將命名標頭檔案的內容視為正在處理的原始檔的一部分,就好像它們是內聯的。

標準 C 要求標頭檔案包含完整的標記。具體而言,您不能將註釋、字串文字或字元常量的開始或結束部分放在標頭檔案中。標頭檔案還必須以換行符結尾。這意味著您不能跨 #include 貼上標記。

為了幫助避免標準標頭檔案和程式設計師程式碼之間的名稱衝突,標準 C 要求實現者以兩個下劃線或一個下劃線和一個大寫字母開頭他們的識別符號。K&R 中的索引僅包含三個宏:EOFFILENULL。它還列出了大約 20 到 30 個庫函式。沒有提到或要求其他函式。另一方面,標準 C 包含數百個保留識別符號,其中大多數是宏或庫函式名。加上您的編譯器使用的系統相關識別符號以及任何第三方庫使用的識別符號,就會產生潛在的命名衝突。

建議:對於您的每個目標環境,按標頭檔案以及跨標頭檔案按字母順序生成一個保留識別符號列表。將此識別符號列表用於兩個目的:在建立自己的識別符號名稱時要避開的名稱,以及查詢所有集合的並集,以便您知道哪些名稱是通用的,可以在通用程式碼中使用它們。請注意,僅僅因為不同環境中出現了相同名稱的宏,並不意味著它們用於相同的目的。對於您建立的名稱,使用一些唯一的 字首(不是前導下劃線)、字尾或命名樣式,以降低衝突的可能性。

#include 指令格式

[編輯 | 編輯原始碼]

K&R 和標準 C 定義了以下兩種形式的 #include 指令。對於以下形式的指令:

#include "header-name"

K&R 指出,“首先在原始原始檔的目錄中搜索標頭檔案,然後在標準位置序列中搜索。”標準 C 指出,標頭檔案以 實現定義的 方式進行搜尋。

K&R 和標準 C 要求僅在 實現定義的 標準位置搜尋以下形式的指令:

#include <header-name>

C89 添加了第三種形式:

#include identifier

前提是 identifier 最終轉換為 "…"<…> 形式。由於宏名稱是一個識別符號,因此這種格式允許使用標記貼上預處理運算子 ## 來構造或定義標頭檔案名稱,或者在編譯器命令列上定義宏。許多編譯器支援以下形式的命令列引數:-Didentifier/define=identifier,這等效於在要編譯的原始碼中使用 #define identifier 1

如果您的目標編譯器支援上述 -D(或 /d)選項以及 #include identifier 格式,則可以在編譯時指定標頭檔案的完整裝置/目錄路徑名,而不是將該資訊硬編碼到 #include 指令中。

一種幫助隔離硬編碼標頭檔案位置的技術如下。一個主標頭檔案包含以下內容:

/* hnames.h - header names, fully qualified */

#define MASTER "DBA0:[data]master.h"
#define STRUCTURES "DUA2:[templates]struct.h"

現在,如果此標頭檔案包含在另一個頭檔案中,則可以使用以下方式使用這些宏名稱:

#include "hnames.h"

#include MASTER
#include STRUCTURES

如果將程式碼移動到另一個系統,或者將標頭檔案移動到同一系統上的不同位置,只需修改 hnames.h 標頭檔案並重新編譯包含它的所有模組。

標頭檔案名稱

[編輯 | 編輯原始碼]

"…"<…> 格式中標頭檔案名稱的格式和拼寫是 實現相關的

使用反斜槓分隔子目錄和檔名作為檔案路徑名的檔案系統會遇到一個奇怪的問題。DOS 磁碟檔案的完全限定名稱具有以下格式:

\dir1\dir2\ ... \filename.ext

問題出現在以下型別的目錄和檔名中:

\totals\accum.h
\summary\files.h
×\filecopy.h
\volume\backup.h

在這裡,目錄或檔名(或兩者)都以 \x 序列開頭,其中 x 是 C 文字字串中可識別的特殊字元序列。然後問題就變成了,“在包含此標頭檔案時,我該如何命名它?”

根據標準 C,雖然標頭檔案以 "…" 形式編寫,看起來像一個字串文字,但它 不是!因此,必須逐字地取其內容。

建議:如果可能,請避免在標頭檔案名稱中嵌入檔案系統裝置、目錄和子目錄資訊。

在建立新的標頭檔案並將標頭檔案直接對映到系統上的檔名時,請牢記跨系統的檔名限制。例如,一些檔案系統區分大小寫,在這種情況下,STDIO.H、stdio.h 和 Stdio.h 可能代表三個不同的檔案。

C89 指出,“實現可能忽略字母大小寫的區別,並將對映限制為句點之前的六個有效字元。”C99 將有效字元的數量增加到八個。

巢狀標頭檔案

[編輯 | 編輯原始碼]

標頭檔案可能包含 #include 指令。允許的標頭檔案巢狀級別是 實現定義的。K&R 指出,標頭檔案可以巢狀,但沒有給出最小要求。標準 C 要求至少有八個級別的標頭檔案巢狀功能。

建議:如果標頭檔案設計合理,則應能夠多次包含,並且可以按任意順序包含。也就是說,每個標頭檔案應透過包含其依賴的任何標頭檔案而變得自包含。僅將相關內容放在標頭檔案中,並將巢狀限制為三級或最多四級。使用 #ifdef 包裝器圍繞標頭檔案的內容,以確保它們在同一範圍內不會被包含多次。

#include 路徑說明符

[編輯 | 編輯原始碼]

K&R 和標準 C 只提供了兩種(主要的)機制來指定標頭檔案位置搜尋路徑,即 "…"<…>。有時需要或希望有更多機制,或者出於測試目的,您暫時想使用其他位置。許多實現允許在編譯時將一個或多個包含搜尋路徑指定為命令列引數。例如:

cc -I''path1'' -I''path2'' -I''path3'' ''source-file''

告訴預處理器首先使用 path1、然後使用 path2path3 搜尋 "…" 格式的標頭檔案,最後在某個系統預設位置搜尋。缺少此功能或支援的路徑數量少於要求的數量可能會在移植程式碼時導致問題。即使您的編譯器可能支援足夠數量的這些引數,命令列緩衝區的最大大小也可能不足以容納大量冗長的路徑說明符。

建議:如果您的所有實現都具備此功能,請檢查每個實現支援的路徑數量。

標準標頭檔案的修改

[編輯 | 編輯原始碼]

建議:不要透過新增定義、宣告或其他 #include 來修改標準標頭檔案。相反,請建立您自己的雜項或本地標頭檔案,並在所有相關位置包含它。當升級到新的編譯器版本或遷移到不同的編譯器時,除了使本地標頭檔案像以前一樣可用之外,無需做任何額外工作。

宏替換

[edit | edit source]

#define 指令用於將字串定義與宏名稱關聯。由於宏名稱是識別符號,因此它與其他識別符號具有相同的命名約束。K&R 要求有意義的八個字元,而標準 C 要求 31 個字元。

建議:對宏名稱使用最低公分母長度的有效字元。

建議:在拼寫宏名稱時,最常見的約定是隻使用大寫字母、數字和下劃線。

標準 C 要求指定為宏定義一部分的標記必須格式正確(即完整)。因此,宏定義不能僅包含註釋、文字字串或字元常量的開頭或結尾部分。

一些編譯器允許在宏中使用部分標記,以便在擴充套件宏時,將其貼上到它前面和/或後面的標記中。

建議:避免在宏定義中使用部分標記。

宏的定義可能包含算術表示式,例如

#define MIN 5
#define MAX MIN + 30

預處理器不會將其識別為表示式,而是將其識別為一個標記序列,它將在呼叫宏的任何地方進行替換。不允許將 MAX 的定義視為

#define MAX 35

預處理器算術僅在條件包含指令 #if 中起作用。但是,如果使用以下程式碼,則上面 MAX 的原始定義將被視為表示式

#if MAX

#endif

這將擴充套件為

#if MIN + 30

#endif

然後

#if 35

#endif

實現可能對宏定義的大小有限制。

建議:如果您計劃使用定義長度超過 80 個字元的宏,請測試您的環境以檢視它們的限制。

帶引數的宏

[edit | edit source]

帶引數的宏具有以下一般形式

#define name(arg1, arg2, ...,argn) definition

K&R 沒有說明允許的最大引數數量。

標準 C 要求至少支援 31 個引數。

建議:如果您計劃使用超過四個或五個引數的宏,請檢查目標實現的限制。

雖然在宏定義中宏名稱和開始引數列表的左括號之間不允許出現空格,但在宏呼叫中沒有這種約束。

沒有要求宏定義引數列表中指定的所有引數都必須出現在該宏的定義中。

C99 添加了對具有可變引數數量的宏的支援(透過省略號表示法和特殊識別符號 __VA_ARGS__)。

重新掃描宏名稱

[edit | edit source]

宏定義可以引用另一個宏,在這種情況下,將根據需要重新掃描該定義。

標準 C 要求在宏展開期間“關閉”宏的定義,以避免“遞迴死亡”。也就是說,出現在其自身定義中的宏名稱不會被重新展開。這允許將宏的名稱作為引數傳遞給另一個宏。

字串文字和字元常量中的替換

[edit | edit source]

一些實現允許在字串文字和字元常量中替換宏引數,如下所示

#define PR(a) printf("a = %d\n", a)

那麼宏呼叫

PR(total);

擴充套件為

printf("total = %d\n", total);

在不允許這種情況的實現中,宏將擴充套件為

printf("a = %d\n", total);

K&R 指出“字串或字元常量中的文字不受替換影響”。

標準 C 不支援在字串和字元常量中替換宏引數。但是,它確實提供了(C89 新增)字串化運算子(#),以便可以實現相同的效果。例如,

#define PR(a) printf(#a " = %d\n", a)

PR(total);

擴充套件為

printf("total" " = %d\n", total);

並且由於標準 C 允許將相鄰字串連線起來,因此這將變為

printf("total = %d\n", total);

命令列宏定義

[edit | edit source]

許多編譯器允許使用 -Didentifier/define=identifier 形式的命令列引數定義宏,這等效於在被編譯的原始碼中包含 #define identifier 1。一些編譯器允許以這種方式定義帶引數的宏。

命令列緩衝區的大小或命令列引數的數量可能不足以指定所有必需的宏定義,尤其是在您使用此機制來指定在許多 #include 指令中使用的識別符號時。

建議:確定此功能是否在所有實現中都存在。只要您將每個識別符號的長度保持在最小值,至少應該支援五個或六個識別符號。(請注意,如果您使用 31 個字元的識別符號名稱,則可能超過命令列緩衝區大小。)

宏重新定義

[edit | edit source]

許多實現允許在現有宏被 #undef 之前重新定義它。這樣做(通常)的目的是允許在多個頭檔案中使用相同的宏定義,這些標頭檔案都包含在同一個作用域中。但是,如果一個或多個定義與其他定義不同,則會發生嚴重問題。例如,

#define NULL 0

#define NULL 0L

導致程式碼的第一部分使用零作為 NULL 的值進行編譯,而最後一部分使用零 long 進行編譯。這在使用 f(NULL) 時會導致嚴重問題,因為傳遞給 f 的物件的尺寸可能與 f 預期的尺寸不同。

標準 C 允許重新定義宏,前提是定義相同。這被稱為 良性重新定義。那麼“相同”到底是什麼意思?基本上,它要求宏定義的拼寫完全相同,並且根據處理標記之間空格的方式,多個連續空格字元可能具有重要意義。例如,

1. #define MACRO a macro
2. #define MACRO a macro
3. #define MACRO a<tab>macro
4. #define MACRO a  macro
5. #define MACRO example

宏 1 和 2 相同。宏 3 和 4 也可能與 1 和 2 相同,具體取決於對空格的處理。宏 5 一定會被標記為錯誤。請注意,這不能解決對同一宏具有不同定義且不在相同作用域中的問題。

建議:在多個位置(通常在標頭檔案中)使用完全相同的宏定義是合理的。實際上,由於其他地方提到的原因,鼓勵使用這種想法。但是,避免對同一宏使用不同的定義。由於使用多個連續空格字元會導致不同的拼寫(如上面的宏 3 和 4 中所示),因此您應該只使用一個空格字元來分隔標記,並且您使用的字元應該保持一致。由於水平製表符可能會轉換為空格,因此建議使用空格分隔符。

透過宏重新定義,我們的意思是將無引數宏重新定義為具有相同名稱但沒有引數的宏,或者將具有引數的宏重新定義為具有相同名稱、相同引數數量和拼寫的宏。

建議:即使您的實現允許這樣做,也不要將無引數宏重新定義為帶引數的宏,反之亦然。標準 C 不支援這樣做。

預定義的標準宏

[edit | edit source]

標準 C 指定了以下預定義宏

  • __DATE__C89 – 編譯日期

  • __FILE__C89 – 被編譯的原始檔的名稱;但是,沒有提及這個名稱是否是一個完全限定的路徑名

  • __LINE__C89 – 被編譯的原始檔中的當前行號

  • __STDC__C89 – 如果編譯器符合標準 C 的某個版本,則值為 1(參見 __STDC_VERSION__)。不要假設此名稱的存在就意味著符合;這需要值為 1。實現可能會將此宏定義為 0 表示“不完全符合”,或定義為 2 表示“包含擴充套件”。要確定編譯器是否符合 C89,請檢查 __STDC__ 是否被定義為 1 以及 __STDC_VERSION__ 是否未被定義

  • __STDC_HOSTED__C99 – 指示實現是託管的還是獨立的

  • __STDC_VERSION__C95 – 此編譯器符合的標準 C 版本(參見 __STDC__),如下所示:C95 199409L、C99 199901L、C11 201112L 和 C17 201710L。

  • __TIME__C89 – 編譯時間

嘗試 #define#undef 任何這些預定義名稱會導致 未定義的行為

__STDC_ 開頭的宏名稱保留供將來標準化使用。

K&R 中沒有預定義的宏。__LINE____FILE__ 在 C89 之前的一些實現中可用,__DATE____TIME__ 也是如此;但是,日期字串格式各不相同。

標準 C 要求“任何其他預定義的宏名都以一個下劃線開頭,後跟一個大寫字母或第二個下劃線”。它還禁止定義宏 __cplusplus(無論是預定義的還是在標準標頭檔案中)。

C++ 注意事項:標準 C++ 預定義了 __cplusplus,它的展開方式與 __STDC_VERSION__ 類似,透過編碼版本號來實現。此外,標準一致的 C++ 實現是否預定義 __STDC____STDC_VERSION__實現定義的

透過編譯器命令列選項定義的宏不被視為預定義宏,即使從概念上講它們是在原始碼被處理之前定義的。

除了標準 C 指定的宏之外,所有其他預定義的宏名都是 實現定義的。沒有已知的名稱集,但 GNU C 編譯器提供了一個龐大而豐富的集,其他實現可能效仿。

符合標準的實現可能會根據條件定義其他宏(參見 條件定義的標準宏)。

條件定義的標準宏

[edit | edit source]

標準 C 允許,但不強制要求以下宏也被預定義

  • __STDC_ANALYZABLE__C11

  • __STDC_IEC_559__C99

  • __STDC_IEC_559_COMPLEX__C99 定義了 __STDC_NO_COMPLEX__ 的實現不能也定義 __STDC_IEC_559_COMPLEX__

  • __STDC_ISO_10646__C99 也由標準 C++ 定義

  • __STDC_LIB_EXT1__C11

  • __STDC_MB_MIGHT_NEQ_WC__C11

  • __STDC_NO_ATOMICS__C11

  • __STDC_NO_COMPLEX__C11

  • __STDC_NO_THREADS__C11

  • __STDC_NO_VLA__C11

  • __STDC_UTF_16__C11

  • __STDC_UTF_32__C11

宏定義限制

[edit | edit source]

實現的預處理器符號表中可以容納的條目數量可能會有很大差異,宏定義可用的總字串空間量也會有很大差異。

C89 要求至少 1024(C99 及更高版本為 4095)個宏識別符號能夠在原始檔中同時定義(包括所有包含的標頭檔案)。雖然此保證可能允許如此多的宏,但符合標準的實現可能要求每個宏定義的長度受限。它當然保證如此多的無限長度和複雜度的宏定義。

K&R 對同時宏定義的數量或大小沒有限制。

建議:如果您預計會有大量的(超過數百個)同時宏定義,請編寫一個程式,該程式可以生成包含任意數量和複雜度的宏的測試標頭檔案,以檢視每個實現可以處理哪些。還有一些激勵措施,只包含那些需要包含的標頭檔案,並將標頭檔案模組化,使其只包含相關內容。在多個頭檔案中包含相同的宏定義是完全可以接受的。例如,一些實現者在幾個標頭檔案中定義 NULL,這樣就不必為了一個宏名而預處理整個 stdio.h

宏定義堆疊

[edit | edit source]

一些實現允許宏堆疊。也就是說,如果一個宏名在作用域內,並且定義了相同名稱的宏,第二個定義將隱藏第一個定義。如果刪除第二個定義,第一個定義將再次回到作用域中。例如,

#define MAX 30

 /* MAX is 30 */


#define MAX 35

 /* MAX is 35 */

#undef MAX

 /* MAX is 30 */

標準 C 不允許宏定義堆疊。

K&R 指出 #undef 的使用“會導致識別符號的預處理器定義被遺忘”,可能完全被遺忘。

# 字串化運算子

[edit | edit source]

這是 C89 的發明。

C99 添加了對空宏引數的支援,每個引數都會生成字串 ""

### 運算子的求值順序是 未指定的

## 符號貼上運算子

[edit | edit source]

這是 C89 的發明。它允許宏展開構造一個可以重新掃描的符號。例如,如果宏定義為

#define PRN(x) printf("%d", value ## x)

宏呼叫

PRN(3);

生成以下程式碼

printf("%d", value3);

在標準 C 之前,解決此問題的常見方法如下

#define M(a, b) a/* */b

在這裡,定義不是 a b(因為註釋被空格替換),而是 ab,因此形成了一個新的符號,然後對其進行重新掃描。K&R 或標準 C 都不支援這種做法。標準 C 對此的處理方法是

#define M(a, b) a ## b

其中 ## 運算子周圍的空格是可選的。

標準 C 指定在 A ## B ## C 中,求值順序是 實現定義的

以下示例存在一個有趣的情況

#define INT_MIN -32767

int i = 1000-INT_MIN;

在這裡,宏展開生成 1000--32767,這看起來可能應該生成語法錯誤,因為 1000 不是左值。但是,標準 C 透過其“翻譯階段”來解決這個問題,要求在傳遞給編譯器時,預處理符號 -32767 保持其含義。也就是說,兩個減號不被識別為自動遞減符號 --,即使它們在展開的文字流中相鄰。但是,非標準實現可能會重新掃描文字,透過將兩個 符號貼上在一起來生成不同的符號序列。

建議:為了避免此類宏定義被誤解,請用括號將其括起來,例如 #define INT_MIN (-32767)

### 運算子的求值順序是 未指定的

重新定義關鍵字

[edit | edit source]

一些實現(包括標準 C)允許重新定義 C 語言關鍵字。例如,

#if __STDC__ != 1
#define void int
#endif

建議:不要無緣無故地重新定義語言關鍵字。

#undef 指令

[edit | edit source]

#undef 可用於刪除庫宏以訪問真實函式。如果不存在宏版本,標準 C 要求忽略 #undef,因為不存在的宏可以作為 #undef 的主題而不會出錯。

有關在堆疊宏實現中使用 #undef 的討論,請參閱 宏定義堆疊

標準 C 不允許預定義的標準宏 (預定義的標準宏) 被 #undef

條件包含

[edit | edit source]

此功能是 C 環境中最強大的部分之一,可用於編寫要在不同目標系統上執行的程式碼。

建議:儘可能地使用條件包含指令。如果您有或建立了一組有意義的宏來區分一個目標環境與另一個目標環境,這將變得更容易。有關主機特性的詳細資訊,請參見 <limits.h><float.h>

#if 算術

[edit | edit source]

#if 指令的目標是一個常量表達式,它將根據值 0 進行測試。

一些實現允許在常量表達式中使用 sizeof 運算子,如下所示

#if sizeof(int) == 4
    int total;
#else
    long total;
#endif

嚴格來說,預處理器是一個宏處理器和字串替換程式,不需要了解資料型別或 C 語言。請記住,sizeof 是一個 C 語言編譯時運算子,而在此時,我們正在進行預處理,而不是編譯。

K&R 對預處理器的常量表達式的定義與語言相同,因此這意味著 sizeof 在此處是被允許的。它沒有提到在常量表達式中使用強制轉換(即使在語言中也是如此)。

標準 C 要求常量表達式中不包含強制轉換或列舉常量。使用標準 C 時,sizeof 運算子在此上下文中是否受支援是 實現定義的。也就是說,雖然允許,但並不保證。請注意,如果存在列舉常量,它將被視為未知宏,因此將預設為值 0。

建議:在條件常量表達式中不要使用 sizeof、強制轉換或列舉常量。為了解決無法使用 sizeof 的問題,您可以透過使用標頭檔案 limits.h 來確定有關環境的某些屬性。

C89 指出,“… 按照使用算術規則對控制常量表達式進行求值,該算術規則至少具有 數值限制 中指定的範圍,除了 intunsigned int 行為方式與 longunsigned long 分別具有相同的表示形式一樣。”

C99 將此更改為,“… 控制常量表達式,根據 6.6 的規則進行評估,但所有帶符號整數型別和所有無符號整數型別都按其分別在標頭檔案 <stdint.h> 中定義的型別 intmax_tuintmax_t 的表示形式進行操作。”

不允許使用浮點常量。

建議: 不要依賴下溢或上溢,因為算術屬性在一位補碼和二位補碼以及打包十進位制機器上差異很大。 如果存在帶符號運算元,請勿使用右移運算子,因為當符號位被設定時,結果是 實現定義的

字元常量可以合法地作為常量表達式的一部分(在其中它被視為整數)。 字元常量可以包含任何任意位模式(透過使用 '\nnn' 或 '\xhhh')。 一些實現支援其值為負數的字元常量(例如,'\377' 和 '\xFA' 的高位被設定)。

標準 C 指出,單字元字元常量是否可以為負值是 實現定義的。 K&R 沒有說明。

一些實現支援多字元常量,標準 C 也是如此。

建議: 不要使用其值為負數的字元常量。 此外,由於多字元常量中字元的順序和含義是 實現定義的,因此不要在 #if 常量表達式中使用它們。

在標準 C 中,如果常量表達式包含當前未定義的宏名稱,則該宏將被視為使用值為 0 定義的。 宏名稱僅以這種方式解釋;它並沒有真正使用該值定義。

K&R 沒有為此情況提供任何規定。

建議: 不要使用未定義宏在常量表達式中評估為 0 的事實。 如果宏定義從標頭檔案或從編譯時的命令列中省略,那麼使用此預設規則會導致它被錯誤地解釋為使用值為 0 定義。 在使用宏之前測試宏是否已定義並不總是切合實際。 但是,對於預計在命令列中定義的宏,值得進行檢查,因為如果您手動鍵入編譯命令列,則很容易省略宏定義。 為了進一步避免此類問題,請使用命令過程或指令碼編譯程式碼,尤其是在命令列中存在大量且冗長的包含路徑和宏時。

常量表達式可能會產生錯誤,例如,如果遇到除以 0 的情況。(如果用作分母的宏名稱尚未定義並預設為 0,則這是可能的。)一些實現可能會將其標記為錯誤,而另一些則不會。 有些可能會繼續處理,假設整個表示式的值為 0。

建議: 不要假設您的實現會在確定 #if 常量表達式包含數學錯誤時生成錯誤。

K&R 沒有在常量表達式中允許的運算子中包含 ! 一元運算子。 這通常被認為是疏忽或排版錯誤。

defined 運算子

[edit | edit source]

有時有必要使用巢狀的條件包含結構,例如

#ifdef VAX
    #ifdef VMS
        #ifdef DEBUG
            
        #endif
    #endif
#endif

這得到了 K&R 和標準 C 的支援。 標準 C(以及 C89 之前的一些實現)提供了 defined 預處理器一元運算子,使此結構更加優雅。 例如,

#if defined(VAX) && defined(VMS) && defined(DEBUG)

#endif

標準 C 本質上保留了識別符號 defined——它不能在其他地方用作宏名稱。

建議: 除非所有環境都支援 defined 運算子,否則不要使用它。

#elif 指令

[edit | edit source]

以下笨拙的結構也常用於編寫可移植程式碼。 它得到了 K&R 和標準 C 的支援。

#if MAX >= TOTAL1
    
#else
    #if MAX >= TOTAL2
    
    #else
        #if MAX >= TOTAL3
            
        #else
            
        #endif
    #endif
#endif

指令 #elif 極大地簡化了巢狀的 #if,如下所示。

#if MAX >= TOTAL1
    
#elif MAX >= TOTAL2
    
#elif MAX >= TOTAL3
    
#else
    
#endif

建議: 除非所有環境都支援 #elif 指令,否則不要使用它。

巢狀條件指令

[edit | edit source]

標準 C 保證至少有八個級別的巢狀。

K&R 指出這些指令可以巢狀,但沒有保證的最小值。

建議: 除非所有實現都允許更多,否則使用不超過兩個或三個級別的條件指令巢狀。

行控制

[edit | edit source]

#line 指令的語法(最終)是以下之一

#line line-number
#line line-number filename

其中行號和檔名分別用於更新 __LINE____FILE__ 預定義宏。

標準 C 允許在檔名位置使用宏名稱或字串文字。 它還允許在行號位置使用宏,前提是其值為十進位制數字序列(其中任何前導零都是多餘的,並不意味著“八進位制”。 事實上,任何預處理標記都可以跟隨 #line,前提是在宏展開之後,存在兩種形式之一。

如果 __LINE__ 用於跨越多個物理行的專案(預處理指令或宏呼叫)中,實現對 __LINE__ 的值存在差異。

空指令

[edit | edit source]

標準 C 允許使用以下形式的空指令

#

此指令沒有效果,通常只在機器生成的程式碼中找到。 雖然它在實現中存在多年,但它沒有在 K&R 中定義。

#pragma 指令

[edit | edit source]

#pragma 是 C89 的發明。 此指令的目的是為實現提供一種機制來擴充套件預處理器的語法。 這是可能的,因為預處理器會忽略它無法識別的任何 pragma。 #pragma 指令的語法和語義是 實現定義的,儘管一般格式是

#pragma token-sequence

pragma 的可能用途是控制編譯列表分頁和行格式,啟用和停用最佳化,以及啟用和停用 lint 之類的檢查。 實現者可以為他們想要的任何目的發明 pragma。

以下形式的 pragma 指令

#pragma STDC token-sequence

保留供標準 C 使用,例如 pragma FP_CONTRACTFENV_ACCESS 和 CX_LIMITED_RANGE(所有這些都是由 C99 新增的)。

Pragma 運算子

[edit | edit source]

C99 添加了此一元、僅預處理器運算子,它具有以下形式

_Pragma ( string-literal )

#error 指令

[edit | edit source]

這是 C89 的發明。 它的格式是

#error token-sequence

它會導致實現生成包含指定令牌序列的診斷訊息。

一種可能的用途是報告您期望定義的宏,但發現未定義。 例如,您正在移植包含可變長度陣列(或執行緒)的程式碼,但條件定義的宏 (條件定義的標準宏) __STDC_NO_VLA__(或 __STDC_NO_THREADS__)已定義。

非標準指令

[edit | edit source]

一些實現接受其他預處理指令。由於這些擴充套件通常與實現的特定環境相關,因此在其他環境中幾乎沒有用處。因此,它們必須在要移植的程式碼中識別出來,並且如果需要,以其他方式實現。

庫簡介

[編輯 | 編輯原始碼]

術語定義

[編輯 | 編輯原始碼]

在 K&R 中,字元型別為 char,字串是 char 的陣列。C89 引入了 多位元組字串移位序列 的概念,以及 寬字元(型別為 wchar_t)和 寬字串(型別為 wchar_t[])。C89 庫還包括用於處理所有這些內容的函式,標準的後續版本添加了更多標頭檔案和函式。

在 C89 之前,C 庫以所謂的“美國英語”模式執行,例如,printf 使用的十進位制點是句號。C89 引入了 區域設定 的概念,以便到那時為止的傳統 C 環境由 "C" 區域設定定義;它還定義了標頭檔案 <locale.h>。一些標準 C 庫函式的行為受當前區域設定的影響;也就是說,它們是 區域設定特定的

標準標頭檔案

[編輯 | 編輯原始碼]

必需內容

[編輯 | 編輯原始碼]

C89 定義了以下標頭檔案:<assert.h><ctype.h><errno.h><float.h><limits.h><locale.h><math.h><setjmp.h><signal.h><stdarg.h><stddef.h><stdio.h><stdlib.h><string.h><time.h>

C95 添加了 <iso646.h><wchar.h><wctype.h>

C99 添加了 <complex.h><fenv.h><inttypes.h><stdbool.h><stdint.h><tgmath.h>

C11 添加了 <stdalign.h><stdatomic.h><stdnoreturn.h><threads.h><uchar.h>.

C17 沒有新增新的標頭檔案。

這些標頭檔案被定義為具有小寫名稱,並且必須使用上述拼寫方式被符合標準的實現正確定位。雖然一些檔案系統支援混合大小寫檔名,但你不應該以任何其他方式拼寫標準標頭檔案名稱,而應該按照標準定義的方式拼寫。

每個標頭檔案都是自包含的。也就是說,它包含呼叫其內部宣告的例程所需的所有宣告和定義。也就是說,標頭檔案並不一定包含其函式可以 返回 的所有宏定義。例如,<stdlib.h> 中的 strtod 可以返回 HUGE_VAL 的值,而 ERANGE 可能會儲存在 errno 中;然而,這些宏並沒有在 <stdlib.h> 中定義。要使用它們,必須同時包含 <errno.h><math.h>

為了自包含,幾個標頭檔案定義了相同的名稱(例如 NULLsize_t)。

標準 C 庫中的所有函式都使用函式原型宣告。

標準標頭檔案可以按任何順序和多次包含在同一個作用域中,而不會產生不良影響。唯一例外是 <assert.h>,如果它被多次包含,它的行為會根據宏 NDEBUG 的存在而有所不同。

為了嚴格符合標準,標準 C 禁止程式從 內部 外部宣告或定義中包含標準標頭檔案。這意味著你不應該從函式內部包含標準標頭檔案,因為函式是外部定義。

標準庫函式的許多原型包含 C89 發明或採用的關鍵字和派生型別。這些包括 constfpos_tsize_tvoid *。在將這些應用於已存在多年的函式時,它們仍然與在 C89 之前呼叫這些函式相容。

標準 C 要求託管的 C 實現支援該標準版本為其定義的所有標準標頭檔案。在 C89 中,獨立實現只需要提供 <float.h><limits.h><stdarg.h><stddef.h>。C95 添加了 <iso646.h>。 C99 添加了 <stdbool.h><stdint.h>。C11 添加了 <stdalign.h><stdnoreturn.h>。C17 沒有新增新的要求。

可選內容

[編輯 | 編輯原始碼]

C11 添加了一個名為“邊界檢查介面”的附錄,它“指定了一系列可選擴充套件,這些擴充套件在減輕程式中的安全漏洞方面可能非常有用,並且包括在現有標準標頭檔案中宣告或定義的新函式、宏和型別。”

如果實現定義了宏 __STDC_LIB_EXT1__,它必須提供該附錄中的所有可選擴充套件。這些擴充套件適用於以下標頭檔案:<errno.h><stddef.h><stdint.h><stdio.h><stdlib.h><string.h><time.h><wchar.h>

定義了宏 __STDC_LIB_EXT1__ 的實現允許透過在 #include 包含包含此類擴充套件的標準標頭檔案之前,使用程式 #define __STDC_WANT_LIB_EXT1__ 為 0 來排除相關的庫擴充套件。如果改為將 __STDC_WANT_LIB_EXT1__ 定義為 1,則啟用這些擴充套件。

保留識別符號

[編輯 | 編輯原始碼]

標準標頭檔案中宣告的所有外部識別符號都是保留的,無論其關聯的標頭檔案是否被引用。也就是說,不要假設僅僅因為你從未包含 <time.h>,你就可以安全地定義自己的名為 clock 的外部函式。請注意,宏和 typedef 名稱 包含在此保留中,因為它們不是外部名稱。

以下劃線開頭的外部識別符號是保留的。所有其他庫識別符號應以兩個下劃線或一個下劃線後跟一個大寫字母開頭。

庫函式的使用

[編輯 | 編輯原始碼]

並非所有標準庫例程都驗證其輸入引數。在這種情況下,如果你傳入無效引數,其行為是 未定義的

允許實現將任何必需的例程實現為宏,該宏在適當的標頭檔案中定義,前提是宏的展開是“安全的”。也就是說,如果使用帶有副作用的引數,則不應觀察到任何不良影響。如果你包含標準標頭檔案,你不應該顯式宣告你打算從該標頭檔案中使用的任何例程,因為該標頭檔案中定義的該例程的任何宏版本都會導致你的宣告被展開(可能是錯誤的或產生語法錯誤)。

在使用庫例程的地址時,你應該小心,因為它當前可能被定義為宏。因此,你應該首先 #undef 該名稱或使用 (name) 而不是 name 來引用它。請注意,可以在同一個作用域中呼叫宏和函式版本的同一個例程,而無需首先使用 #undef

在使用庫例程時,強烈建議你包含適當的標頭檔案。如果你選擇不這樣做,你應該使用原型表示法顯式宣告函式,尤其是對於像 printf 這樣的例程,它們採用可變長度引數列表。這樣做的原因是,當使用原型時,編譯器傳遞引數的機制可能與不使用原型時不同。例如,使用正確的原型在作用域內,編譯器確切地知道期望的引數數量及其型別。對於固定長度引數列表,它可以選擇在暫存器中傳遞前兩個或三個(或更多)引數,而不是在堆疊中傳遞。因此,如果你在沒有原型的條件下編譯程式碼,而庫是使用原型編譯的,則連結可能會失敗。

非標準標頭檔案

[編輯 | 編輯原始碼]

最初與 UNIX 系統一起提供的實際標準 C 庫包含通用例程和特定於作業系統的例程。幾乎所有通用例程都被 C89 採用,而大多數特定於作業系統的例程則被 IEEE POSIX 委員會採納。少數例程被兩個或兩個組都不想要,並且在兩個組之間友好地分配。應該注意的是,少數宏和函式由兩組以不同的方式定義和宣告。特別是,它們對 <limits.h> 的版本並不相同。但是,兩個標準組的意圖都是讓 C 程式能夠同時符合 ISO C 和 POSIX 標準。

標準 C 庫中不包括許多常用的標頭檔案。這些包括 bios.hconio.hcurses.hdirect.hdos.hfcntl/hio.hprocess.hsearch.hshare.hstat.hsys/locking.hsys/stat.hsys/timeb.hsys/types.hvalues.h

其他名稱未被 C89 採納的標頭檔案,其所有或部分功能可透過各種標準 C 標頭檔案獲得。這些包括 malloc.hmemory.hvarargs.h,它們分別在 <stdlib.h><string.h><stdarg.h> 中重生或合併。

<assert.h> – 診斷

[edit | edit source]

C11 添加了對靜態斷言 (靜態斷言) 的支援,其中一部分涉及向此標頭檔案新增一個名為 static_assert 的宏。

C++ 考慮因素:等效的標準 C++ 標頭檔案是 <cassert>

程式診斷

[edit | edit source]

assert

[edit | edit source]

標準 C 要求 assert 作為宏實現,而不是作為實際函式實現。但是,如果該宏定義被 #undef 以訪問實際函式,則行為是未定義的。

訊息輸出的格式是 實現定義的。但是,標準 C 旨在將用作 assert 引數的表示式以其文字形式(如原始碼中存在的那樣)輸出,以及失敗 assert 的呼叫所在的原始檔名和行號(分別由 __FILE____LINE__ 表示)。具體來說,表示式 MAX - MIN 應輸出為 100 - 20,而不是 80(假設 MAX 定義為 100MIN 定義為 20)。

C89 要求傳遞給 assert 的引數的型別為 int。但是,C99 將其擴充套件為任何標量型別。

由於 assert 是一個宏,因此要注意不要給它帶有副作用的表示式——你不能依賴宏只評估你的表示式一次。

<complex.h> – 複數運算

[edit | edit source]

C99 添加了此標頭檔案,並使對複數型別和運算的支援成為可選。

缺少可選的預定義宏 __STDC_NO_COMPLEX__ 表示支援複數型別及其相關的算術運算。此外,可選的預定義宏 __STDC_IEC_559_COMPLEX__ 的存在表示複數支援符合 IEC 60559,如 C 標準的附錄所述。

以下函式名稱保留供標準 C 在此標頭檔案中將來可能使用:cerfcerfccexp2cexpm1clog10clog1pclog2clgammactgamma 以及那些以 fl 為字尾的名稱。

C++ 考慮因素:等效的標準 C++ 標頭檔案是 <ccomplex>。請注意,C++17 棄用 了此標頭檔案。

<ctype.h> – 字元處理

[edit | edit source]

在標準 C 中,所有透過 <ctype.h> 提供的函式都接受一個 int 引數。但是,傳遞的值必須是 unsigned char 中可表示的,或者必須是宏 EOF。如果引數具有任何其他值,則行為是 未定義的

C89 引入了區域的概念。預設情況下,C 程式在 "C" 區域中執行,除非已呼叫 setlocale 函式(或實現的正常作業系統預設區域與 "C" 不同)。在 "C" 區域中,ctype.h 函式具有它們在 C89 之前所具有的含義。當選擇除 "C" 以外的區域時,符合特定字元型別測試的字元集可能會擴充套件以包含其他實現定義的字元。例如,在西歐執行的實現可能會包含帶有變音符號的字元,例如變音符、脫字元和波浪號。因此,例如,isalpha 測試 ä 是否為真 是實現定義的,基於當前區域。

許多實現使用比表示主機字元集所需的位數更多的位表示字元;例如,支援 7 位 ASCII 的 8 位字元系統。但是,此類實現通常使用其他未使用的位來支援擴充套件字元集。此外,C 程式設計師可以自由地將 char 視為一個小整數,並將適合它的任何位模式儲存到其中。

char 包含表示除機器本機字元集以外的其他內容的位模式時,除非您的當前區域允許,否則它不應傳遞給 <ctype.h> 函式。即使這樣,結果也是 實現定義的。此外,您應該確定 char 是否有符號,因為包含 0x80 的 8 位 char,例如,當它是有符號的與無符號的時,可能會被非常不同的對待。

標準 C 要求所有 <ctype.h> 函式實際上都作為函式實現。它們也可以作為宏實現,前提是保證它們是安全的宏。也就是說,它們的引數只評估一次。標準 C 允許對任何 <ctype.h> 名稱進行 #undef 以獲取相應的函式版本。

建議:當引數測試為真時,字元測試函式實際返回的值是 實現定義的。因此,您應該對這些值進行邏輯測試,而不是算術測試。

標準 C 保留所有以 isto 開頭的函式名稱,後跟一個小寫字母(後跟任何其他識別符號字元),以供將來新增到執行時庫。

以下函式具有與區域相關的行為:isalphaisblankisgraphislowerisprintispunctisspaceisuppertouppertolower

在 C89 之前,以下函式透過此標頭檔案廣泛可用:isasciitoasciiiscsymiscsymf。標準 C 不支援這些函式。

C++ 考慮因素:等效的標準 C++ 標頭檔案是 <cctype>

字元分類函式

[edit | edit source]

isalpha 函式

[edit | edit source]

使用 isalpha 而不是以下內容

if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))

因為在某些字元集中(例如 EBCDIC),大寫和小寫字母組不佔用連續的內部值範圍。

isblank 函式

[edit | edit source]

C99 添加了此函式。

islower 函式

[edit | edit source]

參見 isalpha

isupper 函式

[edit | edit source]

參見 isalpha

字元大小寫對映函式

[edit | edit source]

tolower 函式

[edit | edit source]

在非"C"區域設定中,從大寫到小寫的對映可能不是一對一的。例如,大寫字母可能由兩個連在一起的小寫字母表示,或者可能根本沒有小寫等效字母。同樣,對於toupper也是如此。

<errno.h> – 錯誤

[編輯 | 編輯原始碼]

從歷史上看,errno 被宣告為一個extern int 變數;但是,標準 C 要求 errno 是一個宏。(但是,該宏可以擴充套件為對同名函式的呼叫。)具體來說,errno 是一個宏,它擴充套件為型別 int 的可修改左值*。因此,errno 可以定義為類似於 *_Errno() 的東西,其中實現提供的函式 _Errno 返回一個指向 int 的指標。將 errno #undef 以嘗試獲取底層物件會導致 未定義的行為

各種標準庫函式被記錄為在檢測到某些錯誤時將 errno 設定為非零值。標準 C 要求該值為正數。它還指出,沒有庫例程需要清除 errno(即,將其值賦為 0),當然,您永遠不應該依賴庫例程來執行此操作。

從歷史上看,定義 errno 的有效值的宏以 E 開頭。雖然各種系統演化出一組名稱,但這些名稱中的一些拼寫和含義存在很大差異。因此,C89 只定義了兩個宏:EDOMERANGE。C99 添加了 EILSEQ。額外的宏定義,以 E 和數字或大寫字母開頭,可能由符合標準的實現指定。

以下是一些常見的 E* 擴充套件宏

E2BIG /* arg list too long */
EACCES /* permission denied */
EAGAIN /* no more processes */
EBADF /* bad file number */
EBUSY /* mount device busy */
ECHILD /* no children */
EDEADLK /* deadlock avoided */
EEXIST /* file exists */
EFAULT /* bad address */
EFBIG /* file too large */
EINTR /* interrupted system call */
EINVAL /* invalid argument */
EIO /* i/o error */
EISDIR /* is a directory */
EMFILE /* too many open files */
EMLINK /* too many links */
ENFILE /* file table overflow */
ENODEV /* no such device */
ENOENT /* no such file or directory */
ENOEXEC /* exec format error */
ENOLCK /* no locks available */
ENOMEM /* not enough core */
ENOSPC /* no space left on device */
ENOTBLK /* block device required */
ENOTDIR /* not a directory */
ENOTTY /* not a typewriter */
ENXIO /* no such device or address */
EPERM /* not owner */
EPIPE /* broken pipe */
EROFS /* read-only file system */
ESPIPE /* illegal seek */
ESRCH /* no such process */
ETXTBSY /* text file busy */
EXDEV /* cross-device link */

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項: 等效的標準 C++ 標頭是 <cerrno>

<fenv.h> – 浮點環境

[編輯 | 編輯原始碼]

C99 添加了此標頭。

標準 C 為所有以 FE_ 開頭,後跟大寫字母的宏名稱保留了空間,以便將來為此標頭新增內容。

C++ 注意事項: 等效的標準 C++ 標頭是 <cfenv>

<float.h> – 浮點型別的特性

[編輯 | 編輯原始碼]

此標頭透過一系列宏定義目標系統的浮點特性,這些宏的值在很大程度上是 實現定義的

截至 C17,幾乎所有宏都在 C89 中定義。例外情況是 DECIMAL_DIGFLT_EVAL_METHOD,它們在 C99 中新增;以及 FLT_DECIMAL_DIGDBL_DECIMAL_DIGLDBL_DECIMAL_DIGFLT_HAS_SUBNORMDBL_HAS_SUBNORMLDBL_HAS_SUBNORMFLT_TRUE_MINDBL_TRUE_MINLDBL_TRUE_MIN,它們在 C11 中新增。

雖然許多系統使用 IEEE-754 格式來表示浮點型別,但在開發 C89 時,還有其他三種格式被普遍使用,C89 都能容納它們。

標準 C 為宏 FLT_ROUNDS 定義了值 -13。所有其他值指定 實現定義的舍入行為。

標準 C 為宏 FLT_EVAL_METHOD 定義了值 -1 到 2。FLT_EVAL_METHOD 的所有其他負值都表示 實現定義的行為。有關此宏的值可能對浮點常量產生的影響,請參閱 浮點常量

C++ 注意事項: 等效的標準 C++ 標頭是 <cfloat>

<inttypes.h> – 整數型別的格式轉換

[編輯 | 編輯原始碼]

C99 添加了此標頭。

標準 C 為所有以 PRISCN 開頭,後跟小寫字母或 X 的宏名稱保留了空間,以便將來為此標頭新增內容。

C++ 注意事項: 等效的標準 C++ 標頭是 <cinttypes>

<iso646.h> – 替代拼寫

[編輯 | 編輯原始碼]

C95 添加了此標頭。

C++ 注意事項: 等效的標準 C++ 標頭是 <ciso646>。標準 C 在此標頭中定義的宏是標準 C++ 中的關鍵字。

<limits.h> – 數值限制

[編輯 | 編輯原始碼]

此標頭透過一系列宏定義目標系統的整數特性,這些宏的值在很大程度上是實現定義的。

幾乎所有宏都在 C89 中定義。例外情況是 LLONG_MINLLONG_MAXULLONG_MAX,它們是在 C99 中新增的。

C++ 注意事項: 等效的標準 C++ 標頭是 <climits>

<locale.h> – 本地化

[編輯 | 編輯原始碼]

型別 struct lconv 的幾乎所有成員都是由 C89 定義的。例外情況是 int_p_cs_precedesint_n_cs_precedesint_p_sep_by_spaceint_n_sep_by_spaceint_p_sign_posnint_n_sign_posn,它們是在 C99 中新增的。實現可以新增其他成員。

標準 C 已將以 LC_ 開頭,後跟大寫字母的名稱空間保留給實現使用,以便它們可以新增額外的區域設定子類別宏。

標準 C 定義的區域設定是 "C""",後者是 特定於區域設定的本機環境。所有其他用於識別所有其他區域設定的字串都是 實現定義的

C++ 注意事項: 等效的標準 C++ 標頭是 <clocale>

區域設定控制

[編輯 | 編輯原始碼]

setlocale 函式

[編輯 | 編輯原始碼]

如果您修改 setlocale 返回的字串的內容,則行為是 未定義的

<math.h> – 數學

[編輯 | 編輯原始碼]

C99 添加了型別 float_tdouble_t;宏 FP_FAST_FMAFP_FAST_FMAFFP_FAST_FMALFP_ILOGB0FP_ILOGBNANFP_INFINITEFP_NANFP_NORMALFP_SUBNORMALFP_ZEROHUGE_VALFHUGE_VALLINFINITYMATH_ERREXCEPTmath_errhandlingMATH_ERRNONAN;一些函式式宏,以及許多函式。C99 還添加了 FP_CONTRACT 編譯指示。

一些數學函式返回的宏 EDOMERANGE 要求 <errno.h>

在 C89 中,透過新增 fl 字尾建立的數學函式名稱保留用於 floatlong double 版本的實現,分別。但是,符合標準的實現只需要支援 double 集。從 C99 開始,必須提供所有三個版本。

float 集的情況下,這些函式必須在存在適當的原型的情況下呼叫;否則,float 引數將被擴充套件為 double。(請注意,雖然在原型中指定 float 不一定強制停用這種擴充套件;原型的這一方面是 實現定義的。但是,在支援 float 集時這是必要的。)

隨著 C99 中引入 math_errhandling,在某些情況下不需要設定 errno

如果輸入引數超出數學函式定義的域,則會發生域錯誤。在這種情況下,將返回 實現定義的值,並且在 C99 之前,errno 被設定為宏 EDOM

如果函式的結果不能表示為 double,則會發生範圍錯誤。如果結果溢位,函式將返回 HUGE_VAL 的值,其符號與正確值應該具有的符號相同。在 C99 之前,errno 被設定為宏 ERANGE。如果結果下溢,函式將返回 0,而 errno 可能被設定為 ERANGE,也可能不會被設定為 ERANGE這取決於實現的定義

C++ 注意事項: 等效的標準 C++ 標頭是 <cmath>

<setjmp.h> – 非本地跳轉

[編輯 | 編輯原始碼]

標準 C 要求 jmp_buf 是一個適當大小的陣列,用於儲存“當前程式上下文”,無論該上下文是什麼。C99 添加了該上下文,“包括浮點狀態標誌、開啟的檔案或抽象機中的任何其他元件的狀態。”

C++ 注意事項:等效的標準 C++ 標頭檔案是 <csetjmp>

儲存呼叫環境

[編輯 | 編輯原始碼]

setjmp

[編輯 | 編輯原始碼]

標準 C 指出,“setjmp 是否是宏或以外部連結宣告的識別符號是未指定的。如果為了訪問實際函式而抑制了宏定義,或者程式定義了名為 setjmp 的外部識別符號,則行為是未定義的。”

如果 setjmp 在標準 C 定義的上下文之外被呼叫,則行為是未定義的。

恢復呼叫環境

[編輯 | 編輯原始碼]

longjmp 函式

[編輯 | 編輯原始碼]

如果 longjmp 試圖恢復到從未被 setjmp 儲存的上下文,則結果是未定義的。

如果 longjmp 試圖恢復到上下文,並且最初呼叫 setjmp 以儲存該上下文的父函式已終止,則結果是未定義的。

如果從巢狀的訊號處理程式中呼叫 longjmp,則行為是未定義的。不要從退出處理程式中呼叫 longjmp,例如由 atexit 函式註冊的那些處理程式。

<signal.h> – 訊號處理

[編輯 | 編輯原始碼]

C89 添加了型別 sig_atomic_t

標準 C 為其他型別的訊號保留了形式為 SIG*SIG_* 的名稱,其中 * 表示以大寫字母開頭的識別符號的尾部部分。在給定實現中可用的所有訊號集、它們的語義及其預設處理是實現定義的

C++ 注意事項:等效的標準 C++ 標頭檔案是 <csignal>

指定訊號處理

[編輯 | 編輯原始碼]

signal 函式

[編輯 | 編輯原始碼]

如果 signal 無法執行請求的操作,則它將返回一個等於 SIG_ERR 的值。在 C89 之前,signal 返回 -1。不要顯式測試 -1 的返回值,而是使用宏 SIG_ERR。始終測試 signal 的返回值,不要假設它完全按照你的要求執行。

通常,當檢測到訊號並將其傳送到處理程式時,該訊號將在下次發生時以“預設”方式處理。也就是說,如果你希望繼續捕獲和處理訊號,則必須在訊號處理程式中顯式呼叫 signal 來重置訊號機制。(標準 C 要求這樣做,除非是 SIGILL 的情況,在這種情況下,訊號是否自動重置是實現定義的。)

如果從處理程式中呼叫 signal 返回 SIG_ERR,則 errno 的值是不確定的。在其他情況下,將返回 SIG_ERR,而 errno 包含一個正值,其可能的值是實現定義的

在程式啟動期間,實現可以自由地指定某些訊號被忽略或以預設方式處理,具體取決於情況。也就是說,訊號處理的初始狀態是實現定義的

標準 C 對同一個處理程式的第二個訊號在第一個訊號處理完畢之前發生時的行為沒有說明。

<stdalign.h> – 對齊

[編輯 | 編輯原始碼]

C11 添加了此標頭檔案。

C++ 注意事項:等效的標準 C++ 標頭檔案是 <cstdalign>。請注意,C++17 已棄用此標頭檔案。

<stdarg.h> – 可變引數

[編輯 | 編輯原始碼]

此標頭檔案是 C89 的發明,其模型緊密地遵循 UNIX 的 <varargs.h> 標頭檔案。由於標準 C 使用略微不同的方法,因此定義了新的標頭檔案 <stdarg.h>,而不是保留具有更改含義的 <varargs.h>

C++ 注意事項:等效的標準 C++ 標頭檔案是 <cstdarg>

可變引數列表訪問宏

[編輯 | 編輯原始碼]

va_arg

[編輯 | 編輯原始碼]

標準 C 要求 va_arg 是一個宏。如果它是 #undef 的物件,並且實際使用了相同名稱的函式,則行為是未定義的。va_end 是否是宏或函式是未指定的。

va_copy

[編輯 | 編輯原始碼]

C99 添加了此功能。

標準 C 指出,“va_copy 是否是宏或以外部連結宣告的識別符號是未指定的。如果為了訪問實際函式而抑制了宏定義,或者程式定義了相同名稱的外部識別符號,則行為是未定義的。”

va_end

[編輯 | 編輯原始碼]

標準 C 指出,“va_end 是否是宏或以外部連結宣告的識別符號是未指定的。如果為了訪問實際函式而抑制了宏定義,或者程式定義了相同名稱的外部識別符號,則行為是未定義的。”

va_start

[編輯 | 編輯原始碼]

標準 C 要求 va_start 是一個宏。如果它是 #undef 的物件,並且實際使用了相同名稱的函式,則行為是未定義的。

如果將 registerva_start 的第二個引數一起使用,或者該引數的型別是函式或陣列,則行為是未定義的。

<stdatomic> – 原子操作

[編輯 | 編輯原始碼]

C11 添加了此標頭檔案。

標準 C 為將來新增到此標頭檔案保留以下名稱

  • ATOMIC_ 開頭,後面跟著大寫字母的宏名稱

  • atomic_memory_ 開頭,後面跟著小寫字母的型別名稱

  • 對於 memory_order 型別,以 memory_order_ 開頭,後面跟著小寫字母的列舉常量

  • atomic_ 開頭,後面跟著小寫字母的函式名稱

C17 已棄用使用宏 ATOMIC_VAR_INIT

C++ 注意事項:沒有等效的標頭檔案。

<stdbool.h> – 布林型別和值

[編輯 | 編輯原始碼]

C99 添加了型別說明符 _Bool 和相應的標頭檔案 <stdbool.h>,它定義了型別同義詞 bool 和宏 truefalse__bool_true_false_are_defined

C++ 注意事項:C++11 添加了 <cstdbool>,以模擬 <stdbool.h> 的行為。C++17 將標頭檔案名稱更改為 <stdbool.h>,與標準 C 中使用的一致。但是,請注意,C++17 已棄用此標頭檔案。

如何編寫使用布林型別的程式碼,並將其移植到支援和不支援此標頭檔案的多個 C 編譯器,或移植到 C++ 編譯器?我們絕不使用 C99 型別_Bool,我們顯式#include <stdbool.h>;我們只使用名稱booltruefalse。以下是實現此目標的相關程式碼

#ifndef __cplusplus /* in C mode, so no bool, true, and false keywords */
    #ifndef __bool_true_false_are_defined /* <stdbool.h> has not been #included */
        #ifdef true /* complain if any homegrown true macro defined */
            #error "A macro called >true< is defined"
        #else
            #ifdef false /* complain if any homegrown false macro defined */
                #error "A macro called >false< is defined"
            #else
                #ifdef bool /* complain if any homegrown bool macro defined */
                    #error "A macro called >bool< is defined"
                #else
                    #if __STDC_VERSION__ >= 199901L /* If <stdbool.h> exists #include it */
                        #include <stdbool.h>
                    #else
                        typedef int bool;
                        #define true 1
                        #define false 0
                        #define __bool_true_false_are_defined 1
                    #endif
                #endif
            #endif
        #endif
    #endif
#else /* in C++ mode, so have bool, true, and false keywords */
    #ifdef true /* complain if any homegrown true macro defined */
        #error "A macro called >true< is defined"
    #endif
    #ifdef false /* complain if any homegrown false macro defined */
        #error "A macro called >false< is defined"
    #endif
    #ifdef bool /* complain if any homegrown bool macro defined */
        #error "A macro called >bool< is defined"
    #endif
#endif

C++ 注意事項:等效的標準 C++ 標頭檔案是<cstdbool>

<stddef.h> – 通用定義

[edit | edit source]

C89 將此標頭檔案新增為幾個雜項宏定義和型別的儲存庫。宏是NULLoffsetof,型別是ptrdiff_tsize_twchar_t。C11 添加了max_align_t。除了NULL 之外,所有這些都是 C89 C 的發明。

如果offsetof 的第二個引數是位域,則行為是未定義

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項:等效的標準 C++ 標頭檔案是<cstddef>

<stdint.h> – 整數型別

[edit | edit source]

C99 添加了此標頭。

標準 C 為將來新增到此標頭檔案保留以下名稱

  • INTUINT 開頭,並以_MAX_MIN_C 結尾的宏名稱

  • intuint 開頭,並以_t 結尾的型別名稱

C++ 注意事項:等效的標準 C++ 標頭檔案是<cstdint>

<stdio.h> – 輸入/輸出

[edit | edit source]

檔案和檔案系統

[edit | edit source]

檔案和目錄系統許多方面是實現相關的。標準 C 甚至無法對最基本的事物(檔名)做出任何陳述。實現必須支援哪些檔名?至於目錄和裝置名稱,則不存在任何接近通用方法的方法。雖然存在標準標頭檔案名稱,但它們不一定直接對映到具有相同拼寫的檔名。

某些實現可能允許檔名包含萬用字元。也就是說,檔案說明符可以使用*.dat 這樣的約定來引用一組檔案,以引用所有型別為.dat 的檔案。所有標準 I/O 例程都不需要支援這種概念。

許多作業系統可以限制每個使用者的開啟檔案數量。同樣要注意,並非所有系統都允許在同一個目錄中存在同一個檔名的多個版本,這在使用fopen"w" 模式時會導致後果,例如。

一些檔案系統還會對使用者設定磁碟配額,因此當檔案變得太大時,I/O 操作可能會失敗,您可能直到輸出操作失敗才意識到這一點。

回到檔名問題。經過大量調查,標準 C 委員會發現,可移植檔名的格式最多為六個字母字元,後面跟著一個句點,然後是零個或一個字母。鑑於某些檔案系統區分大小寫,這些字母字元應該全部使用相同的大小寫。但是,與其將自己限制在最低公分母的檔名中,不如使用條件編譯指令來處理特定於平臺的檔案系統。

命令列級別上的檔名重定向的整個概念也是實現相關的。如果可能,這意味著例如printffscanf 實際上可能正在處理除使用者終端以外的裝置。它們甚至可能正在處理檔案。請注意,gets 的行為與stdinfgets 略有不同,但如果stdin 被重定向,則gets 可能會從檔案中讀取。

檔案緩衝、磁碟扇區大小等的細節也是實現相關的。但是,標準 C 要求實現能夠處理包含至少 254 個字元的文字檔案,包括尾隨換行符。

在某些系統上,stdinstdoutstderr 對作業系統是特殊的,並由作業系統維護。在其他系統上,這些可能在程式啟動期間建立。這些檔案是否違反您的最大開啟檔案限制是實現相關的

BUFSIZFOPEN_MAXFILENAME_MAXTMP_MAX 展開為實現定義的值。

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項:等效的標準 C++ 標頭檔案是<cstdio>

對檔案的操作

[edit | edit source]

remove 函式

[edit | edit source]

在許多系統上,檔案實際上會被刪除。但是,您可能正在刪除檔名的同義詞,而不是刪除檔案本身。在這種情況下,當刪除最後一個同義詞時,通常會刪除檔案。

如果要刪除的檔案當前處於開啟狀態,則行為是實現定義的。(在共享檔案系統中,另一個程式可能正在訪問您要刪除的檔案。)

rename 函式

[edit | edit source]

標準 C 指出,舊檔名將被刪除(就好像它是對remove 的呼叫一樣)。據推測,這允許檔名同義詞也被重新命名。當old 被刪除時,如果old 當前處於開啟狀態,則行為是實現定義的。(在共享檔案系統中,另一個程式可能正在訪問您要重新命名的檔案。)

如果使用新名稱呼叫的檔案已存在,則行為是實現定義的

具有分層(或其他)目錄結構的檔案系統可能無法直接允許跨目錄重新命名檔案。在這種情況下,重新命名可能會失敗,或者檔案實際上可能會被複制,而原始檔案會被刪除。標準 C 暗示如果需要檔案複製,則rename 可能會失敗;但是,它不要求它這樣做。

tmpfile 函式

[edit | edit source]

如果程式異常終止,則可能無法刪除臨時檔案。

建立檔案的路徑和屬性(目錄名、檔名、訪問許可權等)是實現定義的

tmpnam 函式

[edit | edit source]

如果您呼叫tmpnam 超過TMP_MAX 次,則行為是實現定義的

tmpnam 無法傳達錯誤,因此如果您為它提供一個非NULL 地址,該地址指向一個比L_tmpnam 字元小的區域,則行為是未定義的

雖然檔名保證在呼叫tmpnam 時是唯一的,但您有機會使用它之前,可能已經建立了同名檔案。如果這可能成為問題,請改用tmpfile。然後,如果您需要以"wb+" 以外的模式開啟檔案,請使用setvbufsetbuf 來更改它。

檔名可能包含目錄資訊。如果是這樣,目錄的名稱和屬性是實現定義的

檔案訪問函式

[edit | edit source]

fclose 函式

[edit | edit source]

如果程式異常終止,則不能保證為輸出開啟的流將清空其緩衝區。

在某些實現中,可能無法成功關閉空檔案並將其保留在檔案系統中,您可能需要先寫入某些內容。

fflush 函式

[edit | edit source]

如果流沒有開啟以供輸出,或者它以更新模式開啟而緊接之前的操作不是輸出,則行為是未定義。但是,一些實現允許可靠地對輸入流執行fflush操作。

如果程式異常終止,則不能保證為輸出開啟的流將清空其緩衝區。

可以重新整理“特殊”檔案stdoutstderr。雖然標準 C 指出重新整理輸入檔案(包括stdin)會導致未定義的行為,但一些實現允許這樣做。

fopen函式

[編輯 | 編輯原始碼]

一些實現可能難以在文字檔案中進行查詢;在這種情況下,指定模式'+'也可能意味著模式'b'

一些檔案系統只允許任何給定名稱的檔案存在一個版本;在這種情況下,以'w'模式開啟將導致該檔案被覆蓋。在其他系統上,可能會建立該檔案的新的版本。

在這些序列之後出現的模式字元的集合和含義是實現定義的。您的實現可能會提供其他模式字元來指定各種檔案屬性。

C11 添加了獨佔模式x

一些檔案系統在關閉二進位制檔案時會在檔案末尾追加尾隨'\0'字元。隨後,當您以追加模式開啟此類檔案時,您可能會定位到您寫入的最後一個字元的末尾。

只有當實現能夠確定檔案不是互動式裝置時,才會以完全緩衝模式開啟檔案。

如果fopen成功,它將返回指向已開啟流的FILE指標。失敗時,它將返回NULL。請注意,實現可能會限制當前開啟檔案的數量——FOPEN_MAX指定允許的數量——在這種情況下,如果您嘗試超過此數量,fopen將失敗。標準 C 沒有指定是否設定了errno

freopen函式

[編輯 | 編輯原始碼]

如果freopen成功,它將返回stream的值;否則,它將返回NULL。標準 C 沒有指定是否設定了errno

setbuf函式

[編輯 | 編輯原始碼]

setbuf不返回值。程式設計師有責任確保stream指向開啟的檔案,並且buf要麼是NULL,要麼是指向足夠大緩衝區的指標。

標準 C 不要求實現能夠實現所有這些型別的緩衝。因此,實現可以自由地將其中一種或多種緩衝型別視為等效的。因此,不能保證setbuf能夠滿足您的要求,即使無法返回錯誤程式碼。

setvbuf函式

[編輯 | 編輯原始碼]

mode可以是以下之一:_IOFBF(完全緩衝)、_IOLBF(行緩衝)或_IONBF(無緩衝)。標準 C 要求setvbuf接受這些模式,儘管底層實現不必能夠實現所有這些型別的緩衝。因此,實現可以自由地將其中一種或多種緩衝型別視為等效的。

當程式設計師提供緩衝區時,其內容在任何特定時間都是不確定的。(標準 C 實際上不要求實現使用程式設計師提供的緩衝區,如果提供了緩衝區。)使用者提供的緩衝區必須在流開啟時一直存在,因此如果您使用auto類別的緩衝區,請注意。

setvbuf在成功時返回零,在失敗時返回非零值。失敗可能是由於mode的值無效或其他原因造成的。標準 C 沒有指定在錯誤時是否設定了errno

setvbuf分配的緩衝區的大小是實現定義的,儘管setvbuf的一些實現使用size來確定使用的內部緩衝區的大小。

格式化輸入/輸出函式

[編輯 | 編輯原始碼]

fprintf函式

[編輯 | 編輯原始碼]

標準 C 在fprintf下定義了printf函式系列的通用輸出格式行為,所有其他系列成員的描述都指向此處。

如果格式引數不足,則行為是未定義的

C89 添加了轉換說明符inpp實現定義的格式輸出void指標的值。

C99 添加了轉換說明符FaA,以及長度修飾符hhlljtz。它還以實現定義的格式添加了對無窮大和 NaN 的支援。

如果轉換說明符無效,則行為是未定義的。(請注意,K&R 指出任何無法識別的說明符都被視為文字並傳遞到stdout。例如,%@會生成@。)標準 C 已將所有未使用的 lowercase 轉換說明符保留供其在未來版本中使用。

如果任何引數是或指向聯合、結構或陣列(除了使用%s的陣列和使用%pvoid指標之外),則行為是未定義的

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為

fscanf函式

[編輯 | 編輯原始碼]

標準 C 在fscanf下定義了scanf函式系列的通用輸入格式行為,所有其他系列成員的描述都指向此處。

如果格式引數不足,則行為是未定義的

C89 添加了轉換說明符inpp期望一個型別為指向void的指標的引數,以實現定義的格式。

C99 添加了長度修飾符hhlljtz。它還添加了對無窮大和 NaN 的支援。

如果轉換說明符無效,則行為是未定義的。標準 C 已將所有未使用的 lowercase 轉換說明符保留供其在未來版本中使用。

如果發生錯誤,則返回EOF。標準 C 沒有提到是否設定了errno

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為。

printf函式

[編輯 | 編輯原始碼]

fprintf中提到的輸出格式問題也適用於此函式。

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為

printf維護一個內部緩衝區,它在其中構建格式化的字串,並且該緩衝區的長度是有限的。歷史上,該長度是實現定義的,並不總是記錄的,而且實現之間差異很大。標準 C 要求實現能夠處理至少 509 個字元的任何單個轉換。

scanf函式

[編輯 | 編輯原始碼]

fscanf中提到的輸入格式問題也適用於此函式。

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為。

snprintf函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fprintf中提到的輸出格式問題也適用於此函式。

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為

sprintf函式

[編輯 | 編輯原始碼]

fprintf中提到的輸出格式問題也適用於此函式。

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為

sscanf函式

[編輯 | 編輯原始碼]

fscanf中提到的輸入格式問題也適用於此函式。

在作用域內沒有適當的原型的情況下呼叫此函式會導致未定義的行為

vfprintf函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fprintf中提到的輸出格式問題也適用於此函式。

vfscanf函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fscanf中提到的輸入格式問題也適用於此函式。

vprintf函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fprintf中提到的輸出格式問題也適用於此函式。

vscanf函式

[編輯 | 編輯原始碼]

fscanf中提到的輸入格式問題也適用於此函式。

vsnprintf 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fprintf中提到的輸出格式問題也適用於此函式。

vsprintf 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fprintf中提到的輸出格式問題也適用於此函式。

vsscanf 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

fscanf中提到的輸入格式問題也適用於此函式。

字元輸入/輸出函式

[編輯 | 編輯原始碼]

gets 函式

[編輯 | 編輯原始碼]

C11 已移除此函式。

ungetc 函式

[編輯 | 編輯原始碼]

C99 不建議在二進位制檔案開頭使用此函式。

直接輸入/輸出函式

[編輯 | 編輯原始碼]

fread 函式

[編輯 | 編輯原始碼]

如果發生錯誤,檔案位置指示器的值是 不確定的

如果部分欄位被讀取,其值是 不確定的

標準 C 對輸入時 CR/LF 對可能轉換為換行的翻譯沒有說明,儘管一些實現對文字檔案進行了這種轉換。

fwrite 函式

[編輯 | 編輯原始碼]

如果發生錯誤,檔案位置指示器的值是 不確定的

標準 C 對輸出時換行符可能轉換為 CR/LF 對的轉換沒有說明,儘管一些實現對文字檔案進行了這種轉換。

檔案定位函式

[編輯 | 編輯原始碼]

fgetpos 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

如果失敗,則返回非零值,並且 errno 設定為 實現定義的 正值。

fsetpos 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

如果失敗,則返回非零值,並且 errno 設定為 實現定義的 正值。

錯誤處理函式

[編輯 | 編輯原始碼]

perror 函式

[編輯 | 編輯原始碼]

訊息的內容和格式是 實現定義的

<stdlib.h> – 通用工具

[編輯 | 編輯原始碼]

C89 定義了此標頭檔案。

C99 添加了型別 lldiv_t

EXIT_SUCCESSEXIT_FAILURE 是標準 C 的發明,用作 實現定義的 成功和失敗退出程式碼值,與 exit 一起使用。

標準 C 為此標頭檔案保留所有以 str 開頭,後跟一個小寫字母的函式名,以便將來新增。

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項:等效的標準 C++ 標頭檔案是 <cstdlib>

數值轉換函式

[編輯 | 編輯原始碼]

標準 C 不要求 atofatoiatol 在發生錯誤時設定 errno。如果發生錯誤,行為是 未定義的

atol 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

strtod 函式

[編輯 | 編輯原始碼]

浮點數的格式是 特定於語言環境的

strtof 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

浮點數的格式是 特定於語言環境的

strtol 函式

[編輯 | 編輯原始碼]

整數的值的格式是 特定於語言環境的

strtoll 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

整數的值的格式是 特定於語言環境的

strtold 函式

[編輯 | 編輯原始碼]

C99 添加了此函式。

浮點數的格式是 特定於語言環境的

strtoul 函式

[edit | edit source]

整數的值的格式是 特定於語言環境的

strtoull 函式

[edit | edit source]

C99 添加了此函式。

整數的值的格式是 特定於語言環境的

偽隨機序列生成函式

[edit | edit source]

rand 函式

[edit | edit source]

標準 C 要求 RAND_MAX 至少為 32767。

記憶體管理函式

[edit | edit source]

如果請求的記憶體空間無法分配,則返回 NULL。永遠不要假設分配請求成功而沒有檢查 NULL 返回值。

如果請求的記憶體空間為零,則行為是 實現定義的,並且返回 NULL 或一個唯一的指標。

可用的堆的大小及其管理和操作的細節是 實現特定的

aligned_alloc 函式

[edit | edit source]

C11 添加了此函式。

calloc 函式

[edit | edit source]

分配的記憶體空間初始化為“全零位”。請注意,這並不保證與浮點零或空指標具有相同的表示形式。

free 函式

[edit | edit source]

如果 ptrNULLfree 不會執行任何操作。否則,如果 ptr 不是之前由這三個分配函式之一返回的值,則行為是 未定義的

指向已被 free 的記憶體空間的指標的值是未確定的,並且不應該對這樣的指標進行解引用。

請注意,free 無法在檢測到錯誤時進行錯誤通訊。

malloc 函式

[edit | edit source]

分配的記憶體空間的初始值是 未指定的

realloc 函式

[edit | edit source]

如果 ptrNULLrealloc 的行為類似於 malloc。否則,如果 ptr 不是之前由 callocmallocrealloc 返回的值,則行為是 未定義的。如果 ptr 指向已被 free 的記憶體空間,也是如此。

與環境的通訊

[edit | edit source]

abort 函式

[edit | edit source]

是否重新整理輸出流、關閉開啟的流或刪除臨時檔案是 實現定義的

程式的退出程式碼是表示“失敗”的某個 實現定義的 值。它是透過使用引數 SIGABRT 呼叫 raise 生成的。

atexit 函式

[edit | edit source]

標準 C 要求至少可以註冊 32 個函式。但是,為了規避這方面的任何限制,你始終可以只註冊一個函式,並讓它直接呼叫其他函式。這樣,其他函式也可以具有引數列表和返回值。

at_quick_exit 函式

[edit | edit source]

C11 添加了此函式。

_Exit 函式

[edit | edit source]

C99 添加了此函式。

getenv 函式

[edit | edit source]

環境列表由主機環境維護,可用的名稱集是實現特定的。

如果嘗試修改返回值所指向的字串的內容,則行為是 未定義的

一些實現為 main 提供了第三個引數,稱為 envpenvp 是指向 char 的指標陣列(與 argv 相同),每個指標都指向一個環境字串。標準 C 不包含此引數。

quick_exit 函式

[edit | edit source]

C11 添加了此函式。

system 函式

[edit | edit source]

標準 C 不要求存在命令列處理器(或等效項),在這種情況下,將返回 實現定義的 值。要確定是否存在這樣的環境,請使用 NULL 引數呼叫 system;如果返回非零值,則可以使用命令列處理器。

傳遞的字串的格式是 實現定義的

搜尋和排序實用程式

[edit | edit source]

bsearch 函式

[edit | edit source]

如果兩個成員比較相等,則匹配哪個成員是未指定的。

qsort 函式

[edit | edit source]

如果兩個成員比較相等,則它們在陣列中的順序是未指定的。

整數算術函式

[編輯 | 編輯原始碼]

abs 函式

[編輯 | 編輯原始碼]

如果結果無法表示,行為是未定義的

abs 可以用宏實現。

div 函式

[編輯 | 編輯原始碼]

如果結果無法表示,行為是未定義的

C89 添加了此函式。

labs 函式

[編輯 | 編輯原始碼]

如果結果無法表示,行為是未定義的

labs 可以用宏實現。

ldiv 函式

[編輯 | 編輯原始碼]

如果結果無法表示,行為是未定義的

C89 添加了此函式。

llabs 函式

[編輯 | 編輯原始碼]

C17 添加了此函式。

lldiv 函式

[編輯 | 編輯原始碼]

如果結果無法表示,行為是未定義的

C99 添加了此函式。

多位元組字元函式

[編輯 | 編輯原始碼]

這些函式的行為取決於當前的語言環境,特別是 LC_CTYPE 類別。

C89 添加了對多位元組字元處理的初始支援。

<stdnoreturn.h> – _Noreturn

[編輯 | 編輯原始碼]

C11 添加了此標頭檔案。

C++ 注意事項:沒有等效的標頭檔案。

<string.h> – 字串處理

[編輯 | 編輯原始碼]

實現可以自由地在 C 的任何資料型別上放置某些對齊考慮因素。大概地,你在記憶體中對這種對齊物件所做的任何複製,本身也應該適當對齊。如果情況並非如此,則可能無法訪問建立的副本,或者可能對其進行誤解。程式設計師有責任確保生成的副本物件處於適合進一步和有意義使用的格式和記憶體位置。

標準 C 為所有以 strmemwcs 開頭,後跟小寫字母的函式名稱保留未來新增到此標頭的權利。

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 考慮因素:等效的標準 C++ 標頭是 <cstring>

複製函式

[編輯 | 編輯原始碼]

memcpy 函式

[編輯 | 編輯原始碼]

如果兩個字串重疊,則行為未定義。

memmove 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

strcpy 函式

[編輯 | 編輯原始碼]

如果兩個字串重疊,則行為未定義。

strncpy 函式

[編輯 | 編輯原始碼]

如果兩個字串重疊,則行為未定義。

連線函式

[編輯 | 編輯原始碼]

strcat 函式

[編輯 | 編輯原始碼]

如果兩個字串重疊,則行為未定義。

strncat 函式

[編輯 | 編輯原始碼]

如果兩個字串重疊,則行為未定義。

比較函式

[編輯 | 編輯原始碼]

建議:所有比較函式都返回一個整數,表示小於、大於或等於零。不要假設分別表示大於和小於的正值或負值具有任何可預測的值。始終將返回值與零比較,不要與特定非零值比較。

strcoll 函式

[編輯 | 編輯原始碼]

比較是語言環境相關的

C89 添加了此函式。

strxfrm 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

strstr 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

雜項函式

[編輯 | 編輯原始碼]

strerror 函式

[編輯 | 編輯原始碼]

返回訊息文字的內容是實現定義的

程式設計師不應該嘗試寫入由返回值指向的位置。

<tgmath.h> – 型別泛型數學

[編輯 | 編輯原始碼]

C99 添加了此標頭。

C++ 注意事項: 等效的標準 C++ 標頭檔案為 <ctgmath>。注意 C++17 已棄用此標頭檔案。

<threads.h> – 執行緒

[編輯 | 編輯原始碼]

C11 添加了此標頭檔案。

如果 C 實現支援關鍵字 _Thread_Local(請參閱 條件定義的標準宏 中提到的條件定義宏 __STDC_NO_THREADS__),它也會提供標頭檔案 <threads.h>。 因此,不要直接使用關鍵字,請執行以下操作

#include <threads.h>

void f()
{
    thread_local static int tlsI = 0;
    

其中 thread_local 是在該標頭檔案中定義為 _Thread_Local 的宏,並且與等效的 C++ 關鍵字匹配。

標準 C 保留以 cnd_mtx_thrd_tss_ 開頭,後跟小寫字母的函式名稱、型別名稱和列舉常量,作為對此標頭檔案的可能新增。

C++ 注意事項:沒有等效的標頭檔案。

<time.h> – 日期和時間

[編輯 | 編輯原始碼]

標準 C 保留所有以 TIME_ 開頭,後跟大寫字母的宏名稱,以供將來新增到此標頭檔案中。

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項: 等效的標準 C++ 標頭檔案為 <ctime>

時間元件

[編輯 | 編輯原始碼]

C99 用 CLOCKS_PER_SEC 替換了宏 CLK_TCK

C11 添加了宏 TIME_UTC 和型別 struct timespec

C11 向 struct tm 添加了成員 tv_sectv_nsec

時間操作函式

[編輯 | 編輯原始碼]

difftime 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

mktime 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

timespecget 函式

[編輯 | 編輯原始碼]

C11 添加了此函式。

時間轉換函式

[編輯 | 編輯原始碼]

strftime 函式

[編輯 | 編輯原始碼]

C89 添加了此函式。

C99 添加了以下轉換說明符:CDeFgGhnrRtTuVz

<uchar.h> – Unicode 實用程式

[編輯 | 編輯原始碼]

C11 添加了此標頭檔案。

C++ 注意事項: 等效的標準 C++ 標頭檔案為 <cuchar>

<wchar.h> – 擴充套件多位元組和寬字元實用程式

[編輯 | 編輯原始碼]

C95 添加了此標頭。

標準 C 保留所有以 wcs 開頭,後跟小寫字母的函式名稱,以供將來新增到此標頭檔案中。

有關實現可能需要為此標頭新增的內容以支援 C11 新增的稱為“邊界檢查介面”的附件,請參閱 可選內容

C++ 注意事項: 等效的標準 C++ 標頭檔案為 <cwchar>

<wctype.h> – 寬字元分類和對映實用程式

[編輯 | 編輯原始碼]

C95 添加了此標頭。

標準 C 保留所有以 isto 開頭,後跟小寫字母的函式名稱,以供將來新增到此標頭檔案中。

C++ 注意事項: 等效的標準 C++ 標頭檔案為 <cwctype>

華夏公益教科書