urbanspr1nter urbanspr1nter - 1 month ago 12
C Question

Confused by single pointer and double pointer arguments in function calls

I'm trying to get a deeper understanding on pointer arguments in functions for C. I've written a test program to try to see the difference between passing a single pointer vs a double pointer to a function and then modifying it.

I have a program that has two functions. The first function

modifyMe1
takes a single pointer as an argument and changes the a property to 7. The second function
modifyMe2
takes a double pointer as an argument and changes the a property to 7.

I expected that the first function
modifyMe1
, would be "pass-by-value" that is if I passed in my struct pointer, C would create a copy of the data pointed by it. While with the latter, I am doing a "pass-by-reference" which should modify the structure in place.

However, when I test this program out, both functions seem to modify the structure in place. I know there is a misunderstanding for me on the nature of pointers are arguments for sure. Can someone help clear this up for me?

Thanks!

Here is what I have:

#include <stdio.h>
#include <stdlib.h>

struct myStructure {
int a;
int b;
};

void modifyMe1(struct myStructure *param1) {
param1->a = 7;
}

void modifyMe2(struct myStructure **param1) {
(*param1)->a = 7;
}

int main(int argc, char *argv[]) {
struct myStructure *test1;

test1 = malloc(sizeof(test1));
test1->a = 5;
test1->b = 6;

modifyMe1(test1);

printf("a: %d, b: %d\n", test1->a, test1->b);

// set it back to 5
test1->a = 5;
printf("reset. a: %d, b: %d\n", test1->a, test1->b);

modifyMe2(&test1);

printf("a: %d, b: %d\n", test1->a, test1->b);


free(test1);
return 0;
}


In which my output is:

$ ./a
a: 7, b: 6
reset. a: 5, b: 6
a: 7, b: 6

Answer

You can pass argument in different ways in C (Captain Obvious, yes).

  1. By value. Then it is copied to stack. So function has local copy of variable in function frame. Any changes to argument do not change passed value. It is like "read only" mode

    void fooByValue(myStructure_t arg) {
        printf("passed by value %d %d\n", arg.a, arg.b);
        arg.a = 0;
    }
    
  2. Pass by pointer. Then copy of address of this variable is passed (so yes, it is still passed by value, but you pass value of address, not of whole argument). So this is like "read and write" mode. As you can access to passed variable through its address, you can change value of this variable outside function.

    void fooByPtr(myStructure_t *arg) {
        printf("passed by pointer %d %d\n", arg->a, arg->b);
        arg->a = 0;
    }
    

    But! You still can not modify pointer.

  3. So if you want to modify pointer, then you should pass pointer to pointer. That is like "read-write modify" mode:

    void fooByDblPtr(myStructure_t **arg) {
        *arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (*arg)->a = 10;
        (*arg)->b = 20;
    }
    

    If that was just pointer then there would be a memory leak:

    void fooByDblPtr(myStructure_t *arg) {
        arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (arg)->a = 10;
        (arg)->b = 20;
    }
    

    because here you assign new address to local copy of address, and this argument would be destroyed after function completion.

    UPD. For example, we have

    void fooByPtr(myStructure_t *arg) {
        printf("addr inside foo before %p\n", arg);
        arg = (myStructure_t*) malloc(sizeof(myStructure_t));
        (arg)->a = 10;
        (arg)->b = 20;
        printf("addr inside foo after %p\n", arg);
    }
    
    void main() {
        myStructure_t *x = NULL;
        x = malloc(sizeof(myStructure_t));
        x->a = 10;
        x->b = 20;
        printf("x addr before = %p\n", x);
        fooByPtr(x);
        printf("x addr after = %p\n", x);
        free(x);
    }
    

    inside function memory is allocated and pointer is assigned to local variable. Caller still keeps old value. After function call we lose address of memory so it can not be released.

    Short conclusion: there is a simple rule - if need to change argument, pass pointer to it. So, if want to change pointer, pass pointer to pointer. If want to change double pointer, pass pointer to pointer to pointer.

  4. Passing argument by pointer is also much faster, because you don't need to copy all value on the stack (of course, if value is bigger than pointer to this value, otherwise passing by pointer for read-only is pointless). But it is dangerous, because it could be modified inside function. So you can secure argument defining it with const keyword

    void constFoo(const myStructure_t *arg) {
        arg->a = 10;    //compilation error
        arg->b = 20;    //compilation error
    }
    

    This is really helpful when you work with 3rd party libraries: function signature tells you whether function can modify your argument or not. Though const is optional it is appropriate to write const keyword every time it is possible

  5. Passing array. Usually, one also sends array size (hence, size_t) as argument. You pass array as pointer.

    void foo (int *buf, size_t nbuf) {
        ....
    }
    

    Sometimes you can find code where developer sends pointer to object instead of array, for example

    void foo (int *buf, size_t size) {
        size_t i;
        for (i = 0; i < size; i++) {
            printf("%d ", buf[i]);
        }
    }
    
    int main(int argc, char **argv) {
        int a = 10;
        int buf[1] = { 10 };
        foo(buf, 1);
        foo(&a, 1);
    }
    

    In this case array of one element and pointer to element behave alike (though, they are not the same).

Comments