강좌 첫화면으로 최근 글 보기(Post)
갈래별로 보기 categories




  3.11. 번지 연산자와 포인터



3.11. 번지 연산자와 포인터

** (아래 예제에서 [[기호는 <<기호이고, ]]는 >>기호임. HTML의 앵커 태그 문제로 인해 바꿔쓴 것임)

번지 연산자와 포인터는 C언어에서 최고의 장점이라고 합니다. 물론 C++에서도 최고의 장점입니다. 그래서 C++에서는 포인터와 더불어 클래스가 최고의 장점이라고 말합니다. 이처럼 포인터는 C언어와 C++언어의 융통성을 보여주는 장점입니다. 반대로 사용자를 가장 많이 골탕먹이는 개념이기도 합니다. 때문에 포인터를 제대로 다룰 줄 안다면 C++언어를 상당 부분 이해한 것이나 다름 없습니다. 많은 사람들이 포인터를 이해하지 못하고 중간에 포기하는데 그 까닭은 포인터를 너무 어렵게 생각하기 때문입니다. 또는 포인터를 어려운 방법으로 사용하고자 하기 때문입니다.
너무 지나쳐도 안된다는 말이 있습니다. C++언어 공부에서는 포인터공부가 해당하는 것 같습니다. 즉 포인터공부에 너무 몰두해서 포인터를 복잡하게 사용하는 경우에는 스스로 혼란을 겪습니다. 꼭 필요한 수준까지만 포인터를 배우시기 바랍니다. 그리고 자신이 배운 수준으로는 도저히 해결할 수 없을 때 한 단계 더 복잡한 사용법을 익히시기 바랍니다.
많은 사람들이 포인터를 어렵게 생각하는 이유는 다차원배열이나 포인터를 중첩해서 사용하기 때문입니다. 그러므로 가능하면 일차원배열로 모든 것을 해결하려고 해야 합니다. 일차원배열만 사용하고 번지연산자만 사용한다면 포인터도 무척 쉽게 사용할 수 있습니다. 그럼 포인터란 무엇인지 알아보겠습니다.

3.11.1. 번지를 알려주는 번지연산자 &


&는 변수가 저장된 번지를 알려주는 번지연산자입니다.

포인터를 설명하려면 그전에 먼저 번지연산자 &를 설명해야 합니다. 번지연산자 &는 변수가 저장된 번지를 알려주는 연산자입니다. 예를 들어서 n이라는 변수가 있을 경우 &n은 n의 번지를 알려주는 연산을 합니다. 번지에 대해서는 여러 차례 설명을 드렸으므로 이제는 아실 것으로 생각합니다.

번지는 자료를 저장하는 컴퓨터의 메모리상의 주소(즉 번지)를 말합니다. 쉽게 말해서 1메가바이트의 램이 있는 컴퓨터라면 메모리의 각 바이트마다 나름대로의 주소가 있는 것입니다. 만약 n이 메모리상의 12345라는 주소를 배정받았다고 합시다. 그리고 그 n이라는 변수에 3이라는 숫자를 넣었다고 합시다. 이럴 경우 n의 번지와 그 번지에 저장된 자료값은 다음과 같이 나옵니다.

보기

n=3;

printf("변수 n의 값=%d 변수 n의 번지=%d ",n,&n);


결과

변수 n의 값=3 변수 n의 번지=12345


이제 번지연산자가 무엇을 하는 연산자인지 아실 겁니다. 번지연산자는 변수가 배정받은 번지가 몇 번지인지 알려주는 연산자입니다. 앞으로 나올 복잡한 내용은 다 까먹더라도 이 점만 기억하고 계시면 됩니다.

당연한 이야기지만 변수마다 각기 배정받는 번지는 다릅니다. 만약 두 개 이상의 변수가 같은 주소를 가진다면 서로 자기 자료를 넣겠다고 싸우다가 큰 문제가 생기고 말 겁니다. 컴퓨터가 먹통이 되는 이유는 바로 번지 관리를 잘못해서입니다. 그러니까 하나의 주소에 두 개 이상의 변수가 서로 자료를 넣으려고 우기다보면 먹통이 되는 겁니다. 프로그램끼리 충돌해서 먹통이 되었다는 말은 바로 두 개의 프로그램이 서로 같은 번지를 사용하겠다고 우기다가 먹통이 되었다는 말이나 다름 없습니다.

하나의 변수는 하나의 주소(번지)를 가집니다.

하나의 변수는 하나의 주소를 가집니다. 대신 변수의 크기에 따라서 차지하는 영역은 조금씩 다릅니다. 정수형 변수는 2바이트만을 차지합니다. 그러므로 2바이트 크기만한 공간을 자신의 집으로 삼아서 주소를 결정합니다. 그리고 문자형은 1바이트만한 공간을 차지합니다. 10개의 문자를 저장할 배열이라면 10바이트의 크기를 가져야 합니다. 변수에 따라서 각기 알맞은 주소를 가집니다.

변수의 크기에 따라서 차지하는 번지의 범위가 결정됩니다.

그러니까 문자형 변수 c의 주소가 12345라면 이 변수는 12345 번지만 자신의 주소로 사용합니다. 그러나 정수형변수는 2바이트를 가질 수 있기 때문에 12345 번지와 12346 번지까지 주소로 사용할 수 있습니다. 단 시작주소는 12345입니다. 10개의 문자를 저장하는 배열이라면 10바이트의 주소를 가집니다. 즉 12345 번지부터 12354 번지까지 혼자서 차지합니다. 단독주택 사는 사람은 23번지 33호에 집 한 채를 짓고 살지만 아파트라면 23번지를 통채로 차지하는 것은 물론 24번지와 25번지까지도 사용하는 경우가 있습니다. 들어가서 살 사람이 많을수록 집이 넓고 사용하는 번지수도 넓은 것처럼 컴퓨터에서도 저장해야 할 자료가 많을수록 번지의 점유율이 높아집니다.

그렇기 때문에 번지연산자 &가 구해주는 번지는 변수의 시작 번지입니다. 어떤 변수가 12345 번지부터 12348 번지까지 네 개의 번지를 차지하고 있다고 하더라도 &는 그 변수의 시작번지만을 구해줍니다. 그럼 그 변수가 차지하는 두번째 번지와 세번째 번지는 어떻게 구해야 할까요? 또 각 번지에 든 내용물(자료)은 어떻게 알아낼 수 있을까요? 이런 의문은 포인터가 해결해줍니다.

포인터는 번지임을 나타내는 기호이며 포인터의 내용물은 그 번지에 저장된 내용물입니다.

그럼 포인터란 무엇인가? 포인터는 번지임을 나타내는 기호입니다. 즉 포인터는 번지나 다름 없습니다. 그러므로 포인터의 내용물은 곧 그 번지에 저장된 내용물이나 다름 없습니다. 따라서 포인터의 내용물을 알 수 있다면 그 번지의 내용물을 알 수 있다는 이야기입니다. 하여간 포인터가 번지를 나타내는 기호라는 점을 잘 기억해주시기 바랍니다.

&n이라는 명령문은 변수 n의 번지를 알려주므로 &n은 곧 포인터인 셈입니다.

그럼 앞서 설명한 &n은 무엇이 되겠습니까? &n의 연산결과는 변수 n의 번지입니다. 따라서 &n은 곧 포인터입니다. &n의 결과인 번지는 포인터 상수가 되는 것입니다.
그렇다면 어떤 변수에 &n을 대입한다면 그 변수는 무엇이 되겠습니까? 포인터를 내용으로 삼는 변수가 될 겁니다. 즉 포인터변수가 되는 것입니다.

만약 ptr이라는 변수가 있고 ptr=&n;이라는 명령문을 써서 &n을 ptr에 대입했다고 합시다. 이 경우 &n은 포인터상수이므로 ptr은 포인터상수를 값으로 가지는 변수가 됩니다. 정수를 값으로 가지는 변수를 정수형변수라고 하고 실수를 값으로 가지는 변수가 실수형변수였으니 포인터를 값으로 가지는 ptr은 포인터변수가 되는 것입니다.

