시작...

블로그 이미지
mutjin

Article Category

분류 전체보기 (148)
기록 (3)
개발새발 (8)
2010년 이전 글 (133)

Recent Post

Recent Comment

Recent Trackback

Calendar

«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31

Archive

My Link

  • Total
  • Today
  • Yesterday
C 언어의 진수 - 포인터 사용법
최근 들어와서 C언어가 일반 사람들에게 급속도로 보급되고 있다. 그러나 C의 인기가 올라간다고 해서, C 자체가 개과천선하여 프로그램을 쉽게 작성할 수 있도록 스스로를 개선할리 만무하다. C는 원래부터 배우기 어려운 언어로 악명이 자자하다. 오죽했으면 C를 포터블 어셈블러라고 비꼬는 사람까지 등장했겠는가?

따라서 C를 조금 알지만 깊숙한 부분을 잘 모르는 사람들을 위해서 아래에서는 C의 가장 핵심이자 진수인 포인터 사용법에 대한 설명을 통해 C에 대한 고차원적인 이해를 도모하도록 노력하겠다. 주로 표준 안시 C를 기준으로 몇몇 미묘한 부분에서는 옛날 C(pre-ANSI, K&R)에 대한 이야기도 나올 것이다. 그러나 설명이 복잡해지는 것을 피하기 위해서 C++에 관계된 사항은 일체 언급하지 않겠다.


. 포인터의 중요성
. 포인터와 주소값
. 포인터와 함수의 인수
. 포인터와 배열
. 주소값 연산
. 캐릭터 포인터와 함수
. 포인터 배열과 다차원 배열
. 함수 포인터
. 복잡한 선언
. 생각해볼 문제
. 참고문헌


포인터의 중요성
C에 원수같은 포인터가 없다고 가정하자. 그러면 당장 어떤 일이 일어날 것인가? 포인 터를 지극히 좋아하는 프로그램 작성자는 두말할 것 없고, 병아리 프로그램 작성자까 지도 엄청난 난관에 봉착할 것이다.

포인터는 변수의 주소를 담고 있는 변수이다. 즉 포인터 자체도 하나의 변수인 셈이다. 이러한 포인터는 C에서 매우 중요한 지위를 차지하고 있다. C에서는 call-by-reference 기능이 제공되지 않고 있는 관계로 인해서 포인터를 어쩔 수 없이 사용해야 한다 는 점을 제쳐두고라도, 적절한 상황에 적절히 포인터를 사용한다면 보다 프로그램을 이해하기 쉽고, 효율적으로 작성할 수 있게 된다. 물론 포인터를 잘못 적용할 경우에 는 goto 문을 잘못사용한 것처럼 재앙만이 남을 뿐이지만.



포인터와 주소값
앞서 포인터는 주소를 담고 있는 변수라고 설명하였다. 우리가 어떤 포인터를
선언할 때 포인터자체도 메모리의 어느 한 구석에 자리잡고 있어야 한다. 다음의 간단한 프로 그램을 살펴보자.


----------- test1.c -------------------------------------------
#include <stdio.h>

