C 未定义行为成因列表
blog.llvm.org: "Many seemingly reasonable things in C actually have undefined behavior, and this is a common source of bugs in programs."
未定义的行为(Undefined Behavior)指程序不可预测的执行效果,一般由错误的代码实现引起。出于效率、兼容性等多方面原因,语言标准不便于定义错误程序的明确行为,而是将其统称为“未定义”的行为,可以是崩溃,也可以是非预期的任意表现,有些问题在编译器和执行环境的特殊处理下也可能不会造成实质性的危害,但不具备可移植性。代码质量管理的一个重要目标就是消除会导致未定义行为的代码。
ISO/IEC 9899:2011 在 Annex J.2 中罗列了导致未定义行为的各种情况,但某些条目缺少上下文信息不便于直接阅读,本文对其进行了补充并转译如下:
- 违反了出现在约束之外的 shall 或 shall not 要求 — clause 4
- 非空源文件未以换行符结尾,或以换行符结尾但换行符之前是反斜杠,或以不完整的预处理符号结尾,或以不完整的注释结尾 — 5.1.1.2
- 预处理符号连接产生了通用字符名称(universal character name) — 5.1.1.2
- 宿主环境(hosted environment)中的程序没有按要求定义 main 函数 — 5.1.2.2.1
- 具有数据竞争(data race)的程序 — 5.1.2.4
- 除标识符、字符、字符串、头文件名、注释、不会转换成符号的预处理符号之外,存在非基本字符集中的字符 — 5.2.1
- 标识符、注释、字符、字符串、头文件名中包含非法字符 — 5.2.1.2
- 在一个翻译单元中同一个标识符既具有内部链接性又具有外部链接性 — 6.2.2
- 在对象生命周期之外访问对象 — 6.2.4
- 指针指向的对象生命周期已结束,但指针的值被继续使用 — 6.2.4
- 使用未初始化或已被销毁的具有自动存储期的对象 — 6.2.4, 6.7.9, 6.8
- 通过非字符类型的 lvalue 读取 trap representation — 6.2.6.1
- 通过非字符类型的 lvalue 产生 trap representation — 6.2.6.1
- 某些运算可以产生负 0,但实现不支持负 0 — 6.2.6.2
- 对同一对象或函数进行多次类型不兼容的声明 — 6.2.7
- 由变长数组类型(variable length array,VLA)获取相关的合成类型(composite type)时,数组长度没有求值 — 6.2.7
- 整数类型转换产生了相关类型无法表示的值 — 6.3.1.4
- 浮点类型转换产生了相关类型无法表示的值 — 6.3.1.5
- 对未指定对象的 lvalue 求值 — 6.3.2.1
- 在需要对象值的上下文中使用具有不完整类型的非数组 lvalue — 6.3.2.1
- 通过 lvalue 访问未初始化的数据 — 6.3.2.1
- 数组类型的 lvalue 被转为指向数组起始元素的指针,而数组对象具有寄存器存储类 — 6.3.2.1
- 使用 void 表达式的值,或对 void 表达式进行类型转换(除了转为 void) — 6.3.2.2
- 将指针转为无法表示指针值的整数 — 6.3.2.3
- 指针之间的转换造成了未正确对齐的结果 — 6.3.2.3
- 通过指针调用不兼容类型的函数 — 6.3.2.3
- 在一个逻辑行中单引号或双引号不匹配 — 6.4
- 在预处理阶段使关键字参与和其保留意义无关的工作 — 6.4.1
- 标识符中广义字符名称的值不在许可的范围内 — 6.4.2.1
- 标识符起始字符为表示数字的广义字符名称 — 6.4.2.1
- 两个标识符仅在非重要字符上有所不同 — 6.4.2.1
- 标识符 __func__ 被显式声明 — 6.4.2.2
- 修改字符串字面常量 — 6.4.5
- 在 #include 指令中,'、"、\、//、/* 出现在定界符 < > 之间,或 '、\、//、/* 出现在定界符 " 之间 — 6.4.7
- 表达式求值依赖无确定顺序的副作用 — 6.5
- 表达式的结果未在数学上定义或超出其类型的取值范围 — 6.5
- 通过非法类型的 lvalue 访问对象 — 6.5
- 调用没有原型声明的函数,实参与形参个数不符 — 6.5.2.2
- 调用没有原型但在相关作用域中有定义的函数时实参与形参类型不兼容 — 6.5.2.2
- 调用没有原型且在相关作用域中也没有定义的函数时实参与形参类型不兼容 — 6.5.2.2
- 函数定义时的类型与函数调用时的类型不兼容 — 6.5.2.2
- 访问原子结构体或联合体的成员 — 6.5.2.3
- 一元 * 运算符作用于无效值 — 6.5.3.2
- 将指针转为非整数非指针的其他类型 — 6.5.4
- / 或 % 运算符第二个操作数的值为 0 — 6.5.5
- 指针加减运算产生了超越数组边界的结果 — 6.5.6
- 指针加减运算产生了超越数组边界的结果,并对其解引用 — 6.5.6
- 未指向同一数组的指针相减 — 6.5.6
- 数组下标越界 — 6.5.6
- 指针相减的结果不能用 ptrdiff_t 类型的对象表示 — 6.5.6
- 移位运算符右操作数为负数或超过相关类型比特位的数量 — 6.5.7
- 对负数进行左移运算 — 6.5.7
- 比较未指向同一对象或数组的指针 — 6.5.8
- 将对象的值赋给具有部分重叠区域的对象,或完全重叠但类型不兼容的对象 — 6.5.16.1
- 要求具有常量整数类型的表达式不具有整数类型,或相关操作数不具有整数类型 — 6.6
- 初始化器(initializer)中的常量表达式不是:算术常量表达式,空指针常量,地址常量,或地址常量加减常量偏移量 — 6.6
- 常量算术表达式不具备算术类型,或者相关操作数不是整型常量、浮点数常量、枚举常量、字符常量、sizeof 表达式、_Alignof 表达式等 — 6.6
- 需要常量表达式时,通过 [ ]、.、->、&、* 等运算符访问对象的值 — 6.6
- 声明对象标识符时没有声明链接性,并且对象的类型在其声明符之后或在其初始化声明符之后不完整 — 6.7
- 在局部作用域中使用除 extern 之外的存储类说明符声明函数 — 6.7.1
- 结构体或联合体没有任何具名成员 — 6.7.2.1
- 访问没有元素的柔性数组(flexible array) — 6.7.2.1
- 需要完整类型时,没有提供完整类型定义 — 6.7.2.3
- 通过没有 const 限定的 lvalue 修改常量对象 — 6.7.3
- 通过没有 volatile 限定的 lvalue 访问 volatile 对象 — 6.7.3
- 函数类型被 const、volatile、restrict、_Atomic 等类型限定符限定 — 6.7.3
- 应该兼容的类型不具备相同的类型限定符(const、volatile 等) — 6.7.3
- 通过常量类型的 restrict 指针访问已被修改的对象 — 6.7.3.1
- restrict 指针指向基于另一个 restrict 指针的对象 — 6.7.3.1
- 具有外部链接性的函数被声明为 inline,但没有在相同的翻译单元中定义 — 6.7.4
- 具有 _Noreturn 属性的函数返回至调用方 — 6.7.4
- 同一对象的定义和声明具有不同的对齐说明符(alignment specifier) — 6.7.5
- 不同翻译单元内对同一对象的声明具有不同的对齐说明符(alignment specifier) — 6.7.5
- 应该兼容的指针类型不具备相同的类型限定符(const、volatile 等),或指向不同的类型 — 6.7.6.1
- 数组声明中的长度表达式不是常量表达式且在运行时的结果不是正整数 — 6.7.6.2
- 应该兼容的数组类型不具备兼容的元素类型,或者数组长度不同 — 6.7.6.2
- 为由 static 限定长度的数组参数,提供未指向数组起始元素的指针,或实际数组的长度小于 static 限定的长度 — 6.7.6.3
- 由存储类说明符或类型限定符修饰的 void 作为参数列表 — 6.7.6.3
- 应该兼容的函数类型不具备兼容的返回类型或参数类型 — 6.7.6.3
- 使用结构体或联合体匿名成员的值 — 6.7.9
- 标量(scalar)的初始化器(initializer)既不是单独的表达式,也不是单独的由大括号括起来的表达式 — 6.7.9
- 具有自动存储期的结构体或联合体对象,其初始化器(initializer)既不是初始化列表,也不是具有兼容类型的单独表达式 — 6.7.9
- 除用字符串初始化字符数组之外,数组的初始化器(initializer)不是大括号形式的初始化列表 — 6.7.9
- 具有外部链接性的标识符未被定义,或有多重定义 — 6.9
- 定义函数时没有声明参数的类型 — 6.9.1
- 被调整的参数类型(adjusted parameter type)在函数定义中不是完整的对象类型 — 6.9.1
- 接受可变参数的函数在定义时没有用省略号作为参数列表的最后一个参数 — 6.9.1
- 函数没有通过 return 语句返回明确的值,但函数返回值被使用 — 6.9.1
- 试探性定义(tentative definition)具有内部链接性和不完整类型的对象 — 6.9.2
- 在 #if、 #elif 的条件中,由宏展开产生了 defined 关键字,或 defined 关键字格式不正确 — 6.10.1
- #include 指令不符合标准格式 — 6.10.2
- #include 指令中头文件名称不是以字母开头 — 6.10.2
- 宏的实参列表中出现预处理指令 — 6.10.3
- 预处理运算符 # 的结果不是有效的字符串 — 6.10.3.2
- 预处理运算符 ## 的结果不是有效的符号 — 6.10.3.3
- #line 指令不符合标准格式,或指定的行号为 0,或指定的行号大于 2147483647 — 6.10.4
- 非 STDC 的 #pragma 指令导致翻译失败,或引入其他未定义的行为 — 6.10.6
- #pragma STDC 不符合标准格式 — 6.10.6
- 定义或取消定义保留的宏或标识符名称 — 6.10.8
- 在库函数不允许的情况下复制具有重叠区域的对象 — clause 7
- 包含了与标准头文件名称相同,但未提供标准头文件相同功能的头文件 — 7.1.2
- 在外部声明或定义中包含头文件 — 7.1.2
- 在包含标准头文件之前使用其中的函数、对象、类型或宏 — 7.1.2
- 定义与关键字同名的宏后包含标准头文件 — 7.1.2
- 程序没有通过标准头文件而是自行声明了库函数,且该库函数的声明不具备外部链接性 — 7.1.2
- 程序定义或声明了不被许可的保留标识符名称 — 7.1.3
- 程序去除了以下划线加大写字母以及两个下划线开头的宏定义 — 7.1.3
- 为有可变参数列表的库函数提供具有无效值或无效类型的参数 — 7.1.4
- 对于传递给库函数的数组,不能保证其中所有地址计算和对象访问都是有效的 — 7.1.4
- assert 宏被抑制 — 7.2
- assert 宏的参数不具备标量类型(scalar type) — 7.2
- 在不适当的场合进行与 CX_LIMITED_RANGE、FENV_ACCESS、FP_CONTRACT 相关的预处理设置 — 7.3.4, 7.6.1, 7.12.2
- 提供给字符处理函数的参数既不是 EOF 也不是能用 unsigned char 表示的值 — 7.4
- errno 宏被抑制,或程序定义了同名标识符 — 7.5
- FENV_ACCESS 相关的预处理设置不当 — 7.6.1
- exception-mask 参数没有正确地与浮点异常相关的宏配合使用 — 7.6.2
- fesetexceptflag 没有正确地与 fegetexceptflag 配合使用 — 7.6.2.4
- fesetenv 没有正确地与 fegetenv 配合使用,feupdateenv 没有正确地与 feholdexcept 配合使用 — 7.6.4.3, 7.6.4.4
- 整数运算或转换函数的结果不能被有效表示 — 7.8.2.1, 7.8.2.2, 7.8.2.3, 7.8.2.4, 7.22.6.1, 7.22.6.2, 7.22.1
- setlocale 函数返回的数据被修改 — 7.11.1.1
- localeconv 函数返回的数据被修改 — 7.11.2.1
- math_errhandling 宏被抑制,或定义了与 math_errhandling 相同的标识符 — 7.12
- 为浮点数分类或比较相关的宏提供非真浮点类型的参数 — 7.12.3, 7.12.14
- setjmp 宏被抑制,或程序定义了与 setjmp 相同的外部名称 — 7.13
- 在不被允许的上下文中使用 setjmp — 7.13.2.1
- 用 longjmp 跳向已不存在的执行环境 — 7.13.2.1
- longjmp 后,访问具有自动存储期的,没有 volatile 类型限定的,且在 longjmp 和对应的 setjmp 之间被修改的对象 — 7.13.2.1
- 用无效指针指定信号处理函数 — 7.14.1.1
- 与计算异常(computational exception)对应的信号处理函数正常返回 — 7.14.1.1
- SIGFPE、SIGILL、SIGSEGV 的信号处理函数,或其他由实现定义的与计算异常(computational exception)对应的信号处理函数正常返回 — 7.14.1.1
- 处理 abort 或 raise 函数产生的信号时,在信号处理函数中调用 raise 函数 — 7.14.1.1
- 处理程序外部产生的信号时引用非 lock-free 并具有静态或线程存储期的对象,且这种对象不是 volatile sig_atomic_t 类型,相关操作也不是赋值,或者使用 abort、_Exit、quick_exit、signal 之外的库函数 — 7.14.1.1
- 处理程序外部产生的信号时调用 signal 函数,当其返回 SIG_ERR 时访问 errno 的值 — 7.14.1.1
- 异步信号处理函数产生信号 — 7.14.1.1
- 在多线程环境中使用 signal 函数 — 7.14.1.1
- 没有正确地通过 va_list 访问可变参数,或在使用 va_start 之前访问可变参数 — 7.16, 7.16.1.1, 7.16.1.4
- va_arg 的参数 ap 被传给一个函数,而在这个函数中又使用 va_arg 处理 ap — 7.16
- va_start、va_arg、va_copy、va_end 的宏定义被抑制,或者程序定义了与 va_copy 或 va_end 相同的外部名称 — 7.16.1
- va_start 或 va_copy 没有正确地与 va_end 配合使用 — 7.16.1, 7.16.1.2, 7.16.1.3, 7.16.1.4
- 在 va_arg 的类型参数名称后加 * 号不能表示指针类型 — 7.16.1.1
- 没有实际的下一个参数时使用 va_arg 宏,或者下一个参数的类型与 va_arg 声明的类型不兼容 — 7.16.1.1
- 用 va_copy 或 va_start 重复初始化 va_list,已被 va_end 关闭的除外 — 7.16.1.2, 7.16.1.4
- 可变参数列表中省略号的前一个参数为数组、函数,或具有寄存器存储期,以及与默认参数提升后不兼容的类型 — 7.16.1.4
- offset 的第二个参数不是有效的 . 运算符右子表达式,或者是位域 — 7.19
- integer-constant 宏(如 INT32_C、UINT64_C 等)的参数不是合法的十进制、八进制或十六进制常量,或者参数的值超出相应类型的取值范围 — 7.20.4
- 对面向宽字符的流使用面向字节的输入输出函数,或对面向字节的流使用面向宽字符的输入输出函数 — 7.21.2
- 写入的宽字符覆盖了多节字字符的一部分,而后面的内容被使用 — 7.21.2
- 文件对象在关闭后仍被使用 — 7.21.3
- 对输入流使用 fflush 函数,或对更新流在执行输入操作后使用 fflush 函数 — 7.21.5.2
- fopen 的 mode 参数字符顺序不符合要求 — 7.21.5.3
- 写入流后未调用 fflush 也未调用文件定位函数便读取流,或者读取流后未调用定位函数便写入流 — 7.21.5.3
- 程序显式访问提供给 setvbuf 的数组 — 7.21.5.6
- 提供给格式化函数的参数数量不足,或者参数的类型不符合要求 — 7.21.6.1, 7.21.6.2, 7.29.2.1, 7.29.2.2
- 与格式化函数 format 参数对应的不是符合要求的多字符序列 — 7.21.6.1, 7.21.6.2, 7.27.3.5, 7.29.2.1, 7.29.2.2, 7.29.5.1
- 对 c、p、n、% 等无精度要求的占位符设定精度 — 7.21.6.1, 7.29.2.1
- 设置了可变宽度或精度的格式化占位符,但并未提供相应的参数 — 7.21.6.1, 7.29.2.1
- 用 # 修饰 o、x、X、a、A、e、E、f、F、g、G 之外的格式化占位符,用 0 修饰 d、i、o、u、x、X、a、A、e、E、f、F、g、G 之外的格式化占位符 — 7.21.6.1, 7.29.2.1
- 用 length modifier 修饰 d、i、o、u、x、X、a、A、e、E、f、F、g、G 之外的格式化占位符 — 7.21.6.1, 7.21.6.2, 7.29.2.1, 7.29.2.2
- 格式化输出占位符 s 对应的字符串参数不是以空字符结尾,除非对占位符 s 声明不需要空字符结尾的精度 — 7.21.6.1, 7.29.2.1
- 对格式化占位符 n 声明任何宽度、精度、标志位、赋值抑制字符 — 7.21.6.1, 7.21.6.2, 7.29.2.1, 7.29.2.2
- 格式化占位符 % 不符合 %% 这种格式 — 7.21.6.1, 7.21.6.2, 7.29.2.1, 7.29.2.2
- 使用无效的格式化占位符 — 7.21.6.1, 7.21.6.2, 7.27.3.5, 7.29.2.1, 7.29.2.2, 7.29.5.1
- 格式化输出函数参数过多,数量超过 INT_MAX — 7.21.6.1, 7.29.2.1
- 格式化输入函数参数过多,数量超过 INT_MAX — 7.21.6.2, 7.29.2.2
- 格式化占位符与对应的参数类型不匹配 — 7.21.6.2, 7.29.2.2
- 格式化输入占位符 c、s、[ 对应的参数没有足够的空间容纳输入的数据 — 7.21.6.2, 7.29.2.2
- 由 l 修饰的格式化输入占位符 c、s、[ 对应的输入不是符合要求的多字符序列 — 7.21.6.2, 7.29.2.2
- 格式化输入占位符 %p 对应的参数不是同一个进程中的指针 — 7.21.6.2, 7.29.2.2
- 未正确初始化的 va_list 作为 vfprintf、vscanf 等函数的参数,或者在函数返回后继续使用该参数 — 7.21.6.8, 7.21.6.9, 7.21.6.10, 7.21.6.11, 7.21.6.12, 7.21.6.13, 7.21.6.14, 7.29.2.5, 7.29.2.6, 7.29.2.7, 7.29.2.8, 7.29.2.9, 7.29.2.10
- 在 fgets 或 fgetws 失败后继续使用传入的数组 — 7.21.7.2, 7.29.3.2
- 在调用 ungetc 函数后使用二进制流的 file position indicator,而在调用之前其值为 0 — 7.21.7.10
- 在 fread 或 fwrite 失败后使用 file position indicator — 7.21.8.1, 7.21.8.2
- 使用由 fread 读取的不完整元素 — 7.21.8.1
- fseek 没有正确地与 ftell 配合使用 — 7.21.9.2
- fsetpos 没有正确地与 fgetpos 配合使用 — 7.21.9.3
- 指针指向长度为 0 的内存空间,并通过该指针访问对象 — 7.22.3
- 指针指向的空间被释放后,该指针的值仍被使用 — 7.22.3
- aligned_alloc 要求的对齐方式当前实现不支持,或申请的长度不是对齐方式的整数倍 — 7.22.3.1
- free 或 realloc 的参数不是由匹配的分配函数返回,或是已被释放的地址 — 7.22.3.3, 7.22.3.5
- 使用由 malloc 分配的且未初始化的数据 — 7.22.3.4
- 使用由 realloc 分配的超出原对象大小且未初始化的数据 — 7.22.3.5
- 调用 exit 或 quick_exit 超过一次,或这两个函数都被调用 — 7.22.4.4, 7.22.4.7
- 调用由 atexit 或 at_quick_exit 注册的函数时,调用 longjmp 结束了被注册函数的执行 — 7.22.4.4, 7.22.4.7
- 程序修改了由 getenv 或 strerror 引入的字符串 — 7.22.4.6, 7.24.6.2
- 在 quick_exit 函数执行时产生了信号 — 7.22.4.7
- 用 system 执行可以造成终止的命令,或命令包含未定义的行为 — 7.22.4.8
- 无效指针或空数组作为搜索或排序库函数的参数 — 7.22.5
- 供搜索或排序库函数调用的比较函数修改了数组元素,或返回不一致的结果 — 7.22.5
- 供 bsearch 搜索的数组,其元素未按正确的顺序排列 — 7.22.5.1
- 更改了 LC_CTYPE 类别后仍使用之前获取的 mbstate_t 等对象 — 7.22.7
- 字符串或宽字符串库函数越界访问相关对象 — 7.24.1, 7.29.4
- 无效指针或无效长度作为字符串或宽字符串库函数的参数 — 7.24.1, 7.29.4
- strxfrm、strftime、wcsxfrm、wcsftime 的目标数组过小,无法存储以空字符结尾的完整结果 — 7.24.4.5, 7.27.3.5, 7.29.4.4.4, 7.29.5.1
- 第一次调用 strtok 或 wcstok 时,第一个参数是空指针 — 7.24.5.8, 7.29.4.5.7
- 为泛型宏提供的参数没有与其类型对应的函数 — 7.25
- 为泛型宏提供 complex 参数,但没有与之对应的 complex 函数 — 7.25
- 传递给 asctime 的参数包含超过正常范围的值,或年份超过 4 位数或小于 1000 — 7.27.3.1
- 调用 fwprintf 函数时,如果 s 占位符没有用 l 修饰,与之对应的参数不是有效的多字节字符序列 — 7.29.2.11
- 传递给 wcstok 的 ptr 参数不是来自针对同一个字符串的上一次 wcstok 调用 — 7.29.4.5.7
- 不正确地使用 mbstate_t 对象 — 7.29.6
- 传递给宽字符分类或映射函数的 wint_t 参数,其值即不是 WEOF 也不能用 wchar_t 表示 — 7.30.1
- iswctype 配合 wctype 使用时,作用于这两个函数的 LC_CTYPE 类别不一致 — 7.30.2.2.1
- towctrans 配合 wctrans 使用时,作用于这两个函数的 LC_CTYPE 类别不一致 — 7.30.3.2.1