**요약: 번지연산자 &는 변수의 번지를 구해주는 연산자입니다. 그리고 포인터는 번지임을 나타내는 기호입니다. 즉 포인터는 번지나 다름 없습니다. 따라서 변수에 번지연산자를 붙인 연산결과는 그 변수의 번지를 나타내는 포인터상수가 됩니다. 그리고 포인터상수
를 값으로 가지는 변수를 포인터변수라고 합니다.

3.11.2. 간접연산자 * 와 포인터


간접연산자 *는 포인터변수에 저장된 번지의 내용물을 알려주는 연산자입니다.

그럼 이제 이쯤에서 포인터와 빼놓을 수 없는 간접연산자 *를 소개하겠습니다. 간접연산자 *는 포인터변수에 저장된 번지의 값을 돌려주는 연산자입니다. 여기에서 주의해야 할 부분은 포인터변수에 저장된 번지를 알려주는 것이 아니라, 포인터변수에 저장된 번지에 저장된 내용물을 알려준다는 점입니다. 그러므로 *ptr이라면 n에 저장된 값을 보여주는 것입니다. 즉 n에 3을 저장했고, ptr에 n의 번지수를 저장했다면 *ptr은 n의 값을 알아내는 간접연산자가 됩니다.

다음의 소스파일를 작성해 실행시켜보기 바랍니다.

// test025.cpp
// 포인터의 사용 예제

#include

void main(void)
{
int n; // 정수형 변수 n을 선언
int *ptr; // 정수형 포인터변수 ptr을 선언
n=3; // n에 3을 대입했음.
ptr=&n; // 포인터변수 ptr에 n의 번지를 대입했음.
printf("변수 n의 값은 n=%d \n",n ); // n의 값은?
printf("변수 n의 번지는 &n=%d \n", &n); // 변수 n의 번지는?
printf("포인터변수 ptr의 값은 ptr=%d \n", ptr); // ptr의 값은?
printf("ptr에 저장된 번지의 내용물은 *ptr=%d \n",*ptr ); // ptr에 저장된 번지의 내용물은?
}


**그림: test025.cpp의 소스파일 내용

이 프로그램을 실행시키면 다음과 같은 결과가 나올 겁니다. 이때 변수 n의 번지로 출력되는 숫자는 프로그램을 실행시킬 때마다 달라질 수 있습니다. 컴퓨터의 상태에 따라서 변수 n이 부여받을 수 있는 번지 수는 달라지기 때문에 같은 프로그램을 실행시키더라도 컴퓨터에 따라서, 컴퓨터의 환경설정 내용에 따라서 번지 값은 매번 다를 수 있습니다.

결과
변수 n의 값은 n=3
변수 n의 번지는 &n=8778
포인터변수 ptr의 값은 ptr=8778
ptr에 저장된 번지의 내용물은 *ptr=3


**그림: test025.exe를 실행시킬 때 나오는 결과물

화면의 예를 보면 알겠지만 n의 값은 3이고, n의 번지는 8778 번지입니다. 그리고 포인터 변수 prt에 저장된 값은 n의 주소인 8778이 저장되었습니다. 그리고 *ptr은 ptr에 저장된 번지에 무엇이 들어있냐를 물어보는 연산자입니다. prt에 저장된 번지는 8778 번지이고, 8778번지에는 3이라는 숫자가 저장되어 있습니다. 그래서 *prt은 3이 되는 것입니다. 결국 &n과 ptr이 같고, n과 *ptr이 같은 값을 가지는 셈입니다.

자 예제 프로그램을 잘 살펴보겠습니다.

// test025.cpp

// 포인터의 사용 예제


#include


void main(void)

{

int n; // 정수형 변수 n을 선언


정수형 변수 n을 선언했다는 것에 대해서는 더 이상의 설명이 필요 없을 겁니다.

int *ptr은 정수형 포인터 변수 ptr을 선언한다는 뜻입니다.


int *ptr; // 정수형 포인터변수 ptr을 선언


이 부분을 잘 보시기 바랍니다. int *ptr이라고 선언했습니다. 정수형 변수 ptr을 선언한 것 같은데 ptr 앞에 * 기호가 붙어서 int *ptr;이 되었습니다. 이때 변수이름 ptr 앞에 붙이는 * 기호는 변수 ptr이 포인터변수임을 알려주는 기호입니다. 즉 int ptr이라고 하면 그냥 정수형 변수 ptr을 선언한 것이 되지만 int *ptr이라고 하면 정수형 포인터 변수 ptr을 선언한 것이 됩니다.

n=3; // n에 3을 대입했음.

ptr=&n; // 포인터변수 ptr에 n의 번지를 대입했음.


n에 3을 대입한 것은 바로 아실 겁니다. 그리고 ptr에 무엇을 대입하는가 잘 보십시요. ptr에는 변수 n의 번지를 대입했죠? &n이 변수 n의 변수를 구해주는 연산자이므로 변수 n의 번지가 ptr에 대입되었습니다. 따라서 변수 n의 번지값과 ptr에 저장된 값은 같은 셈입니다.

printf("변수 n의 값은 n=%d \n",n ); // n의 값은?

printf("변수 n의 번지는 &n=%d \n", &n); // 변수 n의 번지는?


변수 n을 출력하라는 명령을 내리면 n에 저장된 변수 3이 출력됩니다. 그리고 번지연산자 &을 이용해서 변수 n의 번지를 출력하면 변수 n이 위치한 번지수가 나옵니다. 단 이때 출력되는 번지수는 매번 다를 수 있습니다.

변수 n의 주소인 &n과 포인터 변수 ptr의 값은 같습니다.


printf("포인터변수 ptr의 값은 ptr=%d \n", ptr); // ptr의 값은?


그럼 포인터 변수 ptr의 값을 출력하면 무엇이 나오겠습니까? 바로 변수 n의 번지겠죠? 그래서 ptr을 출력한 내용이나 &n을 출력한 내용이 같은 겁니다.

printf("ptr에 저장된 번지의 내용물은 *ptr=%d \n",*ptr ); // ptr에 저장된 번지의 내용물은?

}


*ptr은 포인터 변수 ptr에 저장된 번지에 저장된 값을 참조하라는 연산자입니다. 즉 포인터변수에 저장된 변수 n의 번지가 8778 번지일 경우 8778 번지에 저장된 값이 무엇이냐는 명령입니다. 8778 번지에 저장된 값은 곧 변수 n의 값과 같기 때문에 *ptr=3이 나왔습니다.

*ptr의 *를 간접연산자라고 하며, 포인터변수에 저장된 번지의 내용을 참조할 수 있는 연산자입니다.

이때 사용하는 *를 간접연산자라고 합니다. 간접연산자는 포인터변수에 저장된 번지를 이용하여 그 번지에 저장된 내용을 참조할 수 있는 연산자입니다. 즉 포인터변수에 간접연산자를 붙이면 포인터변수에 저장된 번지에 저장된 내용물을 연산결과로 돌려줍니다.
그렇기 때문에 변수 n의 번지를 나타내는 &n과 변수 n의 번지를 저장한 ptr의 값은 같습니다. 즉 ptr=&n입니다. 그리고 변수 n에 저장된 값이나 변수 n의 번지인 &n에 저장된 값도 같습니다. 그러므로 *ptr=n이 되는 것입니다.

**요약: 간접연산자 *은 포인터변수에 저장된 변수를 이용하여 그 변수의 내용을 간접적으로 참조할 수 있는 연산자이기에 간접연산자라고 합니다.


그럼 이번에는 다음과 같은 예제를 실행해보기 바랍니다.

// test026.cpp
// 포인터 변수와 번지의 관계를 살펴보는 예제

#include

