Understanding pointers with structs in c

This is the second part of the series Understanding C pointer shenanigans. If you already understand pointers, go ahead, but if want to get a better understanding of pointers in c, go to the part 1, read that and come back.

Now, as you are here, i hope you understand pointers or have just understood them.

Let's start with a quick refresher on structs. If you are c++ programmer, think of it like a class or if you are a javascript programmer, think of it like an object or if you've just used excel sheets, think of it like a row in your spreadsheet. It basically groups together variables of different data types. Generally we use it to group together related data. For example, for a contact in your phone, you want the name, phone number etc. to be stored together as they all are related and is about one particular person.

Syntactically we declare a struct like

struct Person {
    int id;
    int age;
};

Let's adapt our simple example in part 1 to use structs. Rather than passing two numbers, to the add function, we want to group a and b, and pass a struct.

We'll define our struct Numbers as:

struct Numbers {
   int a;
   int b;
};

And modify our add function as:

int add(struct Numbers numbers) {
    //...
}

How do we access a and b from numbers, Ans: using the . operator:

int add(struct Numbers numbers) {
    return numbers.a + numbers.b;
}

How do we define and pass numbers, Ans:

struct Numbers numbers = {
    .a=1,
    .b=2
};

or

int a=1;
int b=2;
struct Numbers numbers = {
    a=a,
    b=b
};

or

struct Numbers numbers = {1, 2};

So, now our complete code becomes:

#include <stdio.h>

struct Numbers {
    int a;
    int b;
};

int add(struct Numbers numbers) {
    return numbers.a + numbers.b;
}

int main() {

    // int a = 1;
    // int b = 2;
    // struct Numbers numbers = {
    //     a = a,
    //     b = b,
    // };

    // OR

    // struct Numbers numbers = {1, 2};
    // OR

    struct Numbers numbers = {
        .a = 1,
        .b = 2,
    };

    int ret = add(numbers);

    if (ret != 3) {
        printf("Failed expected: 3, got: %d", ret);
    } else {
        printf("Passed expected: 3, got: %d", ret);
    }
    return 0;
}

Run it and it passes. Noice. So far, so good, we are able to use structs to add numbers. Till now we didn't use pointers (at least explicitly). So, nothing to do with pointers. Let's introduce them now. And, you might ask why? In our simple case, here directly passing struct by value doesn't have an issue, but in cases where you have larger structs, or you want the function to make changes to struct itself, we pass them by reference, which means using pointers.

First, we change our add function to use pointers

int add(struct Numbers *numbersPtr) {
    return numbersPtr.a + numbersPtr.b;
}

We go back to the similar two errors as in part 1. First you get:

error: request for member 'a' in something not a structure or union
     return numbersPtr.a + numbersPtr.b;
                   ^

So, we can't access a directly from numbersPtr, as now *numbersPtr is a pointer pointing to the actual address of numbers in the memory. And like earlier, we need to dereference* first the numbers, then get the a and b out of it. Ok, so let's stick asterisks to the statements.

int add(struct Numbers *numbersPtr) {
    return (*numbersPtr).a + (*numbersPtr).b;
}

Note, the brackets also, that is important for the order of operation. Now, if you run the code, atleast this error goes.

The other error says:

error: incompatible type for argument 1 of 'add'
     int ret = add(numbers);

Which basically means, we can't pass struct Numbers to struct Numbers *. So, it needs the address of numbers. How do you get the addresses of something? The ampersand & operator. Let's add that:

// ...
int ret = add(&numbers);
//...

Run, the code now and it works.

Let's circle back to how we dereferenced our *numbersPtr and got a and b out of it. There is a better (and sometimes confusing on when to use what) syntax for that, using the arrow operator:

int add(struct Numbers *numbersPtr) {
    return numbersPtr->a + numbersPtr->b;
}

So, if you have a pointer sPtr to a struct s, both of these are equivalent: (*sPtr).a or sPtr->a

Complete code:

#include <stdio.h>

struct Numbers {
    int a;
    int b;
};

// int add(struct Numbers *numbersPtr) {
//     return (*numbersPtr).a + (*numbersPtr).b;
// }

// OR

int add(struct Numbers *numbersPtr) {
    return numbersPtr->a + numbersPtr->b;
}

int main() {

    // int a = 1;
    // int b = 2;
    // struct Numbers numbers = {
    //     a = a,
    //     b = b,
    // };

    // OR

    // struct Numbers numbers = {1, 2};
    // OR

    struct Numbers numbers = {
        .a = 1,
        .b = 2,
    };

    int ret = add(&numbers);

    if (ret != 3) {
        printf("Failed expected: 3, got: %d", ret);
    } else {
        printf("Passed expected: 3, got: %d", ret);
    }
    return 0;
}

How can i define a pointer to struct directly?

This is more related to how structs are defined and how they work in c, but i guess it's a good to know.

There are three ways:

  1. Declare the struct, and then create a pointer to it:
int main() {
    // Method 1: Declare a struct and then create a pointer to it
    // Memory is stack-allocated - don't free
    struct Numbers numbers = {.a = 1, .b = 2};
    struct Numbers *ptr = &numbers;

    int ret = add(ptr);
}
  1. Directly allocate memory for a struct pointer using malloc. Note, how we also use free to free up the dynamically allocated memory to avoid any memory leaks.
int main() {
    // Method 2: Directly allocate memory for a struct pointer
    // Memory is dynamically allocated in the heap - needs to be freed

    struct Numbers *ptr = malloc(sizeof(struct Numbers));
    if (ptr != NULL) {
        ptr->a = 1;
        ptr->b = 2;
        // or 
        // (*ptr).a = 1;
        // (*ptr).b = 2;
    }
    int ret = add(ptr);
    free(ptr);
}
  1. Using compound literal (C99 and later). Might not be compatible with older compilers or some or most microcontrollers. Again, you don't need to free the memory as it's stack allocated.
int main() {
    // Method 3: Using compound literal (C99 and later)
    // Compound literal, stack-allocated - don't free
    struct Numbers *ptr = &(struct Numbers){.a = 1, .b = 3};
    int ret = add(ptr);
}