The C standard has more restrictions on pointers than what was discussed in the previous blog post. This post covers pointer casts.
Pointer casts and alignment
Casting a pointer invokes undefined behavior if the resulting pointer is not correctly aligned. The standard is written in this way in order to support architectures that use different formats for different kinds of pointers, and such architectures do exist — see for example this mail to the GCC development list about a mainframe architecture that was recently commercially supported with a GCC 4.3 port.
The compiler may use this to optimize memory accesses for processors with strict alignment requirements (such as old ARM processors). Consider for example
The only thing that is guaranteed to work on any implementation is casting between the integer value 0 and pointers:
One other thing to note is that
void foo(void *p) { memset(p, 0, 4); }that clears 32 bits of data. This could be generated as a 32-bit store, but the alignment of
p
is unknown, so the compiler must generate this as four byte operations if it want to inline the memset
mov r3, #0 strb r3, [r0, #0] strb r3, [r0, #1] strb r3, [r0, #2] strb r3, [r0, #3]Consider now
void bar(int *); void foo(void *p) { memset(p, 0, 4); bar(p); }The call to
bar
will convert p
to an int*
pointer, and this invokes undefined behavior if p
is not 32-bit aligned. So the compiler may assume p
is aligned, and the memset
can now be generated as a 32-bit store
mov r3, #0 str r3, [r0, #0]This example illustrates two things that often cause confusion with regard to undefined behavior
- The effect of undefined behavior may go back in time — the undefined behavior is invoked when calling
bar
, but the compiler may use this to optimize code executed beforebar
in a way that would make it misbehave ifp
was not correctly aligned. - The compiler just use the undefined behavior of misaligned conversion to determine that
p
must be aligned, which then affects each use ofp
in the same way as if the alignment had been known by some other means — i.e. the compiler developers do not go out of their way implementing evil algorithms doing obscure transformations back in time based on undefined behavior.
Function pointers
It is not allowed to cast between a function pointer and a pointer to object type (i.e. a "normal" pointer). The reason is that they may be very different on a hardware level, and it may be impossible to represent data pointers as function pointers, or vice versa. One trivial example is when function pointers and data pointers have different width, such as the MS-DOS "medium" memory model that use 32-bit pointers for code but only 16-bit pointers for data.Casting between integers and pointers
Casting between integers and pointers are implementation defined, so the compiler may choose to handle this in any way it want (although the standard has a footnote saying that the intention is that the mapping between pointers and integers should "be consistent with the addressing structure of the execution environment").The only thing that is guaranteed to work on any implementation is casting between the integer value 0 and pointers:
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.
I have read far too many confused discussions about if the value of NULL
is guaranteed to be 0
. In some sense it is, as (void*)0 == NULL
evaluates to true, but the value of (void*)NULL
does not need to be 0
when stored in memory — the implementation may for example choose to implement the cast operator as flipping the most significant bit,1 which means that the code
union { uintptr_t u; void *p; } u; u.p = 0; printf("0x%" PRIxPTR "\n", u.u);prints
0x80000000
on a 32-bit machine.One other thing to note is that
NULL
is defined as being a "null pointer constant", which does not need to be of a pointer type per the quoted text above! This may cause problems when passing NULL
to a va_arg
function.
1. This is not a completely stupid example. Consider a small embedded processor that has some hardware mapped at address 0. The platform does not have much memory, so
0x80000000
is guaranteed not to be a valid address, and the implementation use this for NULL
. There are in general better ways of handling this, but I have seen this done for real hardware...