This is a somewhat complex topic. Generally, unless you are a C veteran, then my advise is to never convert a pointer to a different type. Even conversions to/from void pointers are very often questionable.
If we are to restrict the topic to object pointers (and ignore function pointers), then first there's the mentioned void pointers - every object pointer type in C can be implicitly converted to a void pointer and vice versa. That is, the conversion itself is safe, what happens when you de-reference the data is another story.
Other than void pointers, you generally get a compiler error when trying to assign between pointers to different type. C has a much stronger type system for pointers than for say integers.
C also allows all manner of wild pointer conversions by the means of a cast. The conversion itself is almost always fine - what might not be fine is what happens when you de-reference the pointed-at data. The C standard says this (C17 6.3.2.3/7):
A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned for the referenced type, the behavior is undefined. Otherwise, when converted back again, the result shall compare equal to the original pointer.
So if you have for example a uint8_t*
pointer pointing at an aligned address, increase that one by 1 byte, then convert to uint16_t*
, you may get a misaligned access. Depending on MCU core used, this may or may not be a problem. Generally, 8 bitter MCUs don't care about alignment. Some 16 bitters do, some don't. Pretty much all 32 bitters do. Also there a CPUs which can give instruction traps for misalignment at the point of conversion, even before de-referencing.
And then if we continue to read the same text quoted above, it continues:
When a pointer to an object is converted to a pointer to a character type, the result points to the lowest addressed byte of the object. Successive increments of the result, up to the size of the object, yield pointers to the remaining bytes of the object.
This means that we can use a character type pointer, such as unsigned char*
, to inspect individual bytes of a larger object. (uint8_t*
almost certainly counts as a character type on any mainstream system.) This is useful for serialization of data, if you for example want to send a 32 bit integer over some serial bus, one byte at a time.
However, we cannot grab a chunk of raw bytes and access that through a pointer to a larger type. There is no special rule like the one above for such scenarios, rather it is something called a "strict aliasing violation" (What is the strict aliasing rule?):
uint8_t array [n] = { ... };
uint16_t* ptr = (uint16_t*)array; // C allows this conversion in itself, but...
*ptr = something; // this is BAD, undefined behavior - a strict aliasing violation
In order to dodge this dangerous part of C, we would rather invent custom union
types for "type punning" purposes like the one above:
typedef union
{
uint8_t array8 [n];
uint16_t array16 [n/2]
} array_t;
This allows us to access the data as different types without using dangerous pointer conversions.
Other special exception scenarios do exist, for example we are allowed to convert between a struct pointer and a pointer to that struct's first member. The special rule for this is (C17 6.7.2.1/15):
pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. There may be unnamed padding within a structure object, but not at its beginning.
This is only safe for the first object of the struct! Unless that object is an array (or a union between an array and something else)
And finally, there's the matter of qualified pointers. If you have data that is qualified with const
or volatile
, then the pointer to that data must use the same qualifier(s). We may never "cast away" qualifiers, doing so is undefined behavior and may result in strange program behavior. It is however always fine to go from a non-qualified pointer to a qualified one.
int* i_ptr;
const int* ci_ptr = ptr; // fine, and no need to cast either
int* another_ptr = ci_ptr; // BAD, undefined behavior
volatile uint8_t some_register;
volatile uint8_t* reg = &some_register; // fine
int* another_ptr = reg; // BAD, undefined behavior
void my_func (const uint8_t* data)
{
uint8_t* ptr = (uint8_t*)data; // BAD, undefined behavior
}
But here as well, the undefined behavior doesn't occur until you try to de-reference the pointer. The specific rule (C17 6.7.3/6):
If an attempt is made to modify an object defined with a const-qualified type through use of an lvalue with non-const-qualified type, the behavior is undefined. If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.