a == p will also resolve to true. So will c == p. a and b don't add anything to your example; they're just in there to make c and p look like they're more different than they are.
$ cat zero.c
#include "stdio.h"
int main(int argc, char* argv[]) {
char nul = 0;
void* null = 0;
if( nul == null ) {
printf("compared char to pointer; they are the same\n");
} else {
printf("found a difference between char and pointer\n");
}
return 0;
}
$ gcc -o zero zero.c
zero.c: In function ‘main’:
zero.c:7:10: warning: comparison between pointer and integer
if( nul == null ) {
^~
$ ./zero
compared char to pointer; they are the same
$
You get a warning, but not an error, for making the comparison. By contrast, assigning the integer zero to a void* isn't even a warning -- it's just a natural thing to do. There isn't another way to set a pointer to NULL. There is another way to set a character to 0, the '\0' syntax, but that's not a warning either.
C will think nothing of adding '!' to 'P' and getting 'q'. That's not strange because addition is a pretty normal thing to do with integers. You're right that a char variable should only occupy 8 bits of memory, but that's an implementation artifact, not a theory of what the value '\0' means. That value is unambiguously the integer zero with infinite precision. The reason it only occupies 8 bits is that you can't let it have infinite bits.
$ cat pointers.c
#include "stdio.h"
int main(int argc, char* argv[]) {
char Z = 'Z';
char q = 'q';
void* null = 0;
printf("Z is \\x%02x\n", Z);
printf("But if it were a pointer, it would be %08x\n", Z);
printf("Watch this:\n\n");
null = Z;
/* %p to print a pointer value */
printf("Our void* is now: %p\n", null);
q = null;
printf("And q is: %c\n", q);
return 0;
}
$ gcc -o pointers pointers.c
pointers.c: In function ‘main’:
pointers.c:12:7: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
null = Z;
^
pointers.c:17:4: warning: assignment makes integer from pointer without a cast [-Wint-conversion]
q = null;
^
$ ./pointers
Z is \x5a
But if it were a pointer, it would be 0000005a
Watch this:
Our void* is now: 0x5a
And q is: Z
$
The assignments are warnings. They work just like you'd expect them to work.
Notice all the different printf flags? This is why you need them.
The literal 0 in a pointer context will be converted to a NULL pointer, which can be a non-zero bit pattern (there are some systems where the actual NULL pointer isn't all zeros). Going through a variable might not do what you think. So this:
char *p = 0;
is fine, but
intptr_t a = 0;
char *p = (char *)a;
might not do what you expect (set p to the NULL pointer).
C will think nothing of adding '!' to 'P' and getting 'q'. That's not strange because addition is a pretty normal thing to do with integers. You're right that a char variable should only occupy 8 bits of memory, but that's an implementation artifact, not a theory of what the value '\0' means. That value is unambiguously the integer zero with infinite precision. The reason it only occupies 8 bits is that you can't let it have infinite bits.