Pointers to Arrays

Arrays are contiguous blocks of memory holding multiple objects of the same type. These can be quite large and passing them into functions in the conventional, by value sense, can be quite inefficient both in terms of memory and processor cycles.

Fortunately, pointers come to our rescue here by allowing us to pass a pointer to an array into a function rather than passing the array itself. That way we only store the array in memory in one place and we just refer to or reference that one location whenever we need access to the data in the array.

Arrays themselves are quite lie pointers in fact. An array variable, for example arr in a statement like int arr[3] = {1,2,3} points to the address of the first object in the array. So int *ip = &arr[0] is the same as doing int *ip = arr

Pointers and arrays are related but not quite the same. For example, suppose we set up a couple of arrays: int arr1[3] = {11,12,13}; int arr2[3] = {14,15,16}; You can then do, as we just saw, int *ip = arr1; However, if you were to assume arrays and pointers work identically then you might think you could do arr = ip; or even arr1 = arr2; or arr = &arr2[0]; but all of those will throw compiler errors.

Pointer Types

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.

Null Pointers

The null pointer in C is a bit of an odd beast. It’s a pointer that doesn’t point to anything. Specifically, it doesn’t point to an object in memory and it doesn’t point to a function either.

Now, you might think that’s what you get when you first declare a pointer i.e. the statement int *ptr; creates a null pointer. In fact it probably doesn’t. Instead, it creates an uninitialised pointer, essentially a pointer with random contents. Those contents could have a value that points to somewhere in memory or they could have a value that represents the null pointer.

Interestingly, there may be a variety of null pointers, one for each pointer type, but as far as your C code is concerned it’s usually the case that null pointers are represented by the value zero.

What happens when you try to dereference a null pointer, as in int *ptr = NULL; int val = *ptr;? Bad things. You’ll get a run-time error. But this is a good thing in many ways. What you really don’t want to be doing is dereferencing an uninitialised pointer, because they contain garbage and dereferencing that will probably just give you more garbage and operating on garbage, and random, non-repeatable garbage at that, will often give you silently erroneous execution that can cause all sorts of functionality issues.

Pointer Operators

Pointers, as you would expect given the not insignificant clue in the name, point to something. Although that’s not necessarily as obviously true as you might think. They point to the memory word at the address specified by the contents of the pointer. This is often referred to as the pointee. That’s all fine and dandy from a conceptual standpoint but how do we get from one to the other in the real world of code?

We have two operators in C: * and &. Certainly to begin with the best advice I can give you is to think of * as the ‘what’s at’ operator and & as the ‘where is’ operator. You can connect the two thoughts together by thinking of & as the opposite of *. When you get more familiar with these things you’ll want to refer to them by more grown up names, pretty much entirely so you can bill more to your clients; because in fact there’s nothing wrong with the terminology I just gave you.

Terminology aside, if we have the name of a normal, non-pointer variable, we can use the address-of operator & to return the memory address of that variable. Here’s some sample code, in this case showing the address in a hexadecimal format:

You can run the code here if you want to see the output and/or experiment. Try checking the output of repeated runs. Try adding more variables and see whether there’s a pattern in their output addresses.

Bear in mind that pointers are just variables too so we can do exactly the same operation on them, even though it might take a bit to wrap your head around:


Here we have &ptr pointing to ptr which contains &val which points to val which contains 1234.

Again, have a play with this code. Try creating pointers to pointers to pointers to pointers for example.

You might have noticed that I just skipped over discussion of the * in that code What’s going on there?

Well, we can also go in the opposite direction to &. If you want the big bucks, this is called dereferencing and is the act of getting the value stored at an address, or in other words, the value in the pointee. We can do this using the * operator, also known as the indirection or dereferencing or value-at-address operator, or in our original terminology, it’s the ‘what’s at’ operator.

One final but important note about pointer operators – you need to read pointer code quite carefully until you get used to it. int *ptr says that *ptr (the thing pointed to by ptr) is an int. int pointedto = *ptr says pointedto is an int that contains the value pointed to by ptr.

Memory Electronics

This is just a stub for now but in order to avoid your inevitable disappointment have a lovely link.

From a top level perspective you can think of each bit of computer memory as having the following functionality.

  1. It must retain its state (either zero or one) at least semi-permanently. RAM will often lose its state when power is removed and ROM will often lose its state if left for a period of years, but for our purposes here, it remembers what it is set to.
  2. It must allow external hardware to read its value.
  3. It must allow external hardware to write its value.

Introduction to Memory

You can think of memory as being like the safety deposit boxes in a bank vault:


Each box is numbered so you can find the right one and each box can hold some contents. In computer memory the number of the ‘box’ is referred to as the address and the contents of the ‘box’ being stored at that address are referred to as the value.

Here’s a diagrammatic representation of some hypothetical and somewhat simplified computer memory:


Here, we’ve got five memory locations (the green rounded rectangles numbered 1 to 5 in the top left corner) each of which is the equivalent of a safety deposit box in our bank vault.

The memory at address 1 (the safety deposit box with the number 1 on the door) contains the value 231 and the memory at address 3 (the safety deposit box with the number 3 on the door) contains the value 34 and likewise for the other memory locations.

Well sure but…

  • How come I’m seeing decimal numbers stored at these memory addresses? I thought computers only understood 1s and 0s.
  • What are these 0x blah blah addresses I keep seeing?
  • How come the addresses I’ve seen elsewhere go up in fours (or eights or whatever) rather than one by one?
  • OK But what about the electronics? What’s really going on here?


This is just a stub for now but in order to avoid your inevitable disappointment have a lovely link.

Memory addresses are often written in hexadecimal and will look something like 0x1234abcd. Hexadecimal is a base 16 number system in the same way binary is base 2 and decimal is base ten. Instead of numbers being made up of the binary digits 0 and 1 or the decimal digits 0,1,2,3,4,5,6,7,8 and 9 they are instead made up of the hexadecimal digits 0,1,2,3,4,5,6,7,8,9,a,b,c,d,e and f.

Grouping of Bits

At the most fundamental level computers are dealing with voltage levels. In reality these voltages vary quite a bit but one of the jobs of the electronics is to ‘condition’ or ‘tidy up’ these voltages to ensure they can be considered either a low voltage or a high voltage. Now, what exactly these voltages are and how low and high are defined varies quite widely between machines but for our purposes just assume that voltages in a computer are low or high.

So, we’ve got two voltage states – low and high. These correspond to what are called bit values, short for binary digit, which are digital 0s and 1s. A low voltage represents a 0 and a high voltage represents a 1. Computer memory stores 0s and 1s. And that’s all it stores.

But it can group these bits together using the conventions of the binary number system into nibbles (4 bits), bytes (generally 8 bits) and words (often 16, 32 or 64 bits – so-called 16, 32 or 64 bit architectures). Why would you want to do that? For two reasons.

Firstly for semantic reasons, in a similar manner to the way we group decimal digits together into numbers. For example the digits 3, 7 and 9 have meaning on their own as individual digits but to express the notion of the number 379 you need to combine the three digits together and treat them as a group.

Secondly, for efficiency reasons. If you only have one wire you can only pass one bit (either a high or low voltage) at any one time (unless you do clever multiplexing things that are not relevant here). It’s quicker to be able to transfer information in parallel which you can do if you have, say, eight individual wires bundled together in a cable (often referred to as a bus in computing) all running in parallel and each carrying its own bit or voltage.

When we’re addressing computer memory we’re usually addressing (or accessing) words rather than individual bits or bytes.