Is (int *)0 a null pointer? - c

This could be thought of as an extension to this question (I'm interested in C only, but adding C++ to complete the extension)
The C11 standard at 6.3.2.3.3 says:
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
What my take on this personally is that 0 and (void *)0 represent the null pointer, whose integer value may not actually be 0, but that doesn't cover 0 cast to any other type.
But, the standard then continues:
If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, ...
which covers (int *)0 as null pointer since cast is an explicit conversion (C11, 6.3) which is listed under conversion methods.
However, what still makes me wonder is the following phrase
... or such an expression cast to type void * ...
With the above semantics, this phrase seems completely useless. The question is, is this phrase completely useless? If not, what implications does it have? Consequently, is (int *)0 the null pointer or not?
Another question that can help the discussion is the following. Is (long long)123 considered "123 converted to long long", or "123 with type long long". In other words, is there any conversion in (long long)123? If there is none, then the second quote above doesn't cover (int *)0 as a null pointer.

Short answer:
In both C and C++, (int *)0 is a constant expression whose value is a null pointer. It is not, however, a null pointer constant. The only observable difference between a constant-expression-whose-value-is-a-null-pointer and a null-pointer-constant, that I know of, is that a null-pointer-constant can be assigned to an lvalue of any pointer type, but a constant-expression-whose-value-is-a-null-pointer has a specific pointer type and can only be assigned to an lvalue with a compatible type. In C, but not C++, (void *)0 is also a null pointer constant; this is a special case for void * consistent with the general C-but-not-C++ rule that void * is assignment compatible with any other pointer-to-object type.
For example:
long *a = 0; // ok, 0 is a null pointer constant
long *b = (long *)0; // ok, (long *)0 is a null pointer with appropriate type
long *c = (void *)0; // ok in C, invalid conversion in C++
long *d = (int *)0; // invalid conversion in both C and C++
And here's a case where the difference between the null pointer constant (void *)0 and a constant-expression-whose-value-is-a-null-pointer with type void * is visible, even in C:
typedef void (*fp)(void); // any pointer-to-function type will show this effect
fp a = 0; // ok, null pointer constant
fp b = (void *)0; // ok in C, invalid conversion in C++
fp c = (void *)(void *)0; // invalid conversion in both C and C++
Also, it's moot nowadays, but since you brought it up: No matter what the bit representation of long *'s null pointer is, all of these assertions behave as indicated by the comments:
// 'x' is initialized to a null pointer
long *x = 0;
// 'y' is initialized to all-bits-zero, which may or may not be the
// representation of a null pointer; moreover, it might be a "trap
// representation", UB even to access
long *y;
memset(&y, 0, sizeof y);
assert (x == 0); // must succeed
assert (x == (long *)0); // must succeed
assert (x == (void *)0); // must succeed in C, unspecified behavior in C++
assert (x == (int *)0); // invalid comparison in both C and C++
assert (memcmp(&x, &y, sizeof y) == 0); // unspecified
assert (y == 0); // UNDEFINED BEHAVIOR: y may be a trap representation
assert (y == x); // UNDEFINED BEHAVIOR: y may be a trap representation
"Unspecified" comparisons do not provoke undefined behavior, but the standard doesn't say whether they evaluate true or false, and the implementation is not required to document which of the two it is, or even to pick one and stick to it. It would be perfectly valid for the above memcmp to alternate between returning 0 and 1 if you called it many times.
Long answer with standard quotes:
To understand what a null pointer constant is, you first have to understand what an integer constant expression is, and that's pretty hairy -- a complete understanding requires you to read sections 6.5 and 6.6 of C99 in detail. This is my summary:
A constant expression is any C expression which the compiler can evaluate to a constant without knowing the value of any object (const or otherwise; however, enum values are fair game), and which has no side effects. (This is a drastic simplification of roughly 25 pages of standardese and may not be exact.)
Integer constant expressions are a restricted subset of constant expressions, conveniently defined in a single paragraph, C99 6.6p6 and its footnote:
An integer constant expression96 shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof
operator.
96 An integer constant expression is used to specify the size of a bit-field member of a structure, the value of an enumeration constant, the size of an array, or the value of a case constant. Further constraints that apply to the integer constant expressions used in [#if] are discussed in 6.10.1.
For purpose of this discussion, the important bit is
Cast operators ... shall only convert arithmetic types to integer types
which means that (int *)0 is not an integer constant expression, although it is a constant expression.
The C++98 definition appears to be more or less equivalent, modulo C++ features and deviations from C. For instance, the stronger separation of character and boolean types from integer types in C++ means that the C++ standard speaks of "integral constant expressions" rather than "integer constant expressions", and then sometimes requires not just an integral constant expression, but an integral constant expression of integer type, excluding char, wchar_t, and bool (and maybe also signed char and unsigned char? it's not clear to me from the text).
Now, the C99 definition of null pointer constant is what this question is all about, so I'll repeat it: 6.3.2.3p3 says
An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant. If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
Standardese is very, very literal. Those two sentences mean exactly the same thing as:
An integer constant expression with the value 0 is called a null pointer constant.
An integer constant expression with the value 0, cast to type void *, is also a null pointer constant.
When any null pointer constant is converted to a pointer type, the resulting pointer is called a null pointer and is guaranteed to compare unequal ...
(Italics - definition of term. Boldface - my emphasis.) So what that means is, in C, (long *)0 and (long *)(void *)0 are two ways of writing exactly the same thing, namely the null pointer with type long *.
C++ is different. The equivalent text is C++98 4.10 [conv.ptr]:
A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to zero.
That's all. "Integral constant expression rvalue of integer type" is very nearly the same thing as C99's "integer constant expression", but there are a few things that qualify in C but not C++: for instance, in C the character literal '\x00' is an integer constant expression, and therefore a null pointer constant, but in C++ it is not an integral constant expression of integer type, so it is not a null pointer constant either.
More to the point, though, C++ doesn't have the "or such an expression cast to void *" clause. That means that ((void *)0) is not a null pointer constant in C++. It is still a null pointer, but it is not assignment compatible with any other pointer type. This is consistent with C++'s generally pickier type system.
C++11 (but not, AFAIK, C11) revised the concept of "null pointer", adding a special type for them (nullptr_t) and a new keyword which evaluates to a null pointer constant (nullptr). I do not fully understand the changes and am not going to try to explain them, but I am pretty sure that a bare 0 is still a valid null pointer constant in C++11.

Evaluating the expression (int*)0 yields a null pointer of type int*.
(int*)0 is not a null pointer constant.
A null pointer constant is a particular kind of expression that may appear in C source code. A null pointer is a value that may occur in a running program.
C and C++ (being two distinct languages) have slightly different rules in this area. C++ doesn't have the "or such an expression cast to type void*" wording. But I don't think that affects the answer to your question.
As for your question about (long long)123, I'm not sure how it's related, but the expression 123 is of type int, and the cast specifies a conversion from int to long long.
I think the core confusion is an assumption that the cast in (int*)0 does not specify a conversion, since 0 is already a null pointer constant. But a null pointer constant is not necessarily an expression of pointer type. In particular, the expression 0 is both a null pointer constant and an expression of type int; it is not of any pointer type. The term null pointer constant needs to be thought of as a single concept, not a phrase whose meaning depends on the individual words that make it up.

Related

Why are there two ways of expressing NULL in C?

According to §6.3.2.3 ¶3 of the C11 standard, a null pointer constant in C can be defined by an implementation as either the integer constant expression 0 or such an expression cast to void *. In C the null pointer constant is defined by the NULL macro.
My implementation (GCC 9.4.0) defines NULL in stddef.h in the following ways:
#define NULL ((void *)0)
#define NULL 0
Why are both of the above expressions considered semantically equivalent in the context of NULL? More specifically, why do there exist two ways of expressing the same concept rather than one?
Let's consider this example code:
#include <stddef.h>
int *f(void) { return NULL; }
int g(int x) { return x == NULL ? 3 : 4; }
We want f to compile without warnings, and we want g to cause an error or a warning (because an int variable x was compared to a pointer).
In C, #define NULL ((void*)0) gives us both (GCC warning for g, clean compile for f).
However, in C++, #define NULL ((void*)0) causes a compile error for f. Thus, to make it compile in C++, <stddef.h> has #define NULL 0 for C++ only (not for C). Unfortunately, this also prevents the warning from being reported for g. To fix that, C++11 uses built-in nullptr instead of NULL, and with that, C++ compilers report an error for g, and they compile f cleanly.
((void *)0) has stronger typing and could lead to better compiler or static analyser diagnostics. For example since implicit conversions between pointers and plain integers aren't allowed in standard C.
0 is likely allowed for historical reasons, from a pre-standard time when everything in C was pretty much just integers and wild implicit conversions between pointers and integers were allowed, though possibly resulting in undefined behavior.
Ancient K&R 1st edition provides some insight (7.14 the assignment operator):
The compilers currently allow a pointer to be assigned to an integer, an integer to a pointer, and a pointer to a pointer of another type. The assignment is a pure copy operation, with no conversion. This usage is nonportable, and may produce pointers which cause addressing exceptions when used. However, it is guaranteed that assignment of the constant 0 to a pointer will produce a null pointer distinguishable from a pointer to any object.
Few things in C are more confusing than null pointers. The C FAQ list devotes an entire section to the topic, and to the myriad misunderstandings that eternally arise. And we can see that those misunderstandings never go away, as some of them are being recycled even in this thread, in 2022.
The basic facts are these:
C has the concept of a null pointer, a distinguished pointer value which points definitively nowhere.
The source code construct by which a null pointer is requested — a null pointer constant — fundamentally involves the token 0.
Because the token 0 has other uses, ambiguity (not to mention confusion) is possible.
To help reduce the confusion and ambiguity, for many years the token 0 as a null pointer constant has been hidden behind the preprocessor macro NULL.
To provide some type safety and further reduce errors, it's attractive to have the macro definition of NULL include a pointer cast.
However, and most unfortunately, enough confusion crept in along the way that properly mitigating it all has become almost impossible. In particular, there is so very much extant code that says things like strbuf[len] = NULL; (in an obvious but basically wrong attempt to null-terminate a string) that it is believed in some circles to be impossible to actually define NULL with an expansion including either the explicit cast or the hypothetical future (or extant in C++) new keyword nullptr.
See also Why not call nullptr NULL?
Footnote (call this point 3½): It's also possible for a null pointer — despite being represented in C source code as an integer constant 0 — to have an internal value that is not all-bits-0. This fact adds massively to the confusion whenever this topic is discussed, but it doesn't fundamentally change the definition.
There is just one way to express NULL in C, it's a single 4-character token.
But hold on, when going into its definition it gets more interesting.
NULL has to be defined as a null pointer constant, meaning an integer constant with value 0 or such cast to void*.
As an integer constant is just an expression of integer type with a few restrictions to guarantee static evaluation, there are infinite possibilities for any wanted value.
Of all those possibilities, only an integer literal with value 0 is also a null pointer constant in C++, for what it's worth.
The reason for such variation is history and precedent (everyone did it differently, void* was late to the party, and existing code/implementations trumps all), reinforced with backwards-compatibility which preserves it.
6.3.2.3 Pointers
[...]
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.
67) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
[...]
6.6 Constant expressions
[...]
Description
2 A constant expression can be evaluated during translation rather than runtime, and accordingly may be used in any place that a constant may be.
Constraints
3 Constant expressions shall not contain assignment, increment, decrement, function-call, or comma operators, except when they are contained within a subexpression that is not evaluated.117)
4 Each constant expression shall evaluate to a constant that is in the range of representable values for its type.
Semantics
5 An expression that evaluates to a constant is required in several contexts. If a floating expression is evaluated in the translation environment, the arithmetic range and precision shall be at least as
great as if the expression were being evaluated in the execution environment.118)
6 An integer constant expression119) shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, _Alignof expressions, and floating constants that are the immediate operands of casts.
Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof or _Alignof operator.
C was originally developed on machines where a null pointer constant and the integer constant 0 had the same representation. Later, some vendors ported the language to mainframes where a different special value triggered a hardware trap when used as a pointer, and wanted to use that value for NULL. These companies discovered that so much existing code type-punned between integers and pointers, they had to recognize 0 as a special constant that could implicitly convert to a null pointer constant. ANSI C incorporated this behavior, at the same time as they introduced the void* as a pointer that implicitly converts to any type of object pointer. This allowed NULL to be used as a safer alternative to 0.
I’ve seen some code that (possibly tongue-in-cheek) detected one of these machines by testing if ((char*)1 == 0).
why do there exist two ways of expressing the same concept rather than one?
History.
NULL started as 0 and later better programming practices encouraged ((void *)0).
First, there are more than 2 ways:
#define NULL ((void *)0)
#define NULL 0
#define NULL 0L
#define NULL 0LL
#define NULL 0u
...
Before void * (Pre C89)
Before void * and void existed, #define NULL some_integer_type_of_zero was used.
It was useful to have the size of that integer type to match the size of object pointers. Consider the below. With 16-bit int and 32-bit long, it is useful for the type of zero used to match the width of an object pointer.
Consider printing pointers.
double x;
printf("%ld\n", &x); // On systems where an object pointer was same size as long
printf("%ld\n", NULL);// Would like to use the same specifier for NULL
With 32-bit object pointers, #define NULL 0L is better.
double x;
printf("%d\n", &x); // On systems where an object pointer was same size as int
printf("%d\n", NULL);// Would like to use the same specifier for NULL
With 16-bit object pointers, #define NULL 0 is better.
C89
After the birth of void, void *, it is natural to have the null pointer constant to be a pointer type. This allowed the bit pattern of (void*)0) to be non-zero. This was useful in some architectures.
printf("%p\n", NULL);
With 16-bit object pointers, #define NULL ((void*)0) works above.
With 32-bit object pointers, #define NULL ((void*)0) works.
With 64-bit object pointers, #define NULL ((void*)0) works.
With 16-bit int, #define NULL ((void*)0) works.
With 32-bit int, #define NULL ((void*)0) works.
We now have independence of the int/long/object pointer size. ((void*)0) works in all cases.
Using #define NULL 0 creates issues when passing NULL as a ... argument, hence the irksome need to do printf("%p\n", (void*)NULL); for highly portable code.
With #define NULL ((void*)0), code like char n = NULL; will more likely raise a warning, unlike ``#define NULL 0`
C99
With the advent of _Generic, we can distinguish, for better or worse, NULL as a void *, int, long, ...
According to §6.3.2.3 ¶3 of the C11 standard, a null pointer constant in C can be defined by an implementation as either the integer constant expression 0 or such an expression cast to void *.
No, that a misleading paraphrase of the language spec. The actual language of the cited paragraph is
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. [...]
Implementations don't get to choose between those alternatives. Both are forms of a null pointer constant in the C language. They can be used interchangeably for the purpose.
Moreover, not only the specific integer constant expression 0 can serve in this role, but any integer constant expression with value 0 can do. For example, 1 + 2 + 3 + 4 - 10 is such an expression.
Additionally, do not confuse null pointer constants generally with the macro NULL. The latter is defined by conforming implementations to expand to a null pointer constant, but that doesn't mean that the replacement text of NULL is the only null pointer constant.
My implementation (GCC 9.4.0) defines NULL in stddef.h in the
following ways:
#define NULL ((void *)0)
#define NULL 0
Not both at the same time, of course.
Why are both of the above expressions considered semantically
equivalent in the context of NULL?
Again with the reversal. It's not "the context of NULL". It's pointer context. There is nothing particularly special about the macro NULL itself to distinguish contexts in which it appears from contexts where its replacement text appears directly.
And I guess you're asking for rationale for paragraph 6.3.2.3/3, as opposed to "because 6.3.2.3/3". There is no published rationale for C11. There is one for C99, which largely serves for C90 as well, but it does not address this issue.
It should be noted, however, that void (and therefore void *) was an invention of the committee that developed the original C language specification ("ANSI C" / C89 / C90). There was no possibility of an "integer constant expression cast to type void *" before then.
More specifically, why do there
exist two ways of expressing the same concept rather than one?
Are there, really?
If we accept an integer constant expression with value 0 as a null pointer constant (a source-code entity), and we want to convert it to a runtime null pointer value, then which pointer type do we choose? Pointers to different object types do not necessarily have the same representation, so this actually matters. Type void * seems the natural choice to me, and that's consistent with the fact that, alone of all pointer types, void * can be converted to other object pointer types without a cast.
But then, in a context where 0 is being interpreted as a null pointer constant, casting it to void * is a no-op, so (void *) 0 expresses exactly the same thing as 0 in such a context.
What's really going on here
At the time the ANSI committee was working, many existing C implementations accepted integer-to-pointer conversions without a cast, and although the meaning of most such conversions was implementation and / or context specific, there was wide acceptance that converting constant 0 to a pointer yielded a null pointer. That use was by far the most common one of converting an integer constant to a pointer. The committee wanted to impose stricter rules on type conversions, but it did not want to break all the existing code that used 0 as a constant representing a null pointer.
So they hacked the spec.
They invented a special kind of constant, the null pointer constant, and provided rules around it that made it compatible with existing use. A null pointer constant, regardless of lexical form, can be implicitly converted to any pointer type, yielding a null pointer (value) of that type. Otherwise, no implicit integer-to-pointer conversions are defined.
But the committee preferred that null pointer constants should actually have pointer type without conversion (which 0 does not, pointer context or no), so they provided for the "cast to type void *" option as part of the definition of a null pointer constant. At the time, that was a forward-looking move, but the general consensus now appears to be that it was the right direction to aim.
And why do we still have the "integer constant expression with value 0"? Backwards compatibility. Consistency with conventional idioms such as {0} as a universal initializer for objects of any type. Resistance to change. Perhaps other reasons as well.
The "why" - it is for historical reasons. NULL was used in various implementations before it was added to a standard. And at the time it was added to a C standard, implementations defined NULL usually as 0, or as 0 cast to some pointer. At that point you wouldn't want to make one of them illegal, because whichever you made illegal, you'd break half the existing code.
The C11 standard allows for a null pointer constant to be defined either as the integer constant expression 0 or as an expression that is cast to void *. The use of the NULL macro makes it easier for programmers to use the null pointer constant in their code, as they don't have to remember which of these definitions the implementation uses.
Using a macro also makes it easier to change the underlying definition of the null pointer constant in the future, if necessary. For example, if the implementation decided to change the definition of NULL to be a different integer constant expression, they could do so by simply modifying the definition of the NULL macro. This would not require any changes to the code that uses the NULL macro, as long as the code uses the NULL macro consistently.
There are two definitions of the NULL macro provided in the example you gave because some systems may define NULL as an expression that is cast to void *, while others may define it as the integer constant expression 0. By providing both definitions, the stddef.h header can be used on a wide range of systems without requiring any modifications.

