1. C언어의 함수 인자
- 프로그래밍 언어의 함수 인자 전달 방법에는 값에 의한 전달(Call by Value) 방법과 참조에 의한 전달(Call by Reference) 방법이 있다.
- 그런데 C언어에서 함수의 인자 전달은 항상 Call by Value 방식이다.
- 즉, 함수에 전달되는 값은 복사본이며, 함수 내부에서 원본 변수에 직접 접근할 수는 없다.
- 이러한 이유로 포인터를 사용하여 변수의 주소를 값으로 전달하는 방법으로 Call by Reference 효과를 얻고 있다.
1.1. 함수 인자 전달 방식
| 방식 |
전달 내용 |
원본 변경 |
| 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>
void double_by_value(int n) {
n = n * 2;
}
void double_by_ref(int *p) {
*p = *p * 2;
}
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;
}
- 함수 내부에서 함수로 전달된 매개변수를 수정해도 호출자의 변수는 변하지 않는다.
- 그리고, 함수가 종료되면 복사본은 스택에서 사라지게 된다.
1.2.2. 참조에 의한 전달
void double_by_ref(int *p) {
*p = *p * 2;
}
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);
}
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);
}
- &buff는 포인터 변수 buff의 주소를 표시하는 방법이다.
- 함수에서는 이를 char **buff 와 같은 이중 포인터로 받아, *buff를 통해 원본 포인터에 직접 접근할 수 있다.
- 함수 내부에서
*buff = malloc(size)와 같은 코드를 수행하면, 호출한 쪽의 buff가 가리키는 주소값에 malloc 함수에서 리턴된 값을 저장할 수 있다.
- 이 방식은 단순히 값을 복사해서 전달하는 것이 아니라, 포인터 자체를 수정해야 할 때도 사용할 수 있다.
- 즉, 이중 포인터를 사용하면 함수 안에서 전달된 원본 포인터의 값까지 직접 바꿀 수 있다.
2.2. 이중 포인터 사용 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
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로 설정하여 호출자의buff를 NULL로 초기화 한다. 이는 안전한 포인터 관리를 위하여 해당 포인터가 유효하지 않음을 명확히 하기 위함이다.
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 코드는 함수 내부의 로컬 복사본에만 영향을 미친다.
- 따라서 호출자 측 포인터는 그대로 남아, 댕글링 포인터(해제된 메모리 참조) 및 이중 해제의 위험을 초래할 수 있다.