C Language – Function Arguments Using Pointers

1. Function Arguments in C

  • Programming languages pass function arguments using either Call by Value or Call by Reference
  • However, in C, function arguments are always passed by Call by Value
    • In other words, the value passed to a function is a copy, so the function cannot directly access the original variable.
  • For this reason, C achieves a Call by Reference effect by using pointers to pass a variable's address as a value.

1.1. Function Argument Passing Methods

Call by Valu vs. Call by Reference

Method What is passed Can change original
Call by value The variable's value No
Call by reference The variable's address Yes

1.1.1. Call by Value

  • Call by Value is a method that passes a copy of the value to the function.
  • Therefore, even if the value is changed inside the function, the original variable is not affected.

1.1.2. Call by Reference

  • Call by Reference is a method that uses a pointer to pass the actual address of a variable.
  • With this method, the function can access the original variable through the address passed to it, and it can also change the original variable's value.
  • In C, this kind of reference-style passing (Call by Reference) is implemented using pointers.
    • In C, Call by Reference can be understood as a method of passing an address using a pointer.

1.2. Function Argument Passing Example

#include <stdio.h>

/* receives a copy, caller's variable is unchanged */
void double_by_value(int n) {
    n = n * 2;
}

/* receives address, caller's variable is changed */
void double_by_ref(int *p) {
    *p = *p * 2;
}

/* swap using pointers */
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int x = 10;
    int y = 10;

    printf("before: x = %d\n\n", x);

    puts("# call by value");
    double_by_value(x);
    printf("after:  x = %d  (unchanged)\n\n", x);

    puts("# call by reference");
    double_by_ref(&y);
    printf("after:  y = %d  (changed)\n\n", y);

    int a = 1, b = 2;

    puts("# swap pointers");
    printf("before: a = %d, b = %d\n", a, b);
    swap(&a, &b);
    printf("after:  a = %d, b = %d\n", a, b);

    return 0;
}
before: x = 10

# call by value
after:  x = 10  (unchanged)

# call by reference
after:  y = 20  (changed)

# swap pointers
before: a = 1, b = 2
after:  a = 2, b = 1

1.2.1. Passing by Value

void double_by_value(int n) {
    n = n * 2;   // n is a copy — caller's variable is unaffected
}
  • Even if the parameter passed to the function is modified inside the function, the caller's variable does not change.
  • When the function ends, the copy disappears from the stack.

1.2.2. Passing by Reference

void double_by_ref(int *p) {
    *p = *p * 2;  // dereference(*) to modify original memory directly
}

// pass the address of y when calling
double_by_ref(&y);
  • &y is the address of the variable y. By receiving the variable's address, the function can use *p(dereference) to directly read and write the original variable's value.
  • In this way, changes made inside the function are reflected directly in the caller's variable.

1.2.3. swap Example

void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

swap(&a, &b);
  • When values are passed as function arguments, the function uses only copies of the variable values, so even if it swaps the values inside the function, the originals are not affected.
  • Therefore, to implement a swap operation as a function, you must pass the addresses of the variables as shown in the swap() example so the function can directly access the original data.

2. Double Pointer Function Arguments

  • A double pointer is a pointer that points to another pointer. In other words, it is a pointer that points again to the address of a variable that stores an address.
  • While a regular pointer indirectly accesses original data through a variable's address, a double pointer is used for purposes such as directly modifying the pointer itself or assigning dynamic memory to a pointer.

2.1. Using a Double Pointer

  • To allocate memory with malloc inside a function and pass that pointer back to the caller, you must use a double pointer as the argument.
Item char *buff char **buff
How to pass fn(buf) fn(&buf)
Allocation inside function No Yes 
Changing the value inside function No Yes

2.1.1. Call by Value

void alloc_wrong(char *buff, int size) {
    buff = malloc(size);   // local copy only — caller unchanged
}

Call by Value
  • When alloc_wrong(buf, 8) is called, the value of buff (NULL) is copied and passed to the parameter buff.
  • Even if memory is allocated to the copy of the variable inside the function with code such as buff = malloc(...), the original buff still has the value NULL. 

2.1.2. Call by Reference

void alloc_right(char **buff, int size) {
    *buff = malloc(size);   // modify caller's pointer via dereference
}

Call by Reference
  • &buff is a way to represent the address of the pointer variable buff.
  • The function receives this as a double pointer such as char **buff and can directly access the original pointer through *buff.
    • When code such as *buff = malloc(size) is executed inside the function, the value returned by the  malloc function can be stored in the address value pointed to by the caller's buff.
  • This method is not merely passing a copied value; it can also be used when the pointer itself needs to be modified.
    • In other words, using a double pointer allows the function to directly change even the value of the original pointer that was passed in.

2.2. Double Pointer Usage Example

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

/* Allocate a buffer and caller must call free_buffer() */
int alloc_buffer(char **buff, int size) {
    *buff = (char *)malloc(size);
    if (*buff == NULL)
        return -1;

    memset(*buff, 'A', size - 1);
    (*buff)[size - 1] = '\0';

    printf("buff addr #1: %p[%p] %s\n", &*buff, *buff, *buff);

    return 0;
}

/* Free the buffer and set the caller's pointer to NULL */
void free_buffer(char **buff) {
    if (NULL != *buff) {
        free(*buff);
        *buff = NULL;
    }
}

int main(void) {
    char *buff = NULL;

    if (alloc_buffer(&buff, 8) != 0) {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    printf("buff addr #2: %p[%p] %s\n", &buff, buff, buff);

    free_buffer(&buff);

    return 0;
}
buff addr #1: 0x7ffe250d6e90[0x5ccc164ea2a0] AAAAAAA
buff addr #2: 0x7ffe250d6e90[0x5ccc164ea2a0] AAAAAAA
  • When alloc_buffer(&buff, 8) is called, the address of buff (&buff) is passed to the function.
  • When `*buff = malloc(size) is executed inside the function, the caller's buff pointer value is changed through dereferencing.
  • After that, when execution returns to `main` and `printf("buff addr #2: ..."`) is executed, the same address as `#1` is printed.
  • This means that the address of the allocated heap memory has been passed to the caller's buff.
  • When free_buffer(&buff) is called, &buff is passed in the same way.
  • Inside the function, memory is released with free(*buff), then *buff = NULL sets the caller's buff to NULLThis is done for safe pointer management, clearly indicating that the pointer is no longer valid.

2.3. Comparison of Whether to Use a Double Pointer

2.3.1. Using a Double Pointer

void free_buffer(char **buff) {
    if (NULL != *buff) {
        free(*buff);
        *buff = NULL;
    }
}

free_buffer(&buff);
printf("buff addr #3: %p[%p] %s\n", &buff, buff, buff);
buff addr #3: 0x7fff909667c0[(nil)] (null)
  • After free(*buff), *buff = NULL sets the pointer value to NULL for initialization.
  • This can prevent double-free problems that may occur after memory is released.

2.3.2. Not Using a Double Pointer

void free_buffer2(char *buff) {
    if (NULL != buff) {
        free(buff);
        buff = NULL;
    }
}

free_buffer2(buff);
printf("buff addr #3: %p[%p] %s\n", &buff, buff, buff);
buff addr #3: 0x7ffdb7565e80[0x647f0d7cc2a0] ????
  • In the free_buffer2(char *buff) function, the pointer value is copied and passed in.
  • Memory can be released with free(buff), but the buff = NULL code affects only the local copy inside the function.
  • Therefore, the caller-side pointer remains unchanged, which can lead to a dangling pointer (a reference to released memory) and the risk of double free.