void main(void)
{
int x,y; // 정수형 변수 x와 y를 선언
int *ptr; // 정수형 포인터변수 ptr을 선언
x=3; // x에 3을 대입했음.
ptr=&x; // 포인터변수 ptr에 x의 번지를 대입했음.
y=*ptr; // ptr에 저장된 번지에 있는 내용물을 y에 대입
printf("x=%d \n",x ); // x의 값은?
printf("&x=%d \n", &x); // 변수 x의 번지는?
printf("ptr=%d \n", ptr); // ptr의 값은?
printf("*ptr=%d \n",*ptr ); // ptr에 저장된 번지에 저장된 값은?
printf("&ptr=%d \n",&ptr ); // 포인터 변수 ptr에 번지는?
printf("y=%d \n",y ); // y의 값은?
printf("&y=%d \n", &y); // 변수 y의 번지는?
}


**그림: test026.cpp의 소스파일 내용

이 프로그램의 실행결과는 다음과 같습니다.

x=3
&x=8688
ptr=8688
*ptr=3
&ptr=8684
y=3
&y=8686


**그림: test026.exe의 실행 화면

x도 3, *ptr도 3, y도 3이 나옵니다. 즉 x에 저장된 값이나 *ptr의 연산결과나 y에 저장된 값이 같은 겁니다. 그럴 수밖에 없는 것이 x의 번지를 ptr에 저장했고, *ptr을 통해서 x의 번지에 있는 내용을 참조하여 y에 대입시켰기 때문입니다.

그리고 x의 번지는 8688번지로 배정받았는데 이 번지를 포인터 변수 ptr에 저장했으므로 &x와 ptr의 값은 같습니다.

마지막으로 세 개의 변수는 각기 다른 번지를 부여받은 것을 알 수 있습니다. 번지연산자 &를 이용하여 &x, &y, &ptr의 세 번지를 알아본 결과 제일 먼저 선언한 x는 8688번지를 부여받았고, y는 8686번지를 부여받았습니다. 그리고 다음에 선언한 변수 ptr은 8684번지를 선언받았음을 알 수 있습니다.

결국 다음과 같은 결과를 말씀드릴 수 있습니다.

x=3;

ptr=&x;

y=*ptr; // ptr에 저장된 번지(즉 x의 번지)에 저장된 값(즉 x의 값)을 y에 대입


위의 식은 다음의 식과 동일합니다.

x=3;

y=x;


이 과정을 간단하게 도표로 설명하면 다음과 같습니다.

먼저 x, y, *ptr이라는 세 개의 변수를 선언했습니다.

메모리의 번지수
8688번지
8686번지
8684번지
번지수를 배정받은 변수
x
y
ptr
변수에 저장된 내용



참고



x에 3을 대입했습니다. 즉 x라는 주소에 3이라는 우편물을 배달해서 넣어준 것입니다.

메모리의 번지수
8688번지
8686번지
8684번지
번지수를 배정받은 변수
x
y
ptr
변수에 저장된 내용
3


참고
x=3;


그리고 8684번지에 있는 변수 ptr에는 x의 번지를 저장했습니다.

메모리의 번지수
8688번지
8686번지
8684번지
번지수를 배정받은 변수
x
y
ptr
변수에 저장된 내용
3

8688
참고
ptr=&x;
x의 번지 8688이 ptr에 저장됩니다.
&x -> ptr


그리고 ptr에 저장된 번지를 참고하여 ptr에 저장된 번지의 내용물을 y에 저장합니다.

메모리의 번지수
8688번지
8686번지
8684번지
번지수를 배정받은 변수
x
y
ptr
변수에 저장된 내용
3
3
8688
참고
y=*ptr;
ptr에 저장된 내용 8688이라는 번지를 찾아서 그 안에 든 내용을 y에 저장합니다.
&x <- 8688번지를 찾았음.
3 -> y 8688 번지의 내용물 3을 y에 저장


포인터를 사용하면 번지 안에 저장된 내용을 조작할 수 있으며 번지를 다루기가 쉽습니다.

이제 포인터변수와 간접연산자를 이용한 계산이 어떻게 이루어지는지 이해하셨을 것으로 생각합니다. 그렇다면 간단하게 x=3; y=x;라는 명령문을 이용하지 왜 복잡하게 포인터변수를 사용하고 간접연산자를 사용하는가 하는 의문이 들 겁니다.
포인터를 이용하는 이유는 여러 가지가 있지만 가장 큰 이유는 번지 내의 내용을 조작할 수 있기 때문입니다. 좀 어렵게 말하면 함수를 사용할 때 실매개변수와 형식매개변수를 일치시킬 수 있다는 장점이 있기 때문입니다. 또한 번지를 다루기가 쉽다는 점도 큰 장점입니다.

**요약: 포인터를 이용하면 번지 안에 저장된 내용을 직접 조작할 수 있으며, 번지를 다루기가 쉽다는 장점이 있습니다. 다른 말로 말하면 함수를 사용할 때 실매개변수와 형식매개변수를 일치시킬 수 있는 장점이 있습니다.


포인터를 사용 안하고 두 수를 바꾸어봅니다.

차근차근 보기를 들어서 설명하겠습니다. 만약 두 변수에 저장된 값을 서로 바꾸고 싶다고 합시다. 예를 들어서 변수 x에는 3이 저장되었고, 변수 y에는 5가 저장되어 있는데 이것을 서로 바꾸어 x에 5가 저장되고 y에 3이 저장되게 하고 싶다고 합시다. 이 경우 처음 프로그램을 짜는 사람은 다음과 같이 만드는 경우가 있는데 이러면 결과가 제대로 안 나옵니다.

// test027.cpp
// 두 수를 바꾸는 예제

#include
#include

int swap(int x, int y);

