可移植性和C语言
一位维基教科书用户认为此页面应该拆分为更小的页面,涵盖更窄的子主题。 您可以通过将此大型页面拆分为更小的页面来提供帮助。请务必遵循命名策略。将书籍分成更小的部分可以提供更多重点,并允许每个部分都擅长一件事,这对每个人都有利。 |
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 位浮点数据和操作,在这种情况下,类型float
、double
和long
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
**
,参数名 argc
和 argv
是任意的。)
一个常见的扩展是函数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"
将导致两个参数"abc
和def"
。字母的大小写可能不会被保留,即使在存在引号的情况下也是如此。(在将命令行参数与有效字符串列表进行比较之前,请使用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 要求参数argc
和 argv
以及 argv
指向的字符串可以由用户程序修改,并且在用户程序执行期间,这些参数可能不会被实现更改。
许多环境支持命令行运算符<
、>
和 >>
。在这样的系统中,这些字符(以及与它们相关的文件名)由命令行处理器处理(并删除),然后它将剩余的命令行传递给执行环境。没有以这种方式处理这些运算符的系统会将它们作为命令行的一部分传递给执行环境,在那里它们可以被处理或传递给应用程序程序。这些运算符超出了标准 C 的范围。
上述运算符通常允许重定向stdin
和 stdout
。一些系统允许重定向stderr
。一些系统认为stderr
与stdout
相同。
建议:不要假设普遍支持命令行重定向运算符
<
、>
和>>
。可以使用库函数freopen
从程序内部重定向标准文件。
建议:将错误消息写入
stderr
而不是stdout
,即使这两个文件指针都被视为相同。这样,您就可以利用允许stdout
和stderr
被独立重定向的系统。
在程序启动期间调用main
的方法可能会有所不同。标准 C 要求这样做,就好像使用以下代码一样
exit(main(argc, argv));
在这种情况下,从main
返回的任何值都将作为程序的退出代码传递。
从main
的结束花括号中掉出来会导致退出代码为零。
一些实现可能会将退出代码限制为无符号整数值,或者限制为适合字节的值。有关更多详细信息,请参阅库函数exit
。此外,虽然一些系统将退出代码 0 解释为成功,但其他系统可能不会。标准 C 要求 0 表示“成功”。它还在<stdlib.h>
中提供了实现定义的宏EXIT_SUCCESS
和 EXIT_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
而不是两个标记 total
和 cost
。它依赖于非标准 C 的方法,即用空字符替换宏定义中的注释,而不是用单个空格。标准 C 添加了预处理器标记粘贴运算符 ##
,作为实现所需行为的便携式解决方案。
在 C89 之前,一些预处理器允许在预处理期间创建字符串文字标记。标准 C 添加了预处理器字符串化运算符 #
,作为实现所需行为的便携式解决方案。
建议:避免利用遵循与标准 C 定义的标记化规则不同的标记化规则的预处理器的特性。
以下标记由标准 C 定义为关键字
auto
|
break
|
case
|
char
|
const C89 |
continue
|
default
|
do
|
double
|
else
|
enum
|
extern
|
float
|
for
|
goto
|
if
|
inline C99 |
int
|
long
|
register
|
restrict C99 |
return
|
short
|
signed C89 |
sizeof
|
static
|
struct
|
switch
|
typedef
|
union
|
unsigned
|
void
|
volatile C89 |
while
|
_Alignas C11 |
_Alignof C11 |
_Atomic C11 |
_Bool C99 |
_Complex C99 |
_Generic C11 |
_Imaginary C99 |
_Noreturn C11 |
_Static_assert C11 |
_Thread_local C11 |
过时:虽然 enum
和 void
在 K&R 中没有定义,但在 C89 之前,它们得到了各种编译器的支持。
标准 C 没有定义或保留以前在 K&R 和一些旧的 C 编译器中保留的关键字 entry
。
C++ 注意事项:标准 C++ 没有定义或保留关键字 restrict
。它也没有定义那些以下划线和一个大写字母开头的关键字。(但是,对于其中一些,它提供了替代拼写,例如 alignas
、alignof
、bool
和 thread_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++ 对以下标识符赋予特殊含义:final
、import
、module
和 override
。
建议:如果 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 上,该值可能具有类型 long
或 long
long
而不是 int
,具体取决于类型映射。
建议: 当整数常量的类型很重要时(例如,作为函数调用参数和使用
sizeof
运算符),明确地指定整数常量的类型(或将其强制转换)。
当将零常量传递给期望指针的函数(意图将其表示为“空指针”)但作用域中没有函数原型时,也会出现类似的问题,例如 f(0)
。零的类型是 int
,其大小/格式可能与参数的指针类型不匹配。此外,对于指针不像整数的机器,不会进行隐式转换以弥补这一点。
正确的方法是使用 NULL
库宏,该宏通常使用以下方法之一定义
#define NULL 0
#define NULL 0L
#define NULL ((void *)0)
过时: 在 C89 之前,不同的编译器使用不同的规则对整数常量进行类型化。K&R 要求以下内容:“一个十进制常量,其值超过最大的带符号机器整数,被认为是 long
;一个八进制或十六进制常量,其值超过最大的无符号机器整数,也被认为是 long
。”
标准 C 要求以下规则对整数常量进行类型化:“整数常量的类型是对应列表中其值可以表示的第一个类型。无后缀十进制:int
、long
int
、unsigned
long
int
;无后缀八进制或十六进制:int
、unsigned
int
、long
int
、unsigned
long
int
;用字母 U
(或 u
)作为后缀:unsigned int
、unsigned long int
;用字母 L
(或 l
)作为后缀:long int
、unsigned
long
int
;用 U
(或 u
)和 L
(或 l
)作为后缀:unsigned
long
int
。” C99 添加了 long
long
和 unsigned
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
char
或 unsigned
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
int
,i
也会被提升为 unsigned
int
,从而导致 uc
+
i
的结果为 unsigned
int
。使用 VP 规则,uc
将被提升为 int
(i
的类型),两者相加后的结果将为 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 在移位期间产生了符号位传播,产生了完全不同的结果。
请注意,上述示例只会引起对 uc
和 i
的某些值的关注,并非所有情况下都如此。例如,如果 uc
为 10
,i
为 30,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 规则的代码现在可能会产生不同的结果。具体来说,char
、short
或 int
位域(所有这些都是有符号或无符号的)或枚举类型可以在任何可以使用 int
的地方使用。如果 int
可以表示原始类型的所有值,则该值将转换为 int
;否则,它将转换为 unsigned
int
。
请注意,在标准 C 中,“正常”整数扩展规则也适用于位域,位域可以是有符号的,也可以是无符号的。
C99 添加了 _Bool
类型。C99 还允许添加扩展整数类型。
浮点数和整数
[edit | edit source]浮点类型
[edit | edit source]标准 C 规定,“当实浮点类型的有限值转换为 _Bool 以外的整数类型时,小数部分将被丢弃(即,该值将向零截断)。如果整数部分的值不能用整数类型表示,则行为是未定义的。”
标准 C 规定,“当整数类型的值转换为实浮点类型时,如果被转换的值可以在新类型中精确表示,则它将保持不变。如果被转换的值在可以表示的范围之内,但不能精确表示,则结果是可表示的最近较高值或最近较低值,以实现定义的方式选择。如果被转换的值超出了可表示的范围,则行为是未定义的。”
标准 C 要求,当 double
被截断为 float
,或 long
double
被截断为 double
或 float
时,如果被转换的值不能表示,则行为是未定义的。如果该值在范围内,但不能精确表示,则截断后的结果是两个最近的可表示值之一——哪一个被选择是实现定义的。
请注意,通过使用函数原型,实现可以允许将 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]
,其中 a
和 b
是表达式。其中一个表达式必须具有指向某种类型的指针类型(不包括 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
中声明atof
和atoi
(以及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 要求严格符合的程序在调用具有可变数量参数的函数时,始终在作用域内具有原型(带尾随
...
)。因此,在使用printf
和scanf
函数系列时,始终#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;
如果联合当前包含类型为 rectype1
或 rectype2
的结构,则可以通过检查 inrec.rt1.rectype
或 inrec.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_MIN
和 LLONG_MIN
也是如此。)
在 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
的函数原型在尾随参数中包含省略号,因此无法指定隐式转换。
这是由 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 引入了 float
,long
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
int
、long
int
、unsigned
int
和 long
float
。”目前尚不清楚这是否应被理解为明确禁止int
short
、int
long
等。
在 C99 之前,在块级范围内,所有声明都必须位于所有语句之前。但是,该限制在 C99 中被解除,允许它们交织在一起。C++ 也允许这样做。
很少在代码中看到实际使用的 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
的对象,将被视为存储类为 auto
的对象。标准 C 允许任何数据声明具有此存储类。它还允许将此存储类应用于函数参数。
K&R 说:“... 只有某些类型的变量将存储在寄存器中;在 PDP-11 上,它们是 int
、char
和指针。”
建议:鉴于编译器优化技术的进步,
register
存储类在托管实现中的价值已基本消失。(事实上,K&R 中就预测了这一点,它说:“... 代码生成的未来改进可能使[寄存器声明]变得不必要。”)因此,建议您根本不要使用它们,除非您能证明它们对一个或多个目标实现提供了某些价值。
标准 C 不允许实现扩大具有 register
存储类的变量的分配空间。也就是说,register
char
不能被视为 register
int
。它在所有方面都必须表现得像 char
,即使它存储在大小大于 char
的寄存器中。(一些实现实际上可以将多个 register
char
对象存储在同一个寄存器中。)
C++ 注意事项:虽然 C++14 中还支持此存储类,但其使用已不推荐。在 C++17 中,关键字 register
未被使用,但它为将来使用保留(可能具有不同的语义)。
当尝试向前引用 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
存储类。
这是由 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 添加了以下关键字用于类型说明符:enum
、signed
和 void
。这些导致了以下基本类型声明
void /* function returning, only */
signed char
signed short
signed short int
signed
signed int
signed long
signed long int
enum [tag] /* … */
C89 还添加了对以下新的类型声明的支持(一些实现已经支持 unsigned
char
和 unsigned
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 规定,普通字符(没有 signed
或 unsigned
修饰符的字符)被视为有符号还是无符号是实现定义的。
虽然 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
_Complex
、double
_Complex
和 long
double _Complex
类型。(请参阅<complex.h>
。)
C11 添加了类型说明符 _Atomic
,但将其设置为可选;请参阅条件定义的标准宏 中的条件定义的宏 __STDC_NO_ATOMICS__
。(请参阅<stdatomic.h>
。)
<limits.h>
和 <float.h>
中的宏定义了算术类型的最小范围和精度。标准 C 要求以下内容
_Bool
- 足够大以存储值 0 和 1char
- 至少 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 位扩展精度。因此,针对该机器的编译器可能会将 float
、double
和 long 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 只允许 int
、unsigned int
和 signed int
,并指出,““普通”int
位字段的高位是否被视为符号位是实现定义的。”
C99 指出,“位字段的类型必须是 _Bool
、signed int
、unsigned 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(按引用调用)的函数链接,或者要生成外部名称的不同表示形式。其他实现提供pascal
和cdecl
关键字分别用于调用 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++ *要求* 函数原型表示法。
尽管您可能拥有使用旧式函数定义的生产源代码,但这些代码可以与新式函数原型共存。唯一潜在的陷阱是窄类型。例如,旧式定义具有类型为char
、short
或float
的参数,它将期望以其较宽形式传递参数,即分别为int
、int
和double
,如果作用域内存在包含窄类型的原型,则可能并非如此。
建议:尽可能使用函数原型,因为它们可以确保函数使用正确的参数类型进行调用。原型还可以执行参数转换。例如,在没有原型作用域的情况下调用
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用字符h
、e
、l
、l
和o
初始化,不包含尾随的'\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;
这里,ei1
和si1
的第一个引用是临时定义。如果它们后面没有包含初始化列表的相同标识符的声明,那么这些临时定义将被视为定义。但是,如所示,它们后面有这样的声明,因此它们被视为声明。这样做的目的是允许两个相互引用的变量被初始化为指向彼此。
语句
[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++允许声明和语句交错。
goto
或switch
可用于跳入块。虽然这样做是可移植的,但块中任何“跳过的”自动变量是否能够以可预测的方式初始化尚不清楚。
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
对象会因此而被访问。
关于选择语句中嵌套限制的说明,请参阅 复合语句。
由于控制表达式是一个完整的表达式,因此在它之后立即有一个序列点。
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
语句中复合语句的讨论。
while
、do
和 for
语句中的控制表达式可能包含 expr1 ==
expr2 形式的表达式。如果 expr1 和 expr2< 是浮点表达式,则由于浮点表示、舍入等的实现定义性质,可能难以或不可能实现相等性。
建议:如果
while
、do
和for
语句中的控制表达式包含浮点表达式,请注意浮点相等性测试的结果是实现定义的。例如,最好使用类似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 没有指定最低限制。
由于控制表达式是一个完整的表达式,因此在它之后立即有一个序列点。
由于控制表达式是一个完整的表达式,因此在它之后立即有一个序列点。
由于三个表达式都是完整的表达式,因此在每个表达式之后立即有一个序列点。
C99 添加了对 for
的第一部分是声明的支持,如 int
i
=
0
,而不是要求 i
已经定义。
C++ 注意事项:标准 C++ 也支持此 C99 功能。C++ 在 for
语句中声明的任何变量的范围方面与 C 不同。
请参阅 带标签的语句,了解有关单独标签命名空间含义的讨论。请参阅 复合语句,了解有关跳入复合语句的含义的讨论。
当使用 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 中的索引仅包含三个宏:EOF
、FILE
和 NULL
。它还列出了大约 20 到 30 个库函数。没有提到或要求其他函数。另一方面,标准 C 包含数百个保留标识符,其中大多数是宏或库函数名。加上您的编译器使用的系统相关标识符以及任何第三方库使用的标识符,就会产生潜在的命名冲突。
建议:对于您的每个目标环境,按头文件以及跨头文件按字母顺序生成一个保留标识符列表。将此标识符列表用于两个目的:在创建自己的标识符名称时要避开的名称,以及查找所有集合的并集,以便您知道哪些名称是通用的,可以在通用代码中使用它们。请注意,仅仅因为不同环境中出现了相同名称的宏,并不意味着它们用于相同的目的。对于您创建的名称,使用一些唯一的 前缀(不是前导下划线)、后缀或命名样式,以降低冲突的可能性。
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
包装器围绕头文件的内容,以确保它们在同一范围内不会被包含多次。
K&R 和标准 C 只提供了两种(主要的)机制来指定头文件位置搜索路径,即 "…"
和 <…>
。有时需要或希望有更多机制,或者出于测试目的,您暂时想使用其他位置。许多实现允许在编译时将一个或多个包含搜索路径指定为命令行参数。例如:
cc -I''path1'' -I''path2'' -I''path3'' ''source-file''
告诉预处理器首先使用 path1
、然后使用 path2
和 path3
搜索 "…"
格式的头文件,最后在某个系统默认位置搜索。缺少此功能或支持的路径数量少于要求的数量可能会在移植代码时导致问题。即使您的编译器可能支持足够数量的这些参数,命令行缓冲区的最大大小也可能不足以容纳大量冗长的路径说明符。
建议:如果您的所有实现都具备此功能,请检查每个实现支持的路径数量。
建议:不要通过添加定义、声明或其他
#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 指出,“… 按照使用算术规则对控制常量表达式进行求值,该算术规则至少具有 数值限制 中指定的范围,除了 int
和 unsigned
int
行为方式与 long
和 unsigned
long
分别具有相同的表示形式一样。”
C99 将此更改为,“… 控制常量表达式,根据 6.6 的规则进行评估,但所有带符号整数类型和所有无符号整数类型都按其分别在头文件 <stdint.h>
中定义的类型 intmax_t
和 uintmax_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_CONTRACT
、FENV_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>
。
为了自包含,几个头文件定义了相同的名称(例如 NULL
和 size_t
)。
标准 C 库中的所有函数都使用函数原型声明。
标准头文件可以按任何顺序和多次包含在同一个作用域中,而不会产生不良影响。唯一例外是 <assert.h>
,如果它被多次包含,它的行为会根据宏 NDEBUG
的存在而有所不同。
为了严格符合标准,标准 C 禁止程序从 内部 外部声明或定义中包含标准头文件。这意味着你不应该从函数内部包含标准头文件,因为函数是外部定义。
标准库函数的许多原型包含 C89 发明或采用的关键字和派生类型。这些包括 const
、fpos_t
、size_t
和 void
*
。在将这些应用于已存在多年的函数时,它们仍然与在 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.h
、conio.h
、curses.h
、direct.h
、dos.h
、fcntl/h
、io.h
、process.h
、search.h
、share.h
、stat.h
、sys/locking.h
、sys/stat.h
、sys/timeb.h
、sys/types.h
和 values.h
。
其他名称未被 C89 采纳的头文件,其所有或部分功能可通过各种标准 C 头文件获得。这些包括 malloc.h
、memory.h
和 varargs.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
定义为 100
,MIN
定义为 20
)。
C89 要求传递给 assert
的参数的类型为 int
。但是,C99 将其扩展为任何标量类型。
由于 assert 是一个宏,因此要注意不要给它带有副作用的表达式——你不能依赖宏只评估你的表达式一次。
<complex.h> – 复数运算
[edit | edit source]C99 添加了此头文件,并使对复数类型和运算的支持成为可选。
缺少可选的预定义宏 __STDC_NO_COMPLEX__
表示支持复数类型及其相关的算术运算。此外,可选的预定义宏 __STDC_IEC_559_COMPLEX__
的存在表示复数支持符合 IEC 60559,如 C 标准的附录所述。
以下函数名称保留供标准 C 在此头文件中将来可能使用:cerf
、cerfc
、cexp2
、cexpm1
、clog10
、clog1p
、clog2
、clgamma
、ctgamma
以及那些以 f
和 l
为后缀的名称。
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 保留所有以 is
或 to
开头的函数名称,后跟一个小写字母(后跟任何其他标识符字符),以供将来添加到运行时库。
以下函数具有与区域相关的行为:isalpha
、isblank
、isgraph
、islower
、isprint
、ispunct
、isspace
、isupper
、toupper
和 tolower
。
在 C89 之前,以下函数通过此头文件广泛可用:isascii
、toascii
、iscsym
和 iscsymf
。标准 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
被声明为一个extern
int
变量;但是,标准 C 要求 errno
是一个宏。(但是,该宏可以扩展为对同名函数的调用。)具体来说,errno
是一个宏,它扩展为类型 int
的可修改左值*
。因此,errno
可以定义为类似于 *_Errno()
的东西,其中实现提供的函数 _Errno
返回一个指向 int
的指针。将 errno
#undef
以尝试获取底层对象会导致 未定义的行为。
各种标准库函数被记录为在检测到某些错误时将 errno
设置为非零值。标准 C 要求该值为正数。它还指出,没有库例程需要清除 errno
(即,将其值赋为 0),当然,您永远不应该依赖库例程来执行此操作。
从历史上看,定义 errno
的有效值的宏以 E
开头。虽然各种系统演化出一组名称,但这些名称中的一些拼写和含义存在很大差异。因此,C89 只定义了两个宏:EDOM
和 ERANGE
。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>
。
C99 添加了此标头。
标准 C 为所有以 FE_
开头,后跟大写字母的宏名称保留了空间,以便将来为此标头添加内容。
C++ 注意事项: 等效的标准 C++ 标头是 <cfenv>
。
此标头通过一系列宏定义目标系统的浮点特性,这些宏的值在很大程度上是 实现定义的。
截至 C17,几乎所有宏都在 C89 中定义。例外情况是 DECIMAL_DIG
和 FLT_EVAL_METHOD
,它们在 C99 中添加;以及 FLT_DECIMAL_DIG
、DBL_DECIMAL_DIG
、LDBL_DECIMAL_DIG
、FLT_HAS_SUBNORM
、DBL_HAS_SUBNORM
、LDBL_HAS_SUBNORM
、FLT_TRUE_MIN
、DBL_TRUE_MIN
和 LDBL_TRUE_MIN
,它们在 C11 中添加。
虽然许多系统使用 IEEE-754 格式来表示浮点类型,但在开发 C89 时,还有其他三种格式被普遍使用,C89 都能容纳它们。
标准 C 为宏 FLT_ROUNDS
定义了值 -1
到 3
。所有其他值指定 实现定义的舍入行为。
标准 C 为宏 FLT_EVAL_METHOD
定义了值 -1
到 2。FLT_EVAL_METHOD
的所有其他负值都表示 实现定义的行为。有关此宏的值可能对浮点常量产生的影响,请参阅 浮点常量。
C++ 注意事项: 等效的标准 C++ 标头是 <cfloat>
。
C99 添加了此标头。
标准 C 为所有以 PRI
或 SCN
开头,后跟小写字母或 X
的宏名称保留了空间,以便将来为此标头添加内容。
C++ 注意事项: 等效的标准 C++ 标头是 <cinttypes>
。
C95 添加了此标头。
C++ 注意事项: 等效的标准 C++ 标头是 <ciso646>
。标准 C 在此标头中定义的宏是标准 C++ 中的关键字。
此标头通过一系列宏定义目标系统的整数特性,这些宏的值在很大程度上是实现定义的。
几乎所有宏都在 C89 中定义。例外情况是 LLONG_MIN
、LLONG_MAX
和 ULLONG_MAX
,它们是在 C99 中添加的。
C++ 注意事项: 等效的标准 C++ 标头是 <climits>
。
类型 struct
lconv
的几乎所有成员都是由 C89 定义的。例外情况是 int_p_cs_precedes
、int_n_cs_precedes
、int_p_sep_by_space
、int_n_sep_by_space
、int_p_sign_posn
和 int_n_sign_posn
,它们是在 C99 中添加的。实现可以添加其他成员。
标准 C 已将以 LC_
开头,后跟大写字母的名称空间保留给实现使用,以便它们可以添加额外的区域设置子类别宏。
标准 C 定义的区域设置是 "C"
和 ""
,后者是 特定于区域设置的本机环境。所有其他用于识别所有其他区域设置的字符串都是 实现定义的。
C++ 注意事项: 等效的标准 C++ 标头是 <clocale>
。
如果您修改 setlocale
返回的字符串的内容,则行为是 未定义的。
C99 添加了类型 float_t
和 double_t
;宏 FP_FAST_FMA
、FP_FAST_FMAF
、FP_FAST_FMAL
、FP_ILOGB0
、FP_ILOGBNAN
、FP_INFINITE
、FP_NAN
、FP_NORMAL
、FP_SUBNORMAL
、FP_ZERO
、HUGE_VALF
、HUGE_VALL
、INFINITY
、MATH_ERREXCEPT
、math_errhandling
、MATH_ERRNO
和 NAN
;一些函数式宏,以及许多函数。C99 还添加了 FP_CONTRACT
编译指示。
一些数学函数返回的宏 EDOM
和 ERANGE
要求 <errno.h>
。
在 C89 中,通过添加 f
或 l
后缀创建的数学函数名称保留用于 float
和 long
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>
。
标准 C 要求 jmp_buf
是一个适当大小的数组,用于存储“当前程序上下文”,无论该上下文是什么。C99 添加了该上下文,“不包括浮点状态标志、打开的文件或抽象机中的任何其他组件的状态。”
C++ 注意事项:等效的标准 C++ 头文件是 <csetjmp>
。
标准 C 指出,“setjmp
是否是宏或以外部链接声明的标识符是未指定的。如果为了访问实际函数而抑制了宏定义,或者程序定义了名为 setjmp
的外部标识符,则行为是未定义的。”
如果 setjmp
在标准 C 定义的上下文之外被调用,则行为是未定义的。
如果 longjmp
试图恢复到从未被 setjmp
保存的上下文,则结果是未定义的。
如果 longjmp
试图恢复到上下文,并且最初调用 setjmp
以保存该上下文的父函数已终止,则结果是未定义的。
如果从嵌套的信号处理程序中调用 longjmp
,则行为是未定义的。不要从退出处理程序中调用 longjmp
,例如由 atexit
函数注册的那些处理程序。
C89 添加了类型 sig_atomic_t
。
标准 C 为其他类型的信号保留了形式为 SIG*
和 SIG_*
的名称,其中 *
表示以大写字母开头的标识符的尾部部分。在给定实现中可用的所有信号集、它们的语义及其默认处理是实现定义的。
C++ 注意事项:等效的标准 C++ 头文件是 <csignal>
。
如果 signal
无法执行请求的操作,则它将返回一个等于 SIG_ERR
的值。在 C89 之前,signal
返回 -1
。不要显式测试 -1
的返回值,而是使用宏 SIG_ERR
。始终测试 signal
的返回值,不要假设它完全按照你的要求执行。
通常,当检测到信号并将其发送到处理程序时,该信号将在下次发生时以“默认”方式处理。也就是说,如果你希望继续捕获和处理信号,则必须在信号处理程序中显式调用 signal
来重置信号机制。(标准 C 要求这样做,除非是 SIGILL
的情况,在这种情况下,信号是否自动重置是实现定义的。)
如果从处理程序中调用 signal
返回 SIG_ERR
,则 errno
的值是不确定的。在其他情况下,将返回 SIG_ERR
,而 errno
包含一个正值,其可能的值是实现定义的。
在程序启动期间,实现可以自由地指定某些信号被忽略或以默认方式处理,具体取决于情况。也就是说,信号处理的初始状态是实现定义的。
标准 C 对同一个处理程序的第二个信号在第一个信号处理完毕之前发生时的行为没有说明。
C11 添加了此头文件。
C++ 注意事项:等效的标准 C++ 头文件是 <cstdalign>
。请注意,C++17 已弃用此头文件。
此头文件是 C89 的发明,其模型紧密地遵循 UNIX 的 <varargs.h>
头文件。由于标准 C 使用略微不同的方法,因此定义了新的头文件 <stdarg.h>
,而不是保留具有更改含义的 <varargs.h>
。
C++ 注意事项:等效的标准 C++ 头文件是 <cstdarg>
。
标准 C 要求 va_arg
是一个宏。如果它是 #undef
的对象,并且实际使用了相同名称的函数,则行为是未定义的。va_end
是否是宏或函数是未指定的。
C99 添加了此功能。
标准 C 指出,“va_copy
是否是宏或以外部链接声明的标识符是未指定的。如果为了访问实际函数而抑制了宏定义,或者程序定义了相同名称的外部标识符,则行为是未定义的。”
标准 C 指出,“va_end
是否是宏或以外部链接声明的标识符是未指定的。如果为了访问实际函数而抑制了宏定义,或者程序定义了相同名称的外部标识符,则行为是未定义的。”
标准 C 要求 va_start
是一个宏。如果它是 #undef
的对象,并且实际使用了相同名称的函数,则行为是未定义的。
如果将 register
与 va_start
的第二个参数一起使用,或者该参数的类型是函数或数组,则行为是未定义的。
C11 添加了此头文件。
标准 C 为将来添加到此头文件保留以下名称
以
ATOMIC_
开头,后面跟着大写字母的宏名称以
atomic_
或memory_
开头,后面跟着小写字母的类型名称对于
memory_order
类型,以memory_order_
开头,后面跟着小写字母的枚举常量以
atomic_
开头,后面跟着小写字母的函数名称
C17 已弃用使用宏 ATOMIC_VAR_INIT
。
C++ 注意事项:没有等效的头文件。
C99 添加了类型说明符 _Bool
和相应的头文件 <stdbool.h>
,它定义了类型同义词 bool
和宏 true
、false
和 __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>
;我们只使用名称bool
、true
和false
。以下是实现此目标的相关代码
#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 将此头文件添加为几个杂项宏定义和类型的存储库。宏是NULL
和offsetof
,类型是ptrdiff_t
、size_t
和wchar_t
。C11 添加了max_align_t
。除了NULL
之外,所有这些都是 C89 C 的发明。
如果offsetof
的第二个参数是位域,则行为是未定义。
有关实现可能需要为此标头添加的内容以支持 C11 添加的称为“边界检查接口”的附件,请参阅 可选内容。
C++ 注意事项:等效的标准 C++ 头文件是<cstddef>
。
<stdint.h> – 整数类型
[edit | edit source]C99 添加了此标头。
标准 C 为将来添加到此头文件保留以下名称
以
INT
或UINT
开头,并以_MAX
、_MIN
或_C
结尾的宏名称以
int
或uint
开头,并以_t
结尾的类型名称
C++ 注意事项:等效的标准 C++ 头文件是<cstdint>
。
<stdio.h> – 输入/输出
[edit | edit source]文件和文件系统
[edit | edit source]文件和目录系统许多方面是实现相关的。标准 C 甚至无法对最基本的事物(文件名)做出任何陈述。实现必须支持哪些文件名?至于目录和设备名称,则不存在任何接近通用方法的方法。虽然存在标准头文件名称,但它们不一定直接映射到具有相同拼写的文件名。
某些实现可能允许文件名包含通配符。也就是说,文件说明符可以使用*.dat
这样的约定来引用一组文件,以引用所有类型为.dat
的文件。所有标准 I/O 例程都不需要支持这种概念。
许多操作系统可以限制每个用户的打开文件数量。同样要注意,并非所有系统都允许在同一个目录中存在同一个文件名的多个版本,这在使用fopen
的"w"
模式时会导致后果,例如。
一些文件系统还会对用户设置磁盘配额,因此当文件变得太大时,I/O 操作可能会失败,您可能直到输出操作失败才意识到这一点。
回到文件名问题。经过大量调查,标准 C 委员会发现,可移植文件名的格式最多为六个字母字符,后面跟着一个句点,然后是零个或一个字母。鉴于某些文件系统区分大小写,这些字母字符应该全部使用相同的大小写。但是,与其将自己限制在最低公分母的文件名中,不如使用条件编译指令来处理特定于平台的文件系统。
命令行级别上的文件名重定向的整个概念也是实现相关的。如果可能,这意味着例如printf
和fscanf
实际上可能正在处理除用户终端以外的设备。它们甚至可能正在处理文件。请注意,gets
的行为与stdin
的fgets
略有不同,但如果stdin
被重定向,则gets
可能会从文件中读取。
文件缓冲、磁盘扇区大小等的细节也是实现相关的。但是,标准 C 要求实现能够处理包含至少 254 个字符的文本文件,包括尾随换行符。
在某些系统上,stdin
、stdout
和stderr
对操作系统是特殊的,并由操作系统维护。在其他系统上,这些可能在程序启动期间建立。这些文件是否违反您的最大打开文件限制是实现相关的。
宏BUFSIZ
、FOPEN_MAX
、FILENAME_MAX
和TMP_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+"
以外的模式打开文件,请使用setvbuf
或setbuf
来更改它。
文件名可能包含目录信息。如果是这样,目录的名称和属性是实现定义的。
文件访问函数
[edit | edit source]fclose
函数
[edit | edit source]如果程序异常终止,则不能保证为输出打开的流将清空其缓冲区。
在某些实现中,可能无法成功关闭空文件并将其保留在文件系统中,您可能需要先写入某些内容。
fflush
函数
[edit | edit source]如果流没有打开以供输出,或者它以更新模式打开而紧接之前的操作不是输出,则行为是未定义。但是,一些实现允许可靠地对输入流执行fflush
操作。
如果程序异常终止,则不能保证为输出打开的流将清空其缓冲区。
可以刷新“特殊”文件stdout
和stderr
。虽然标准 C 指出刷新输入文件(包括stdin
)会导致未定义的行为,但一些实现允许这样做。
一些实现可能难以在文本文件中进行查找;在这种情况下,指定模式'+'
也可能意味着模式'b'
。
一些文件系统只允许任何给定名称的文件存在一个版本;在这种情况下,以'w'
模式打开将导致该文件被覆盖。在其他系统上,可能会创建该文件的新的版本。
在这些序列之后出现的模式字符的集合和含义是实现定义的。您的实现可能会提供其他模式字符来指定各种文件属性。
C11 添加了独占模式x
。
一些文件系统在关闭二进制文件时会在文件末尾追加尾随'\0'
字符。随后,当您以追加模式打开此类文件时,您可能会定位到您写入的最后一个字符的末尾。
只有当实现能够确定文件不是交互式设备时,才会以完全缓冲模式打开文件。
如果fopen
成功,它将返回指向已打开流的FILE
指针。失败时,它将返回NULL
。请注意,实现可能会限制当前打开文件的数量——FOPEN_MAX
指定允许的数量——在这种情况下,如果您尝试超过此数量,fopen
将失败。标准 C 没有指定是否设置了errno
。
如果freopen
成功,它将返回stream
的值;否则,它将返回NULL
。标准 C 没有指定是否设置了errno
。
setbuf
不返回值。程序员有责任确保stream
指向打开的文件,并且buf
要么是NULL
,要么是指向足够大缓冲区的指针。
标准 C 不要求实现能够实现所有这些类型的缓冲。因此,实现可以自由地将其中一种或多种缓冲类型视为等效的。因此,不能保证setbuf
能够满足您的要求,即使无法返回错误代码。
mode
可以是以下之一:_IOFBF
(完全缓冲)、_IOLBF
(行缓冲)或_IONBF
(无缓冲)。标准 C 要求setvbuf
接受这些模式,尽管底层实现不必能够实现所有这些类型的缓冲。因此,实现可以自由地将其中一种或多种缓冲类型视为等效的。
当程序员提供缓冲区时,其内容在任何特定时间都是不确定的。(标准 C 实际上不要求实现使用程序员提供的缓冲区,如果提供了缓冲区。)用户提供的缓冲区必须在流打开时一直存在,因此如果您使用auto
类别的缓冲区,请注意。
setvbuf
在成功时返回零,在失败时返回非零值。失败可能是由于mode
的值无效或其他原因造成的。标准 C 没有指定在错误时是否设置了errno
。
由setvbuf
分配的缓冲区的大小是实现定义的,尽管setvbuf
的一些实现使用size
来确定使用的内部缓冲区的大小。
标准 C 在fprintf
下定义了printf
函数系列的通用输出格式行为,所有其他系列成员的描述都指向此处。
如果格式参数不足,则行为是未定义的。
C89 添加了转换说明符i
、n
和p
。p
以实现定义的格式输出void
指针的值。
C99 添加了转换说明符F
、a
和A
,以及长度修饰符hh
、ll
、j
、t
和z
。它还以实现定义的格式添加了对无穷大和 NaN 的支持。
如果转换说明符无效,则行为是未定义的。(请注意,K&R 指出任何无法识别的说明符都被视为文本并传递到stdout
。例如,%@
会生成@
。)标准 C 已将所有未使用的 lowercase 转换说明符保留供其在未来版本中使用。
如果任何参数是或指向联合、结构或数组(除了使用%s
的数组和使用%p
的void
指针之外),则行为是未定义的。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
标准 C 在fscanf
下定义了scanf
函数系列的通用输入格式行为,所有其他系列成员的描述都指向此处。
如果格式参数不足,则行为是未定义的。
C89 添加了转换说明符i
、n
和p
。p
期望一个类型为指向void
的指针的参数,以实现定义的格式。
C99 添加了长度修饰符hh
、ll
、j
、t
和z
。它还添加了对无穷大和 NaN 的支持。
如果转换说明符无效,则行为是未定义的。标准 C 已将所有未使用的 lowercase 转换说明符保留供其在未来版本中使用。
如果发生错误,则返回EOF
。标准 C 没有提到是否设置了errno
。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
fprintf
中提到的输出格式问题也适用于此函数。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
printf
维护一个内部缓冲区,它在其中构建格式化的字符串,并且该缓冲区的长度是有限的。历史上,该长度是实现定义的,并不总是记录的,而且实现之间差异很大。标准 C 要求实现能够处理至少 509 个字符的任何单个转换。
fscanf
中提到的输入格式问题也适用于此函数。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
C99 添加了此函数。
fprintf
中提到的输出格式问题也适用于此函数。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
fprintf
中提到的输出格式问题也适用于此函数。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
fscanf
中提到的输入格式问题也适用于此函数。
在作用域内没有适当的原型的情况下调用此函数会导致未定义的行为。
C99 添加了此函数。
fprintf
中提到的输出格式问题也适用于此函数。
C99 添加了此函数。
fscanf
中提到的输入格式问题也适用于此函数。
C99 添加了此函数。
fprintf
中提到的输出格式问题也适用于此函数。
fscanf
中提到的输入格式问题也适用于此函数。
C99 添加了此函数。
fprintf
中提到的输出格式问题也适用于此函数。
C99 添加了此函数。
fprintf
中提到的输出格式问题也适用于此函数。
C99 添加了此函数。
fscanf
中提到的输入格式问题也适用于此函数。
C11 已移除此函数。
C99 不建议在二进制文件开头使用此函数。
如果发生错误,文件位置指示器的值是 不确定的。
如果部分字段被读取,其值是 不确定的。
标准 C 对输入时 CR/LF 对可能转换为换行的翻译没有说明,尽管一些实现对文本文件进行了这种转换。
如果发生错误,文件位置指示器的值是 不确定的。
标准 C 对输出时换行符可能转换为 CR/LF 对的转换没有说明,尽管一些实现对文本文件进行了这种转换。
C89 添加了此函数。
如果失败,则返回非零值,并且 errno
设置为 实现定义的 正值。
C89 添加了此函数。
如果失败,则返回非零值,并且 errno
设置为 实现定义的 正值。
消息的内容和格式是 实现定义的。
C89 定义了此头文件。
C99 添加了类型 lldiv_t
。
宏 EXIT_SUCCESS
和 EXIT_FAILURE
是标准 C 的发明,用作 实现定义的 成功和失败退出代码值,与 exit
一起使用。
标准 C 为此头文件保留所有以 str
开头,后跟一个小写字母的函数名,以便将来添加。
有关实现可能需要为此标头添加的内容以支持 C11 添加的称为“边界检查接口”的附件,请参阅 可选内容。
C++ 注意事项:等效的标准 C++ 头文件是 <cstdlib>
。
标准 C 不要求 atof
、atoi
和 atol
在发生错误时设置 errno
。如果发生错误,行为是 未定义的。
C99 添加了此函数。
浮点数的格式是 特定于语言环境的。
C99 添加了此函数。
浮点数的格式是 特定于语言环境的。
整数的值的格式是 特定于语言环境的。
C99 添加了此函数。
整数的值的格式是 特定于语言环境的。
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]如果 ptr
为 NULL
,free
不会执行任何操作。否则,如果 ptr
不是之前由这三个分配函数之一返回的值,则行为是 未定义的。
指向已被 free
的内存空间的指针的值是未确定的,并且不应该对这样的指针进行解引用。
请注意,free
无法在检测到错误时进行错误通信。
malloc
函数
[edit | edit source]分配的内存空间的初始值是 未指定的。
realloc
函数
[edit | edit source]如果 ptr
为 NULL
,realloc
的行为类似于 malloc
。否则,如果 ptr
不是之前由 calloc
、malloc
或 realloc
返回的值,则行为是 未定义的。如果 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
提供了第三个参数,称为 envp
。envp
是指向 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
可以用宏实现。
如果结果无法表示,行为是未定义的。
C89 添加了此函数。
如果结果无法表示,行为是未定义的。
labs
可以用宏实现。
如果结果无法表示,行为是未定义的。
C89 添加了此函数。
C17 添加了此函数。
如果结果无法表示,行为是未定义的。
C99 添加了此函数。
这些函数的行为取决于当前的语言环境,特别是 LC_CTYPE
类别。
C89 添加了对多字节字符处理的初始支持。
C11 添加了此头文件。
C++ 注意事项:没有等效的头文件。
实现可以自由地在 C 的任何数据类型上放置某些对齐考虑因素。大概地,你在内存中对这种对齐对象所做的任何复制,本身也应该适当对齐。如果情况并非如此,则可能无法访问创建的副本,或者可能对其进行误解。程序员有责任确保生成的副本对象处于适合进一步和有意义使用的格式和内存位置。
标准 C 为所有以 str
、mem
或 wcs
开头,后跟小写字母的函数名称保留未来添加到此标头的权利。
有关实现可能需要为此标头添加的内容以支持 C11 添加的称为“边界检查接口”的附件,请参阅 可选内容。
C++ 考虑因素:等效的标准 C++ 标头是 <cstring>
。
如果两个字符串重叠,则行为未定义。
C89 添加了此函数。
如果两个字符串重叠,则行为未定义。
如果两个字符串重叠,则行为未定义。
如果两个字符串重叠,则行为未定义。
如果两个字符串重叠,则行为未定义。
建议:所有比较函数都返回一个整数,表示小于、大于或等于零。不要假设分别表示大于和小于的正值或负值具有任何可预测的值。始终将返回值与零比较,不要与特定非零值比较。
比较是语言环境相关的。
C89 添加了此函数。
C89 添加了此函数。
C89 添加了此函数。
返回消息文本的内容是实现定义的。
程序员不应该尝试写入由返回值指向的位置。
C99 添加了此标头。
C++ 注意事项: 等效的标准 C++ 头文件为 <ctgmath>
。注意 C++17 已弃用此头文件。
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++ 注意事项:没有等效的头文件。
标准 C 保留所有以 TIME_
开头,后跟大写字母的宏名称,以供将来添加到此头文件中。
有关实现可能需要为此标头添加的内容以支持 C11 添加的称为“边界检查接口”的附件,请参阅 可选内容。
C++ 注意事项: 等效的标准 C++ 头文件为 <ctime>
。
C99 用 CLOCKS_PER_SEC
替换了宏 CLK_TCK
。
C11 添加了宏 TIME_UTC
和类型 struct
timespec
。
C11 向 struct
tm
添加了成员 tv_sec
和 tv_nsec
。
C89 添加了此函数。
C89 添加了此函数。
C11 添加了此函数。
C89 添加了此函数。
C99 添加了以下转换说明符:C
、D
、e
、F
、g
、G
、h
、n
、r
、R
、t
、T
、u
、V
和 z
。
C11 添加了此头文件。
C++ 注意事项: 等效的标准 C++ 头文件为 <cuchar>
。
C95 添加了此标头。
标准 C 保留所有以 wcs
开头,后跟小写字母的函数名称,以供将来添加到此头文件中。
有关实现可能需要为此标头添加的内容以支持 C11 添加的称为“边界检查接口”的附件,请参阅 可选内容。
C++ 注意事项: 等效的标准 C++ 头文件为 <cwchar>
。
C95 添加了此标头。
标准 C 保留所有以 is
或 to
开头,后跟小写字母的函数名称,以供将来添加到此头文件中。
C++ 注意事项: 等效的标准 C++ 头文件为 <cwctype>
。