C / POINTERS
Pointer types and why int* is not char*
Explain why int* and char* can hold the same address yet are not interchangeable, and predict what one store through each changes.
What you will learn
- Separate pointer width from pointee width using sizeof p versus sizeof *p
- Predict how many bytes one store through int* changes versus through char*
- Explain why the compiler must diagnose int *q = cp; even when sizes match
- Inspect an object's representation legally through an unsigned char* view
Understanding Pointer types and why int* is not char*
An address on its own is just a number; the machine has no idea whether the byte living there belongs to a char, to an int, or to the middle of a double. The type in the declaration int *ip is a promise you make to the compiler about what is stored at that address, and when it generates code for *ip it uses nothing but that promise. This is why sizeof(int *) and sizeof(char *) are normally identical while the two types remain incompatible: the size of a pointer tells you nothing about the size of the thing it points at.
The pointee type fixes three things at every access: the width of the load or store (*ip moves four bytes on a typical machine, *cp moves exactly one), the decoding of those bytes (a two's complement integer versus a single character), and how far p + 1 advances. Because all three decisions come from the type alone, C treats a mismatched assignment such as char *cp = ip; as a constraint violation and the compiler is required to complain. Adding a cast makes the message vanish without changing one byte of memory; it only changes the width and interpretation used at the next dereference.
A pointer cast is therefore a claim, and you own its consequences. Two bite in practice: an int * built from an arbitrary byte address may be misaligned, which is undefined behaviour and a genuine crash on some ARM and SPARC targets, and accessing an object through a pointer to an unrelated type breaks the aliasing rules optimizers rely on, so code can behave at -O0 and produce wrong answers at -O2. The character types are the deliberate exception: an unsigned char * may examine the bytes of any object, which is the sanctioned escape hatch when you want a representation rather than a value.
Keeping the two ideas apart, address versus access width, also explains the diagnostics you will meet later: a function that takes int * is asking for permission to write four bytes, and handing it the address of a short is not a formatting problem but a promise you cannot keep.
<stdio.h>
int main(void)
{
int n = 0;
int *ip = &n;
unsigned char *cp = (unsigned char *)ip; /* same address, different type */
printf("sizeof ip = %zu sizeof cp = %zu\n", sizeof ip, sizeof cp);
printf("sizeof *ip = %zu sizeof *cp = %zu\n", sizeof *ip, sizeof *cp);
for (size_t i = 0; i < sizeof n; i++)
cp[i] = 0x01; /* four one-byte stores */
printf("n after 4 byte stores = %d (0x%08X)\n", *ip, (unsigned)*ip);
*ip = 0; /* one four-byte store */
printf("n after 1 int store = %d\n", n);
return 0;
}
A pointer's type tells the compiler how wide an access at that address is and how to decode the bytes it finds there; the address itself carries neither.
Worked examples
One store, four bytes or one
The same array is written through an int-typed lvalue and a char-typed lvalue, showing how many bytes each access reaches.
<stdio.h>
<string.h>
int main(void)
{
int a[2];
unsigned char *bytes = (unsigned char *)a; /* byte view of the same storage */
memset(a, 0xFF, sizeof a); /* every byte set to 0xFF */
a[1] = 0; /* one int-typed store */
bytes[0] = 0; /* one char-typed store */
for (size_t i = 0; i < sizeof a; i++)
printf("%s%02X", i ? " " : "", bytes[i]);
putchar('\n');
printf("char store touched %zu byte, int store touched %zu\n",
sizeof *bytes, sizeof a[0]);
return 0;
}
Example explained
Line 1memset fills all eight bytes, so both ints start out as all-ones.
Line 2a[1] = 0 is one store through an int lvalue: bytes 4 through 7 clear together.
Line 3bytes[0] = 0 is one store through an unsigned char lvalue: only byte 0 clears.
Line 4The cast created no new memory; it only changed the width of each later access.
Same size, still a different type
int* and unsigned int* point at the same address with the same access width, yet decode the bytes differently and still need a cast.
<stdio.h>
int main(void)
{
int n = -1;
int *ip = &n;
unsigned int *up = (unsigned int *)&n; /* cast required, sizes are equal */
printf("through int* : %d\n", *ip);
printf("through unsigned* : %u\n", *up);
printf("same address? : %d\n", (void *)ip == (void *)up);
printf("same read size? : %d\n", (int)(sizeof *ip == sizeof *up));
return 0;
}
Example explained
Line 1Both pointers hold identical bits, which the third line confirms by comparing them as void*.
Line 2*ip decodes four all-ones bytes as a signed int, giving -1.
Line 3*up decodes the very same bytes as unsigned, giving 4294967295; nothing in memory moved.
Line 4Equal pointee size did not make the types compatible, which is why the initialiser needs the cast.
Important notes
char, signed char and unsigned char are three distinct types, so char * and unsigned char * also require a cast between them; only these character types may legally alias other objects.
The 4 and 8 in the output are this platform's int and pointer sizes, not language guarantees; C does not even promise all pointer types share one size, so never park an address in an int.
Common mistakes
Casting away a warning that widens a store, as in short s; scanf("%d", (int *)&s); the call writes four bytes into a two-byte object and corrupts whatever sits beside s.
Assuming equal pointer sizes mean equal pointer types, then reading *cp where cp is (char *)&n for int n = 300; you get a single byte of the representation, 44 or 0 depending on byte order, never 300.
Casting an offset inside char buf[16] to int * and dereferencing it: the access may be misaligned and violates aliasing, so it can work on x86, crash on ARM, or silently change behaviour when optimisation is turned on.
Try it yourself
Change, predict, then run
Declare int n = 300; print it through an int *, then loop over sizeof n bytes through an unsigned char * printing each byte as %02X. Set the first byte to 0, print n again, and explain the new value from the byte order you observed.
Open the C workspaceCheck your understanding
On a machine with 8-byte pointers and 4-byte int, unsigned char *cp = (unsigned char *)&n; holds exactly the same address bits as int *ip = &n;. Why must the compiler still reject int *q = cp; without a cast?
- Because the pointee type, not the address, decides the width and decoding of each access, so the two types are not interchangeable
- Because character pointers are stored in a smaller representation than object pointers on this machine
- Because cp might legally be NULL while ip cannot be, so the assignment could lose information
- Because pointer assignment is allowed only between pointers whose pointee types have the same size
Show answer
Pointer compatibility is a question about types: the compiler generates a 1-byte access for *cp and a 4-byte access for *q, so it cannot silently accept the assignment even though both hold the same address. The size-based option is tempting but wrong in both directions, since int * and unsigned int * have identical pointee sizes and still need a cast, while char * to int * is accepted once you write the cast.