Francis Tayag

C pointers are taught wrong.

Published on

I remember struggling a lot with C pointers back in college. It just seemed like such an arcane concept that's hopelessly out of reach. I remember feeling despondent thinking I'd never understand it, and that I wasn't cut out to be a True Programmer.

Last year, I did a sort of "skills reset" for my programming and art skills, feeling like I had picked up a lot of bad habits and assumptions over the years and I needed to let go of the baggage, so to speak. Finally learn them properly instead of the unfocused, haphazard, cowboy-ish way I was used to doing since I first started at 11. It's funny to realize looking back just how brain-dead simple C pointers really are and it's frankly a bit embarrassing it took me this long to figure out. But it's not entirely my fault. Not like it was a lack of trying for my part.

It's just that C pointers are taught...very badly. Like...super badly.

We all know from introductory programming courses the quintessential program to "teach" pointers:

#include <stdio.h>

int main() {
    int a = 2;
    int *p = &a; // store the address of variable a

    printf("%d", *p); // dereference, get the value
    return 0;
}

Now there is nothing wrong with this example. It's a fairly simple way to illustrate what pointers are. One could be forgiven for stopping here and leaving it at that. But! Consider what's running through a person's mind who hasn't even seen such a thing before:

  1. Why do I need to use pointers if I can just...print int a directly?
  2. (Optional*) Does this work like...Java? C#? Python?

* If the person has prior programming language exposure.

And they'd be right! Why in the world would you use a pointer here if you can just use int a directly?

And then the instructor will ask: "Any questions? No? Okay let's move on to implementing a basic tree structure". But, actually, you do have many questions, right? You just can't quite place your finger on it. This is confusing. And it feels like asking "Wait, why would we ever use a pointer?" seems a bit of a jerk move, right? So you stay quiet. And then the class moves on to trees and data structures. And now you can't follow the lecture because you're missing the single foundation that makes these data structures work. It's awful.

Why use pointers at all?

So, the question here is...why would you need to use a pointer?

The answer is simple. C is a dumb 70s language with all the baggage of 70s computing. It's old. It sits in a rocking chair and doesn't even remember your name half the time you pass him by. I don't mean "dumb" as an insult. I meant that literally. This language doesn't do anything for you. You have to practically spoonfeed it and its only job as far as it's concerned is to clock in, copy-paste the preprocessor macros (the #include and #define), and convert your C code into machine code (with all the bugs included), and call it a day. The lazy bastard.

I was being a bit facetious there, but the point here is that C is an old programming language and you should treat it as such. One thing you have to know is that everything in C (and I mean everything) is assigned or copied by value. What does this mean? It means that when you do this:

int a = 3;
int *b = &a;

...C is just copying the address of int a into int *b. That's it. Not the variable, but the address. There's no reference magic here. There's nothing else. That's all it does. int *b is now pointing to the address of int a.

"But wait!", I hear you say. "This is still confusing. Why the heck would I ever want to do that?" You're right, and the answer again here is: everything in C is assigned or copied by value. Consider the following program that takes in a student and automatically enrolls them if they're at least 18 years old:

#include <stdio.h>
#include <stdbool.h>

#define MIN_AGE_TO_ENROLL   (18)

struct Student {
    char *name;
    int age;
    bool is_enrolled;
};

void enrollStudent(struct Student s1) {
    if(s1.age >= MIN_AGE_TO_ENROLL)
        s1.is_enrolled = true;
}

int main(void) {
    struct Student s1 = {
        .name = "John",
        .age = 22,
        .is_enrolled = false
    };

    printf("Enrolling %s, who is %d years old...\n", s1.name, s1.age);
    enrollStudent(s1);

    if(s1.is_enrolled == true)
        printf("%s is now enrolled!\n", s1.name);
    else
        printf("%s failed to enroll because he is not at least %d years old!\n", s1.name, MIN_AGE_TO_ENROLL);

    return 0;
}

Try to compile and run this program (visit the online Programiz C compiler).

