포인터를 학습하고 난 뒤, 첫번째로 겪게 되는 어려움 중 하나가 Call-by-Value(값에 의한 호출) & Call-by-Reference(참조에 의한 호출) 일 것이다.
C++ 언어에서는 레퍼런스 혹은 참조자(&)라는 개념이 따로 있으므로, 이를 Call-by-Address(주소에 의한 호출)라고 하지만 C 언어라는 것에 한정을 두고 Call-by-Reference(참조에 의한 호출)의 명칭으로 설명한다.
포인터와 메모리에 대한 개념을 명확하게 알고 있다면 이에 대한 이해가 어렵지 않으나, 사실상 메모리에서 어떻게 주소와 값이 흘러가는지 제대로 파악을 할 수 없다면, Call-by-Value와 Call-by-Reference에 대하여 많은 어려움을 겪을 것이다.
이번 포스팅은 Call-by-Value과 Call-by-Reference에 대한 간단한 코드와 어떻게 흘러가는 지에 대한 도식화를 통해 두 가지 방법에 대한 차이점을 살펴보고 Call-by-Reference의 유용한 점을 파악해본다.
1. Call-by-Value (값에 의한 호출)
Call-by-Value는 함수에 인자를 변수에 대입된 값을 던져주는 것을 의미한다. 일단 아래의 코드를 보자.
다음과 같이 int a 변수에는 10을 대입하고, int b 변수에는 20을 대입하였다. 아래는 소스에 대한 변수의 주소와 값의 결과이다.
main() 함수에서 swap() 함수로 변수 a, b를 넘겨주면, swap() 함수는 a, b 두개 변수의 값을 서로 치환하는 역할을 한다.
하지만 다음의 결과처럼 swap() 함수 내에서는 a, b 값이 변경 되었지만, 정작 main() 함수에서는 a, b 값이 그대로 인 것을 확인할 수 있다.
이해를 돕기 위해, Call-by-Value 코드의 메모리 흐름에 대하여 아래 그림처럼 도식화하였다.
최초 프로그램이 실행되면, 변수에 대하여 아래 그림처럼 메모리에 할당될 것이다.
a, b 인자값을 넘기는 swap() 함수가 호출되면, 다음과 같이 swap() 함수의 a = 10, b = 20으로 값들이 대입 될 것이다.
호출된 swap() 함수 내부에서는 다음과 같은 순서로 변수 a↔b 값을 서로 치환하게 될 것이다. 하지만 여기서 중요한 것은 아래의 흐름처럼 할당된 변수들이 swap() 함수 내부의 지역 변수이며, 이는 main() 함수 내의 지역변수에 아무런 영항을 미치지 않는다는 것이다.
결과적으로 다음과 같이swap() 함수 내부에서는 변수 값이 치환된 것을 확인할 수 있다. 하지만, 이는 main() 함수의 변수에 아무런 영향을 미치지 않으며, 오직 swap() 함수 내의 지역 변수에서만 유효함을 확인할 수 있다.
메모리의 변화 과정을 요약하면 동작은 다음과 같다.
2. Call-by-Reference (참조에 의한 호출)
다음으로, 지역 변수의 주소와 포인터를 활용한 Call-by-Reference를 한다. Call-by-Reference는 주소값을 참조하고 이를 호출하는 의미이다. 일단 아래의 코드를 보자.
Call-by-Value 예제코드와는 다르게 main() 함수의 변수 a, b의 주소를 넘겨준다. 소스에 대한 결과는 아래 그림과 같다.
Call-by-Value와는 다르게 swap() 함수 내에서 변경된 a, b 값이 main() 함수에서도 똑같이 적용된 것을 확인할 수 있다. 이에 대한 설명을 아래 그림과 같이 Call-by-Reference 코드의 메모리 흐름에 대하여 도식화하였다.
Call-by-Value처럼 최초 실행 시, 다음과 같은 메모리가 할당될 것이다.
a, b의 주소를 인자값으로 넘기기 때문에 swap() 함수가 호출되면, 다음과 같이 swap() 함수의 포인터 변수 a, b에 주소값들이 대입될 것이다.
swap() 함수에서는 넘겨 받은 a, b의 주소 참조하여, 아래와 같이 치환을 한다. 단, swap() 함수 내의 변수들은 포인터 변수이며, 해당 포인터 변수의 주소를 참조하여, main()함수의 a, b 값을 바꾸므로 실질적으로는 참조된 주소값인 main() 함수의 a, b 값을 변경한다.
다음과 같이 포인터를 활용하여 main() 함수 내의 변수 a, b에 대한 값을 치환하는 것을 확인할 수 있다.
메모리의 변화 과정을 요약하면 동작은 다음과 같다.
3. Call-by-Reference의 유용성
Call-by-Reference를 사용하는 것은 많은 이유가 있겠지만, 일단은 메모리에 대한 절감이다. 아래와 같은 그림을 가정하여 생각해보자.
프로그램이 수행되는 하드웨어 환경의 한계에 의해 다음과 같이 메모리가 거의 가득 찬 상태에서 프로그램이 구동되어야 한다고 가정해보자.
프로그램이 실행하는 시점에 다음과 같이 메모리가 거의 가득차 main() 함수의 int temp[1000]을 temp() 함수에 인자로 넘겨할 경우, temp() 함수는 이를 다시 메모리에 int temp[1000](약 1000 * int(4) = 4000 Byte)의 크기로 할당하여야 한다.
하지만, 현재 메모리는 가득 찬 상태로 결국 변수에 대한 메모리 용량을 확보하지 못한 채로 프로그램 수행에 문제점이 발생할 것이다.
하지만, 이를 Call-by-Reference를 사용할 경우, 예를 들어보자.
포인터 변수인 4Byte의 할당만으로도 main()의 int temp[1000] 변수를 참조하여, 프로그램 수행이 가능하다. 물론 예제를 int temp[1000](약 1000 * int(4) = 4000 Byte)로 설명하였기 때문에 실질적으로 그럴 일은 없겠지만 변수에 대한 할당량이 크면 클수록 다음과 같이 포인터 변수에 대한 유용성은 더 증가할 것이다.
다음으로는 속도일 것이다. 포인터 변수 1개에 대한 데이터 쓰기와 다량의 큰 할당이 필요한 데이터 크기에 대한 연산 작업에서 작업량이 많으면 많아질수록 그 격차는 더 심해질 것으로 생각된다.