3-7-다.포인터

다음은 C의 데이터 타입중에 가장 이해하기 어려운 포인터에 대해 간략하게 소개한다. C 공부를 시작할 때 벌써 소문을 들어서 알고 있겠지만 C의 문법중에서 가장 어렵고 난해한 주제이면서 또한 C를 가장 강력한 언어로 만들어주는 일등 공신이기도 하다. 포인터를 직접 다룰 수 있기 때문에 C언어를 고급언어가 아닌 중급언어로 분류하며 어셈블리와 같은 수준의 시스템 프로그래밍까지도 가능하다.

정수형이나 실수형의 일반적인 변수들은 수치값을 저장한다. 이에 비해 포인터형은 번지를 기억한다는 면에서 일반적인 변수와는 조금 다르다. 데이터가 보관되어 있는 메모리 위치를 기억하고 있기 때문에 직접 값을 조작할 수도 있고 주변의 다른 값까지도 손델 수 있다. 또한 위치는 단순히 4바이트의 번지이기 때문에 함수의 인수로 전달하거나 받기도 효율적이며 함수로 포인터를 전달하면 포인터가 가리키는 메모리를 함수가 직접 조작하는 것이 가능하다.

포인터를 알려면 우선 번지(Address)의 개념에 대해 알아야 한다. 컴퓨터의 주 기억 장치로 사용되는 RAM은 용량이 아주 커서 보통 백만(M) 또는 10억(G) 단위를 사용한다. 컴퓨터에 실제 RAM이 얼마만큼 장착되어 있는가에 상관없이 32비트 운영체제 환경에서 각 프로그램은 32비트의 가상 메모리 공간을 사용할 수 있다. 즉 값을 기억할 수 있는 메모리의 용량은 최대 4G가 된다.

컴퓨터는 메모리의 위치를 구분하기 위해 순서대로 번호를 붙여 관리하는데 이 번호를 번지라고 한다. 최초의 시작 번지는 0번이고 순서대로 1번, 2번, 3번,... 순으로 번호가 매겨져서 40억까지 번지가 붙어 있다. 그래서 컴퓨터는 메모리 중의 특정 바이트를 지칭할 때 이 번지를 사용하여 정확한 위치의 값을 읽고 쓴다.

 

int Num;

 

이 선언문은 앞으로 Num이라는 이름의 정수형 변수를 사용하고 싶다는 뜻이다. 컴파일러는 이 선언문을 만났을 때 당장 사용되지 않는 메모리 공간 4바이트를 할당하고 이 번지에 Num이라는 이름을 붙여 준다. Num이 실제 어떤 번지에 할당될 것인가는 여유 메모리가 어디쯤에 있는지에 따라 달라지기 때문에 실행할 때마다 다르다. 설명의 편의상 1234번지에 할당되었다고 한다면 이때의 메모리 상황은 다음과 같을 것이다.

사용자는 Num 변수가 메모리의 어느 위치에 할당되어 있는가에 상관없이 Num=629; 이런 식으로 변수의 값을 읽거나 쓰기만 하면 된다. 컴파일러는 Num이 할당된 번지를 기억하고 있다가 이 변수를 참조하면 해당 메모리의 값을 읽거나 변경한다.

이때 Num으로 읽고 쓰는 것은 Num이 할당된 번지에 기억되어 있는 값이지 Num의 실제 위치인 번지가 아니다. 즉, C 컴파일러는 변수에 대한 참조문을 변수의 값에 대한 참조로 해석하며 변수의 번지를 참조하는 것이 아니다. 따라서 Num을 읽으면 629가 읽혀지며 1234가 읽혀지지 않는다. 만약 Num이 할당되어 있는 번지값을 알고 싶거나 직접 다루고 싶다면 이때 포인터라는 것이 필요하다. 포인터란 변수의 값이 아닌 변수가 저장되어 있는 메모리의 번지를 기억하는 타입이다. 포인터는 다음과 같이 선언한다.

 

타입 *변수명;

 

타입은 포인터가 가리키는 변수가 어떤 종류인가를 지정하며 int, char, double 등의 기본형과 배열, 구조체, 사용자 정의형 등의 모든 타입이 가능하다. 이 선언문에 사용된 *는 뒤쪽의 명칭이 포인터 변수임을 지정하는 구두점이다. 정수형 변수의 번지를 기억하는 변수 pi는 다음과 같이 선언한다.

 

int *pi;

 

이후 pi는 임의의 정수형 변수가 기억된 번지를 가질 수 있다. 다음 두 연산자는 포인터 변수와 함께 사용되어 번지와 관련된 연산을 한다.

 

* : 포인터가 가리키는 번지의 값을 읽는다.

& : 변수가 기억되어 있는 메모리 번지를 읽는다.

 

일단 두 연산자의 의미를 암기한 후 다음 예제를 작성해 보자.

 

: pointer

#include <Turboc.h>

 

void main()

{

     int Num=629;

     int *pi;

 

     pi=&Num;

     printf("Num의 값은 %d입니다.\n",*pi);

}

 

