附录 C 转换 NULL 指针 (Casting the NULL Pointer)

      +

      核心结论

      • NULL 可定义为 0 或 (void *)0——C 标准允许实质等同;用 0 在普通上下文(指针上下文)合法且隐式转换。

      • 变参函数中的 NULL 必须显式 castexecl(prog, arg, NULL) 在 variadic call 中编译通过可能给错指针——必须 execl(prog, arg, (char *) NULL)

      • 关键点:编译器无法确定 variadic 参数类型;C 标准不保证 null 指针与 integer 0 位模式相同;多字节时 null 指针可能 ≥ int 大小。

      • 指针与整数表示不必相同int *p = 0 合法(0 转 null pointer in pointer context),但 variadic call 不经上下文分析,int 0 直接入参。

      • char * / void * 表示相同:C 标准强制这两种类型在内部表示相同;execl(prog, arg, (void *) NULL) 在 portable 上仍不安全(execl 要求 char * 数组),但 Linux 上意外能工作。

      附录主旨

      本附录解释一个看似琐碎但能引发跨平台 bug 的 C 语义陷阱——「NULL 在 variadic 函数中是否要 cast」。读者应掌握:(1) int 0 与 (void *)0 的标准定义;(2) 何时必须 cast(变参函数或非原型函数);(3) Linux/x86 习惯上不开 cast 也能跑通,但不可移植;(4) 任意指针类型指针应 cast 成目标指针类型才能进入 variadic 调用。

      一、核心概念

      本附录围绕 3 个核心概念:NULL 的两种可能定义、variadic 函数需要 cast、指针类型之间表示相同。

      概念 定义 + 重要性 实现提示

      NULL 的实现定义

      <stddef.h> 等中 NULL 宏定义为 0((void *)0);C 标准允许;两者实质等价(pointer context)。

      §C;Linux glibc / 自带 stddef.h 多用 ((void *)0);BSD / 自定义可能用 0 整型常量。

      variadic 函数的 cast 强制要求

      variadic 函数(execlprintf)参数类型对调用者不可见;编译器不做隐式转换;int 0 直接入参时被解释为指针可能导致 random pointer。

      §C;execl("ls","ls","-l",(char *)NULL) 是正确写法;(void *)NULL 在 char * context 不一定能跨平台。

      不同指针类型不必表示相同

      除 char * / void * 之外,C 不保证 int *long *struct foo * 内部表示相同;因此 cast 类型必须严格等于形参类型。

      §C;POSIX AIO 例:struct aiocb * 跟 void * 不同,但 glibc 上恰好相同;不要赌。

      二、详细笔记

      C.1 不用 cast 也能用的场景

      What:在「编译器知道指针类型」的上下文中,0 / NULL 自动转换成 null pointer——不需要 cast。

      Why:避免每处都写 (int *)0 这种视觉噪音。

      How

      int *p;
      p = 0;                  /* 0 → null pointer(编译器知上下文) */
      p = NULL;               /* 同 p = 0 */
      
      sigaction(SIGINT, &sa, 0);            /* 原型要求 sigaction 第三参为 int */
      sigaction(SIGINT, &sa, NULL);         /* NULL → 0 → null pointer */
      
      int *array[] = { NULL, NULL };        /* 指针 context 显式 */

      When:(1) 任何「有原型、固定类型」函数中传 NULL;(2) 任何带类型的赋值右值。

      Example:所有标准库的有原型函数调用 NULL 参数都不必 cast(除非是 variadic)。

      C.2 variadic 函数必须 cast

      What:传 NULL 到 variadic 函数(execlexeclpsprintfsetenv 等)必须显式 cast。

      Why:编译器无法做隐式 int→pointer 转换;int 0 直接被压栈,按 variadic 取参规则被解释为指针;与实际 null pointer 可能位模式不同。

      How

      /* INCORRECT(语法 OK,语义危险) */
      execl("/bin/ls", "ls", "-l", NULL);
      execl("/bin/ls", "ls", "-l", 0);
      
      /* CORRECT(portable) */
      execl("/bin/ls", "ls", "-l", (char *) NULL);
      
      /* 多参数场景 */
      sprintf(buf, "%s=%d", key, (int) value);  /* 即使非 NULL 也需 cast int */

      注:Linux/x86 上 intvoid * 同 4 字节时,cast 与不 cast 几乎无差别;但 Cray / 旧 UNICOS 上 int = 8B、指针 = 16B 时,无 cast 直接传 0 给变参,行为完全随机。

      When:(1) execl* / execv* / sprintf / vsprintf / setenv* 等所有变参函数;(2) errExit 这类自定义变参 helper。

      Example:TLPI 全部示例 errExit(fmt, …​) 调用都把指针参数 cast 为目标类型。

      C.3 NULL 在不同标准 C 中的语义

      What:C89/99/11 都规定 NULL 可以是 0 或 (void *)0;两者在 pointer context 等价。

      Why:写 portable C 不能依赖实现选哪个;只依赖「指针 context 隐式转换 0 → null pointer」。

      How:约定俗成的规则: . 用 NULL 表示 null pointer 常量(不是整型 0)。 . 比较时用 ptr == NULL 而不是 !ptr(后者依赖语境)。 . 任何与整型上下文会引发编译器告警时用 0;指针上下文统一 NULL。

      When:(1) 函数声明中用 void *p = NULL; 而非 void *p = 0;(简洁)。

      Example:glibc <stddef.h>#define NULL ((void *)0);但在 <stdio.h> / <stdlib.h> 都直接 include 这个。

      C.4 char * 与 void * 表示相同

      What:C 标准强制 char *void * 位模式相同——这是 variadic 调用中两个可互转的代表。

      Why:execl 等函数需要 char * 数组结尾;可写成 (void *)0 在 Linux 上仍工作(因为 char * 和 void * 兼容),但 SUSv3 严格要求 cast 为 char *

      How

      /* SUSv3 严格 */
      execl(p, a, (char *)NULL);
      
      /* SUSv3 非严格但 Linux 实际接受 */
      execl(p, a, (void *)NULL);
      
      /* 错误 */
      execl(p, a, (int *)NULL);   /* int * 与 char * 表可能不同 */

      When:(1) 跨平台 variadic 调用:必须 cast 到目标指针类型;(2) Linux 内一致性:可用 (void *)NULL

      Example:标准库内部把 char environ 作为 void 传在 Linux 上可工作(因 char*/void* 兼容),但非系统性保证。

      C.5 实际编译器告警示例

      What:gcc / clang 在 -Wall / -Wextra 下对无 cast 变参调用会告警——把潜在 bug 提前。

      Why:靠编译器 lint 减少 variadic NULL bug。

      How

      $ cat > test.c << 'EOF'
      #include <unistd.h>
      int main(void) {
          execl("/bin/ls","ls","-l",NULL);   /* warning */
          return 0;
      }
      EOF
      $ gcc -Wall -Wextra -Werror -c test.c
      warning: passing argument 4 of 'execl' makes pointer from integer without a cast
          execl("/bin/ls","ls","-l",NULL);
                      ^
      note: expected 'char *' but argument is of type 'int'

      When:(1) 任何新写 C 项目加 -Werror=int-conversion / -Wformat;(2) CI 上 lint 必备。

      Example:使用 cppcheck --enable=warning,style test.c 可看到 missingCastForVariadic arg 类警告。

      三、关键图表

      case-by-case cast 要求
      场景 是否需要 cast 例子

      int *p = NULL;

      标准赋值

      func(NULL); (有原型,参数类型是指针)

      sigaction(…​,NULL); 之后无 cast

      execl(p, a, NULL);

      execl(p, a, (char *)NULL);

      printf("%s", p);

      printf 显式得 %s 转换规则

      printf(NULL); (故意)

      printf 读取 format string,违规无意义

      func(NULL, NULL); 两指针

      否(如果两参都指针)

      同原类型 NULL 自动转 |

      variadic user function

      myLog("%s %d", (char *)msg, (int)val);

      四、思维导图

      mindmap
        root((附录 C NULL 转换))
          NULL 定义
            0 整型
            void 0 指针
            stddef 头
            两者等价
          pointer context
            赋值 NULL
            函数原型参
            数组初始化
            隐式转换
          variadic 函数
            execl
            printf sprintf
            必须 cast
            char 0 推荐
          指针表示
            char void 等价
            int long 不一定
            struct 不一定
            不要靠赌
          编译器告警
            int conv
            Wformat
            Wint conversion
            cppcheck

      五、重点与易错点

      1. variadic 函数 NULL 必须 cast——execl, sprintf, execlp 等都算,缺 cast 在 Cray / 旧平台立刻坏;Linux 上侥幸正确。

      2. 整型 0 在 pointer context 自动转 null pointer——但仅编译器看得见上下文时,variadic 函数中编译器看不到。

      3. NULL 与 0 选哪个风格——统一团队约定;常见是「指针用 NULL,整型用 0」,使代码意图清晰。

      4. 不要 execl(p, a, NULL);要 execl(p, a, (char *)NULL);长时间看可读性高。

      5. printf("%s", NULL)` UB——glibc 上 Linux 会打印 "(null)",但 SUSv3 中性;写安全代码前用 printf("%s", str ? str : "");

      6. 跨平台写 helper variadic wrapper:(如 errExit) 要对每个指针参数做强转;否则集成其它平台时崩。

      7. cast (void *)0 当 (char *)0 用——多数场景没问题(glibc 上两者相同),但严格 SUSv3 不保证。

      8. NULL 在 VA_ARGS 宏里——C99 规定 VA_ARGS 至少 1 个参;GCC 扩展允许空,但不要依赖。

      9. (intptr_t)NULL 是合法对所有指针安全的 cast——变长整数包含 null pointer value;但 variadic 中仍需 cast 到目标指针类型。

      10. 编译器 -Wformat / -Wint-conversion 启用——CI 默认开起来,把潜在 variadic NULL bug 提前消除。

      11. 不要 memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX;——指针字段 0 自动是 null pointer,正确。

      12. C 没用 NULL*——用 `nullptr`(强类型),variadic 调用编译期报错;C11+ 推荐;C 中没 nullptr 可自己写 ((void)0)

      13. NULL 与 '\0' / 0 的关系——'\0' 是 char 0;NULL 是 pointer 0;都值等于 0 但类型不同。

      14. 写共享库时 variadic call 一定要慎重——ABI 跨语言(C ↔ Fortran ↔ Lua)时 NULL 转很微妙。

      15. 跨章衔接:本书大部分程序都引用 errExit(fmt, …​),helper 必须支持 variadic;附录 B getopt() 也是 variadic string 助手;第六章 _exit(int) 与 waitpid 父子有 variadic 关系。