附录 C 转换 NULL 指针 (Casting the NULL Pointer)
核心结论
-
NULL 可定义为 0 或 (void *)0——C 标准允许实质等同;用 0 在普通上下文(指针上下文)合法且隐式转换。
-
变参函数中的 NULL 必须显式 cast:
execl(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 的实现定义 |
|
§C;Linux glibc / 自带 stddef.h 多用 |
variadic 函数的 cast 强制要求 |
variadic 函数( |
§C; |
不同指针类型不必表示相同 |
除 char * / void * 之外,C 不保证 |
§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 函数(execl、execlp、sprintf、setenv 等)必须显式 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 上 int 与 void * 同 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 要求
|
四、思维导图
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
五、重点与易错点
-
variadic 函数 NULL 必须 cast——
execl,sprintf,execlp等都算,缺 cast 在 Cray / 旧平台立刻坏;Linux 上侥幸正确。 -
整型 0 在 pointer context 自动转 null pointer——但仅编译器看得见上下文时,variadic 函数中编译器看不到。
-
NULL 与 0 选哪个风格——统一团队约定;常见是「指针用 NULL,整型用 0」,使代码意图清晰。
-
不要
execl(p, a, NULL);要execl(p, a, (char *)NULL);长时间看可读性高。 -
printf("%s", NULL)` UB——
glibc上 Linux 会打印 "(null)",但 SUSv3 中性;写安全代码前用printf("%s", str ? str : "");。 -
跨平台写 helper variadic wrapper:(如
errExit) 要对每个指针参数做强转;否则集成其它平台时崩。 -
cast (void *)0 当 (char *)0 用——多数场景没问题(glibc 上两者相同),但严格 SUSv3 不保证。
-
NULL 在
VA_ARGS宏里——C99 规定VA_ARGS至少 1 个参;GCC 扩展允许空,但不要依赖。 -
(intptr_t)NULL是合法对所有指针安全的 cast——变长整数包含 null pointer value;但 variadic 中仍需 cast 到目标指针类型。 -
编译器
-Wformat/-Wint-conversion启用——CI 默认开起来,把潜在 variadic NULL bug 提前消除。 -
不要
memset(&sa, 0, sizeof(sa)); sa.sun_family = AF_UNIX;——指针字段 0 自动是 null pointer,正确。 -
C 没用 NULL*——用 `nullptr`(强类型),variadic 调用编译期报错;C11+ 推荐;C 中没
nullptr可自己写((void)0)。 -
NULL 与 '\0' / 0 的关系——
'\0'是 char 0;NULL是 pointer 0;都值等于 0 但类型不同。 -
写共享库时 variadic call 一定要慎重——ABI 跨语言(C ↔ Fortran ↔ Lua)时 NULL 转很微妙。
-
跨章衔接:本书大部分程序都引用
errExit(fmt, …),helper 必须支持 variadic;附录 B getopt() 也是 variadic string 助手;第六章_exit(int)与 waitpid 父子有 variadic 关系。