포인터의 개념을 설명하기 위한 아주 간단한 예제이다. 실행하면 "Num의 값은 629입니다."라는 문자열이 출력된다. int Num=629; 선언에 의해 4바이트의 메모리 공간이 할당되고 629라는 값으로 초기화된다. Num이 어떤 번지에 할당될 것인가는 알 수 없지만 설명의 편의상 1234번지에 할당되었다고 가정하자.

int *pi; 선언에 의해 정수형 변수의 번지를 가리킬 수 있는 포인터 변수 pi가 생성된다. 그리고 pi=&Num; 대입문에 의해 pi는 Num이 기억되어 있는 메모리 번지 1234라는 값을 가지게 될 것이다. &연산자를 변수명앞에 붙이면 변수의 값이 아닌 번지가 조사된다. 여기까지의 메모리 상황은 다음과 같다.

Num은 1234번지에 할당되어 있고 629라는 값을 가지고 있다. pi=&Num; 대입문에 의해 pi에 1234라는 번지값이 기억되는데 pi는 Num이 저장된 번지를 가리키고 있는 것이다. 이 상태에서 *pi로 값을 읽으면 Num 변수의 값이 읽혀진다.

*연산자는 포인터 변수가 가리키는 번지의 값을 읽는데 pi가 1234번지를 가리키고 있으므로 이 번지로 찾아가 그 값을 읽어온다. pi가 &Num의 값, 즉 Num의 번지값을 가지고 있는 상황에서는 *pi가 Num과 동일하다. Num에 어떤 값을 대입하는 문장 대신 *pi=값; 식으로 대입하는 것도 가능하다. 이 경우 pi가 가리키는 1234번지를 찾아가 값을 변경한다.

그렇다면 Num 변수를 바로 읽으면 되는데 왜 이렇게 포인터라는 간접적인 방법을 사용하는가? 한단계 더 중간 과정을 거치게 되면 그 중간 과정에서 많은 유용한 조작이 가능해지기 때문이다. 소프트웨어 공학에서는 융통성을 위해 중간 과정(전문 용어로 레이어라고 한다)을 삽입하는 경우가 아주 빈번하며 한 번 더 과정을 거침으로써 많은 것들이 가능해진다. 예를 들어 비디오 드라이버, 자바 가상 머신 등등이 레이어의 좋은 응용예이며 네트워크는 무려 7개의 레이어로 구성되어 있다.

위 예제는 실용적인 가치는 없지만 포인터의 개념을 이해하는데 많은 도움을 준다. 이 예제가 어떻게 실행되는지 잘 감이 잡히지 않는 사람을 위해 비슷한 예제 하나를 더 작성해 보도록 하자. 다음 예제는 포인터를 사용하여 간접적으로 값을 대입한다.

 

: pointer2

#include <Turboc.h>

 

void main()

{

     double Num1, Num2;

     double *pd;

 

     Num1=3.14;

     pd=&Num1;

     Num2=*pd;

     printf("Num2의 값은 %f입니다.\n",Num2);

}

 

이번에는 실수형 변수를 사용해 보았다. Num1, Num2 두 개의 실수형 변수를 선언하고 Num1에 3.14라는 상수를 기억시켜 놓았다. Num2는 초기화되지 않았으므로 쓰레기값을 가지고 있을 것이다. 실수형이므로 둘 다 8바이트의 메모리를 점유하고 있다. 이 변수들이 할당된 실제 번지는 알 수 없지만 편의상 Num1은 1234, Num2는 5678에 할당되었다고 하자.

실수형 포인터 변수 pd는 &Num1, 즉 Num1이 할당된 번지값을 대입받았으므로 그 값은 1234일 것이다. 이때의 메모리 상황은 다음과 같다.

pd가 Num1의 번지를 가리키고 있다. 이 상태에서 Num2=*pd; 대입문을 실행하면 다음과 같은 연산이 수행된다.

pd가 가리키고 있는 번지를 먼저 찾아 가고 *연산자로 그 값을 읽는다. 그리고 Num2에 값을 대입하였다. 결국 이 예제는 포인터를 통해 간접적인 연산을 하지만 결과적으로는 Num2=Num1; 이라는 대입 연산을 하고 있다. 이 예제를 이해하면 포인터의 개념과 *, & 연산자에 대해 이해했다고 할 수 있다. 이 예제는 *, & 연산자의 동작을 잘 설명하는 전형적인 예제인데 만약 이 두 연산자가 헷갈린다면 항상 이 예제를 생각해 보기 바란다.

이상으로 포인터 타입에 대한 간략한 소개만 했다. 포인터는 한 장을 다 할애해도 설명하기 힘들고 포인터만 다루는 전문 서적이 있을 정도로 어려운 개념이므로 현재 단계에서 크게 욕심 내지 말고 개념만 익히기 바란다. 누가 "포인터가 뭐야?" 라고 물으면 짧게나마 "포인터란 이런 것이야" 라고 대답할 수 있을 정도면 충분하다.

 

앞의 두 예제는 포인터의 기본 개념과 *, & 연산자의 동작을 이해하는데 아주 중요한 의미를 가지고 있다. 논리를 이해한 후 소스를 다시 복원해 보고 이왕이면 메모리속에서 일어나는 일들을 그림으로 그려 설명해 보자.