void main(void)
{
int x=3, y=5; // 정수형 변수 x와 y를 선언했음.
cout[["Main: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력했음.
swap(x,y); // swab라는 함수를 실행시켜 x와 y를 바꿈
}

int swap(int x, int y)
{
x=y; // x에 y에 대입했음.
y=x; // y에는 x를 대입했음.
cout[["Swap: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력해봅니다.
return 0; // 정수형 함수이므로 의미 없는 복귀값을 돌려줍니다.
}


**그림: test027.cpp의 소스파일 내용

이 경우 원하는 결과가 나오지 않을 겁니다. 이 프로그램의 실행결과는 다음과 같습니다.

결과
x=3 y=5
x=5 y=5


**그림: test027.exe의 실행 결과

두 수의 교환이 안된 이유는 첫번째 교환으로 변수 x가 y와 같은 가져서입니다.

처음 main() 함수에서 printf() 함수를 이용했을 때는 당연히 x=3 y=5가 나올 겁니다. 아직 아무런 행동도 취하지 않았으니까요. 그 다음에 swap() 함수 안에서 printf() 함수를 했을 때는 엉뚱하게도 'x=5 y=5'라는 결과가 나왔습니다. swab함수를 잘 보기 바랍니다.

x=y; // x에 y에 대입했음.


위 문장에 의하여 x에는 y의 값이 대입되어 치환되었습니다. 즉 x=3;가 같은 명령문입니다. 그러므로 x는 y의 값인 5가 저장되었습니다. 문제는 다음 문장입니다.

y=x; // y에는 x를 대입했음.


y에 x를 대입하라는 소리인데, 이미 x는 앞서의 문장에 의해서 5가 대입되었으므로 이 명령문은 y=5;라는 명령문이 되는 겁니다. 그래서 두 명령문의 결과 x도 y도 5가 되는 겁니다.

임시변수를 만들어 두 수를 교환하는 프로그램을 만들어봅니다.

따라서 이런 문제를 해결하려면 중간에 임시변수를 하나 만들어서 x의 저장해두는 기술이 필요합니다. 즉 다음과 같이 프로그램을 짜면 제대로 나옵니다.

// test0272.cpp
// 두 수를 바꾸는 예제

#include
#include

int swap(int x, int y);

void main(void)
{
int x=3, y=5; // 정수형 변수 x와 y를 선언했음.
cout[["Main: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력했음.
swap(x,y); // swab라는 함수를 실행시켜 x와 y를 바꿈
}

int swap(int x, int y)
{
int imsi; // 임시로 imsi라는 변수를 하나 만듭니다.
imsi=x; // imsi에 x의 값을 저장합니다.
x=y; // x에 y에 대입합니다.
y=imsi; // y에는 imsi에 저장해둔 x의 값을 대입합니다.
cout[["Swap: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력해봅니다.
return 0; // 정수형 함수이므로 의미 없는 복귀값을 돌려줍니다.
}


**그림: test0272.cpp의 소스파일 내용


이렇게 하면 다음과 같이 결과가 나옵니다.

결과
x=3 y=5
x=5 y=3


**그림: test0272.exe는 두 수가 교체되어 나옵니다.

어떻습니까? x와 y의 값이 바뀌었죠? 보기와 같이 imsi라는 변수를 하나 더 만든 뒤에 여기에 x의 값을 저장해두고, x에 y를 대입합니다. 그리고 y에는 imsi에 저장해둔 x의 값을 대입하면 x와 y의 값이 바뀝니다.

main() 함수와 swap() 함수에서 사용한 변수 x는 각기 다른 함수입니다.

이때 이런 의문을 가져보기 바랍니다. main() 함수에서도 x, y를 변수이름으로 사용했고, swab에서도 x, y를 변수이름으로 사용했습니다. 이럴 경우 main() 함수와 swap() 함수에 사용된 변수는 같은 변수일까요? 아닐 겁니다. 이 의문을 풀어보기 위해서 번지연산자 &를 이용해봅시다.

// test0273.cpp
// swap 함수의 변수 주소를 알아보는 예제

#include
#include

int swap(int x, int y);
void main(void)
{
int x=3,y=5;
cout[["Main: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력했음.
printf("main() 함수에서 &x=%d \n",&x); // x의 번지를 출력했음.
swap(x,y); // swab라는 함수를 실행시켜 x와 y를 바꿈
}

int swap(int x, int y)
{
int imsi; // 임시로 imsi라는 변수를 하나 만듭니다.
imsi=x; // imsi에 x의 값을 저장합니다.
x=y; // x에 y에 대입합니다.
y=imsi; // y에는 imsi에 저장해둔 x의 값을 대입합니다.
cout[["Swap: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력해봅니다.
printf("swap() 함수에서 &x=%d \n",&x); // x의 번지를 출력했음.
return 0; // 정수형 함수이므로 의미 없는 복귀값을 돌려줍니다.
}


**그림: test0273.cpp의 소스파일 내용

이 프로그램을 실행시키면 다음과 같은 결과가 나옵니다.

결과
x=3 y=5
main() 함수에서 &x=9094
swab: x=5 y=3
swap() 함수에서 &x=9088


**그림: test0273.exe를 실행시킨 결과 두 변수 x의 주소가 다릅니다.

결과를 통해 알 수 있는 것처럼 main() 함수에서의 x와 swap() 함수에서의 x는 다릅니다. main() 함수에서의 x는 9094 번지를 배정받았고, swap() 함수에서의 x는 9088 번지를 부여받았습니다. 즉 이름만 같은 뿐 실제로 다른 변수라는 이야기입니다. 이름이 같은데 다른 변수라니? 말이 안된다고 생각하시면 다음과 같이 예제프로그램을 만들어보시기 바랍니다. 그럼 쉽게 이해할 수 있을 겁니다.

// test0274.cpp
// swap 함수의 변수 주소를 알아보는 예제

#include
#include

int swap(int a, int b);
void main(void)
{
int x=3,y=5;
cout[["Main: x="[[x[[" y="[[y[[" \n"; // x와 y의 값을 출력했음.
printf("main() 함수에서 &x=%d \n",&x);
swap(x,y);
}

int swap(int a, int b) // 매개변수 이름으로 x y 대신에 a와 b를 사용했습니다.
{
int imsi;
imsi=a; // imsi에 a의 값을 저장합니다.
a=b; // a에 b에 대입합니다.
b=imsi; // b에는 imsi에 저장해둔 a의 값을 대입합니다.
cout[["Swap: a="[[a[[" b="[[b[[" \n"; // a와 b의 값을 출력해봅니다.
printf("swap() 함수에서 &a=%d \n",&a); // a의 번지를 출력했음.
return 0;
}


**그림: test0274.cpp의 소스파일 내용

이 프로그램의 결과는 다음과 같습니다. 번지수를 잘 보시면 알겠지만 test0273.exe와 같음을 알 수 있습니다.

결과
x=3 y=5
main() 함수에서 &x=9094
swab: a=5 b=3
swap() 함수에서 &a=9088


**그림: test0274.exe의 &x와 test0273.exe의 &a의 실행결과가 같습니다.

이름이 같은 변수라 하더라도 선언된 지역이 다르면 다른 변수입니다.

결과 내용은 이전의 test0273.cpp와 별 다를 바 없지만 swab에서 사용하는 매개변수 이름이 a와 b로 바뀌었다는 점이 다릅니다. 이제 왜 test0273.cpp의 main() 함수의 x와 swap() 함수의 x가 다른 변수인지 알 수 있을 겁니다. 애초부터 main() 함수와 swap() 함수에서 사용하는 변수 이름을 x,y와 a,b로 정해서 사용했다면 두 함수의 변수가 서로 다르다는 것을 쉽게 납득했을 겁니다. 변수의 이름이 다르면서 같은 번지를 쓸 수는 없으니까요. 이름이 다르면 주소가 다르다라는 것은 확실합니다.

그러나 같은 이름이라 하더라도 서로 다른 주소에 살 수는 있는 것처럼 C++에서는 같은 이름을 가진 변수를 여러 곳에서 사용할 수 있습니다. 같은 블럭 내에서는 같은 이름을 가진 함수가 하나만 존재할 수 있지만 블럭이 다르다면 여러 개의 같은 이름이 존재할 수 있습니다. test0273.cpp에서 main() 함수와 swap() 함수에서 사용한 변수 이름을 x y로 정한 이유는 두 함수에서 사용하는 변수가 이름은 같지만 서로 틀리다는 것을 다시 한 번 확인시켜드리기 위함입니다. 그리고 이들 변수가 왜 다른지는 차지하는 번지가 서로 다른 것으로 확인할 수 있었습니다. 즉 같은 이름의 변수라도 번지가 다르면 다른 변수이므로 두 변수가 같은 변수인지 아닌지는 번지를 통해 확인할 수 있습니다. 예를 들어서 김중태라는 사람이 서울에 여러 명 살더라도 주소를 보고서 서로 다른 곳에 사는 사람이라는 것을 알 수 있는 것과 같습니다.

지금까지의 소스파일로는 main() 함수 안의 두 변수 값은 교환되지 않습니다.

swap() 함수의 변수이름을 a와 b로 사용하니 main() 함수의 변수 x y와 다른 변수라는 점을 확실하게 알 수 있습니다. 그럼 이런 생각을 가질 수 있습니다. swap() 함수를 이용해서 a와 b의 값을 바꾸면 main() 함수의 x y 값도 바뀌는 것인가 하는 생각입니다.

결론부터 말씀드리면 안 바뀝니다. 그 까닭은 조금전에 말씀드린대로 main() 함수의 x y와 swap() 함수의 a b가 전혀 다른 변수이기 때문입니다. 즉 swap() 함수의 a b 값은 바뀌지만 이것이 swap() 함수 바깥에 있는 x y의 값을 바꾸지는 못하는 겁니다.

swap() 함수에서 사용하는 매개변수는 x y이므로 바뀌어야 할 것으로 생각하는 분도 있겠지만 x y의 값을 넘겨만 주고 다시 돌려받아서 변수 x와 y에 대입하지 않는 이상 바뀔리가 없습니다. 즉 넘겨주는 값은 x와 y에서 받아서 넘겨주었지만 바뀌는 것은 a와 b의 값일 뿐입니다. 그러므로 x와 y에 다시 새로운 값을 대입하는 과정이 없다면 x와 y의 값은 바뀔리가 없습니다.

**요약: main() 함수와 swap() 함수의 변수는 각기 다른 것이기 때문에 swap() 함수 안의 변수를 교환한다 하더라도 main() 함수 내의 변수는 교환되지 않음에 주의해야 합니다.


3.11.3. 번지에 의한 참조와 값에 의한 참조의 차이


main() 함수 안의 변수도 교환하려면 번지를 이용한 참조를 이용합니다.

그럼 어떻게 해야 x와 y의 값도 바뀔까요? 두 가지 방법이 있습니다. 첫번째는 복귀값을 이용하는 것입니다. 함수의 결과값을 돌려주면서 이 값을 x나 y에 대입하는 방법입니다. 그러나 이 방법은 사용법이 번거롭고 프로그램이 길어지므로 효율적이지 않습니다.

그래서 사용하는 것이 바로 번지를 이용한 참조입니다. 즉 변수의 번지를 불러내고 그 번지의 내용물을 교체함으로써 변수의 내용을 바꾸는 방법입니다. 먼저 다음과 같은 프로그램을 실행시켜 보기 바랍니다.

// test0275.cpp
// 번지 참조를 이용하지 않은 swap 함수의 사용 예제

#include
#include

int swap(int a, int b);
void main(void)
{
int x=3,y=5;
cout[["Main: x="[[x[[" y="[[y[[" \n";
swap(x,y); // swab라는 함수를 실행시켜 x와 y를 바꿈
cout[["Main again: x="[[x[[" y="[[y[[" \n"; // 다시 x와 y의 값을 출력했음.
}

int swap(int a, int b)
{
int imsi;
imsi=a;
a=b;
b=imsi;
cout[["Swap: a="[[a[[" b="[[b[[" \n"; // a와 b의 값을 출력해봅니다.
return 0;
}


**그림: test0275.cpp의 소스파일 내용

이 프로그램을 실행시키면 다음과 같이 결과가 나옵니다.

결과
x=3 y=5
swab: a=5 b=3
Main again: x=3 y=5


**그림: main() 함수 내에서의 두 변수 사이에 교환이 일어나지 않았습니다.

함수에 넣어주는 매개변수를 실매개변수, 함수 본체의 변수는 형식매개변수라 합니다.

즉 swab함수를 실행시키면 swab() 함수 안에서는 a와 b가 바뀝니다. 그러나 이것은 swab() 함수 안에서만 일어나는 변화입니다. 이 변화가 바깥의 변수 x와 y에는 영향을 주지 않습니다.

이처럼 어떤 함수를 실행시킬 때 swab(x, y)라는 문장에 넣어주는 매개변수를 실매개변수 또는 실인자, 실인수라고 합니다. 그리고 swab() 함수 본체에서 동작하는 변수 a와 b는 형식매개변수, 형식인자, 형식인수라고 말합니다. 즉 실매개변수를 매개변수(또는 인수)로 받았다 하더라도 같은 블럭 안의 형식매개변수에만 영향을 미칠 뿐 함수나 블럭 바깥 쪽에는 영향을 미치지 않습니다.

변수에 직접 값을 대입해 참조하는 형식을 '값에 의한 참조'라고 합니다.

이와 같이 변수에 직접 값을 대입해서 참조하는 형식을 '값에 의한 참조'라고 말합니다. 즉 swab() 함수는 x의 값과 y의 값인 3과 5를 대입한 함수입니다. 그리고 대입된 값인 3과 5를 서로 교환하기는 했습니다. 그러나 값에 의한 참조이기 때문에 변수 x와 y의 값을 실제로 교환하지는 못합니다.
그러나 실질적으로 우리가 바꾸고자 하는 것은 x와 y의 내용입니다. 즉 실매개변수인 것입니다. 실매개변수의 내용을 손쉽게 바꾸는 방법은 '값에 의한 참조'가 아니라 '번지(또는 주소)에 의한 참조'를 하는 것입니다. 번지에 의한 참조란 변수의 값을 직접 대입하지 않고, 변수의 번지를 대입한 다음에 간접연산자를 이용하여 내용을 바꾸는 것입니다.

**요약: 실매개변수는 함수에게 넘겨주는 매개변수를 말하고, 형식매개변수는 넘겨받은 실매개변수를 이용하여 함수가 연산할 때 사용하는 매개변수를 말합니다. 그리고 값에 의한 참조란 변수의 값을 직접 넘겨받아서 참조하는 것을 말하고 번지에 의한 참조란 변
수의 번지를 넘겨받아 그 번지에 저장된 내용을 참조하는 방법을 말합니다.

번지에 의한 참조를 통해 두 변수를 교환해봅니다.

번지에 의한 참조의 예를 들어보겠습니다. 다음의 프로그램을 만들어 실행해보기 바랍니다.

// test0276.cpp
// 번지 참조를 이용한 swap 함수의 사용 예제

#include
#include

int swap(int *a, int *b);
void main(void)
{
int x=3,y=5;
cout[["Main: x="[[x[[" y="[[y[[" \n";
swap(&x,&y); // x와 y의 값이 아니라 x와 y의 번지를 넘겨줌에 주의
cout[["Main again: x="[[x[[" y="[[y[[" \n";
}

int swap(int *a, int *b) // 매개변수로 포인터변수를 사용했음에 주의
{
int imsi;
imsi=*a; // imsi에 변수 a에 저장된 번지의 내용 저장합니다.
*a=*b; // a에 저장된 번지의 내용물로 b의 번지에 저장된 내용을 대입
*b=imsi; // imsi에 저장해둔 값을 b에 저장된 번지의 내용물로 대입
cout[["Swap: a="[[a[[" b="[[b[[" \n"; // 넘겨받은 번지가 출력됩니다
return 0;
}


**그림: test0276.cpp의 소스파일 내용


이제 이 프로그램의 결과를 보면 다음과 같습니다.

결과
x=3 y=5
swab: a=0x2304 b=0x2302
Main again: x=5 y=3


**그림: test0276.exe를 실행시킨 결과 main() 함수 안의 두 변수가 교환되었습니다.

어떻습니까? swab() 함수를 실행하자 main() 함수의 x y값이 바뀌었습니다. 이 과정을 자세히 살펴보겠습니다.

// test0276.cpp

// 번지 참조를 이용한 swap 함수의 사용 예제


#include

#include


int swap(int *a, int *b);

void main(void)

{

int x=3,y=5;

cout[["Main: x="[[x[[" y="[[y[[" \n";


이 부분까지는 여러 번 실행한 것이니 설명을 생략하겠습니다. 이 과정까지 x는 3의 값을 가지고 있습니다.

swap(&x,&y);


이 부분이 매우 중요합니다. swab() 함수로 실매개변수를 넘겨주면서 x와 y의 값을 넘겨주는 것이 아니라 번지를 넘겨준다는 점을 잘 살펴보셔야 합니다. 즉 매개변수로 x라고만 쓰면 x의 값인 3을 넘겨주는 것입니다. 그래서 x를 넘겨주면 값에 의한 참조라고 합니다. 그러나 &x를 넘겨주면 x의 번지를 넘겨주는 것이 됩니다. 그래서 번지에 의한 참조라고 합니다. x의 번지는 0x2304를 배정받았습니다. 그리고 y는 0x2302를 배정받았습니다.

0x는 16진수임을 나타내는 기호입니다.

여기에서 번지를 표기하는 방식이 0x2304로 표기되었는데 이는 16진수로 표기하기 때문입니다. 그러니까 0x2304의 0x는 16진수임을 나타내는 기호입니다. 뒤의 숫자가 중요한 것이므로 신경쓰지 말고 다음으로 넘어가겠습니다.
하여간 이렇게 하여 swab() 함수에 넘겨지는 값은 3과 5가 아닌 0x2304와 0x2302를 넘겨받는 것입니다.

cout[["Main again: x="[[x[[" y="[[y[[" \n";

}


main() 함수의 끝부분입니다. cout에 대해서는 더 이상 설명할 것이 없으니 다음 부분을 설명하고 다시 이 부분을 설명하겠습니다.

번지를 넘겨받기 위해서 매개변수로 포인터 변수를 사용합니다.


int swap(int *a, int *b)


역시 주의해서 볼 부분은 매개변수의 자료형입니다. 이전까지는 int a라고 사용했기 때문에 일반적인 정수형 변수를 사용했습니다. 그러나 이번에는 포인터변수를 사용합니다. 포인터를 사용해야만 번지를 인수로 넘겨받을 수 있기 때문입니다. 그래서 변수 *a와 *b는 포인터를 가질 수 있는 변수가 된 것입니다.

{

int imsi;

imsi=*a;


변수 imsi에 a의 값을 대입한 것이 아니라 a에 저장된 번지의 내용물을 대입했다는 점을 유의해서 보십시요. 만약 imsi=a라고 했다면 a에 저장된 내용인 x의 번지 즉, 0x2304가 대입됩니다. 그러나 imsi=*a라고 했으므로 imsi에는 0x2304 번지에 저장된 내용인 3이 대입되는 것입니다. *a는 a에 저장된 번지의 내용물을 돌려주는 간접연산자라고 말씀드렸습니다.

*a=*b;


이 부분도 주의해서 봐야 합니다. 이제 *b가 뜻하는 것은 아실 겁니다. b에 저장된 번지를 참조하여 그 번지의 내용물을 a의 번지 내용물로 대입하라는 뜻입니다. 따라서 b에 저장된 y의 번지인 0x2302 번지의 내용물을 돌려줍니다. 즉 5를 a의 번지 안에 저장해줍니다.

신경을 써야 할 부분은 대입연산자 = 앞에 쓴 *a라는 기호입니다. *a는 *b와 마찬가지로 a에 저장된 번지의 내용물 즉 x의 값이 됩니다. 그러므로 *a=*b;는 b에 저장된 번지의 값 즉 y의 값을 x의 값으로 대입하라는 뜻이 됩니다.

만약 *a에서 * 기호를 생략하고 a=*b라고만 쓰면 포인터변수 a에 *b가 대입되는 결과가 발생합니다. 즉 a=5가 되는 것이고 졸지에 a는 5라는 번지수를 저장하는 것입니다. 그렇기 때문에 *a=*b;라는 문장을 주의 깊게 살펴보고 어떤 연산이 이루어지고 있는지 잘 알아두어야 합니다.

*b=imsi;


이제 *b는 b에 저장된 번지에 저장된 값이라는 사실을 알 수 있습니다. 그러므로 이번에는 imsi에 저장된 값인 3을 b에 저장된 번지인 y의 값으로 대입하는 것입니다. 이렇게 해서 x의 값은 5가 되었고 y의 값은 3이 되는 것입니다. 이것이 바로 번지연산자와 포인터를 이용하는 방법입니다. 즉 번지에 의한 참조를 이용한 연산방법의 하나입니다.

위의 문장은 *b=3이라는 문장과 같습니다. 따라서 *n=999라고 하는 명령문은 포인터변수 n에 저장된 어떤 변수의 번지 안에 999를 저장하라는 뜻입니다. 대입연산자 왼쪽에 *이 오는 것이 조금은 이상하게 느껴지지만 간접연산자의 의미를 제대로 파악하고 있다면 *b=3이라는 문장의 뜻을 바로 알 수 있을 겁니다.

cout[["Swap: a="[[a[[" b="[[b[[" \n";

return 0;

}


swab() 함수의 마지막 명령문은 a와 b의 값을 출력하는 것입니다. 이전의 프로그램에서는 x와 y의 값이 출력되었지만 이제는 x의 번지와 y의 번지가 출력될 겁니다.
다시 main() 함수로 돌아가서 printf() 함수를 실행시키면 x와 y의 값이 바뀌었음을 알 수 있습니다. 번지에 든 내용을 직접 바꾸었기 때문에 x와 y의 값이 바뀐 겁니다.

지금까지의 과정을 이해하신다면 번지연산자와 간접연산자를 거의 이해하신 것과 다름 없습니다. 더 이상의 설명을 한다면 오히려 머리 속을 혼란스럽게 만들 것 같기에 포인터에 대한 설명은 이 정도로 끝내겠습니다.

포인터의 포인터를 이용한 예제를 만들어봅니다.

그러나 여러분들의 이해정도를 확인해보기 위해서 포인터의 포인터에 대한 예제 하나를 더 만들어보겠습니다. 아래의 프로그램을 실행해보기 바랍니다. 단 test028.c를 실행하기 전에 화면에 어떤 결과가 출력되어 나올지 예상해보기 바랍니다. 특히 마지막 세번째 줄이 어떻게 나올지 생각해보시기 바랍니다.

/* test028.c */
/* 포인터의 포인터를 사용하는 예제 */

#include
void main(void)
{
int x=3, *p, **pt;
p=&x;
pt=&p;
printf("x=%d p=%d pt=%d \n",x,p,pt);
printf("&x=%d &p=%d &pt=%d \n",&x,&p,&pt);
printf("x=%d *p=%d **pt=%d \n",x,*p,**pt);
}


**그림: test028.c의 소스파일 내용

test028.c를 실행해보면 다음과 같은 결과가 나옵니다.

결과
x=3 p=8692 pt=8690
&x=8692 &p=8690 &pt=8688
x=3 *p=3 **pt=3


**그림: test028.exe를 실행시킨 화면

어떻습니까? 예상한 내용과 같습니까? 만약 예상한 내용과 결과가 같다면 포인터에 대해서 훌륭하게 이해하고 있는 상태입니다. 특히 세번째 printf() 함수문의 결과를 정확하게 예측했다면 번지연산자와 간접연산자, 포인터가 무엇인지 확실하게 이해하신 셈입니다.
예제 프로그램을 하나씩 살펴보겠습니다.

/* test028.c */

/* 포인터의 포인터를 사용하는 예제 */


#include

void main(void)

{

int x=3, *p, **pt;


*p가 포인터변수 p를 선언했다는 뜻임은 알 겁니다. 그렇다면 **pt는 무엇이겠습니까? *pt라는 포인터 변수에 대한 포인터를 선언한 것입니다. 즉 포인터의 포인터 변수가 되는 것입니다. *pt는 포인터(즉 번지)를 값으로 가지는 변수입니다. 즉 포인터 변수입니다. 그러므로 *를 하나 더 붙이면 포인터변수의 포인터(즉 번지)를 값으로 가지는 변수를 선언하겠다는 뜻이 됩니다. 그래서 포인터의 포인터 변수가 선언되는 것입니다.

p=&x;


포인터 변수 p에 x의 번지값을 대입시켰습니다. x의 번지값은 8692이므로 p는 8692라는 숫자를 값으로 가집니다.

pt=&p;


번지연산자를 사용했으니 &p는 포인터변수 p의 번지를 가리킵니다. 변수 p의 번지는 8690번지입니다. pt에는 변수 p의 번지인 8690이 대입됩니다.

printf("x=%d p=%d pt=%d \n",x,p,pt);


그래서 변수 x의 값은 3이고 p는 x의 번지인 8692가, pt는 p의 번지인 8690이 출력되어 나옵니다.

printf("&x=%d &p=%d &pt=%d \n",&x,&p,&pt);


그리고 세 변수의 각각의 번지는 8692, 8690, 8688입니다. 즉 pt의 번지는 8688로 배정받은 것입니다. 그래서 '&x=8692 &p=8690 &pt=8688'이 출력되어 나옵니다.

printf("x=%d *p=%d **pt=%d \n",x,*p,**pt);

}


이 부분이 가장 중요한 부분입니다. x의 값이 3이라는 것이야 당연한 일이고 *p의 가격이 문제겠죠. *p는 p에 저장된 x의 번지 즉 8692번지에 저장된 값을 나타내는 간접연산자입니다. 즉 p가 가리키는 포인터(이는 곳 x의 번지요, 8692번지입니다.)에 저장된 값을 알려주는 연산을 합니다. 그래서 8692번지에 저장된 자료 3을 화면에 출력합니다.

마지막으로 **pt는 두 번의 과정을 거쳐서 결과를 화면에 출력합니다. 먼저 처음 붙은 *에 의해서 pt에 저장된 번지의 내용을 참고합니다. pt에 저장된 번지는 p의 번지인 8690입니다. 그리고 8690번지에는 x의 번지인 8692가 저장되어 있습니다. 따라서 *pt만 하면 8692가 나옵니다. 그런데 여기에 다시 한 번 간접연산자 *가 붙었습니다. 따라서 8692 번지에 저장된 값을 다시 계산해서 보여주어야 합니다. 8692번지에는 3이 저장되어 있습니다. 그래서 **pt는 3이 나오는 것입니다.

그렇다면 *pt는 무엇이 되겠습니까? 바로 앞줄에서 설명을 했지만 pt에 저장된 p번지의 값 즉, 8692가 나옵니다. 즉 pt=8690(p의 번지)이가 되고 *pt=8692(p의 번지인 8690번지에 저장된 내용. 즉 x의 번지)가 되고 **pt=3(pt에 저장된 번지의 값 즉, p의 번지인 8690에 저장된 번지인 x의 번지에 저장된 값, 결국 변수 x의 값)이 되는 것입니다.

이제 여기까지 잘 따라오셨다면 포인터는 물론 포인터의 포인터도 충분히 이해하신 셈입니다. 이제는 포인터의 포인터 뿐만 아니라 '포인터의 포인터의 포인터'와 '포인터의 포인터의 포인터의 포인터'도 어떻게 되는 것인지 알 수 있을 겁니다.

뒷부분에는 포인터에 관한 내용을 조금 정리했습니다. 그러나 지금까지 말씀드린 내용만 이해하고 있다면 뒷부분은 대충 넘어가셔도 큰 지장이 없을 겁니다. 오히려 너무 많은 것을 머리 속에 담거나 '포인터의 포인터의 포인터'와 같이 복잡하게 포인터를 다루는 것이 해가 될 수 있습니다. 가능하면 포인터 하나로 끝내시기 바랍니다. 그러다가 포인터 하나만으로는 부족하다면 포인터의 포인터까지 사용하셔도 좋습니다. 그러나 그 이상 포인터를 중첩해서 사용하지 말기 바랍니다. 포인터를 중첩해서 사용하다가는 머리에 쥐가 날지도 모르니까요. 포인터의 포인터를 설명한 것은 포인터가 무엇이라는 것을 좀더 이해할 수 있도록 도와주기 위함이지 포인터를 중첩해서 사용하는 것이 좋다는 뜻은 아닙니다. 가능하면 포인터나 배열은 일차원적으로 사용하는 것이 좋습니다. 다차원 배열이나 중첩된 포인터는 프로그램을 이해하는데 도움이 되지 않습니다.

**요약: 포인터의 포인터변수란 포인터를 값으로 가지는 포인터변수의 번지를 값으로 가질 수 있는 변수를 말합니다. 즉 포인터의 포인터는 번지를 값으로 가지는 변수의 번지를 가리키는 말입니다.


3.11.4. 포인터에 대한 내용들


포인터에 관한 규칙은 복잡합니다. 간단하게 요약 정리하면 다음과 같습니다.

POINTER형 변수와 정수형 변수끼리는 연산할 수 있으며 이 경우 번지값을 변화시킬 수 있습니다.

포인터는 곧 번지(또는 주소)를 뜻합니다. 그러므로 번지수(즉 번지값)를 저장할 수 있는 변수는 포인터변수가 됩니다. 이때 포인터변수에 저장하는 번지는 선두번지가 됩니다. 즉 문자열을 저장한다면 실질적으로는 문자열 상수가 저장된 곳의 선두번지를 대입시키는 것과 같습니다.

보기

char *ptr

ptr = "Kim JoongTae"


보기는 포인터 변수 ptr을 선언하고 대입식 'ptr = "Kim JoongTae"'을 통해서 문자열 상수가 저장되어 있는 곳의 선두번지를 포인터 번수 ptr에 대입했습니다. 즉 'ptr = "Kim JoongTae"'는 ptr이라는 변수에 "Kim JoongTae"를 대입하라는 뜻이 아니라, ptr이라는 포인터 변수에 "Kim JoongTae"가 저장된 곳의 번지를 대입하라는 뜻입니다.

배열명 역시 그 배열의 선두번지를 가리키는 포인터입니다. 즉 배열명 자체는 포인터변수처럼 메모리를 차지하는 것이 않는다는 이야기입니다.

포인터 변수는 2 바이트의 크기를 가집니다. 즉 정수형과 같은 크기입니다.

포인터에 포인터를 더할 수는 없지만 포인터에 포인터를 뺄수는 있습니다. 단 두 포인터는 동일한 자료형을 가져야 함이 물론이며 그 값은 정수여야 합니다. 이 때 두 포인터 사이의 상대 위치는 상대위치에 sizeof(*px)를 곱하면 상대 번지가 됩니다. 그리고 두 포인터의 차를 구하고자 한다면 동일한 자료형인 동일한 배열 안의 서로 다른 두 배열 배열요소를 가리키고 있어야 한다는 점입니다.

단항 * 연산자의 피연산자는 언제나 포인터(즉 번지값)이어야 합니다. 알다시피 간접연산자 *는 그 포인터가 가리키고 있는 번지에 저장되어 있는 내용을 읽어내는 일을 하는데 이 일을 참조한다고 표현합니다.

아래의 명령문은 모두 같은 의미를 가진 명령문입니다. 왜 같은지 잘 생각해보시기 바랍니다.

보기

x[i][j][k]

*(x[i][j] + k)

(*(x + i)[j][k]

(*(*(x + i) + j))[k]

*(*(*(x + i) + j) + k)


간접지정 연산자인 *은 간접지정을 나타내며 피연산자가 가리키고 있는 대상체나 함수를 구한다고 말씀드렸습니다. 그렇기 때문에 다차원배열을 포인터로 표시할 경우에는 자동적으로 다차원배열의 배열요소를 포인터로 변환시켜 줍니다.
그래서 아래의 수식은 모두 같은 것입니다.

보기

*(*(y + i) + j)

*(y[i] + j)

(*(y + i))[j]

y[i][j]

i[y][j]

j[y[i]]

j[i[y]]


이중에서도 보통 사용할 때는 *(*(y + i) + j)나 y[i][j]를 많이 사용합니다. 지금까지 설명한 내용은 이해하지 않으셔도 됩니다.

포인터에 직접 쓸수 없는 연산자는 다음과 같습니다.

기본 연산자 .
단항 연산자 - ~
이항 연산자 * / % [[ ]] & ^ |
전처리기 연산자 # ## difine

비교연산자인 == != < <= > >= 등은 사용할 수 있으나 포인터의 자료형이 같아야 함은 물론입니다. 만약 자료형이 다르다면 캐스트연산자를 이용해서 자료형을 맞추어주어야 합니다.

*p + 1 은 *(p + 1)과 연산순서가 다릅니다. 어떻게 다를까요? 증감연산자를 이용할 때의 연산순서는 다음과 같습니다.

보기

*p++ // *p의 값을 먼저 참조한 뒤에 p의 값을 1 증가시킵니다.

(*p)++ // *p의 값을 참조한 뒤에 *p의 값 자체를 1 증가시킵니다.

*++p // p를 1만큼 증가시킨 후에 *p의 값을 구합니다.

++*p // *p의 값 자체를 1만큼 증가 시킵니다.


이를 다시 풀어서 설명하겠습니다. *p++는 *(p++)와 같은 수식입니다. 즉 *p값을 구한 다음에 p를 1만큼 증가시켜 놓습니다.
(*p)++는 먼저 *p값을 참조합니다. 그리고 괄호가 *p 전체를 싸고 있으므로 *p 자체를 1만큼 증가시킵니다. 따라서 p는 증가되지 않습니다.
*++p는 연산순위에 따라서 *(++p)와 동일합니다. 따라서 먼저 p를 1만큼 증가시킨 다음에 *p를 구합니다. 따라서 처음 p에 저장된 변수 다음의 번지에 저장된 내용을 참조하게 되는 셈입니다.
++*p는 연산순위에 따라서 ++(*p)와 동일하다. *p의 값을 먼저 구한 다음에 이 값에 1을 더해주는 것입니다. 따라서 px는 증가하지 않습니다.

당연한 이야기지만 배열도 변수처럼 번지를 가지고 있습니다. 그러므로 배열의 선두번지를 포인터에게 넘겨줌으로써 배열을 손쉽게 다룰 수 있습니다.

만약 배열 int x[10];이라는 것을 선언한 뒤에 포인터변수 int *p;를 선언했다고 합시다. 그리고 p=&x[0]을 넘겨주면, 배열 x[숫자]에 해당하는 요소는 *(p+숫자)로서 얻을 수 있습니다. 즉 1111번지부터 배열 x의 첫번째 요소 x[0]이 시작된다면 1112번지에는 배열 x의 두번째 요소인 x[1]이 저장되어 있을 것이고 1113번지에는 x[2]가 저장되어 있을 겁니다.
따라서 포인터 변수 p에 1111번지가 저장되어 있다면 *p는 1111번지에 저장된 값을 구할 수 있고 *(p+1)은 *(1111+1)이 되므로 *(1112)가 되어 1112번지에 저장된 내용을 참조하여 돌려줍니다. 또한 *(p+2)은 *(1111+2)가 되므로 *(1113)가 되어 1113번지에 저장된 내용을 참조하여 돌려줍니다. 즉 배열 x의 n번째 요소는 *(p+n)으로 참조할 수 있습니다.

이처럼 실제로 배열이름은 그 배열의 선두 번지를 가리키는 포인터 상수인 것입니다. 이때 배열보다는 포인터를 이용한 계산이 좀더 빠릅니다. 그 까닭은 배열을 이용할 경우에는 배열의 선두번지 계산하고 첨자를 계산하여 그 번지에서 몇 번째 뒤의 요소인가를 계산해서 최종번지를 확인합니다. 그리고 난 뒤에 최종 확인된 번지를 참조하여 저장된 내용을 꺼내옵니다. 하지만 포인터는 그 자체가 번지이므로 바로 메모리의 번지를 참조할 수 있고 바로 저장된 내용을 꺼내오기 때문에 좀더 빠른 겁니다. 그래서 같은 결과를 얻는 프로그램이라고 하더라도 포인터를 이용하는 것이 배열을 이용하는 것보다 더 빠른 결과를 얻을 수 있고 이런 이유로 배열보다는 포인터를 자주 사용하는 것입니다.

이때 주의할 점은 포인터변수의 자료형에 따라서 번지수를 건너뛰는 폭이 다른 점입니다. 즉 문자열이나 문자형 배열일 경우에는 1바이트씩 건네 뛰지만 정수형이라면 2바이트씩 건너띌 겁니다. 정수형은 두 개의 번지를 차지하면서 저장되기 때문입니다. 따라서 실제로 p+n이 가리키는 번지는 p의 번지에 n바이트를 더한 수가 아니라 p + (n* sizeof(*p)바이트)가 되는 겁니다.

배열을 이야기할 때 다차원배열도 한 줄로 풀어서 저장한다고 말씀드린 적이 있습니다. 이말은 즉 C는 일차원 배열만을 지원한다는 이야기입니다. 실질적으로 다차원 배열은 C에는 전혀 존재하지 않습니다. 그리고 배열크기도 배열을 정의할 때 어떠한 방법으로든 반드시 미리 알려주어야 합니다.

이런 까닭에 C에서의 고유한 배열연산은 2가지 밖에 없습니다. 배열의 크기를 결정하는 연산과 배열의 요소를 가리키고 있는 포인터를 얻는 것이 전부입니다. 그외의 연산은 실실적으로는 모두 포인터 연산을 통하여 이루어지는 것입니다. 심지어는 배열 연산처럼 보이는 첨자 연산도 사실은 포인터 연산에 해당하는 것입니다. 특히 배열 전체를 통째로 함수에 전달하는 경우도 포인터를 이용해서 가능합니다. 배열 자체로는 함수에 배열 전체를 전달할 수 있는 방법이 없기 때문입니다.

포인터 변수(보통은 그냥 포인터라고 말합니다.)를 선언하는 방법의 몇 가지 에제를 적어놓았으니 참고하기 바랍니다.

int *p; // 정수형 포인터 변수 p를 선언합니다.

int *p[10]; // 정수형 포인터를 원소로 하는 배열을 선언합니다.

char *c[10]; // 문자형 포인터를 원소로 하는 배열 c를 선언합니다.

int *p(); // 정수형에 대한 포인터를 함수값으로 돌려주는 함수 p를 선언합니다.

int **p; // 정수형 포인터에 대한 포인터를 선언합니다.


문자 배열과 포인터 배열의 초기화를 혼동하는 경우가 많은데 다음과 같이 구별할 수 있습니다.

보기

char s[] = "ABCDE"; // 문자배열 선언과 초기화

char *p = "ABCDE"; // 포인터 배열 선언과 초기화


즉 첫번째 선언문은 배열의 크기가 6인 일차원 문자 배열 s를 정의한 것입니다. 이때 글자는 다섯 글자인데 왜 크기가 6인가 의문 가지는 분도 있을 겁니다. 글자는 다섯 글자이지만 문자열임을 알려주는 널문자를 넣어야 하기 때문에 한 글자가 더 추가되는 겁니다. 그래서 글자는 다섯 글자이지만 6글자 크기를 가지는 문자 배열 s가 선언되는 것입니다.
두번째 문장은 문자열 상수 "ABCDE"를 가리키는 포인터 변수 p를 선언하고 초기화한 것입니다.

널 포인터(null pointer)는 포인터가 정수 수치로 0의 값을 가지고 있으면 그 포인터는 개념상 아무 것도 가리키지 않는 포인터, 즉 널 포인터라고 합니다. 달리 말하면 널 종료문자열을 가리키는 포인터, 널 문자열의 번지를 가지고 있는 포인터라고 말할 수 있습니다. 보통은 널 포인터의 확실한 의미를 부여하기 위하여 NULL 이라는 매크로 상수를 많이 이용합니다. 널 포인터는 주로 포인터를 리턴하는 함수에서 입출력 에러와 같은 모종의 에러가 발생했음을 알려주기 위하여 널 포인터를 리턴합니다. 또 매개변수의 갯수가 가변인 함수에서 마지막 매개변수를 나타낼 때 사용합니다.

[[연습문제]]
다음의 내용대로 실행되도록 소스파일을 만들고 ex15.cpp로 저장합니다.
'Hello, World!'라는 문장을 문자형 배열 text에 저장하고, 문자형 포인터 변수 str을 선언합니다. text 배열에 저장된 문장을 str 변수를 이용하여 화면에 보여주도록 프로그램을 만듭니다.

[[연습문제 정답]]

// EX15.CPP -- 문자형 포인터 변수를 이용하여 문자열 보여주기

#include
#include

void main(void)
{
unsigned char text[]={"Hello, World!"}; //문자형 배열을 선언하고 문장을 대입함
unsigned char *str; //문자형 포인터 변수 str을 선언함
str=text; //text 배열의 선두번지를 str에 넘겨줌
cout[[str[["\n"; //str의 내용을 출력함. 즉 text 배열의 내용을
} //출력해주는 것과 같음




첫줄로(go top, go first line) 문화원첫화면으로(go dal site home) 강좌차림으로(go Chair) 사이트맵으로(go sitemap)




total chairpost