강좌 & 팁
이번 강좌는 포인터에 대한 설명의 마지막 시간이 되겠습니다. 몇 회 더 진행하려 했는데, 필요 이상의 내용이 될 것 같아서 이번 회로 정리하려 합니다. 아울러 포인터 이해에 대해 쐐기를 박는 시간이 되었으면 좋겠습니다. 지금까지 올린 글은 아래와 같습니다.
포인터를 헷갈리게 하는 것 중 하나가 재미있게도 배열입니다. 그래서 배열과 비교하여 포인터에 대해 말씀을 올렸습니다만, 그렇다면 아래의 코드 내용을 이해하실 수 있나요?
ary[3], *(ary+3), *(&ary[0]+3), (&ary[0]+3)[0], (ary+3)[0], (ary)[3]
어떠세요? 이해되시나요. ^^
포인터를 헷갈리게 하는 배열
포인터 변수와 배열 변수는 메모리에 생성되는 모습부터 다르다는 것을 여러 번 말씀 드렸습니다. char *ptr; 이라고 선언하면 주소를 받는 정수 변수 하나가 생성되지만, char array[10]이면 char 변수 10개가 생성되며, 시작 주소를 갖는 *ptr 같은 변수는 없습니다. 이것은 배열 때문에 혼란을 받지 않고 포인터를 이해하는데 매우 중요한 내용입니다.
안타깝게도 포인터와 배열의 구분을 정확히 모르고, 심하게는 같다고 생각하는 분을 여럿 만났습니다. 사실 저도 C 언어 입문 시절에는 그런 줄 알았습니다. 그러나 지금까지의 강좌 내용을 이해하셨다면 아래의 선언문은 잘못 되었다는 것을 바로 아실 수 었습니다.
char *ptr; char ary[];
char *ptr은 매우 자연스러운 정의이지만, char ary[]는 에러 입니다. 이유 아시겠지요. ptr 이름의 변수가 메모리 영역에 생성되지만, 배열은 ary[0], ary[1], ary[2]는 있어도 ary 에 대한 생성은 없기 때문에, 반드시 그 크기를 지정해 주어야 합니다.
char ary[100]; char ary[] = "Jang Kilseok";
*와 []는 연산자
그런데 왜? C언어에 자신이 생겼다는 분도 포인터와 배열이 같다고 생각할까요? 그것은 C언어의 자유스러움 때문이라고 하는데, 이 말씀이 틀린 것은 아니지만, 옳다고는 생각되지 않습니다. 아마도 포인터 선언이나 포인터의 값을 구하기 위해 사용하는 '*' 문자를 그냥 구분자로만 생각하기 때문이라고 생각합니다. '*'는 변수가 가지고 있는 값을 주소로 변화해 주는 엄연한 연산자입니다. 배열의 [] 문자도 단순 표시가 아니라 역시 주소 연산자입니다. 그래서 연산 우선 순위에도 포함되어 있지 않습니까? 그것도 우선 순위가 매우 높습니다. 그렇다면 * 와 [] 중에 누가 더 높을까요? 네, []가 더 높습니다.
이렇게 *와 []는 연산자라고 이해하실 수 있어야 본문 시작에 올린 문제를 정확히 이해하실 수 잇습니다. 다시 말씀드려 *와 []는 연산자이므로 모든 변수에 사용할 수 있습니다. 모든 변수에요? 그렇습니다. 연산자라니까요. 자, 아래는 포인터를 설명하는 자주 애용되는 예제입니다.
int n_num = 200; int *ptr = &n_num;
이제 값을 확인해 보죠.
printf( "숫자는 = %d\n", n_num); printf( "숫자는 = %d\n", *ptr);
어떻습니까? 여기까지는 매우 자연 스럽죠. ptr 앞에 '*'는 주소 연산자로 ptr이 가지고 있는 것은 정수 값( n_num 변수의 실제 주소)인데, 그 정수 값을 주소로 변환합니다. 변환한다면 n_num의 주소가 되겠지요. 그러므로 n_num과 *ptr은 모두 같은 메모리 위치를 가리키게 됩니다. 그러므로 같은 값을 출력합니다.
정수 변수도 포인터 변수처럼 사용할 수 있을까?
그렇다면 정수 변수에 주소 값을 넣고 주소 연산자 '*'를 사용할 수 있을까요? 있습니다. 자, 아래 예제를 보시죠.
int n_num = 200; int n_ptr = &n_num;
ptr과 달리 n_ptr에는 '*' 가 없는 것 외에는 모두 똑같습니다. n_ptr은 정수 변수로 주소값을 충분히 방을 수 있습니다. 그래서 n_num의 주소를 직접 받았습니다. warning이 발생하지만, 잘 실행됩니다. 출력해 볼까요?
printf( "n_ptr= %d\n", *( int *)n_ptr);
n_ptr은 정수 변수이기 때문에 그냥 사용하면 컴파일러가 긴장해서 에러를 출력합니다. 그러므로 n_ptr을 (int *)로 형 변환해 줍니다. 그리고 '*' 연산자로 n_ptr의 값을 주소화합니다. 실행해 보십시오. 매우 잘 됩니다. 그러므로 정수 변수이지만, 주소 연산자 '*'를 이용하면 얼마든지 포인터 변수처럼 사용할 수 있습니다.
int n_num; int n_ptr; n_num = 200; n_ptr = ( int)&n_num; printf( "n_ptr= %d\n", *( int *)n_ptr); *( int *)n_ptr = 600; printf( "%d\n", n_num);
포인터와 배열은 다른가? 네 번째 시간
[]도 연산자입니다. '*'를 '[]'로 바꾸어 볼까요?
int n_num;
int n_ptr;
n_num = 200;
n_ptr = ( int)&n_num;
printf( "n_ptr= %d\n", *( int *)n_ptr );
이것을 [] 연산자로 변경하면?
int n_num;
int n_ptr;
n_num = 200;
n_ptr = ( int)&n_num;
printf( "n_ptr= %d\n", ( (int *)n_ptr)[0] );
[]는 배열 변수가 가지고 있는 주소 값에 더하기를 해주는 변수입니다. 위의 예를 설명 드리면 n_ptr의 주소 값에 +0을 했다는 얘기가 되겠습니다.
포인터와 배열을 헷깔리게 만드는 연산자 *, []
C언어는 다른 언어에 비해 자유로움이 많은 언어입니다. 정확히 말씀을 드리면 다른 언어에 비해 표현하는 방법이 매우 다양합니다. 포인터 변수와 배열 변수를 다루는 아래의 예를 보십시오. 포인터와 배열에 대해 정확한 이해를 하지 못한 상태에서 본다면 포인터와 배열은 마치 같은 변수 타입으로 오해할 수 있지 않겠습니까?
#include <stdio.h> int main( void){ char ary[] = "Jang Kilseok"; char *ptr = &ary[0]; printf( "%c\n", *ptr); printf( "%c\n", *ary); printf( "%c\n", ptr[1]); printf( "%c\n", ary[1]); return 0; }
거기에다가 서로 표현 방법을 바꿀 수 있습니다.
int main( void){ char ary[] = "Jang Kilseok"; char *ptr = &ary[0]; printf( "%c\n", ptr[3]); printf( "%c\n", *(ary+3)); return 0; }
포인터와 배열을 헷갈리게하는데 일조하는 C의 친절함?
다른 언어에서는 매우 생소한 모습입니다. 아니 상상하기 어려운 코드입니다. 그러나 C에서 가능한 것은 *나 [] 는 연산자라는 점입니다. *는 변수가 가지고 있는 정수 값을 주소화하는 것이고, []는 주소 값을 증가 또는 감소 시켜 주는 연산자입니다. 그러므로 char *로 선언했다고 해서 반드시 *를 사용해야 하고, []로 선언했다고 해서 []만 사용하지 않아도 되는 것이죠.
이래서 혼란을 줍니다. 거기다가 C 언어의 친절함이 더욱 혼동을 줍니다. 다시 말씀드리지만, 배열 ary[10]은 ary[0], ary[1]은 있어도 ary 이름의 메모리 생성은 없습니다. 그렇다면 위의 예도 잘못된 것이 아닐까요? ary 이름만의 변수는 없다면 어떻게 아래와 같이 쓸 수 있느냐는 것이죠.
printf( "%c\n", *(ary+3));
정확히는 이렇게 써야 합니다.
printf( "%c\n", *(&ary[0]+3));
그렇죠? ary[0]의 주소인 &ary[0] 값에 3을 더하고, 그 숫자를 '*' 연산자로 하여 주소화하고, 그래서 그 주소에 들어 있는 값을 출력한다. 오케이? 그럼에도 배열 변수임에도 ary+3 과 같은 코드가 가능한 것은 C언어의 친절함 때문입니다. 물론 이 친절론은 저의 개인적인 생각입니다만, 결과적으로 이런 코드 때문에 포인터와 배열이 같다고 생각하시는 분이 계십니다. 그러나 아니죠.
자, 그렇다면 아래의 코드까지 이해가 되겠지요?
printf( "%c\n", (&ary[0]+3)[0] )
* 연산자를 없애고 [] 연산자로 모두 바꾼 것입니다. 그래서 결국 C에서는 아래와 같이 단순한 것이 복잡한 표현이 가능합니다.
printf( "%c\n", ary[3] ); printf( "%c\n", *(ary+3) ); printf( "%c\n", *(&ary[0]+3) ); printf( "%c\n", (&ary[0]+3)[0] ); printf( "%c\n", (ary+3)[0] ); printf( "%c\n", (ary)[3] );
모두 같은 값을 출력하며, 모두 ary[3]을 달리 표현한 모습입니다. 정말 헷갈리게 하는 표현이죠. 그러나 *와 []가 연산자라는 것을 생각한다면 모두 이해할 수 있는 당연한 코드입니다.
ary[3], *(ary+3), *(&ary[0]+3), (&ary[0]+3)[0], (ary+3)[0], (ary)[3]
이제 이해 되시나요? ^^