C is a statically typed language, meaning that we explicitly tell the compiler what type our objects are. For example, in Python, which is dynamically rather than statically typed, we’d write value = 12 whereas in C we’d write int value = 12.

What about pointers then? Given that we write int value1 and float value2 you may expect us to be writing things like pointer ptr or maybe pointer *ptr but that’s not right.

In fact we write int *ptr. This states that *ptr is an int. It doesn’t state that ptr is an int. The int tells the compiler what type of data is to be found at the memory location pointed to by the pointer; in this case an integer or int.

Pointers just hold memory addresses. So why are data types needed in pointer declarations? What extra data is being stored and where?

Well, pointer type information only exists at compile time. The compiler knows the size of objects types and converts this into raw memory locations in the final output machine code. In this way, explicitly declared pointer types allow programmers to show their intent and compilers to check it but it allows for executable machine code to avoid the overhead of performing these checks at runtime.

Additionally, declared pointer types are needed when dereferencing pointers. They tell the compiler how much memory to read and in what format.

Finally, they allow pointer increment and decrement operations (for example when stepping through array-like structures) to move by the correct number of bytes to the correct memory location.

So pointer types are extremely useful but you might have heard about void pointers - as in void *ptr rather than int *ptr. You can think of these as being generic pointers in the sense that they just point to a chunk of memory and they specify no information about the type of data held in that memory. They effectively bypass the compiler’s type checking mechanisms.

If that sounds scary then your coding sensibilities are developing well. They’re very dangerous beasties. But, when treated with respect, they’re also useful and powerful tools. In practice they’re often used to hold any pointer type, for example it’s perfectly legal to assign a pointer to integer into a pointer to void: int i = 1; int *ip = &i; void *vp = ip;.

You wouldn’t normally do things like this because you’re throwing away information and taking off your metaphorical safety harness and that’s how kittens die. If you do, you’ll probably want to be explicit about it and use pointer type casts.

Sometimes you do want to go the other way though, from void * to int * For example memory allocation functions like malloc just return chunks of raw, un-typed memory and you usually want to convert this to a specific type; for example int *value = malloc(sizeof *value) will reserve a chunk of raw heap memory the size of an int and then get the compiler to convert it from the somewhat risky void * pointer returned by malloc into a nice, safely typed int * pointer.