Adventures in C (References)

We often ask if something is passed by value or passed by reference. Technically, nothing is ever passed by reference in C. Thinking in passing references is an abstraction and the ability to see through it helps us reason with the code once things start to get messy.

Let’s do some examples to test this claim.

#include <stdio.h>

void f(int a)
{
    printf("(f) Value of a: %d\n", a);
    printf("(f) Memory location of a: %p", &a);
}

int main()
{
    int a = 3;

    printf("(main) Value of a: %d \n", a);
    printf("(main) Memory location of a: %p \n\n", &a);

    f(a);
}

Output:

(main) Value of a: 3
(main) Memory location of a: 0x7ffd9ab4f618

(f) Value of a: 3
(f) Memory location of a: 0x7ffd9ab4f5fc

In the code above variable a was passed by value. This is shown by different memory locations that a represents in different scopes.

Let’s do another example.

#include <stdio.h>

void f(int * ptr_a)
{
    printf("(f) Value of a: %d\n", *ptr_a);
    printf("(f) Memory location of a: %p", ptr_a);
}

int main()
{
    int a = 3;
    int * ptr_a = &a;

    printf("(main) Value of a: %d \n", a);
    printf("(main) Memory location of a: %p \n\n", ptr_a);

    f(ptr_a);
}

Output:

(main) Value of a: 3
(main) Memory location of a: 0x7ffc55239738

(f) Value of a: 3
(f) Memory location of a: 0x7ffc55239738

A similar example where a pointer to a is passed, resulting in same addresses. This would be an example of passing by reference. But C doesn’t support passing by reference and what instead happened was a pointer passed by value.

What’s the difference?

Talking about passing a pointer by value instead of passing a reference demystifies what a reference is and what is happening on the function stack.

Once the pointer to a is copied to the function stack, we can access a outside of the functions scope by dereferencing *a. But no more than that.

An example.

#include <stdio.h>

int new_a = 4;
int * ptr_new_a = &new_a;

void f(int * ptr_a)
{
    printf("(f) Value of a: %d\n", *ptr_a);
    printf("(f) Memory location of a: %p\n", ptr_a);

    //Let me just change the reference to a. Ups!
    ptr_a = ptr_new_a;
    printf("(f) Chaged reference to a. \n");

    printf("(f) Value of a: %d\n", *ptr_a);
    printf("(f) Memory location of a: %p\n\n", ptr_a);
}

int main()
{
    int a = 3;
    int * ptr_a = &a;

    printf("(main) Value of a: %d\n", *ptr_a);
    printf("(main) Memory location of a: %p\n\n", ptr_a);

    f(ptr_a);

    printf("(main) Back in main. \n");
    printf("(main) Value of a: %d\n", *ptr_a);
    printf("(main) Memory location of a: %p\n", ptr_a);
}

Output:

(main) Value of a: 3
(main) Memory location of a: 0x7ffc1a16d5d8

(f) Value of a: 3
(f) Memory location of a: 0x7ffc1a16d5d8
(f) Chaged reference to a.
(f) Value of a: 4
(f) Memory location of a: 0x601030

(main) Back in main.
(main) Value of a: 3

In the example a was passed by reference and it’s reference was updated in the function scope. The changes didn’t show up in main. Why was that?

What really happened, was that a pointer to a was passed by value.

In the first example, it was clear the passed value of a was located on the functions stack and that changing it won’t change the value of a in main. This is the exact same thing but instead of a int value, we’re passing a * int value.

What was const is const no more

The basic const

The const is a keyword that marks a variable read only. The const variables value can only be given upon initialization and is later impossible to change.

int main()
{
    int const a = 3;    // I am free forever!
    a = 4;              // The hell you will
}

To figure out to what the const is applied, read the declaration from the right to the left starting from the varible name.

int main()
{
    int a = 3;
    int const * ptr_a = &a;     // Is the int constant? Or the pointer constant?
    * ptr_a = 4;                // The hell you will
    ptr_a = (int *) 1234;       // Hey, I no error here :)

    // Cool, so ptr_a is a pointer to a constant int

    int b = 3;
    const int * ptr_b = &b;     // What about now?!
    * ptr_b = 4;                // The hell you will, again
    ptr_b = (int *) 1234;       // Hmmm, this works again

    // So ptr_b behaves the same. Okay. From right to left until the very end.
}

