(这个问题指的是汇编语言。)我有点困惑。我多次遇到过应该返回句柄的 Windows 函数,如果不返回,则返回 NULL。为什么检查后检查零?零不等于 NULL。
例如:GetModuleHandleA:
https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea
(这个问题指的是汇编语言。)我有点困惑。我多次遇到过应该返回句柄的 Windows 函数,如果不返回,则返回 NULL。为什么检查后检查零?零不等于 NULL。
例如:GetModuleHandleA:
https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea
在 C 和许多其他低级编程语言中,该术语NULL
等效于0
.
C 标准要求将 NULL 设为#define
“实现定义的值”,但是所有实现都选择(出于显而易见的原因)0
用于该目的。出于这个原因,如果您尝试“查看定义” NULL
,许多 IDE 会让您排成一行#define NULL 0
或类似的东西。
这有一个额外的好处,即NULL
评估false
使条件语句可读和直观。
从严格的标准遵循角度来看,正确的方法是使用NULL
而不是 0,这就是大多数开发人员所做的。然而,编译器(或在 的情况下为预处理器#define NULL 0
)会将其转换为0
.
一些高级语言(例如 javascript 和 C++)使用特殊表达式来表示 null。一个例子是 C++ 的nullptr
,因为 C++11 是NULL
. Javascript 使用一个特殊的对象,null
.
在查看 Windows API 调用或 C/C++ 代码的反汇编时,NULL 始终为 0,在 Visual Studio 中这是在 vcruntime.h 中定义的
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
但是,如果您正在查看更高级别的语言,NULL 不一定为零,例如在 .NET C# 代码中,如下所示:
if (args == null)
{
Console.WriteLine("null!");
}
将编译为通用中间语言 (CIL)。你可以看到ldnull null 不仅仅是零。
IL_0001: ldarg.0
IL_0002: ldnull
IL_0003: ceq
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: brfalse.s IL_0016
IL_000a: ldstr "null!"
IL_000f: call void [mscorlib]System.Console::WriteLine(string)
ISO C 和 C++允许实现使用非零位模式作为空指针的对象表示,尽管需要将文字0
或(void*)0
源中(在指针上下文中)作为空指针进行评估,相当于NULL
. 基于像源定义推理#define NULL 0
是不用C或C ++足够。
不过幸运的是每个人的理智,所有现代的C和C ++实现的x86(以及其他现代ISA)中做用0
在ASM为NULL的位模式。 这使得不可移植的代码像memset(ptr_array, 0, size)
预期的那样工作,相当于将每个元素设置为NULL
.
NULL 宏何时不为 0?正在询问源级非零定义,但我认为这在现代 C 中是不允许的。答案提到了几台具有非零空指针位模式的历史机器。(即您在 asm 中看到的代码,例如do {...} while(p = p->next);
)
请记住,在 asm 中,指针只是 64 位(或 32 位)整数。整个想法NULL
是带内信令,而不是一些甚至不是指针大小的整数的特殊事物。所以我们必须选择一些常量。
0
是一个方便的标记值,因为与检查任何其他值相比,许多 ISA 可以在非零值上更有效地进行分支。例如,ARM 必须cbnz
在非零上进行分支,而无需cmp
. x86 对test eax, eax
/jnz
而不是cmp eax, 0
/ 进行了较小的代码大小优化jnz
。(使用 CMP reg,0 与 OR reg,reg? 测试寄存器是否为零?)。如果 FLAGS 已经由另一条算术指令设置,则test
不需要,但这对于空指针测试来说是不寻常的:通常你不会对指针进行数学运算,然后是 NULL。
(您没有在 asm 中看到该优化,因为您的调试版本在测试之前存储到内存中。)
此外,0
易于生成。一些大的数字可能需要更大的指令,或者大多数指令,才能在寄存器中创建。(例如 x86xor eax,eax
而不是mov eax, imm32
)。零初始化静态存储static int *table = NULL;
可以在 BSS 中而不是.data
- 现代系统零初始化 BSS。
在某些系统(尤其是嵌入式)上,0
地址并不特殊,实际上那里有系统管理的东西,比如中断处理程序表的开头。So0
可以是有效地址,也可以等于NULL
。这有点糟糕,所以这就是人们可能真正想要一个非零对象表示的空指针的地方。@Simon里氏评论有关黑客攻击的ARM编译器使用0x20000000
的空位模式。
在使用虚拟内存的系统(如 Windows)上,我们可以简单地避免映射包含该地址的页面,这有助于通过确保 NULL 取消引用实际出错来帮助检测错误。(请记住,在C和C ++的是,不确定的行为不是必需的故障,但它是如果它确实方便。)