章节大纲

  • 程序执行环境

    在编写 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,参数名 argcargv 也不固定。

    一种常见扩展是 main 接收第三个参数:char *envp[],用于传递环境变量的指针数组。若定义了三个以上参数,该程序就不再是严格符合标准的,即不可移植。

    建议:使用 getenv() 来访问环境变量,而不是依赖 envp。但请注意,getenv 返回的字符串格式及支持的变量名是实现定义的。

    有些文献错误地建议将 main 定义为 void 类型,这种做法不可取。

    建议:总是将 main 定义为 int 类型,并返回合理的退出码。

    标准要求 argc 非负,通常至少为 1,即使 argv[0] 是空字符串。

    建议:不要假设 argc 始终大于 0。

    标准规定 argv[argc] 必须是空指针,因此 argvargc + 1 个元素。

    关于命令行参数中的引号(如 "abc def")是否保留、是否大小写敏感,这些行为是实现定义的。

    建议:不要假设命令行处理会特殊处理引号。不同系统可能会保留引号、将其视为字符串一部分,或者大小写不一致等。为避免此问题,可使用 tolower()toupper() 处理命令行参数。

    命令行常用于设置程序行为的参数,例如:

    textpro /left_margin=10 /page_length=55
    

    如果参数较多或较长,应使用配置文件:

    textpro /command_file=commands.txt
    

    标准规定 argv[0] 表示程序名称,但如不可用时,应指向空字符串。

    建议:不要假设程序名称总是可用,即使 argv[0] 存在,也可能已被系统转换大小写或路径。

    标准规定 argcargv 及其所指向的字符串可由用户程序修改,且实现不应在程序运行时更改它们。

    多数系统支持 <, >, >> 命令行重定向符。这些符号可能会在传入执行环境前被剥离。

    建议:不要假设所有系统都支持命令行重定向。可以用 freopen() 在程序中实现重定向。

    建议:将错误信息输出到 stderr,即使某些系统将其与 stdout 视为相同,这样可在支持独立重定向的系统中获得更好控制。

    标准要求以如下方式调用 main

    exit(main(argc, argv));
    

    即,main 的返回值作为程序的退出码。

    main 直接结束(无 return),其退出码为0。某些系统限制退出码为无符号整数或字节大小。

    建议:使用 EXIT_SUCCESS 而非 0 表示成功退出。因为返回值的范围和意义是实现定义的,标准提供 <stdlib.h> 中的 EXIT_SUCCESSEXIT_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>