... How the implementation maps [the source-code null-pointer
construct(s)] to a pointer is implementation defined. I've often
wondered how one might get around this, especially in an embedded
environment where every location might be legal. ...
The original PDP-11 implementation used address 0, which never
occurred in data because data came after code, and never occurred
in code because location 0 contained the a.out header.
Interestingly, when the Unix systems were ported to separate-I&D
PDP-11 systems, things broke. Now NULL *was* the address of
some valid data item, because while code-location-0 still
contained the a.out header, there was a separate data-location-0
and the C compiler put whatever data object happened to be first
in data-location-0. So translating NULL to address 0 resulted in
sometimes having the expression:
p == NULL && p == &var
be true!
The fix used was to insert a "shim" at data-location-zero, so that
the first C-code variable had data-address 2 rather than data-address
0.
Note that a C system could legitimately do this "the other way":
instead of choosing a particular *fixed* location (such as 0 or 0xffff)
and stuffing a secret object into that address, the compiler could
compile "p = NULL" as if it read:
p = (void *)&__runtime_nil_object;
The linker would then insert the "__runtime_nil_object" object
somewhere -- anywhere! -- in memory, and all tests for NULL pointers
would just compare the pointer value with (void *)&__runtime_nil_object.
Fixed locations are generally easier to handle, and if 0 is for
some reason not a good choice, (char *)(-1) is often just as good:
"p == NULL" tests can compile to "ld reg,p; inc reg" on conventional
inc-sets-condition-code systems, or "addcc p_reg,1,%g0" on SPARC-like
systems, for instance. Note that this is often the same number of
instructions that are required to test whether p==0!