본문 바로가기
IT/Programming

C 포인터, 확실히 알자(5) - Call by Reference

by Jany 2009. 3. 31.
반응형
아시다시피 C언어는 main() 그 자체도 함수(Function)인 것처럼, 뼈속까지 함수를 기본 단위로 해서 프로그램을 구성하고 있습니다. (객체지향 언어에서는 객체(Object)가 하나의 구성 단위이듯이 말이죠.)

C에서 함수는 하나의 기능을 수행하는 단위로 볼 수 있는데, 함수가 뭔가의 일을 해내기 위해서는 대게의 경우 그 작업에 필요한 뭔가의 데이터를 같이 전달해 줘야 합니다. 그것이 바로 인수(Argument)라고 불리는 것들이죠.

그런데 이렇게 함수에게 인수를 전달하는 방법에는 여러가지가 있습니다. 그치만 가장 많이 쓰이는 방법은 '값에 의한 호출(Call by Value)'과 '참조에 의한 호출(Call by Reference)' 정도입니다.


1. 값에 의한 호출(Call by Value)

C에서는 기본적으로 함수에게 인수를 전달할 때, 인수로 넘겨주는 값을 복사(Copy)해서 전달하게 됩니다.
int main()
{
 sum(10, 20);

 return 0;
}

int sum(int argA, int argB)
{
 return argA + argB;
}
위 소스코드에서 볼 때, sum() 에게 전달하는 10, 20 이라는 인수=값(Value)은 다음과 같이 변수에 직접 대입되어서 전달된다고 보면 됩니다.
int argA = 10;
int argB = 20.
마찬가지로 다음의 경우를 볼까요.
int main()
{
 int A = 10;
 int B = 20;

 sum(A, B);

 return 0;
}

int sum(int argA, int argB)
{
 return argA + argB;
}
이것역시 마찬가지로 sum()의 인수 선언문에 직접 대입해 보면 됩니다.
int argA = A;
int argB = B;
이때 argA에는 A가 가지고 있는 값 = 10이 전달되겠죠.
마찬가지로 argB에도 B에 저장되어있는 20이란 값이 전달될 겁니다.


2. 참조에 의한 호출(Call by Reference)

그렇다면 아래처럼 포인터(Pointer)를 인수로 전달하는 경우는 어떻게 될까요?
int main()
{
 sum(10, 20);

 return 0;
}

int sum(int *argA, int *argB)
{
 return (*argA) + (*argB);
}
이번에도 아까와 같은 방식으로 생각해 봅니다.
int *argA = 10;
int *argB = 20;
이 문장은 컴파일러가 에러(Error)를 내보냅니다. 포인터형 변수에는 이처럼 상수를 직접 대입할 수가 없기 때문이죠.

즉, 포인터를 인수로 받는 함수에게는 인수값이 반드시 주소(Address)이어야만 합니다. 따라서 위 소스코드는 다음과 같이 수정되어야 합니다.
int main()
{
 int A = 10, B = 20;

 sum(&A, &B);

 return 0;
}

int sum(int *argA, int *argB)
{
 return (*argA) + (*argB);
}
그럼 이제 sum()의 인수에 각각 대입을 해봅시다.
int *argA = &A;
int *argB = &B;
이제는 각 변수의 주소값을 넘겨주고 있기때문에 컴파일러는 아무 불평도 하지 않을 겁니다.

결론적으로, C에서는 전부 값에 의한 호출(Call by Value)를 하고 있다고 보면 됩니다.

다만 포인터의 경우는 그 값이 주소(Address)라는 것에 차이가 있습니다. 때문에 포인터를 값에 의한 호출로 수행하게 되면, 실질적으로 주소값이 전달되기 때문에 참조에 의한 호출(Call by Reference)이 이루어지는 것과 같은 효과가 나오는 것입니다.

따라서 C에서는 포인터형을 인수로 사용하게 되면 무조건 Call by Reference라고 보면됩니다.


3. 이걸 어따써?

우선, 변수 A와 변수 B에 있는 값을 서로 교환하는 swap() 함수를 작성해야 된다고 생각해 봅니다. 그래서 일단은 손이 가는 대로 다음과 같이 코딩을 했습니다.
int main()
{
 int A = 10, B = 20;

 swap(A, B);
 printf("A: %d, B: %d", A, B);

 return 0;
}

void swap(int argA, int argB)
{
 int temp;

 temp = argA;
 argA = argB;
 argB = temp;

 return;
}
그런데 여기에는 중대한 문제가 있습니다. 인수에 값을 전달하는 부분을 다시 생각해 봅시다.
int argA = A;
int argB = B;
이겨서 argA에게는 A가 가지고 있는 값인 10이 복사되어서 대입이 됩니다. argB의 경우도 마찬가지죠. 이렇게 값이 복사되어서 함수에게 전달하기 되기 때문에, 함수 내부에서 이걸 가지고 지지고 볶고 뭔 짓을 다해도 main()에 있는 A와 B에게는 아무런 영향을 줄 수가 없습니다. 따라서 여기서의 swap()은 저혼자 북치고 장구만 칠 뿐이지 실질적인 효과는 전혀 없습니다.

그럼 호출받은 함수에서 원본을 직접 건드리고자 하려면 어떻게 해야 하는가?

앞서 포인터를 인수로 전달 받으면, 인수로 넘겨주는 쪽의 메모리 상의 주소(Address)가 전달된다고 했습니다. 그리고 포인터라는 것은 특정 메모리의 주소에 있는 값을 꺼내오고 기록할 수 있는 변수입니다. 이말인즉, 포인터로 전달 받은 인수는 원본의 값을 수정할 수 있다라는 얘기가 됩니다.