Is there such a thing as nullptr (or equivalent) in modern C standards?

I included a check for nullptr in a line of C code. The compiler (gcc) complained when using -std=c17 as well as -std=gnu17.
Is there such a thing as nullptr (or equivalent) in modern C standards? (C11, C17)
If not, then why?
No, C still uses NULL for a null pointer.
C++ needs a dedicated null pointer literal because it has overloading and template type deduction. These features get confused by NULL, which expands to 0 (or something like that) in C++. However, the risk of such confusion in C is small (maybe _Generic can get confused by this), and in addition, C can use (void*)0 for a null pointer, which mitigates this risk even more.
The closest thing to C++'s nullptr is C's NULL. Which may be
an integer constant expression with the value ​0​,
an integer constant expression with the value 0 cast to the type void*.
A null pointer constant may be converted to any pointer type; such conversion results in the null pointer value of that type.
The formal C17 specifications state that the stddef.h header defines NULL "which expands to an implementation-defined null pointer constant." (7.19)
A null pointer constant is defined as follows (6.3.2.3)
An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.
Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null
pointers shall compare equal.
Note that this makes the following program ambiguous, as NULL could be an integer constant expression (accepted by the function) or of the type void* (not accepted by the function).
#include <stdio.h>
void printInt(int n)
{
printf("%d\n", n);
}
int main(void)
{
printInt(NULL);
}
Which is why nullptr was introduced in C++11. For C, having no function overloading or type deduction, this is less of an issue.
A null pointer in C is a pointer object pointing at "null". You can turn a pointer into a null pointer by assigning it to a null pointer constant. Valid null pointer constants are 0 and (void*)0. The macro NULL is guaranteed to be a null pointer constant.
The internal representation of the pointer then becomes a "null pointer", which could in theory point at an address different from zero on some exotic system. Similarly, NULL could in theory expand to something different from zero in old, pre-standard C.
When creating C++, Bjarne Stroustrup found all of this to be needlessly complex and decided that "NULL is 0" (source: https://www.stroustrup.com/bs_faq2.html#null). Notably C++ was created long before the first standardization of C, so his arguments are less relevant to standard C than they were to pre-standard C.
For more info about null pointers vs NULL in C, see What's the difference between null pointers and NULL?
ISO C 23 now has nullptr, as well as the nullptr_t type. The proposal that introduced it has some rationale.

Is it safe to assume that the NULL constant is zero?

The book Understanding and Using C Pointers, by Richard Reese says:
The null concept is an abstraction supported by the null pointer
constant. This constant may or may not be a constant zero. A C
programmer need not be concerned with their actual internal
representation.
My question is, since "this constant may or may not be a constant zero," is it safe for me to do things like the below in my code:
int *ptr = NULL;
// Some code which probably sets ptr to a valid memory address
if(!ptr)
{
ERROR();
}
If NULL is not 0, there is a chance that the if clause will evaluate to true.
Is it safe to assume that the NULL constant is zero?
NULL will compare equal to 0.
NULL is very commonly a zero bit pattern. It is possible for NULL to be a non-zero bit pattern - but not seen these days.
OP is mixing as least 4 things: NULL, null pointer constant, null pointer, comparing a null pointer to 0. C does not define a NULL constant.
NULL
NULL is a macro "which expands to an implementation-defined null
pointer constant" C17dr § 7.19 3
null pointer constant
An integer constant expression with the value 0, or such an expression
cast to type void *, is called a null pointer constant. C17dr § §
6.3.2.3 3
Thus the type of a null pointer constant may be int, unsigned, long, ... or void * .
When an integer constant expression1, the null pointer constant value is 0. As a pointer like ((void *)0), its value/encoding is not specified. It ubiquitously does have the bit pattern of zeros, but is not specified so.
There may be many null pointer constants. They all compare equal to each other.
Note: the size of a null pointer constant, when it is an integer, may differ from the size of an object pointer. This size difference is often avoided by appending a L or two suffix as needed.
null pointer
If a null pointer constant is converted to a pointer type, the
resulting pointer, called a null pointer, is guaranteed to compare
unequal to a pointer to any object or function. C17dr § § 6.3.2.3 3
Conversion of a null pointer to another pointer type yields a null
pointer of that type. Any two null pointers shall compare equal. C17dr
§ § 6.3.2.3 4
The type of null pointer is some pointer, either an object pointer like int *, char * or function pointer like int (*)(int, int) or void *.
The value of a null pointer is not specified. It ubiquitously does have the bit pattern of zeros, but is not specified so.
All null pointer compare as equal, regardless of their encoding.
comparing a null pointer to 0
if(!ptr) is the same as if(!(ptr != 0)). When the pointer ptr, which is a null pointer, is compared to 0, the zero is converted to a pointer, a null pointer of the same type: int *. These 2 null pointers, which could have different bit patterns, compare as equal.
So when it is not safe to assume that the NULL constant is zero?
NULL may be a ((void*)0) and its bit pattern may differ from zeros. It does compare equal to 0 as above regardless of its encoding. Recall pointer compares have been discussed, not integer compares. Converting NULL to an integer may not result in an integer value of 0 even if ((void*)0) was all zero bits.
printf("%ju\n", (uintmax_t)(uintptr_t)NULL); // Possible not 0
Notice this is converting a pointer to an integer, not the case of if(!ptr) where a 0 was converted to a pointer.
The C spec embraces many old ways of doing things and is open to novel new ones. I have never came across an implementation where NULL was not an all zeros bit pattern. Given much code exist that assumes NULL is all zero bits, I suspect only old obscure implementations ever used a non-zero bit-pattern NULL and that NULL can be all but certain to be an all zero bit pattern.
1 The null pointer constant is 1) an integer or 2) a void*. "When an integer ..." refers to the first case, not a cast or conversion of the second case as in (int)((void*)0).
if(!ptr) is a safe way to check for a NULL pointer.
The expression !x is exactly equivalent to 0 == x. The constant 0 is a NULL pointer constant, and any pointer may be compared for equality against a NULL pointer constant.
This holds true even if the representation of a null pointer is not "all bits 0".
Section 6.5.3.3p5 of the C standard regarding the ! operator states:
The result of the logical negation operator ! is 0 if the
value of its operand compares unequal to 0, 1 if the value of its
operand compares equal to 0. The result has type int. The
expression !E is equivalent to (0==E).
And section 6.3.2.3p3 regarding pointer conversions states:
An integer constant expression with the value 0, or such an
expression cast to type void *, is called a null pointer
constant. If a null pointer constant is converted to a pointer type,
the resulting pointer, called a null pointer, is guaranteed to compare
unequal to a pointer to any object or function.
chux has written a good, detailed answer, but regarding that book specifically, I'd be sceptic about its quality:
This constant may or may not be a constant zero
This is wrong, it must always be a zero or a zero cast to a void*. The definition of a null pointer constant is found in C17 6.3.2.3/3:
An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant. If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
This means that all integer constant expressions like 0, 0L, 0u, 0x0, '\0' etc are null pointer constant. If any of them is cast to a void*, it is also a null pointer constant.
A C programmer need not be concerned with their actual internal representation.
The author is obviously mixing up the two formal terms null pointer constant and null pointer. A programmer do not need to concern themselves with the internal representation of a null pointer. They do need to know what makes a valid null pointer constant though. The safest, most readable way being to use the NULL macro, which is guaranteed to be a null pointer constant.
So regarding your question "is it safe for me to do things like the below in my code" - yes it is perfectly safe to do !ptr to check for a null pointer, even though ptr==NULL is more readable code.

