C언어에서 선언된 배열을 포인터로 표현이 가능하다.

아래 예제 코드는 선언된 배열을 포인터로 표현하여 출력하는 간단한 코드이다.

 

 


위의 예시코드를 컴파일하여 실행하면, 다음과 같이 동일한 값이 나오는 것을 확인할 수 있다.

 

 


이를 통하여 [] = *() 연산자가 동일함을 알 수 있는데, 이를 교환법칙으로 괴랄스럽게 변형을 해보았다.

 

 


1. Temp[0]을 동일한 의미인 *(Temp+0)으로 변환

2. 교환법칙에 의한 *(0+Temp)으로 변환

3. 동일한 표기인 0[Temp]으로 변환

 

위의 예시코드를 컴파일하여 실행하면, 에러 없이 컴파일이 되며 값 또한 정상적으로 나오는 것을 확인할 수 있다.

 

 


물론, 이와 같은 방법의 코딩 스타일은 절때 추천하지 않는다.

 

 

포인터를 학습하고 난 뒤, 첫번째로 겪게 되는 어려움 중 하나가 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개에 대한 데이터 쓰기와 다량의 큰 할당이 필요한 데이터 크기에 대한 연산 작업에서 작업량이 많으면 많아질수록 그 격차는 더 심해질 것으로 생각된다.

 

C언어에서가 현재까지 사라지지 않고 언어 사용 순위 중 상위권을 차지하는 이유 중 하나는 포인터라고 해도 과언이 아닐 것이다.

그만큼이나 C언어를 할 줄 안다고 이야기를 하려고 한다면 포인터는 자유자재로 활용할 정도의 수준을 의미하며, 즉, C언어에서 포인터를 사용할 줄 모른다면 C언어를 할 줄 아는 것이 아니라 컴퓨터 프로그래밍이라는 것을 할 줄 안다라고 표현해야 맞을 것이다.


프로그램의 실행은 기본적으로 하드디스크에 저장된 실행파일을 메모리(RAM)로 로드하여, CPU를 통해 명령어를 처리하는 일련의 과정이다. 흔히, C언어를 포기하는 첫 번째 장벽 중 하나가 포인터이며, 이는 메모리 구조에 대한 기초적인 지식조차 없다면 추상적인 의미의 한계에 의해 포기할 수밖에 없게 된다.


이번 포스팅에서는 포인터라는 것이 무엇이고, 어떻게 사용되는지에 관련된 간단한 메모리 참조와 기초 지식에 대해서 다룬다.

Stack, Heap과 같은 메모리 영역에 대해서는 다음 포스팅에서 다룰 예정이며, 간단히 포인터 사용에 있어서 주소(Address)에 대한 개념과 포인터 사용원리에 대한 내용을 중점으로 다룰 것이다.

또한, 32-bit의 4GB 메모리를 기준으로 설명할 것이며, 테스트는 Ubuntu 14.04에서 메모리 가드에 대한 옵션을 제거한 상태에서 설명한다.

 


포인터 사용에 있어 가장 혼동이 오는 부분은 &(앰퍼샌드)와 *(에스테리스크) 사용일 것이다.

이번 포스팅에서는 &(앰퍼샌드)와 *(에스테리스크)​ 사용에 대한 설명을 상세히 설명하고자 한다.

 

 

1. &(앰퍼샌드)의 활용

아래와 같은 코드를 컴파일하여 각각의 변수들에 대한 주소값을 확인해보자.

 


 

&(앰퍼샌드)를 이용하면 할당한 변수의 주소값을 확인할 수 있으며, 해당 주소값은 실행한 시점에서 아래 그림과 같이 할당된 것을 확인할 수 있다.

 

 


이것을 메모리로 도식화한다면 다음과 같이 나타낼 수 있을 것이다.

 

 


포인터 변수는 다음과 같이 변수의 &(앰퍼샌드) 연산자를 이용하여 실질적으로 저장된 메모리 주소(Memory Address)를 저장하는 용도로 사용되며, 이와 같이 변수가 저장된 메모리의 실질적인 주소를 알기 위해서는 &(앰퍼샌드)연산자를 이용하면 가능하다.


이와 같은 원리로 포인터를 사용하여 메모리에 삽입되는 순서를 도식화해보자. 예제 코드는 아래 그림과 같다.

 

 


예제 코드를 실행하면 아래와 같이 각 변수들에 대한 주소값들을 확인할 수 있다.

 

 


이를 도식화하면 메모리에 들어간 변수와 값들은 아래와 같을 것이다.

 

 


이전에 설명한 것처럼 다음과 같이 &(앰퍼샌드)를 이용하여 변수 temp의 메모리 주소를 포인터 변수 ptemp에 저장하는 것을 확인할 수 있다.

 

 

2. *(에스테리스크)의 활용

&(앰퍼샌드)가 변수의 메모리 주소를 확인하는 연산자​라면, 반대로 저장된 변수의 메모리 주소 참조에 활용되는 연산자가 *(에스테리스크)이다.


아래 그림처럼 *(에스테리스크) 활용을 위한 예제 코드를 추가하였다.


 


이를 실행하면 다음과 같은 결과가 나온다.

 

 


이를 도식화하였을 때, *(에스테리스크)를 활용하여 temp2의 변수에 10이 대입​되는지 확인이 가능하다.

 

 

 

3. 흔히 벌어지는 실수

​포인터를 사용하다 보면, 다음과 같은 실수들을 흔하게 마주하게 된다.

 

 


이는 대부분이 포인터를 사용하면서 잘못된 메모리 주소를 참조할 경우 생기는 오류(Segmentation fault)이며, 다음과 같은 간단한 예제를 통해 원인을 살펴보겠다.

 

 


위의 그림처럼 포인터 변수에 프로그래머의 실수로 &(앰퍼샌드)​를 이용하지 않은 채 temp의 값(10)을 대입​하는 경우가 발생하였다. 이를 도식화한 것이 아래의 그림이다.

 

 


다음과 같이 잘못된 메모리 주소 참조로 인해 실행 중 에러가 발생하며, 포인터를 사용할 때 초보자들이 흔히 겪게 되는 오류 중 하나이다. 포인터에 대한 개념과 메모리 주소 참조에 대한 개념 그리고 &(앰퍼샌드), *(에스테리스크)에 대한 용도를 명확하게 이해하고 있는 프로그래머라면 이런 에러가 발생하더라도 적절하게 대응이 가능할 것이다.


+ Recent posts