이번에는 함수에 배열(포인터) 대상의 인자전달에 관해 이야기를 하려고 한다. 하지만 그전에 함수에서 인자의 전달이 어떻게 되는지 먼저 짚고 그 다음으로 넘어가자.
인자 전달의 기본방식 - 복사
함수호출시 전달되는 인자의 값은 매개변수에 복사가 된다.
다음 예제를 살펴보자.
#include <stdio.h>
int test_func (int num) {
return num + 1;
}
int main () {
int test = 1;
int a = test_func(test); // test에 저장된 값이 num에 복사된다.
}
이 코드에서 test_func함수에 test라는 값을 인자로 전달하고 있다. 언뜻보면 test변수 자체를 넘기는 것 같지만, 사실은 test에 저장된 값을 test_func의 인자인 num에 복사하는 것 뿐이다. 따라서 이 코드에서 test와 num은 별개의 변수이다.
그렇다면 배열은 어떻게 함수에 전달할까? 일단 배열을 통째로 넘기는 방법은 C에는 존재하지 않는다. 그럼 어떻게 해야하지??
배열을 함수의 인자로 전달하는 방법
책에 정말 좋은 표현이 있다. 정말 잘 설명한 비유라고 느껴진다.
아파트를 보고 싶어 하는 사람 앞에 아파트를 통째로 복사해다 놓을 수 없다면, 아파트의 주소를 가르쳐줘서 직접 찾아가게 하면 된다.
배열을 통째로 복사할 수는 없으니 배열의 주소를 넘겨주어 접근하도록 하면 된다.
예제를 보자.
#include <stdio.h>
void test (int * var, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", var[i]);
}
printf("\n");
}
int main () {
int arr[3] = {1,2,3};
test(arr, sizeof(arr)/sizeof(int));
}
실행 결과
1 2 3
배열의 주소를 넘겨주어 접근할 수 있도록 한다.
변수의 주소만 알면 외부에서도 접근이 가능하므로, 이런식으로 값을 참조할 뿐만 아니라 값을 변경하는 것도 가능하다.
예제를 보자.
#include <stdio.h>
void plus(int * arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", ++arr[i]);
}
}
int main () {
int arr[] = {1,2,3,4,5};
plus(arr, 5);
}
실행 결과
2 3 4 5 6
정상적으로 값이 변경된 것을 볼 수 있다.
함수에 전달하는 배열의 주소값을 인자로 전달받도록 선언하는 방법은 다른 한가지 방법이 있는데, 다음을 보자.
void plus(int * arr, int len);
void plus(int arr[], int len);
이 둘은 완전히 같은 의미이지만, 후자의 표기가 좀 더 배열을 전달하는 느낌이 크다.
Call by Value / Call by Reference
번역하자면, 값에 의한 호출 / 참조에 의한 호출이다. 이는 함수의 호출방식을 의미한다. Call by Value는 함수를 호출할 때 인자에 값을 전달하는 호출방식이고, Call by Reference는 인자에 주소를 전달하는 방식이다.
생각해보면 굳이 나눠서 부를 필요가 있나 싶긴 하다. 왜 나누었나 하면 다음과 같은 실수를 막기 위해서이다.
// Call by Value
#include <stdio.h>
void swap(int n1, int n2) {
int temp = n1;
n1 = n2;
n2 = temp;
}
int main () {
int num1 = 1, num2 = 2;
printf("num1 : %d, num2 : %d\n", num1, num2);
swap(num1, num2);
printf("num1 : %d, num2 : %d", num1, num2);
}
swap을 통해 num1과 num2를 바꾸고자 했지만, 바뀔리가 없다.
앞서 전달하는 인자의 값이 매개변수에 복사가 된다고 했다. 즉 값을 전달받은 n1과 n2, 그리고 값을 전달해준 num1과 num2는 별개이기 때문이다.
어떻게 해결해야 할까? 이때 쓰이는 것이 Call by reference이다. 주소를 넘겨주어 값에 직접적으로 접근하도록 해주는 것이다.
// Call by Reference
#include <stdio.h>
void swap(int *n1, int *n2) {
int temp = *n1;
*n1 = *n2;
*n2 = temp;
}
int main () {
int num1 = 1, num2 = 2;
int *ptr1 = &num1, *ptr2 = &num2;
printf("num1 : %d, num2 : %d\n", num1, num2);
swap(ptr1, ptr2);
printf("num1 : %d, num2 : %d", num1, num2);
}
이렇게 주소값을 넘겨주어 직접적으로 접근하는 것이다.
포인터 대상의 const 선언
const는 변수를 상수화하는데 사용되는 키워드이다. 하지만 포인터변수에서는 선언순서에 따라 그 역할이 조금 달라진다.
첫번째는 포인터가 참조하는 값의 변경을 막는 const선언이다.
이 경우는 const를 맨 처음에 선언한다. 다음 예제를 보자.
#include <stdio.h>
int main () {
int num = 1;
const int * ptr = #
num = 40;
*ptr = 20;
}
이를 실행해보면 *ptr = 20; 에서 오류가 날 것이다. 포인터를 통해 포인터가 참조하는 값을 수정할 수 없도록 const를 선언했기 때문이다.
하지만 여기서 헷갈리면 안되는 것은, 포인터를 통해 값을 수정하는 것이 안될 뿐이지, 변수 자체를 상수화하는 것은 아니기 때문에, 변수 자체를 이용해서 값을 바꾸는 것은 가능하다. 그렇기 때문에 num = 40;에서는 오류가 나지 않는 것이다.
이러한 경우는 값의 변경을 제한하는 것 뿐이지 무언가를 상수화 하지는 않는다.
두번째는, 포인터 변수 자체를 상수화 하는 것이다.
이 경우는, 포인터 변수의 이름 바로 앞에 const를 선언한다. 예제를 보자.
#include <stdio.h>
int main () {
int num1 = 1, num2 = 2;
int * const ptr = &num1;
*ptr = 3;
ptr = &num2;
}
이 경우 ptr = &num2;에서 오류가 나는데, 포인터변수를 상수화했으므로, 한번 가르킨 주소를 바꿀 수 없기 때문이다.
물론 이 두가지 경우를 모두 사용할 수도 있다.
const int * const ptr = &num1;