Pointers, arrays, and the pass-by-reference model

Recall our earlier discussion about the computer’s main memory. In your computer, the main memory is made up of individual bits or switches that can be either on or off. An average computer these days has about 35 billion bits worth of main memory. Every piece of information the computer stores, it stores in terms of 0’s and 1’s by flipping bits on and off in main memory. In order to keep this massive array organized, the memory is divided up into eight-bit pieces called bytes and each byte is given a number called an address. This address tells the computer where in main memory the given byte is located.

In C, a variable that stores a memory address is called a pointer. Pointers are stored in memory just like any other variable. The reason why it is called a pointer is because, instead of storing a useful value, it simply “points” to another location in memory. (Think of a citation in a paper; it doesn’t give you any immediate information, it simply tells you where to find it.) The & operator can be used to retrieve the address of a variable. If a variable is a pointer, the * operator can be used to refer the value stored at that address. The & and * operators are called the reference and dereference operators respectively. Consider the following example:

int x;
int y;
int *p;

x = 5;
p = &x;  /* p now holds the address of x */
y = *p;  /* y now holds the value 5 */
*p = 7;  /* x now holds the value 7 */
p = &y;  /* p now points to y */

This example demonstrates that you can use pointers both to read from and to write to the given location in memory. In the above example, y = *p assigns the value of x to y. This is because, at that point in the program, p points to x and the expression *p evaluates to the value stored at the address stored in p. On the other hand, the line *p = 7 assigns the value of 7 to x. This is because *p = <expression> stores the value of the expression in whatever memory location is pointed to by p; in this case p is pointing to x.

This example also gives us an example of a pointer declaration: int *p;. This notation is suggestive of the fact that *p is an integer. The type of a pointer depends on the type of the data to which it is pointing. This allows the compiler to know the type of *p. When it comes to the type of a pointer, you can think of it in two ways. One is to consider p as being a variable of type int * or “pointer to integer”. Another perspective is to think of p as being a pointer to a variable of type “integer”. Depending on the context, both of these perspectives are useful.

The pass-by-reference model

When it comes to function arguments, there are two major models: pass-by-reference and pass-by-value. In C, the default is to pass arguments by value. When a function is called, the values of each of the arguments are copied into the newly created argument variables. Because the function is working with a copy of the data, any changes it may make get thrown away when the function returns. Another way to pass arguments is by passing a reference or a pointer to the original variable. In this way, changes that happen to that variable in the function are reflected in the value of the variable when the function returns.

In C, you can implement the pass-by-reference model using pointers. Consider the following function:

void swap(int *x, int *x)
{
    int z;

    z = *x;
    *x = *y;
    *y = z;
}

Suppose the swap function were called as follows:

int a;
int b;

a = 5;
b = 23;
swap(&a, &b);

When the swap function returns, the value of a will be 23, while the value of b will be 4. But how does this work when C copies the values of the arguments into the argument variables? Since the variables are pointers, the values of &a and &b are copied. However, these values are addresses in memory, so x points to the same address as &a which is the address of a. In this way, when we execute *x = *y, the value of *y (which is the value of b) gets stored at the address pointed to by x (which is the address of a). In this way, we can get around the restrictions of the pass-by-value model.

You need to be careful with the pass-by-reference model. First, it is very easy to abuse and you should always think about what you’re doing before you assume that pass-by-reference is the right way to do it. However, there are some places where pass-by-reference is exactly what we need. For example, the scanf function uses pass-by-reference to return the values it reads from the terminal. There are two reasons why scanf uses pass-by-reference. First is that it is already doing something else useful with its return value: it returns the number of fields properly read from the input. Second is that scanf is capable of reading multiple values with one function call and you can only return one value at a time. Returning multiple values is probably the most common reason for pass-by-reference.

You should also be wary when using pass-by-reference that you do not use the same argument for both input and output. There are a few times where this is useful but it is not generally a good idea. In the case of our swap function above, both variables are inputs and both are outputs; however this makes perfect sense for a function that swaps two things. What you should never do is use the same variable for both input and output just because you want to save on the number of arguments. It is also a good idea to document somewhere in the comments what variables are inputs and what variables are outputs.

Structures and pointers

It is frequently useful to use the pass-by-reference model with structures to write functions that modify the structure in-place. More complicated data structures frequently cannot be directly copied, so you have to refer to them using a pointer. If you have a pointer to a structure, you can access the structure’s members using the -> (arrow) operator. The expressions (*p).member and p->member are exactly equivalent. We will see some of these more complicated structures when we start talking about dynamic memory.

Arrays and pointers

Previously, we had a brief discussion of arrays in which we declared them and talked about accessing elements using the [] operator. But how are arrays connected to pointers? Recall that arrays are stored sequentially by simply placing the elements of the array next to each other in memory. The resulting variable is basically just a pointer to the first element in the array. The only effective difference between an array and a pointer is that if p is a pointer, sizeof(p) will give you the size a memory address consumes in memory while sizeof(arr) will give you the amount of memory consumed by the array arr. Beyond the effects of sizeof, pointers and arrays are identical; specifically, *p is equivalent to p[0].

You can also perform integer arithmetic on pointers. If you add an integer to a pointer, it simply advances it that many places in memory. In this way, arr + 6 points to the sixth element of arr. The exact address of <pointer> + <integer> depends on the type of the data pointed to by <pointer>. This is because the distance between the first and second elements in an array of type char is one byte, while the distance between the first and second elements in an array of type int may be 4 bytes. Because of this, the expressions *(arr + n) and arr[n] are equivalent. You can also use the increment and decrement operators on pointers.