How To Avoid Using Globals For Information Passing

If you’ve run into the classic noob C programmer’s problem of “Why won’t my function update my variable?” then you might have resorted to using a global variable instead of passing the variable into the function as a parameter. Global variables allow ‘write access’ to number from both the caller and the callee, like this,

but global variables are considered bad for lots of reasons. Not least of which is that your function can no longer double anything other than your specific number.

Instead you need to pass number into doubleIt as a pointer parameter.

How Many Threes Are There In The Universe?

The obvious answer is “quite a lot”. There’s the three on the front of my house, there are a couple more in my phone number and there’s one on the number plate of the car across the street from me right now. There are also several copies of my house number three, stored in the databases of various shopping sites who have been kind enough to deliver goodies to my home address over the years. Ditto for my phone number threes; and the DVLA, police ANPR, and several insurance companies presumably have copies of the three on my neighbour’s car number plate. Now in some ways these threes are very different from each other – my neighbour’s car has little to do with my phone. And neither of these have very much at all to do with that group of three wildebeest over yonder on the African savannah. And yet, in some ways, some of them are very similar – the pair of threes in my phone number, for example, share quite a lot of context and, other than their position in the string of digits, you’d be hard pressed to distinguish one from the other. You’ll notice though that I’ve not used the digit ‘3’ anywhere in the discussion so far. And yet that’s what’s encoded in Amazon’s database, even though my house is adorned with the word ‘three’ on a little piece of green Lakeland slate. If I happened to live in certain parts of Canada or in France, I might have the word ‘trois’ in place of the word ‘three’. So, not only are there many different threes, there are also many different representations of the same three. What they all have in common though is some notion of ‘threeness’. All threes are clearly distinct from all fours, fives and 3.14159s. That’s what it means to be three. We can take this platonic idea of three and call it the master three. If we only have one master three then it makes sense that all the other threes we have, the real-world threes rather than the platonic, idealised, ‘perfect’ three, can really just refer to the master three to get their threeness characteristic. In fact they probably should do that so that we’re not duplicating information needlessly, and potentially having conflicting ideas of threeness. Threeness is threeness and it only needs to be defined in one place. All the other bits and pieces of information related to the real-world threes are specific to to those threes rather than having anything to do with the general idea of threeness. The encoding format of the bit pattern on my phone’s SIM card, the font and colour of the numberplate, the origin of the stone used to make my house number. None of these things help us understand the threeness of these threes. This idea of a separation of concerns is at the heart of referential semantics. But, apart from the very real benefit of letting me express clearly what I mean, what does it allow me to do in practice? Well, as we’ve seen, I can refer to the threeness attribute of the master three from my own threes, maintaining a strong and secure threeness without risk of corrupting it and crucially, from a practical standpoint, without incurring any extra storage or processing costs.

Referential Semantics

This is going to be a bit abstract but it’s honestly really worth trying to get your head around if you want a good solid foundation in pointers and referential semantics in general.

Pretty much everybody starts their programming career by doing some Hello World stuff and then moving on to some simple variable manipulation like this:

If you run this code you’ll see that b is equal to 2 but a is still equal to 1. This is known as value semantics or sometimes copy semantics. When we do int b = a we’re making b store a copy of the value stored in a. Subsequent changes to b will have no effect on a.

In reference semantics we’d do this instead:

This would result in both a and *b having a value of 2. Here, a is set to 2 indirectly and implicitly through the reference provided by b.

You can read about pointer operators if you don’t understand all those * and & thingies but what’s important here is the idea of references.

In value semantics we’re dealing with objects directly but in reference semantics, a reference provides a link to another thing. The referrer refers to the referent. C pointers are one example of a reference but there are other ways to implement reference semantics – in other words reference is a generic term and pointer is a specific example of a reference.

Referencing in turn is a specific case of indirection, the process of gaining access to one thing via another. The opposite of referencing is dereferencing. There’s more info in my article on C’s pointer syntax.

That’s the top and bottom of references really but if you think about them, they do open up your mind to some fairly deep observations about the world. For example here’s a question about numbers.

Finally, here are a couple of subtleties. See if you can answer them yourself before reading my take on them.

What’s the difference between a variable and a pointer? Both are ‘indirections’. The defining difference is that a pointer is a variable whose value is an address which further indirects to a value whereas a variable indirects to a value in a single step.

What’s the difference between an address and a pointer? Not very much really. Both can be used in the same places e.g.

int value;
int i = *(&value);

is equivalent to

int value;
int i;
int *ptr = &value;
i = *ptr;

Introduction to C Pointers

Pointers are variables that point to a pointee. Like so:

pointer-to-pointee

Behind that deceptively simple definition lurk some pretty hairy pitfalls and subtleties but it’s still the best place to start learning.

In computing, when we’re talking about locations to point to and/or from, we’re really talking about memory addresses. So, given that we’re talking about accessing data at a specified memory location, we should probably start with a basic understanding of how memory is organised and how it works.

hex-memory

If you’re happy with what this diagram represents then you’re good to go, if not you might want a quick tutorial on the basics of memory.

That’s the underlying hardware out of the way, at least for our minimal needs here, so now we can move on to talk about how C implements access to memory in software. If you understand what int *ptr = &value; then you can probably skip ahead to the next paragraph but if not, we need to talk about pointer operators and how to read pointer code.

We glossed over the issue of types when we talked about pointer operations but it’s worth delving a little deeper into how C’s type system integrates with pointers. In addition if you step beyond the basic built-in types you need to be aware of how arrays, structs and pointers to pointers operate.

At this point we’ve dealt with the basic mechanics of pointers so we should really step out into the real world and address the perennial question “Why should I use pointers?” and it’s less often encountered brother “Why should I not use pointers?

Finally let’s address some of the more subtle and underlying ideas around pointers and the terminology associated with them by talking about reference semantics and indirection.

Why Use Pointers?

The top-level answer to this is that pointers often lead to smaller and faster code. As answers go, that’s not very enlightening though so here are a few concrete examples.

  1. To allow functions to change data owned by their caller. This allows you to avoid using global variables for changing data within functions. It also allows you to avoid passing large chunks of data as parameters and as return values.
  2. To use memory efficiently both in terms of execution speed and memory usage. For example, nodes in a linked list point to each other. Deleting an element in an array means moving all the ‘higher up’ elements down by one to fill the gap. Deleting an element from a linked list simply involves changing one pointer.
  3. Dynamic memory allocation. If you don’t know how much you’ll need at compile time or if the stack isn’t big enough you can grab chunks from the heap at run time. You can’t do this using arrays because they have to have their size defined at compile time.
  4. Untyped things. e.g. malloc, free, memset, memcpy etc just deal with memory rather than typed objects.
  5. Pointers can hide data and implementation in structs by passing pointers to opaque struct tags. e.g. FILE *

Pointers To Structs

There’s just one little wrinkle you need to know about structs when it comes to pointers. If you have a pointer to a struct and you want to get access to one of its fields then there’s a shorthand: ptrToStruct->fieldName is the same as *(ptrToStruct).fieldName

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 a value (N.B. not a pointee) set to whatever happens to be in the memory where it is created. Those contents could have a value that points to somewhere in memory or they could have a value that represents the null pointer. So what does a null pointer representation look like?

According the C standard NULL must be defined to be either a zero valued integer constant or such a constant cast to void *. In other words NULL can be defined as either 0, 0L or (void *) 0. Interestingly, there may be a variety of null pointers, one for each pointer type, but as far as your C code is concerned, 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.