Does Standard define null pointer constant to have all bits set to zero?

( I'm quoting ISO/IEC 9899:201x )
Here we see that, integer constant expression has an integer type:
6.6 Constant expressions
6.
An integer constant expression shall have integer type and shall only have operands
that are integer constants, enumeration constants, character constants, sizeof
expressions whose results are integer constants, _Alignof expressions, and floating
constants that are the immediate operands of casts. Cast operators in an integer constant
expression shall only convert arithmetic types to integer types, except as part of an
operand to the sizeof or _Alignof operator.
Then this holds true for any integer type:
6.2.6.2 Integer types
5.
The values of any padding bits are unspecified.A valid (non-trap) object representation
of a signed integer type where the sign bit is zero is a valid object representation of the
corresponding unsigned type, and shall represent the same value. For any integer type,
the object representation where all the bits are zero shall be a representation of the value
zero in that type.
Then we see that a null pointer constant is defined using an integer constant expression with the value 0.
6.3.2.3 Pointers
3.
An integer constant expression with the value 0, or such an expression cast to type
void*, is called a null pointer constant. If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
Therefore the null pointer constant must have all it's bits set to zero.
But there are many answers online and on StackOverflow that say that that isn't true.
I have a hard time believing them given the quoted parts.
( Please answer using references to the latest Standard )
Does Standard define null pointer constant to have all bits set to zero?
No, it doesn't. No paragraph of the C Standard impose such a requirement.
void *p = 0;
p for example is a null pointer, but the Standard does not require that the object p must have all bit set.
For information the c-faq website mentions some systems with non-zero null pointer representations here: http://c-faq.com/null/machexamp.html
No, NULL doesn't have to be all bits zero.
N1570 6.3.2.3 Pointers paragraph 3:
An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant. 66) If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
See my emphasis above: Integer 0 is converted if necessary, it doesn't have to have same bit presentation.
Note 66 on bottom of the page says:
66) The macro NULL is defined in (and other headers) as a null pointer constant; see 7.19.
Which leads us to a paragraph of that chapter:
The macros are
NULL
which expands to an implementation-defined null pointer constant
And what is more, on Annex J.3.12 (Portability issues, Implementation-defined behaviour, Library functions) says:
— The null pointer constant to which the macro NULL expands (7.19).
Asking about the representation of a null pointer constant is quite pointless.
A null pointer constant either has an integer type or the type void*. Whatever it is, it is a value. It is not an object. Values don't have a representation, only objects have. We can only talk about representations by taking the address of an object, casting it to char* or unsigned char*, and looking at the bytes. We can't do that with a null pointer constant. As soon as it is assigned to an object, it's not a null pointer constant anymore.
A major limitation of the C standard is that because the authors want to avoid prohibiting compilers from behaving in any ways that any production code anywhere might be relying upon, it fails to specify many things which programmers need to know. As a consequence, it is often necessary make assumptions about things which are not specified by the standard, but match the behaviors of common compilers. The fact that all of the bytes comprising a null pointer are zero is one such assumption.
Nothing in the C standard specifies anything about the bit-level representation of any pointer beyond the fact that every possible value of each and every data type--including pointers--will be representable as a sequence of char values(*). Nonetheless, on nearly all common platforms platforms zeroing out all the bytes associated with a structure is equivalent to setting all the members to the static default values for their types (the default value for a pointer being null). Further, code which uses calloc to receive a zeroed-out a block of RAM for a collection of structures will often be much faster than code which uses malloc and then has to manually clear every member of every structure, or which uses calloc and but still manually clears every non-integer member of every structure.
I would suggest therefore that in many cases it is perfectly reasonable to write code targeted for those dialects of C where null pointers are stored as all-bytes-zero, and have as a documented requirement that it will not work on dialects where that is not the case. Perhaps someday the ISO will provide a standard means by which such requirements could be documented in machine-readable form (such that every compiler would be required to either abide by a program's stated requirements or refuse compilation), but so far as I know none yet exists.
(*) From what I understand, there's some question as to whether compilers are required to honor that assumption anymore. Consider, for example:
int funcomp(int **pp, int **qq)
{
int *p,*q;
p = (int*)malloc(1234);
*pp = p;
free(p);
q = (int*)malloc(1234);
*qq = q;
*q = 1234;
if (!memcmp(pp, qq, sizeof p))
return *p;
return 0;
}
Following free(p) any attempt to access *p will be Undefined Behavior. Although there's a significant likelihood that q will receive the exact same bit pattern as p, nothing in the standard would require that p must be considered a valid alias for q even in that scenario. On the other hand, it also seems strange to say that two variables of the same type can hold the exact same bits without their contents being equivalent. Thus, while it's clearly natural that the function would be allowed to either return 0 along with values of *pp and *qq that don't compare bit-wise equal, or 1234 along with values of *pp and *qq that do compare bit-wise equal, the Standard would seem to allow the function to behave arbitrarily if both malloc happen to yield bitwise-equivalent values.

Is NULL in C required/defined to be zero?

NULL appears to be zero in my GCC test programs, but wikipedia says that NULL is only required to point to unaddressable memory.
Do any compilers make NULL non-zero? I'm curious whether if (ptr == NULL) is better practice than if (!ptr).
NULL is guaranteed to be zero, perhaps casted to (void *)1.
C99, §6.3.2.3, ¶3
An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant.(55) If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
And note 55 says:
55) The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant.
Notice that, because of how the rules for null pointers are formulated, the value you use to assign/compare null pointers is guaranteed to be zero, but the bit pattern actually stored inside the pointer can be any other thing (but AFAIK only few very esoteric platforms exploited this fact, and this should not be a problem anyway since to "see" the underlying bit pattern you should go into UB-land anyway).
So, as far as the standard is concerned, the two forms are equivalent (!ptr is equivalent to ptr==0 due to §6.5.3.3 ¶5, and ptr==0 is equivalent to ptr==NULL); if(!ptr) is also quite idiomatic.
That being said, I usually write explicitly if(ptr==NULL) instead of if(!ptr) to make it extra clear that I'm checking a pointer for nullity instead of some boolean value.
Notice that in C++ the void * cast cannot be present due to the stricter implicit casting rules that would make the usage of such NULL cumbersome (you would have to explicitly convert it to the compared pointer's type every time).
From the language standard:
6.3.2.3 Pointers
...
3 An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant.55) If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.
...
55) The macro NULL is defined in <stddef.h> (and other headers) as a null pointer constant; see 7.17.
Given that language, the macro NULL should evaluate to a zero-valued expression (either an undecorated literal 0, an expression like (void *) 0, or another macro or expression that ultimately evaluates to 0). The expressions ptr == NULL and !ptr should be equivalent. The second form tends to be more idiomatic C code.
Note that the null pointer value doesn't have to be 0. The underlying implementation may use any value it wants to represent a null pointer. As far as your source code is concerned, however, a zero-valued pointer expression represents a null pointer.
In practice is the same, but NULL is different to zero. Since zero means there's a value and NULL means there isn't any. So, theoretically they are different, NULL having a different meaning and in some cases that difference should be of some use.
in practice no, !ptr is correct

Resources