main()
{
int *pI;
char *pC;

printf("sizeof(integer) == %d\n'', sizeof(int));
printf("sizeof(pointer to integer) == %d\n'', sizeof(pI));
printf("sizeof(character) == %d\n'', sizeof(char));
printf("sizeof(pointer to character) == %d\n'', sizeof(pC));
}
---------------------------------------------------------------

% cc test1.c && a.out
sizeof(integer) == 4
sizeof(pointer to integer) == 4
sizeof(character) == 1
sizeof(pointer to character) == 4
sizeof 함수는 어떤 객체의 크기를 반환하는 함수이다. 정수와 캐릭터의 크기는 달라 도(각각 4, 1), 각 객체를 가리키는 포인터의 크기는 동일하다는 점(SunOS 4.1.x와 같 은 32비트 운영체제인 경우 모두 4)에 유의하라.

하나의 원을 가지는 연산자인 &를 통해서 어떤 객체의 주소를 획득한다. 그리고 역시 하나의 원을 가지는 연산자인 *를 통해서 그 포인터가 가리키는 위치의 객체에 접근하 도록 한다. 다음의 예를 살펴보자.


int *pI, a, b;

a=3;
pI=&a; /* pI는 a를 가리킨다. */
*pI=7; /* 이제 a는 7이 된다. */
b=*pI; /* b도 역시 7이 된다. */
위의 예에서 한가지 눈여겨봐야 할 사실은 pI 변수자체는 포인터이지만 *pI로
표현될 경우에는 그 값이 정수가 된다는 점이다.(dereferencing or indirection operator)

또한 포인터가 반드시 특정한 종류의 객체를 가리키고 있어야 한다는 점을 명심하기 바란다. 이에 대한 예외는 안시 C에서 제공하는 void 형의 포인터 뿐이다. 이제 골치 아픈 문제 하나를 같이 해결해보고 C에서 어떤 방법으로 call-by-reference를 흉내내 는지 살펴보겠다.

다음 예제의 차이점을 생각해보라. pI는 정수 포인터이며 아래 5개의 예 가운데에서 하나가 다른 의미를 지니고 있다.


*pI += 1;
++*pI;
*pI++;
(*pI)++;
++(*pI);
이제 틀린 것을 발견했는가? 하나의 원을 가지는 연산자인 *와 ++가 오른쪽에서 왼쪽으로 결합되는 성질을 가지고 있다는 점으로부터 시작하라.



포인터와 함수의 인수
call-by-reference의 설명이 나올때마다 빠지지 않고 등장하는 함수가 있다. 바로 우리가 잘 알고 있는 교환 함수인 swap이다. 앞서 C는 call-by-reference를 지원하지 않는다고 설명한 바 있다. 그러면 다음의 예를 보자.


void swap(int x, int y)
{
int temp;

temp=x;
x=y;
y=temp;
}

위의 함수를 작성하고 나서 다음과 같이 호출할 경우 얻어지는 결과는 무엇일까?


a=10; b=9;
swap(a, b);
printf("a is %d, b is %d\n");
생각과는 달리 a와 b는 서로 교환되지 않음을 확인할 수 있다. 그 이유는 C는
call-by-value 형태로 인수를 전달하기 때문이다. 실제 위의 함수는 a와 b의 복사본만을 교환 했을 따름이다. 그러면 이의 해결책은? 바로 포인터의 사용이다.


void swap(int *x, int *y)
{
int temp;

temp=*x;
*x=*y;
*y=temp;
}

swap 함수의 포인터 버전도 겉으로 보기에 *이 몇개 더 붙었을 뿐 크게 달라진 것은 없어보인다. 그럼에도 불구하고 다음과 같이 호출해서 사용하면 원하는 결과를 얻을 수 있다.


a=10; b=9;
swap(&a, &b);
printf(``a is %d, b is %d\n'');


포인터와 배열
C의 포인터와 배열 사이에는 밀접한 관련이 있다. C에서 배열을 사용해서 처리하는 모든 연산을 포인터로도 처리할 수 있다. 포인터를 사용하면 시스템에 따라 속력향상을 기대할 수 있지만, 자칫하면 사람이 이해하기 어렵게 된다.

다음 선언이 어떻게 기억장소를 확보하는지 알아보자.


int a[10];
위의 선언은 원이 10개인 배열을 정의한다. 아래의 그림을 살펴보기 바란다.


+----+----+----+----+----+----+----+----+----+----+
a: | | | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9]
이제 다음과 같이 포인터에 배열을 대입해보자.


int *pa;
pa=&a[0];
위의 코드를 시각적으로 표현하면 다음과 같을 것이다.


pa: +----+
| |--+
+----+ |
|
V pa+1 pa+2 ...
+----+----+----+----+----+----+----+----+----+----+
a: | | | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+
a[0]
이제 위의 그림을 보고 어떻게 포인터와 배열이 매우 밀접한 관계가 있는지를
설명하도록 하겠다. 배열의 첫번째 원을 나타내려면 a[0]을 사용한다는 것은 누구나 다 알고 있을 것이다. 그러면 포인터로 배열의 첫번째 원을 나타내려면? 앞서 배운 *을 유용 하게 사용해서 *pa라고 쓰면 된다. 자 그러면 두번째 원(a[1])을 포인터로 표현하려면 어떻게 할 것인가? 위의 그림에서 나타난 것과 같이 *(pa+1)이라고 쓰면 된다. 이를 일반화시켜 i번째 원(a[i])을 포인터로 표현하려면 *(pa+i)라고 쓰면 된다는 사실을 알 수 있을 것이다.

여기서 확실히 이해하고 넘어가야 할 것이 있다. 바로 *pa + 1과 *(pa + 1)의
차이점 이다. *pa + 1은 a[0] + 1과 같은 말이다. 즉 a의 첫번째 원에 1을 더한 것이다. 그러나 *(pa + 1)은 pa[1]과 동일한 말이다. 비록 정수형이 1바이트가 아니라 2바이트(PC), 4바이트(Sun)로 커지더라도 정수 포인터로 설정해두면 그 객체의 크기(sizeof(int)) 만큼 알아서 증가하여 지정된 위치의 원에 접근할 수 있음에 유의하라.



주소값 연산
포인터도 일종의 변수이므로 이에 연산을 적용시킬 수가 있을 것이다. 다시 위의 a[10]와 *pa의 예로 되돌아가보자. pa가 a의 첫번째 원을 가리키고 있었다면 pa++의 결과 는 무엇인가? pa는 이제 두번째 원을 가리키고 있게된다. 마찬가지로 pa =+ i; 라는 형식도 성립함을 기억하자. 그러면 포인터의 더하기 연산 이외에 빼기 연산은 성립하는가? 다음의 예를 보자.

int strlen(char *s)
{
char *p=s;
while (*p != '\0')
p++;
return p - s;
}

위의 예는 평상시에도 많이 사용하는 strlen(3)이라는 함수의 한 구현 예이다.
strlen 함수가 무슨 일을 하는지는 코드를 통해서 쉽게 알 수 있을 것이다.

포인터의 주소값 연산이 무작위로 된다고 생각하면 큰 오산이다. 유효한 포인터 연산은 동일한 형의 포인터의 대입, 포인터와 정수의 더하기와 빼기, 동일한 배열에 있는 두원의 빼기와 비교, 0의 대입과 0과의 비교뿐이다. 나머지는 모두 불법적인 사용임을 명심하라.


캐릭터 포인터와 함수
이제 스트링에 관계된 함수를 많이 사용해보신 분들께서 그 편리함(?)에 이를
갈았던 캐릭터 포인터에 대한 설명을 진행하겠다.

파스칼(표준 파스칼이 아닌 터보파스칼)과는 달리 C는 언어 자체에서 스트링을 제공하지 않는다. 그러면 우리가 C에서 사용하는 스트링은 무엇인가? 바로 라이브러리 형태 로 제공되는 스트링이다. 먼저 스트링 상수에 대해서 알아보겠다. 스트링 상수는 아래의 예와 같이 "(double quotation mark)으로 묶여 있다.

"hello, world!\n"

스트링 상수는 항상 내부적으로 '\0'으로 끝나도록 되어있어서, 스트링
라이브러리의 각종 함수들이 스트링의 끝을 쉽게 찾을 수 있도록 해준다. 스트링 상수는 일반적으로 스트링을 처리하는 함수의 인자로 많이 사용된다.

그러면 스트링 상수를 어떻게 변수에 대입할 것인가? 다음의 두가지 예를 살펴보자.


main()
{
char aT[]="hello, world!\n";
char *pT="hello, world!\n";
}

K&R C에서는 자동 배열 초기화가 불가능하나 안시 C에서는 얼마든지 가능하다. 그러면 위의 두 경우가 어떻게 다른가? 그림으로 도시해보겠다.


+-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+
aT: |h|e|l|l|o|,| |w|o|r|l|d|!|\n|\0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+

+--+ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+
pT: | +-->|h|e|l|l|o|,| |w|o|r|l|d|!|\n|\0|
+--+ +-+-+-+-+-+-+-+-+-+-+-+-+-+--+--+

pT의 경우 고정된 기억장소를 가리키는 형태이며, aT의 경우 그 자체가 고정된 기억장소에 할당되어 있는 형태이다. 그러면 초기화 과정 이외의 경우에 스트링 상수를 변수에 대입하는 방법은 없는가? 아래에서는 가장 많이 사용하는 일반적인 방법인 strcpy(3) 함수를 소개하면서, 보다 어려운 별들의 세계로 무대를 옮기겠다.


void strcpy(char *s, char *t)
{
while (*s++=*t++);
}

위의 단 두줄자리 strcpy를 보고 한숨을 푹 내쉰 분들도 많으리라. 그러나 C의
포인터는 여전히 냉혹할 따름이다. 이제 포인터 배열에 대한 설명으로 들어간다.


포인터 배열과 다차원 배열
이제부터 포인터 사용의 절정 기술이 소개된다. 바로 포인터 배열(포인터의
포인터)과 다차원 배열이다. 먼저 포인터를 원으로 가진 배열을 알아보자.


char *pMsg[MAX_ITEMS];

pMsg[0]=strdup("foo");
pMsg[1]=strdup("bar");
......
pMsg[MAX_ITEMS]=strdup("foobar");
위의 선언을 시각화시키면 다음과 같다.


pMsg: +--+ +--------------------------+
0 | +---------->| |
+--+ +--------------------------+
1 | +-----+ +---------------+
+--+ +--+---->| |
2 | +--+ | +---------------+
+--+ | +-----------------+
3 | +-+ +---->| |
+--+ | +-----------------+
.......+.....
+--+ +----------------------+
MAX_ITEMS | +---------->| |
+--+ +----------------------+
만일 위의 선언에서 i번째 스트링과 j번째 스트링을 교환하려면 어떤 swap 함수를 정의해야 하는가? 다음의 예를 살펴보자.


main()
{
......
swap(pMsg[i], pMsg[j]);
......
}

void swap(char *v[], int i, int j)
{
char *temp;

temp=v[i];
v[i]=v[j];
v[j]=temp;
}
이제 *과 []이 난무하는 세상에 적응하겠는가? 그렇다면 다차원 배열에 대한 설명을 진행하도록 하겠다. 다차원 배열은 위에서 설명한 포인터의 배열보다는 적게 사용된다. 그러나 포트란이나 파스칼에 익숙한 분들이라면 오히려 다차원 배열을 선호할 것이다.

다차원 배열은 다음과 같이 선언한다.


Life lives[MAX_COL*3][MAX_ROW*3];
앞의 꺽쇠안이 행이고 뒤의 꺽쇠안이 열이다. 그러나 제발 다음과 같이 적지는 말기 바란다. 비록 gcc에서는 아래의 어리숙한 프로그램이 동작하지만, 세상에 설치되어 있는 모든 컴파일러가 gcc라는 보장은 그 어디에도 없다.


Life lives[MAX_COL*3,MAX_ROW*3];
그러면 다차원 배열을 인수로 받는 함수의 선언은 어떻게 할 것인가? 다음의
네가지중 다른 성질의 것이 하나가 있다.


void foo(Life lives[MAX_COL*3][MAX_ROW*3]);
void foo(Life (*lives)[MAX_ROW*3]);
void foo(Life *livex[MAX_ROW*3]);
void foo(Life lives[][MAX_ROW*3]);
*과 []의 우선 순위를 생각해보기 바란다.

이제 포인터 배열과 다차원 배열의 초기화 과정의 비교를 통해 포인터 배열과
다차원 배열의 차이점을 생각해보도록 하겠다. 아래의 예를 보자.


char *pMesg[]={"foo", "bar", "foobar"};
char Mesg[][7]={"foo", "bar", "foobar"};
위의 두 선언을 도식화시켜 비교하면 다음과 같다.


pMesg: +--+ +-+-+-+--+
0 | +------->|f|o|o|\0|
+--+ +-+-+-+--+
1 | |----+ +-+-+-+--+
+--+ +-->|b|a|r|\0|
2 | |-----+ +-+-+-+--+
+--+ | +-+-+-+-+-+-+--+
+->|f|o|o|b|a|r|\0|
+-+-+-+-+-+-+--+

0 7 14
+-+-+-+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+--+
Mesg: |f|o|o|\0| | | |b|a|r|\0| | | |f|o|o|b|a|r|\0|
+-+-+-+--+-+-+-+-+-+-+--+-+-+-+-+-+-+-+-+-+--+

포인터 배열과 다차원 배열의 차이점을 그림을 통해서 직관적으로 감지하기 바란다.
and