Issue with NULL pointers on Harvard Architecture platform - c

Interesting issue we came across here this week.
We are working in C on a Harvard Architecture embedded platform, which has 16-bit data addresses and 32-bit code addresses.
The issue occurs when you are working with function pointers. If you have code like
if (fp) fp();
or
if (fp != 0) fp();
everything is fine.
However if you have code like
if (fp != NULL) fp();
then, because NULL is defined as (void *) 0, the compiler (gcc in this case) a) does not warn and b) does a 16-bit comparison against your function pointer instead of a 32-bit comparison. Fine as long as your function pointer doesn't happen to lie on a 64k boundary so all bottom 16 bits are 0.
At the moment we have large swathes of code which contain explicit checks against NULL. Most of them will be data pointers, but some of them will be function pointers. A quick grep for != NULL or == NULL revealed over 3000 results, to many to go through manually to check.
So, what we would like now would be either
a way to find all the cases where function pointers (but not data pointers) are compared (so we can instead have them compare against FP_NULL which we would define as a 32-bit 0), or
to redefine NULL in such a way that it would do the right thing.
(Or, I suppose, to
update our gcc port to detect and
correctly handle this case).
I can't think of any approach that works for 1. The only approach I can think of for 2 is to redefine NULL as a 0 function pointer, which would be very wasteful for the vast majority of comparisons which are against data pointers. (A 32-bit compare is 4 instructions, a 16-bit compare is 1 instruction).
Any thoughts or suggestions?

