[C] 함수 호출 시 파라미터는 어떻게 전달되나?

이전 글 중 하나인  함수로 전달된 포인터라는 글과 관련하여 혼동이 있다는 지적이 있어서 좀 더 간결하게 정리해보고자 한다. 해당 댓글에서 나그네님이 지적하신 것과는 달리, C의 인자 전달은, 그리고 C의 변수타입은 매우 간단하다.

값으로 호출? 참조로 호출?

C++이나 Java 관련한 질문 중에는 값으로 호출과 참조로 호출에 대해서 어려워하는 사람들(아마도 학생들이겠지)이 많은 것 같다. 이 두 언어에 대해서는 내가 정확하게 모르니 뭐라 해줄 말이 없지만, C에 있어서는 매우 명확하게 말할 수 있다. 일단 C의 모든 데이터타입은 “값 타입”(Value Type)이다. 기본적인 스칼라형 데이터타입, 구조체, 포인터 이런 모든 것이 값 타입의 자료형이다. 값 타입은 “대입이 일어날 때 우변의 값이 복사되는 타입”이라고 말할 수 있다.

int a, b;
a = 2;
b = a;

예를 들어 위와 같은 코드가 있다고 하면 a, b는 int형이고 이는 값타입이다. 두 번째 행에서 변수 a가 가리키는 메모리 주소에는 ‘2’를 나타내는 4바이트 값이 복사되어 쓰여진다. 그리고 3번째 행의 의미는 b가 가리키는 정적 영역의 메모리 주소로부터 4바이트에 이르는 구간에 대해 a라는 변수가 가리키는 곳을 4바이트만큼 읽어서 쓰라는 의미이고 이를 조금 간결하게 말하면 b에 a의 값을 복사해서 쓴다고 말할 수 있는 것이다.

함수가 호출될 때 인자로 넣어주는 모든 값(보다 엄밀히 말하면 정말 그 타입의 변수가 담고 있는 진짜 값이다. 예를 들어 포인터가 전달되면, 포인터가 가리키는 값이 아닌 포인터 변수에 들어있는 메모리 주소 값)이 스택 영역으로 복사되어 넘어간다. 즉, C에서의 모든 변수는 값타입이고, C의 모든 함수는 “값으로 호출”된다.

int add2nums(int a, int b){ return a + b; }

이렇게 정의된 함수가 있고,

int x, y, z;
x = 1
y = 2
z = add2nums(x, y);

라고 실행했다고 치자. 그렇다면 add2nums의 함수 내부에서는 전역 변수 x, y에 들어있는 값은 전혀 관심이 없으며, 이 함수 내부에서 이를 조작할 수도 없다.

함수 내부에서 함수 외부의 값을 조작하는 것은 그렇다면 영영 불가능할까? 아니다, 이럴 때 쓰라고 포인터가 있는 거다. 우리는 흔히, ‘문자열’을 ‘char형 포인터’와 같은 것으로 착각하는데, 정확하게 char * bufferchar 포인터형 변수 buffer이며, 이는 ‘문자열이 시작하는 곳의 주소’가 되는데, 공교롭게도 C컴파일러는 “문자열이 시작하는 곳의 주소”를 “배열이름”과 똑같이 쓰는데, 이것이 char 포인터형을 문자열 객체로 혼동하는 가장 큰 원인이 되겠다. 따라서

void temp(char * buffer){
    buffer[2] = 'A';
}

라는 함수는 다음과 같이 동작한다고 봐야 한다.

  1. 호출 시점에 buffer에는 특정 문자열의 시작 번지가 들어간다. (char * 타입이거나 char[] 타입)
  2. 이 포인터는 32비트 시스템에서는 4바이트 크기의 메모리 주소값일 뿐, 문자열이 전달되지는 않는다.
  3. buffer[2] = *(buffer + 2) 이므로 문자열의 세 번째 글자가 ‘A’로 바뀐다.

다시 한 번 강조하지만, C의 데이터타입은 모두 값이 복사되는 값 타입이며, 함수 호출시에는 함수의 인자로 주어진 값이 대입되므로 복사가 일어난다. 만약 넘겨주는 데이터 타입이 구조체라면 구조체를 정의하기 따라서 엄청나게 많은양의 데이터가 스택영역으로 복사되어야 하고 이는 성능의 저하로 이어지게 되므로, 포인터는 이때도 유용하게 쓰인다. 포인터를 넘기면 실제로는 아무리 커다란 데이터 타입을 다룬다고 하더라도 4바이트(64비트 시스템에서는 8바이트)의 메모리 주소값만 넘기면 되기 때문이다.

따라서 다시 한 번 정리하면 다음과 같다.

  1. 함수로 넘겨지는 인자는 종류에 상관없이 모두 복사되어 함수로 들어간다고 봐야 한다.
  2. 포인터 타입을 인자로 넘기는 경우, 당연히 넘겨지는 포인터 변수의 값(메모리 주소)이 복사되어 넘겨진다.
  3. 매우 커다란 구조체 타입을 그대로 넘기면 엄청 많은 양의 데이터가 복사되며, 상당한 양의 스택 메모리를 사용하게 된다. 따라서 단순한 int 형 따위가 아닌 경우에는 포인터 타입을 받는 함수를 작성하는 것이 좋다.