Losin’ it

Let’s look at an example on how to override const, and make it a misleading.

int main()
{
    const int a = 3;            // I shall never be changed!
    int * ptr_a = &a;
    * ptr_a = 4;                // The hell you won't
    printf("%d", * ptr_a);
}

Output:

4

However, despair not, the complier noticed the error and has printed it out in form of a warning:

int * ptr_a = &a;  warning: initialization discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers]

The unsafety here was caused by casting a const int * -> int * which occurred in the line:

int * ptr_a = &a;

Once again accessing a by dereferencing is proving to be a different thing tha accessing a directly.

Losin’ it… again

In the above example unsafety was provoked by casting a const to non const.

It’s intuitive where the unsafety comes from, right? It was defined as read only and we forced writing capabilities on it.

However, it is also possible to generate unsafety by casing of non const to const.

Follow on.

Functions and the guarantees they make

void f(const int * ptr_a){
};

int main(int argc, char *argv[])
{
    int a;
    int * ptr_a = &a;
    f(ptr_a);
}

a on it’s own is mutable (doesn’t have a const qualifier on declaration). The function assures us it won’t allow changes on a to happen, regardless of it’s mutable nature.

What happens here is an implicit conversion int * -> const int *. That’s safe, the function is treating the mutable * ptr_a as read only.

Let’s do another example.

void f(const int ** ptr_ptr_a){
};

int main(int argc, char *argv[])
{
    int a;
    int * ptr_a = &a;
    f(& ptr_a);
}

This time the function protects ** ptr_ptr_a. A mutable & ptr_a is passed to a function that promises to treat it more strictly than needed.

If you take a look at the warnings though, you might notice the compiler complaining.

f(& ptr_a);  warning: passing argument 1 of ‘f’ from incompatible pointer type [-Wincompatible-pointer-types]

I’m casting into a stricter setting, old chump, what’s bothering you?

It seems that old chump thinks you might be reading what the function is saying to rashly and assuming you’re safer then you actually are.

The double pointer const

Let’s take a look at the example again and extend the function to show what it can and can’t do with the assurances it gave.

static int b = 15;

void f(const int ** ptr_ptr_a){
// ptr_ptr_a = 0x1234;  Passes, but will have no influence outside of the function
// ** ptr_ptr_a = 10;   Nope, the function explicitly guaranteed it won't do that

* ptr_ptr_a = &b;   //  Oh my!
};

int main(int argc, char *argv[])
{
    int a = 10;
    int * ptr_a = &a;
    f(& ptr_a);

    printf("%d", * ptr_a);      //Functiooon! But you promised it nothing would change!
}

What happened here? What about all the promises? All the boasting on additional strictness and safety?

Everything is as it should be, we’ve just overestimated how const is protecting us. Take another look.

The function promises to disallow writing to ** ptr_ptr_a, and it keeps the promise. It never committed to anything regarding *ptr_ptr_a.

The compiler actually assumes you might assume wrongly and launches a warning:

f(& ptr_a);    warning: passing argument 1 of ‘f’ from incompatible pointer type [-Wincompatible-pointer-types]

Once again observing the things as they really are can prove helpful.

Accessing a directly is different from accessing a by dereferencing *a, and that is again different from accessing by double dereferencing **a. Each has a different path to reach the value of a and these paths can impose there own restrictions.

Is **a a reference to a? A double reference to a? It is, until it no longer isn’t.

Does const int ** ptr_ptr_a protects a? No, it protects ** ptr_ptr_a. If that ends up being the same as a, good for us.

Conclusion

When thinking and dealing with references in C, always remember what they actually are. The issues adressed in the examples are caused by equating variables with dereferenced pointers.

While that is a tool C applications are build on, it is important to remember that it’s an abstraction and it’s on us to keep it in mind and expect nothing more than what is explicitly stated.

Happy coding,

Be Well :)

Written on July 18, 2017