C언어 - 포인터를 이용한 함수 인자

1. C언어의 함수 인자

  • 프로그래밍 언어의 함수 인자 전달 방법에는 값에 의한 전달(Call by Value) 방법과 참조에 의한 전달(Call by Reference) 방법이 있다. 
  • 그런데 C언어에서 함수의 인자 전달은 항상 Call by Value 방식이다.
    • 즉, 함수에 전달되는 값은 복사본이며, 함수 내부에서 원본 변수에 직접 접근할 수는 없다.
  • 이러한 이유로 포인터를 사용하여 변수의 주소를 값으로 전달하는 방법으로 Call by Reference 효과를 얻고 있다.

1.1. 함수 인자 전달 방식

Call by Valu vs. Call by Reference

방식 전달 내용 원본 변경
Call by value 변수의 불가
Call by reference 변수의 주소 가능

1.1.1. Call by Value

  • Call by Value는 인자로 전달되는 값의 복사본을 함수에 전달하는 방식이다.
  • 따라서, 함수 내부에서 값을 변경하더라도, 원본 변수에는 아무런 영향을 미치지 않는다.

1.1.2. Call by Reference

  • Call by Reference는 포인터로 변수의 실제 주소를 전달하는 방식이다.
  • 이 방식으로는 함수로 전달된 주소를 통해 원본 변수의 주소에 접근할 수 있으며, 원본 변수의 값을 변경하는 것도 가능하다.
  • C언어에서는 이러한 참조 전달(Call by Reference)을 포인터(pointer)를 사용하여 구현한다.
    • C언어에서 Call by Reference는 포인터를 이용해 주소를 전달하는 방식으로 이해할 수 있다.

1.2. 함수 인자 전달 예제

#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. 값에 의한 전달

void double_by_value(int n) {
    n = n * 2;   // n is a copy — caller's variable is unaffected
}
  • 함수 내부에서 함수로 전달된 매개변수를 수정해도 호출자의 변수는 변하지 않는다.
  • 그리고, 함수가 종료되면 복사본은 스택에서 사라지게 된다.

1.2.2. 참조에 의한 전달

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는 변수 y 변수의 주소이다. 변수의 주소를 받아 *p(역참조, dereference)로 원본 변수의 값을 직접 읽고 쓸 수 있게 된다.
  • 이러한 방법으로 함수 안에서의 변경이 호출자의 변수에 그대로 반영이 되는 것이다.

1.2.3. swap 예제

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

swap(&a, &b);
  • 함수 인자로 값을 전달하는 경우, 함수는 변수 값의 복사본 만을 사용하므로 함수 내부에서 각 변수의 값을 교환하더라도 원본에는 반영되지 않는다.
  • 따라서, 값을 서로 교체하는 swap 기능을  함수로 구현하려면 제시된 swap()함수 예제와 같이 변수의 주소를 전달하여 원본 데이터에 직접 접근해야 한다.

2. 이중 포인터 함수 인자

  • 이중 포인터(double pointer)는 포인터를 가리키는 포인터이다. 즉, 주소를 저장한 변수의 주소’를 다시 가리키는 포인터이다.
  • 일반 포인터가 변수의 주소를 통해 원본 데이터에 간접적으로 접근한다면, 이중 포인터는 포인터 자체를 직접 변경하거나 포인터에 동적 메모리 할당 등의 목적으로 사용된다.

2.1. 이중 포인터 사용

  • 함수 안에서 malloc 함수로 메모리를 할당하고, 그 포인터를 호출자에게 전달하려면 이중 포인터를 인자로 사용해야 한다.
항목 char *buff char **buff
전달 방법 fn(buf) fn(&buf)
함수 내 할당 불가 가능 
함수 내 값 변경 불가 가능

2.1.1. Call by Value

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

Call by Value
  • alloc_wrong(buf, 8)을 호출하면 buff의 값(NULL)이 복사되어 매개변수 buff에 전달된다.
  • 함수 안에서 buff = malloc(...)와 같은 코드로 변수의 복사본에 메모리를 할당하여도 원본 buff는 여전히 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는 포인터 변수 buff의 주소를 표시하는 방법이다.
  • 함수에서는 이를 char **buff 와 같은 이중 포인터로 받아, *buff를 통해 원본 포인터에 직접 접근할 수 있다.
    • 함수 내부에서 *buff = malloc(size)와 같은 코드를 수행하면, 호출한 쪽의 buff가 가리키는 주소값에  malloc 함수에서 리턴된 값을 저장할 수 있다.
  • 이 방식은 단순히 값을 복사해서 전달하는 것이 아니라, 포인터 자체를 수정해야 할 때도 사용할 수 있다.
    • 즉, 이중 포인터를 사용하면 함수 안에서 전달된 원본 포인터의 값까지 직접 바꿀 수 있다.

2.2. 이중 포인터 사용 예제

#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
  • alloc_buffer(&buff, 8) 호출 시, buff  주소(&buff)가 함수로 전달된다.
  • 함수 내부에서 `*buff = malloc(size)를 수행하면, 역참조를 통해 호출자의 buff 포인터 값이 변경된다.
  • 이후 `main으로 돌아와 `printf("buff addr #2: ..."`)를 실행하면 `#1과 동일한 주소가 출력된다.
  • 이는 할당된 힙 메모리의 주소가 호출자의 buff에 전달되었음을 의미한다.
  • free_buffer(&buff) 호출 시에도 동일하게 &buff가 전달된다.
  • 함수 내부에서는 free(*buff)로 메모리를 해제한 뒤, *buff = NULL로 설정하여 호출자의buffNULL로 초기화 한다. 이는 안전한 포인터 관리를 위하여 해당 포인터가 유효하지 않음을 명확히 하기 위함이다.

2.3. 이중 포인터 사용 여부 비교

2.3.1. 이중 포인터 사용

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)
  • free(*buff) 이후 *buff = NULL로 설정해 포인터 값을 NULL로 초기화 처리하고 있다.
  • 이를 통해 메모리 해제 후 발생할 수 있는 이중 해제 문제를 방지할 수 있다.

2.3.2. 이중 포인터 미사용

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] ????
  • free_buffer2(char *buff) 함수에는 포인터 값이 복사되어 전달된다.
  • free(buff)로 메모리 해제는 가능하지만, buff = NULL 코드는 함수 내부의 로컬 복사본에만 영향을 미친다.
  • 따라서 호출자 측 포인터는 그대로 남아, 댕글링 포인터(해제된 메모리 참조) 및 이중 해제의 위험을 초래할 수 있다.