이제 swap() 함수를 올바르게 고쳐보기로 합니다.
int main()
{
 int A = 10, B = 20;

 swap(&A, &B);
 printf("A: %d, B: %d", A, B);

 return 0;
}

void swap(int *argA, int *argB)
{
 int temp;

 temp = *argA;
 *argA = *argB;
 *argB = temp;


 return;
}
이번에는 argA와 argB를 포인터로 선언했습니다.
따라서 호출할 때도 A와 B의 주소를 전달합니다.
int *argA = &A;
int *argB = &B;
함수 내부에서는 포인터 연산자인 * 를 사용해서 원본(A와 B)의 주소를 참조하고 있습니다.
temp = *argA;
*argA = *argB;
*argB = temp;
이처럼 포인터를 인수로 삼는 경우, 즉 Call by Reference를 이용하는 경우에는 인수에게 결과를 넘겨줄 수 있습니다.

일반적으로 함수는 return 문을 이용해서 작업에 대한 결과를 말그대로 반환(return)하게 됩니다. 그런데 이런 방식에 있어서 한가지 단점이 있다면, 반환되는 값이 하나 뿐이라는 것입니다. 만일 두개 이상의 결과를 반환해야 할 필요가 있을 때는 어떻게 해야할까요?

return 문을 고집해야 한다면 구조체나 배열에 담아서 넘겨주는 수 밖에 없겠지요. 하지만 포인터형 인수를 사용하는 경우(Call by Reference)는 원본에다가도 값을 기록할 수 있다는 특성을 이용하면, 인수에다가도 결과를 넘겨줄 수 있게 되는 겁니다.

C의 표준 입력 함수인 scanf()의 경우를 한번 보기로 하죠.
int main()
{
 int age;

 printf("당신의 나이는? ");
 scanf("%d", &age);

 return 0;
}
scanf() 함수는 포인터형으로 인수를 전달 받아서, 입력받은 내용을 다시 인수에게 되돌려주는 구조를 가지고 있습니다. 때문에 scanf()를 사용할 때 인수앞에 & 연산자를 붙여서 변수에 대한 주소를 넘겨주어야만 하는 것이죠.


4. 문자열을 인수로 전달하기

그럼 이번엔 scanf()로 문자열을 입력받는 경우를 생각해 봅시다.
int main()
{
 char name[20];

 printf("당신의 이름은? ");
 scanf("%s", name);

 return 0;
}
이번에는 변수앞에 & 연산자를 붙이지 않고 그냥 scanf()를 호출했습니다. 초심자에게는 이처럼 어느 때 &를 붙이고 혹은 붙이지 말아야 하는지 다소 햇갈리기 쉬울 겁니다. 하지만 포인터를 이해하고 있다면 전혀 어려울 것이 없습니다.

여기서 문자열 변수를 char형에 대한 배열(Array of char)로 선언했습니다. 배열이라는 것은 결국 포인터의 일종입니다. 배열을 포인터 변수로 취급한다면, 배열은 그 이름 자체에 자기 자신에 대한 주소값을 내포하고 있는 것입니다.

앞서, 포인터형을 인수로 전달할 때는 반드시 주소값을 넘겨줘야 한다고 했습니다. 일반 변수에게서 그 주소를 알아내려면 & 연산자를 사용하는 수밖에 없었지만, 포인터 변수는 그 자신이 가지고 있는 값이 주소이기 때문에 & 연산자를 쓸 필요가 없는 것이죠.
결론: 포인터 자체를 인수로 넘길 때는 & 연산자를 사용할 필요가 없다.

5. 배열을 인수로 전달하기

그런데 scanf()를 통해 배열의 각 요소에다 값을 기억시키는 경우는 어떻게 해야할까요? 배열이니까 역시 & 연산자는 필요 없을까요?
int main()
{
 int i, data[10];

 for (i=0; i<10; i++) {
  printf("DATA %d: ", i);
  scanf("%d", data[i]);
 }

 return 0;
}
하지만 이렇게 하면 바로 에러가 발생합니다. 결론부터 말하면 data[i]는 포인터가 아니기 때문입니다. 배열이름 data 자체는 포인터로 취급되지만 뒤에 [ ]가 붙게되면 그냥 변수일 뿐입니다. (※ 사실 정확하게는 [] 자체도 일종의 연산자로서 포인터의 * 연산자와 유사한 기능을 하고 있기 때문입니다)

따라서 이런 경우는 & 를 붙여주어야만 하죠.
int main()
{
 int i, data[10];

 for (i=0; i<10; i++) {
  printf("DATA %d: ", i);
  scanf("%d", &data[i]);
 }

 return 0;
}

한편 배열은 포인터의 일종이기 때문에, 포인터형으로 인수를 받는 함수에게 포인터 대신 배열을 넘겨주어도 되고, 반대로 배열을 인수로 받는 것에 포인터를 대신 넘겨주어도 상관없습니다.
int main()
{
 int data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

 printf("Sum of Array: %d", sumArray(data, 10));

 return 0;
}

int sumArray(int arr[], int count)
{
 int i;
 int sum = 0;

 for (i=0; i<count; i++)
  sum += arr[i];

 return sum;
}
여기서 sumArray()의 인수 선언을 다음과 같이 해도 문제가 없습니다.
int sumArray(int *arr, int count)
{
 int i;
 int sum = 0;

 for (i=0; i<count; i++)
  sum += arr[i];

 return sum;
}
반응형

댓글