Passing pointers by value

In C, all values are passed by value. This seems a little confusing when you consider the fact that C also has pointers. What does that mean?

Let’s look at the example from class:

#include <stdio.h>

void add(int *x, int *y, int *z) {
  *z = *x + *y;
}

int main() {
  int x = 1;
  int y = 3;
  int z;
  add(&x, &y, &z);
  return z;
}

Let’s walk through this program step-by-step. At the beginning, there is nothing. Note that the red arrow points at the line about to be evaluated. This arrow is called the instruction pointer.

When we enter a function, the function preamble runs. The preamble allocates storage for the variables in the function. We call this “setting up the stack frame.” If there are any parameters, at this time, their values are copied into the local storage allocated for them. In this case, there aren’t any parameters.

This line is straightforward: assign the value 1 to the local variable x.

Ditto. Assign 3 to y.

In this line, we declared z but we didn’t assign anything. In C, nothing actually happens when we execute this line. The compiler already allocated local storage before the function ran, in the preamble. That line did have to be there, though, otherwise the compiler would not have known that it needed storage for z.

Now we call the add function. But before we do that, technically there are three more steps. Why? We have to evaluate the parameters to add so that we know their values when it is time to copy them into add’s stack frame.

First, evaluate &x, which means “get the address of x,” and store that value (an address) in the first parameter to add.

Second, evaluate &y and store it in the second parameter to add.

Finally, evaluate &z and store it in the third parameter to add.

Now that all of the arguments to add are evaluated, we transfer control to the add method. On most computers, this is implemented with some kind of jump instruction. Again, in the beginning (of add), there is nothing.

Run add’s preamble. As before, if there are parameters, copy their values into local storage. Since we do have values, x, y, and z, we do have things to copy. Since they are pointers (they all have type int *), they actually store addresses. I will draw them here using arrows to make things clearer. Note that x, y, and z in add aren’t just different variables than x, y, and z, in main, the variables in add have a different type (int * as opposed to int). Remember that variable names are just names, and as in real life, where two people can have the same name, two variables in C can have the same name. We will talk about why, exactly, this fact does not confuse C when we discuss scope. For now, observe that names are local to a function.

It’s worth noting that z in add really does point to an undefined variable z in main. Yes, C lets you do things like that.

At this point, we execute the body of the add function. There’s a lot going on, so as we did with function parameter evaluation, let’s break the evaluation down into steps.

At a high level, we are assigning a value to a variable. What are we assigning? The result of an addition. But, since there are a bunch of * symbols in here, hopefully you suspect that there’s more to it than that.

First, we need to get the value of the right side of the assignment:

*x + *y

Well, to get the value of this expression, we need to know the value of the left side of the addition:

*x

And to know that, we have to dereference x (that’s what *x says in code, literally). What does it mean to dereference something? It means that we follow the pointer to x and fetch that value. Now we know *x. It’s 1.

1 + *y

See what to do next? We need to know the right side of the addition. *y gets the same treatment as *x. When we dereference y, we follow its pointer and find: 2.

1 + 2

We now can evaluate the right side of the assignment. It’s 3.

*z = 3

But wait… we still have a * on the left side of the assignment. That’s because z is a pointer value. We don’t actually want to store 3 in z in add. We want to store 3 in z in main. Any guesses about what to do?

When you see a pointer on the left side of an assignment, what happens is the following:

  1. Evaluate the right hand side (we did this already).
  2. Store the value of the right hand side in the location pointed to by the left hand side.

So what happens now is that we store 3 in z in main.

Note that this is why we can get away with returning void: we directly manipulate memory in main. Also notice that we did all this pointer stuff without any mention of allocated storage duration (i.e., without using malloc). Pointers and storage duration are different concepts, as will become clear in the next step.

Now that we’re at the end of the function, the function epilogue runs. It says how to restore the stack to its state before add was called. The storage for x, y, and z in add goes away. It goes away because it was allocated with automatic storage duration.

It is worth mentioning that the dirtly little secret in most C implementations is that those values don’t really go away. There’s still there. If you are clever, you can still even read them. What you should not do, however, is count on them staying there, because the moment you call another function, they’ll probably be overwritten.

Now we return z. Ever wondered what return z really meant? It means “copy the value stored in z in the location specified by the calling function and then jump to the function epilogue.” So we copy 3 and move to the epilogue.

Finally the program is done; the epilogue is run and the stack frame is town down.

At this point, 3 is somebody else’s problem.

  • CSCI 334: Principles of Programming Languages, Fall 2018