环境
章节大纲
-
程序执行环境
在编写 C 程序时,需要考虑两个主要的环境:编译环境(即翻译环境)和执行环境。对绝大多数 C 程序而言,这两个环境是相同的。然而,C 语言现在正被广泛用于执行环境与编译环境不一致的场景中。
概念模型
翻译环境(Translation Environment)
翻译阶段(Translation Phases)
在 C89 之前,不同的编译器对源代码的词法分析和处理方式各异。为此,C 标准明确制定了一套规则,称为翻译阶段,用于规定源代码的处理顺序。这些规则可能会使一些依赖旧有翻译顺序的程序出错。
建议:请阅读并理解标准 C 的“翻译阶段”规则,以判断你的实现是否遵循这些规范。
标准 C 并不要求预处理器是独立程序(虽然可以如此实现)。通常情况下,预处理器无需了解目标实现的细节(一个例外是:标准要求预处理器的算术表达式计算必须使用指定类型,详见
#if
语法)。
诊断机制(Diagnostics)
标准 C 规定了在何种情况下实现必须提供诊断信息,但诊断的形式由实现定义。标准并未涉及诸如“变量 x 在初始化前被使用”或“不可达代码”的信息提示或警告。这类问题被认为属于实现质量的范畴,由市场自由决定。
标准 C 允许扩展,前提是它们不会使**严格符合的程序(strictly conforming programs)**失效。符合标准的编译器必须能禁用或诊断扩展。扩展的范围仅限于赋予语法新的语义,或定义未定义或未指定行为。
执行环境(Execution Environments)
标准 C 定义了两类执行环境:独立环境(freestanding)和宿主环境(hosted)。在这两种环境中,程序启动时执行环境调用一个指定的 C 函数。
静态初始化的方式与时机未作具体规定,但所有静态存储对象必须在程序启动前初始化。宿主环境中,通常启动函数是
main
,但也不一定非得如此。若使用main
以外的入口函数,程序就不再是“严格符合”的。建议:在宿主环境中,请始终使用
main
作为程序入口,除非有非常充分的理由,并且你对其做了详细文档说明。程序终止意味着控制权返回给执行环境。
独立环境(Freestanding Environment)
独立环境运行时不依赖操作系统,因此程序可从任何方式开始执行。这类环境本质上不可移植,但如果设计合理,很多代码(例如设备驱动)还是可以在相似平台间移植的。嵌入式系统开发者经常需要面对此类移植需求。
启动函数的名称、类型及终止方式由实现定义。
提供的标准库功能(如果有)也是实现定义的。但标准要求支持以下头文件:
<float.h>, <iso646.h>, <limits.h>, <stdalign.h>, <stdarg.h>, <stdbool.h>, <stddef.h>, <stdint.h>, <stdnoreturn.h>
宿主环境(Hosted Environment)
标准 C 允许
main
有以下两种声明方式:int main(void) { /* ... */ } int main(int argc, char *argv[]) { /* ... */ }
当然也可写成
char **argv
,参数名argc
和argv
也不固定。一种常见扩展是
main
接收第三个参数:char *envp[]
,用于传递环境变量的指针数组。若定义了三个以上参数,该程序就不再是严格符合标准的,即不可移植。建议:使用
getenv()
来访问环境变量,而不是依赖envp
。但请注意,getenv
返回的字符串格式及支持的变量名是实现定义的。有些文献错误地建议将
main
定义为void
类型,这种做法不可取。建议:总是将
main
定义为int
类型,并返回合理的退出码。标准要求
argc
非负,通常至少为 1,即使argv[0]
是空字符串。建议:不要假设
argc
始终大于 0。标准规定
argv[argc]
必须是空指针,因此argv
有argc + 1
个元素。关于命令行参数中的引号(如
"abc def"
)是否保留、是否大小写敏感,这些行为是实现定义的。建议:不要假设命令行处理会特殊处理引号。不同系统可能会保留引号、将其视为字符串一部分,或者大小写不一致等。为避免此问题,可使用
tolower()
或toupper()
处理命令行参数。命令行常用于设置程序行为的参数,例如:
textpro /left_margin=10 /page_length=55
如果参数较多或较长,应使用配置文件:
textpro /command_file=commands.txt
标准规定
argv[0]
表示程序名称,但如不可用时,应指向空字符串。建议:不要假设程序名称总是可用,即使
argv[0]
存在,也可能已被系统转换大小写或路径。标准规定
argc
、argv
及其所指向的字符串可由用户程序修改,且实现不应在程序运行时更改它们。多数系统支持
<
,>
,>>
命令行重定向符。这些符号可能会在传入执行环境前被剥离。建议:不要假设所有系统都支持命令行重定向。可以用
freopen()
在程序中实现重定向。建议:将错误信息输出到
stderr
,即使某些系统将其与stdout
视为相同,这样可在支持独立重定向的系统中获得更好控制。标准要求以如下方式调用
main
:exit(main(argc, argv));
即,
main
的返回值作为程序的退出码。若
main
直接结束(无return
),其退出码为0。某些系统限制退出码为无符号整数或字节大小。建议:使用
EXIT_SUCCESS
而非0
表示成功退出。因为返回值的范围和意义是实现定义的,标准提供<stdlib.h>
中的EXIT_SUCCESS
和EXIT_FAILURE
。如果你用退出码与父进程通信,可自定义数值,因为主机系统通常不会解释退出码。
程序执行(Program Execution)
标准 C 描述了一种抽象机器。在程序执行的“序列点(sequence point)”处,所有副作用必须完成,之后的副作用尚未发生。
终端输入输出的处理方式因实现不同而异,有的使用缓冲,有的无缓冲。
优化编译器可跨越序列点优化,只要结果等同于按序列点严格执行。
C11 引入了多线程支持,早期多线程需依赖库函数或编译器扩展。
环境相关注意事项
字符集
C 程序涉及两种字符集:源字符集与执行字符集。它们在跨平台(如交叉编译)时可能不同。
-
除明确规定外,源字符集与执行字符集中的字符及其值都是实现定义的。
-
执行字符
'\0'
必须是全零位表示。
许多 C 程序仍在 ASCII 或 Unicode 环境中翻译与运行,但其他字符集(如 EBCDIC)也在使用。因此处理大小写字符时要特别注意。
建议:如程序依赖特定字符集,应使用条件编译或标注为特定实现模块。使用
<ctype.h>
函数代替直接用整数比较字符。
三字符序列(Trigraph Sequences)
某些平台(如ISO 646键盘)缺失必要标点,为此 C89 引入三字符序列:
三字符 替代符号 ??= # ??( [ ??/ \ ??) ] ??' ^ ??< { ??! ??> } ??- ~ 例:
printf("??(error??)");
将输出:
[error?]
即
"??("
会变为'['
。建议:使用程序搜索
??
序列,若频繁出现,请检查是否为三字符。建议:使用
\?
避免误判,例如"\??("
的长度为4,而"??("
的长度为2。建议:即使实现不支持三字符,也建议使用
\?
,以便未来支持时保持兼容。
多字节字符(Multibyte Characters)
C89 引入了多字节字符的概念,其处理方式受当前区域设置(locale)影响。C95 引入 双字符(digraphs),用于提供某些替代符号(如
<:
代表[
),不同于三字符,它们是单独的语言符号,不能出现在字符串或字符常量中。
字符显示语义
某些转义序列(如
\a
、\v
)或\n
的行为与系统有关。有的系统将\n
视为换行和回车,有的仅视为换行。
信号与中断
标准 C 限定信号处理函数只能修改特定类型的对象。除
signal()
外,其他库函数并不保证可重入。
环境限制(Environmental Limits)
编译限制(Translation Limits)
C17 标准要求实现至少支持如下内容:
-
块嵌套:127 层
-
条件包含:63 层
-
指针/数组/函数声明嵌套:12 层
-
括号声明器嵌套:63 层
-
表达式嵌套括号:63 层
-
内部标识符有效前缀:63 字符
-
外部标识符有效前缀:31 字符
-
每个翻译单元的外部标识符数:4095
-
每个块的局部标识符数:511
-
同时定义的宏:4095
-
每个函数的参数数:127
-
每次宏调用的参数数:127
-
每个字符串字面量:4095 字符
-
对象最大字节数:65535(仅宿主环境)
-
嵌套
#include
文件:15 层 -
switch
语句标签数:1023 -
单个结构/共用体成员数:1023
-
单个枚举值数:1023
-
嵌套结构定义层数:63 层
这些数字只是下限,不表示所有组合都必须支持。
数值限制(Numerical Limits)
这些限制通过
<limits.h>
、<float.h>
、<stdint.h>
中的宏定义记录。-
自 C99 起,宏
__STDC_IEC_559__
表示支持 IEC 60559(IEEE 754)浮点标准。 -
缺少宏
__STDC_NO_COMPLEX__
表示支持复数类型。 -
存在宏
__STDC_IEC_559_COMPLEX__
表示复数计算也遵循 IEEE 标准。
相关头文件:
<complex.h>
与<fenv.h>
。 -