Now, confusingly, you will get this nonsense of a response:

  Enrolling John, who is 22 years old...
  John failed to enroll because he is not at least 18 years old!

What happened? The code checks out, right? We only enroll a student if they are at least 18 years old, and John is already a grown man, 22 years of age. He should have been enrolled! But he wasn't...why? Let's look at this block of code:

void enrollStudent(struct Student s1) {
    if(s1.age >= MIN_AGE_TO_ENROLL)
        s1.is_enrolled = true;
}

What happens here is that this function copies-by-value the data passed onto it by the caller (which is main()). It doesn't know anything else. It doesn't care about what's outside it or what main() does with the data. As far as enrollStudent() is concerned, it just gets a 1-to-1 copy of the data, modifies its local copy, and clocks out. It doesn't pass the changes back to main(). It, again, just modifies its local copy and leaves it at that.

So what now? It only modifies its local copy of the student data. How do we make it modify the student data living inside main(), not enrollStudent()'s local copy? If only we could...point to it. Instead of passing in a copy of the data, let's modify the source code a bit and point to the data instead:

void enrollStudent(struct Student *s1) {
    if(s1->age >= MIN_AGE_TO_ENROLL)
        s1->is_enrolled = true;
}

// in main()
enrollStudent(&s1);

Changes to the source code are highlighted.

Now, we get this response, just as we expected:

  Enrolling John, who is 22 years old...
  John is now enrolled!

What happened here is that, instead of enrollStudent() getting its local copy of the student data, it now has a pointer to that data instead. It now knows the data's memory address, and that means the function can finally read and modify the student data to its heart's content.

Side note: You can do (*s1).age instead of s1->age. Try it! But the second syntax is preferred over the first because it's more readable.

Interesting, but...why?

So again, why do this whole dance? As said before, C is an old primitive language where everything is assigned or copied by value. Why such a seemingly dumb design decision, then? Well, hearken back to the 1970s where computers had kilobytes of memory. If you try to copy a struct that is 256 kilobytes large, that'd make the computer EXPLODE (not literally, but you get my point). So instead of copying that large data twice, let's just...point to the existing data.

Pointers. Deceptively simple things.

Now, to truly lock in what pointers are, try creating a linked list and create functions to push, display, update, and delete data. I'll leave you with this starter code:

#include <stdio.h>
#include <stdbool.h>

struct Node {
    int data;
    struct Node *next;
};

void push(struct Node **head, int value) {
    // Your logic here
}

void print_list(struct Node *head) {
    // Your logic here
}

void update(struct Node *head, int old_value, int new_value) {
    // Your logic here
}

void delete(struct Node **head, int value_to_delete) {
    // Your logic here
}

int main(void) {

    struct Node node2 = {
        .data = 14,
        .next = NULL
    };

    struct Node node1 = {
        .data = 72,
        .next = &node2
    };

    struct Node head = {
        .data = 61,
        .next = &node1
    };

    printf("Value of head is: %d\n", head.data);
    printf("Value of head->node is: %d\n", head.next->data);
    printf("Value of head->node->node is: %d\n", head.next->next->data);

    return 0;
}

Notice that push() and delete() has a pointer to a pointer (denoted by **). How does that work? Why is that? I'll let you answer those two questions, in that order :-)

Don't feel bad if you can't accomplish this one for now. It doesn't necessarily mean you don't understand pointers. It might just mean you don't understand linked lists (or data structures) yet, and that's okay! They're two different topics that get conflated and mixed a lot in introductory courses, hence the fusion-confusion of where pointers end and data structures begin. You don't have to study them all at once, but remember that you need to know the syntax, mechanisms and basics of pointers before you tackle the bigger subjects like data structures and algorithms.

Just remember that in computer science (and a lot of things in life), everything has a stepping stone and builds upon previous knowledge. Like a skyscraper - can't build one without the foundations now, can you? If you get confused about a topic, ask yourself first which steps you might have skipped or only half-understood. One step at a time :-)

Shoot me a message at franztayag@gmail.com if you have any questions!

Go back to posts