It seems to me the easiest way is replace all occurrences of NULL by 0. This works for function pointer (as you say) and object pointers.
This is a variant of (2) Redefine NULL to plain 0.
But the fact that you cannot compare function pointers with NULL is a bug in your implementation. C99 states that comparison of the null pointer constant is possible with both object and function pointers, and that NULL should expand to this constant.
Small addition from the C-FAQ question 5.8:
Q: Is NULL valid for pointers to functions?
A: Yes (but see question 4.13)
Mixing function pointers with (void *) 0
(A reply to R..'s comment). I believe using function pointers and (void *) 0 together is well-defined. In my reasoning I will refer to sections of the C99 draft 1256, but will not quote large parts to keep it readable. It should also be applicable to C89.
6.3.2.3 (3) defines the integer constant expression 0 and such an expressions cast to (void *) as null pointer constant. And: "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.8.9 defines the == and != operands for (among other) a pointer operand and a null pointer constant. For these: "If one operand is a pointer and the other is a
null pointer constant, the null pointer constant is converted to the type of the pointer."
Conclusion: In fp == (void *) 0, the null pointer constant is converted to the type of fp. This null pointer can be compared to fp and is guaranteed to be unequal to fp if it points to a function. Assignment (=) has a similar clause, so fp = (void *) 0; is also well-defined C.

The way you describe should work:
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.
So, either NULL has been redefined to something not 0 or (void*)0 (or equivalent?) or your compiler is non-conformant.
Try redefining NULL yourself (to plain 0) after all the #includes in all files :-)
just for kicks: try gcc -E (to output the preprocessed source) on a problem file and check the expansion of NULL

You could try this:
#ifdef NULL
#undef NULL
#endif
#define NULL 0

Here are a few suggestions:
temporarily change NULL to (char*)0 or something else that is not implicitly convertible to a function pointer. This should give a warning about every comparison with a non-matching pointer. You could then run the produced compiler output through a tool like grep and look for a typical pattern for function pointers, like (*)(.
redefine NULL as 0 (without the cast to void*). This is another valid definition for NULL and might do the right thing for you, but no guarantees.

You could try a segment hack (really only a hack), so you can use the fast 16bit comparision, without any risk.
Create segements at each n*0x10000 boundary with size of 4 (or even smaller), so there never can't exists a real function.
It depends on your embedded device memory space, if this is a good or a really bad solution.
It could work if you have 1MB normal Flash, which will never change.
It will be painfull if you have 64MB Nand Flash.

Edit the implemention's system headers to replace all occurrances of
#define NULL ((void *)0)
with
#define NULL 0
Then file a bug report with the vendor. You should not have to modify your (perfectly correct, albeit ugly style) code because of a bug in the vendor's compiler.

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.

Why is there a NULL in the C language?

Why is there a NULL in the C language? Is there a context in which just plain literal 0 would not work exactly the same?
Actually, you can use a literal 0 anyplace you would use NULL.
Section 6.3.2.3p3 of the C standard 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.
And section 7.19p3 states:
The macros are:
NULL
which expands to an implementation-defined null pointer constant
So 0 qualifies as a null pointer constant, as does (void *)0 and NULL. The use of NULL is preferred however as it makes it more evident to the reader that a null pointer is being used and not the integer value 0.
NULL is used to make it clear it is a pointer type.
Ideally, the C implementation would define NULL as ((void *) 0) or something equivalent, and programmers would always use NULL when they want a null pointer constant.
If this is done, then, when a programmer has, for example, an int *x and accidentally writes *x = NULL;, then the compiler can recognize that a mistake has been made, because the left side of = has type int, and the right side has type void *, and this is not a proper combination for assignment.
In contrast, if the programmer accidentally writes *x = 0; instead of x = 0;, then the compiler cannot recognize this mistake, because the left side has type int, and the right side has type int, and that is a valid combination.
Thus, when NULL is defined well and is used, mistakes are detected earlier.
In particular answer to your question “Is there a context in which just plain literal 0 would not work exactly the same?”:
In correct code, NULL and 0 may be used interchangeably as null pointer constants.
0 will function as an integer (non-pointer) constant, but NULL might not, depending on how the C implementation defines it.
For the purpose of detecting errors, NULL and 0 do not work exactly the same; using NULL with a good definition serves to help detect some mistakes that using 0 does not.
The C standard allows 0 to be used for null pointer constants for historic reasons. However, this is not beneficial except for allowing previously written code to compile in compilers using current C standards. New code should avoid using 0 as a null pointer constant.
It is for humans not compilers.
if I see in the code p = NULL; instead of p = 0; it is much easier for me to understand that p is a pointer not the integer.
For compilers it does not matter, for humans does.
Same as we use definitions instead of "raw" values or expressions or human readable variable names like loopCounter instead of p755_x.
Why is there a NULL in the C language?
To help make clear the assignment implies a pointer and not an integer.
Example: strtok(char *s1, const char *s2); in both cases below receive a null pointer as the NULL and 0 are both converted to a char *. The first is usually considered better self-documentation. As a style issue, follow your group's coding standard.
strtok(s, NULL);
strtok(s, 0;
Is there a context in which just plain literal 0 would not work exactly the same?
Yes - when the original type is important.
0 is an int
NULL is a void *, or int, unsigned or long or long long, etc. It is implementation defined.
Consider a function that takes a variable number of pointers, with a sentinel null pointer to indicate the last.
foo("Hello", "World", NULL); // might work if `NULL` is a pointer.
foo("Hello", "World", 0);
As the arguments as 0 and NULL are not converted when passed to a ... function (aside from some promotions), the function foo() might not access them the same. Portable code would use:
foo("Hello", "World", (char*) NULL);
// or
foo("Hello", "World", (char*) 0);
A difference may also occur when NULL, 0 are passed to _Generic
The integer constant literal 0 has different meanings depending upon the context in which it's used. In all cases, it is still an integer constant with the value 0, it is just described in different ways.
Namely, the most common purposes of NULL pointer are:
To initialize a pointer variable when that pointer variable isn’t
assigned any valid memory address yet.
To check for a null pointer before accessing any pointer
variable. By doing so, we can perform error handling in pointer
related code e.g. dereference pointer variable only if it’s not
NULL.
To pass a null pointer to a function argument when we don’t want
to pass any valid memory address.(ref)

Does C have more macros like NULL?

Background:
When deleting a cell in a hash table that uses linear probing you have to indicate that a value once existed at that cell but can be skipped during a search. The easiest way to solve this is to add another variable to store this information, but this extra variable can be avoided if an guaranteed invalid memory address is known and is used to represent this state.
Question:
I assume that since 0 is a guaranteed invalid memory address (more often than not), there must be more than just NULL. So my question is, does C provide a standard macro for any other guaranteed invalid memory addresses?
Technically, NULL is not guaranteed to be invalid. It is only guaranteed not to be the address of any object (C11 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(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.
(66) The macro NULL is defined in (and other headers) as a null pointer constant
Your usage does not require the special address value to be invalid either: obviously, you are not accessing it, unless segfaulting is part of the normal behavior of your program.
So you could use the addresses of as many objects as you like, as long as the addresses of these objects are not intended to be part of the normal contents of a cell.
For instance, for an architecture where converting between pointers to objects preserve the representation, you could use:
char a, b, …;
#define NULL1 (&a)
#define NULL2 (&b)
…
Strictly speaking, NULL is not required to be numerically zero at runtime. C translates 0 and NULL, in a pointer context, into an implementation-defined invalid address. That address is often numerically zero, but that is not guaranteed by the C standard. To the best of my knowledge, C itself does not provide any invalid addresses guaranteed to be distinct from NULL.
You can also create your own 'invalid' address pointer:
const void* const SOME_MARKER = (void*) &x;
If you make sure that x (or its address) can never be actually used where you want to use SOME_MARKER you should be safe and 100% portable.

Pointer manipulation in C

I am new to C programming. While solving one of my class assignments, I came across the following code snippet. I did not understand what it does.
Can any one tell me what is the meaning of following C syntax,
((char *)0 +1) or ((int*)0 +1))
The (char *) 0 part creates a pointer to character data, at address 0. This address is then incremented by one, triggering undefined behavior since pointers to address 0 (also known as NULL in C) cannot be used in pointer arithmetic. The second part does the same but for pointer to integer data.
If the compiler simply treats NULL as the address (which is common but, again, not required which is why this is undefined behavior) the resulting addresses, if viewed numerically, will not be the same, since pointer arithmetic in C is done in terms of the type being pointed at, and typically sizeof (int) > sizeof (char).
Can any one tell me what is the meaning of following C syntax,
((char *)0 +1) or ((int*)0 +1))
Nothing by the terms of the C standard, because it's not defined. This code invokes undefined behavior on part of the C compiler. Let me explain:
In C every pointer may either point to some object of the type the pointer dereferences to or it may be 0, which is then called a null pointer. Null pointers can not be used in →pointer arithmetic.
Note that the actual representation of a null pointer on the metal, i.e. the bits the variable has on the machine may be something different than all zeros. But on the C side of things the null pointer always compares equal to an integer of the value 0. Moreover null pointers of different types also compare equal by definition. However comparisons of non null pointers of different types invokes undefined behavior. Also you can cast any pointer to a void* pointer, and back. Also you can cast every pointer to an integer of type uintptr_t and back. But casting from a pointer to type A to a pointer of type B (where B is not void*) invokes undefined behavior.
The special function malloc is defined by the C language specification to return a void* pointer that can be cast to any pointer type, though. But say you use it to allocate some memory for an array of char and later you cast that to int this again invokes undefined behavior.
Now you may ask: "What is undefined behavior?". Well, it just means, that the language standard doesn't define it and an implementer may go about it in any way seen fit. On most plattforms writing something like ((char*)0 + 1) may do something naively expected (creating a pointer, pointing to address 1), but it may as well make the compiler build an artificial intelligence, that at first chases you down the street, then gains consciousness and finally takes over the world, turning humans into batteries. So be careful about what you do ;)
In C you have to tell compiler which type you mean to use, this is called "casting".
For example:
char *c; //define c as "char pointer" (pointer to char)
c = ((char *)0 + 1); //this casts "0 + 1" to "char pointer" type, in this example not strictly necessary but adds some clarification to code

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