주소
C++에는 메모리의 위치를 직접 나타내는 '포인터'라는 기능이 준비되어 있다.
우선, 메모리 상의 위치를 직접 나타내는 주소(address)에 대해 학습하기로 한다.
C++의 주소란 메모리의 장소를 직접 나타내기 위해 사용되는 메모리 상의 주소인 것이다. 컴퓨터 내의 주소로 추측되는 16진수의 수치 등을 사용해서 0x1000, 0x1004, ...라는 수치로 나타내는 것이 대부분이다.
변수의 주소
변수의 값이 저장되어 있는 메모리의 주소를 알려면 주소 연산자(address operator)의 &를 사용한다.
[구문 : 주소 연산자]
&변수명
그러면 & 연산자를 사용해서 주소를 실제로 출력해 보도록 한다.
예제) 주소를 출력한다
정수형 변수를 선언한 후 주소를 출력하는 코드를 작성해 보도록 한다.
#include <iostream>
using namespace std;
int main()
{
int a;
a = 5;
cout << "변수의 값은 " << a << "입니다.\n"; //...①
cout << "변수 a의 주소는 " << &a << "입니다.\n"; //...②
return 0;
}
① 여기서는 지금까지와 같이 변수 a의 값인 5가 출력되어 있다.
② 이 코드에서는 주소 연산자를 사용한 '&a'를 출력한다. 이것으로 변수 a의 주소를 출력할 수 있다. '&a'에 의해 변수 a의 값이 메모리 상의 '어느 위치'에 기억되어 있는가를 알 수 있다.
&a의 값을 보면 '0077FAE0'로 출력되어 있는 것을 알 수 있다. 이 컴퓨터의 경우 변수 a의 값은 그림과 같은 '0077FAE0'라는 메모리의 위치에 저장되어 있는 것을 알 수 있다.
또 여기서는 '0077FAE0'라는 값이 출력되었지만 주소의 값은 사용하는 환경과 프로그램의 실행 상황에 따라 바뀐다. 다른 컴퓨터 혹은 다시 컴파일하면 '0077FAE0'이 아니고, 다른 값이 출력된다.
그러나 변수의 주소가 실제로 몇인가 하는 것은 별로 의미가 없다. 여기서 중요한 것은 '주소를 사용해서 메모리 상의 '위치'를 나타낼 수 있다'라는 것이다.
포인터
변수의 값이 저장되어 있는 메모리의 장소(주소)를 알 수 있었기 때문에 포인터를 이용해 보는 것으로 한다. 우선, 주소를 사용하는 코드를 기술하기 위해 주소를 지정하는 특수한 변수를 학습하는 것으로 한다. 이 변수는 포인터(pointer)라고 한다. 포인터의 사용 방법은 원칙적으로 지금까지의 변수와 똑같다. 이전의 변수와 같이 포인터도 사용하기 전에 'pA' 등이라는 이름을 정해서 선언한다. 단, 포인터를 나타내는 변수에는 반드시 * 기호를 붙여서 선언해야 한다. 다음의 포인터의 선언 방법을 살펴보도록 한다.
[구문 : 포인터의 선언]
형명 * 포인터명;
즉, 포인터의 선언은 다음과 같이 수행된다.
int *pA; //포인터 pA를 선언한다.
이 문장은 int형의 변수의 주소를 저장할 수 있는 포인터 pA라는 것을 선언한 것이다. 이것을 int형의 포인터 pA라고 한다.
포인터에는 원칙상 지정한 형 이외의 값의 주소는 저장할 수 없다. 즉, pA에는 int형 이외의 값의 주소를 저장할 수는 없다. 형명을 저장할 때에는 주의해야 한다.
그러면 포인터 pA에 주소를 저장해 보도록 한다.
예제) 포인터에 주소 저장
포인터 pA를 선언하고 정수형 변수의 주소를 저장한 후 변수의 주소와 내용을 출력하는 코드를 작성해 보도록 한다.
#include <iostream>
using namespace std;
int main()
{
int a;
int* pA; //변수 pA를 포인터로 선언한다.
a = 5;
pA = &a; //변수 a의 주소를 pA에 저장한다.
cout << "변수 a의 값은 " << a << "입니다.\n";
cout << "변수 a의 주소는 " << &a << "입니다.\n";
cout << "포인터 pA의 값은 " << pA << "입니다.\n";
return 0;
}
조금 전에 본 바와 같이 &a는 int형의 변수 a의 주소를 나타낸다. 그래서 이 &a라는 값을 포인터 pA에 대입할 수 있다. [pA = &a;] 즉, 이 대입에 의해 포인터 pA에 변수 a의 주소를 저장하는 작업이 가능했다. 이 때문에 포인터 pA의 값으로 출력되는 값은 변수 a의 주소이고 &a의 값과 같게 된다. [pA = &a;]와 같은 대입에 의해 변수 a와 포인터 pA 사이에는 여러 가지 관계가 생겼다고 볼 수 있다. 이 '관계'를 pA는 변수 a를 가리킨다라고 한다. 포인터 pA에는 변수 a의 메모리 상의 위치(주소)가 들어 있다. pA(의 값)가 변수 a(의 장소)를 가리킨다고 생각하면 된다.
포인터로부터 변수의 값 알기
사실은 포인터에 변수의 주소가 저장된다면 그 포인터로부터 역으로 추적해 가면 원래 변수의 값을 알 수 있다. 포인터로부터 변수의 값을 찾으려면 포인터에 대해서 * 연산자를 사용한다. * 연산자는 간접 참조 연산자(indirection operator)라고 한다.
[구문 : 간접 참조 연산자]
*포인터명
이 연산자를 사용하면 그 포인터에 저장되어 있는 주소에 대응하는 변수의 값을 알 수 있다. 포인터 pA에 변수 a의 주소가 저장되어 있는 경우에는 *pA로 기술하면 변수 a의 값을 '간접적'으로 알 수 있다. 즉, *pA의 의미는 pA가 가리키는 주소의 내용을 나타낸다. 그러면 다음의 코드로 확인해 보도록 한다.
예제) 간접 참조
포인터 pA를 선언하고 정수형 변수의 주소를 저장한 후 포인터 변수의 내용과 포인터가 가리키는 값(간접 참조)을 출력하는 코드를 작성해 보도록 한다.
#include <iostream>
using namespace std;
int main()
{
int a;
int* pA;
a = 5;
pA = &a;
/*이 코드에서도 최초에 포인터 pA에 변수 a의 주소를 대입한다.
즉, 포인터 pA가 변수 a를 가리키도록 하는 것이다.*/
cout << "변수 a의 값은 " << a << "입니다.\n";
cout << "변수 a의 주소는 " << &a << "입니다.\n";
cout << "포인터 pA의 값은 " << pA << "입니다.\n";
cout << "*pA의 값은 " << *pA << "입니다.\n";
/*그 뒤에 pA에 *연산자를 사용하면, 변수 a의 값을 알 수 있다.
pA에 *를 붙인 *pA는 변수 a와 같다는 것을 주목하기 바란다.*/
return 0;
}
즉,
*pA ↔ a
(같다)
로 된다. 확실하게 *pA의 값을 출력해 보면, 변수 a의 값인 5가 출력되는 것을 알 수 있다.
포인터에 관한 정리
지금까지 나온 것으로부터 순서대로 정리해 보도록 한다.
a | 변수 a |
&a | 변수 a의 주소 |
여기서 'pA = &a'로 대입한다. 즉, 포인터 pA가 변수 a를 가리키도록 한다. 그러면 pA는 다음과 같이 된다.
pA | 변수 a의 주소를 저장하는 포인터 |
*pA | 변수 a의 주소를 저장하는 포인터가 가리키는 변수 ▶ 변수 a |
여기가 가장 중요한 부분이다. 'pA = &a;'라는 대입을 하지 않으면 후반의 2개는 성립되지 않는다는 것에 주의하도록 한다.
포인터에 다른 주소를 대입
예제) 포인터 변수의 값 변경
지금까지 설명한 것처럼 포인터는 주소를 저장하는 변수로 되어 있다. 그래서 이번에는 변수 a가 아니고 다른 변수 b의 주소를 pA에 저장하도록 포인터의 값을 변경해 보도록 한다.
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
int* pA;
pA = &a; //...①
cout << "변수 a의 값은 " << a << "이다.\n";
cout << "포인터 pA의 값은 " << pA << "이다.\n";
cout << "*pA의 값은" << *pA << "이다.\n"; //...②
pA = &b; //...③
cout << "변수 b의 값은 " << b << "이다.\n";
cout << "포인터 pA의 값은 " << pA << "이다.\n";
cout << "*pA의 값은" << *pA << "이다.\n"; //...④
return 0;
}
① 우선 포인터 pA에 변수 a의 주소를 대입한다.
② 이 때문에 *pA의 값을 출력해 보면 변수 a의 값과 같이 10이 출력된다.
③ 다음에 pA = &b;를 대입해서 포인터의 값을 변경해 보았다. 이번에는 포인터 pA에 변수 b의 주소가 저장된 것이다.
④ 한 번 더 *pA를 출력해 보면, 이번에는 변수 b의 값이 20이 출력되어 있다는 것을 알 수 있다. 즉, 포인터 pA가 변수 b를 가리키도록 변경한 것이다. 이와 같이 포인터의 값을 변경하여 다른 변수를 가리키도록 할 수 있다.
또한 포인터에는 저장한 형의 값의 주소 이외는 원칙적으로 저장할 수 없다. pA의 경우에는 int형 이외의 값의 주소를 저장할 수 없기 때문에 주의해야 한다.
포인터에 대입을 하지 않으면?
포인터를 취급하는데 주의하지 않으면 안 되는 것이 있다. 'pA = &a;'라는 대입을 하지 않고 *pA를 출력한 경우를 생각해 보도록 한다.
//이 코드는 오류
int a = 10;
int* pA;
cout << "포인터 pA의 값은 " << pA << "이다.\n";
cout << "*pA의 값은" << *pA << "이다.\n";
여기서는 'pA = &a;'라는 문장이 없다. 즉, 포인터 pA에는 어떤 주소도 저장되어 있지 않다. 포인터가 아무것도 가리키지 않는 상태로 되어 있다는 것이다. 이것으로는 *pA라고 기술해도 의미가 있는 값을 얻을 수 없다.
이와 같이 어디를 가리키는지 알 수 없는 포인터를 사용하면 프로그램 실행 시 생각하지 못하는 오류가 발생한다.
포인터는 가능한 다음과 같이 초기화를 수행하도록 기술하는 것이 좋다.
int a;
int *pA; = &a;
이와 같이 선언 시에 변수의 주소를 초기화하도록 하면 무심코 포인터에 값을 대입하는 것을 잊어버리고 포인터가 아무것도 가리키지 않는 상태가 되지 않을 수 있다.
포인터를 사용해서 변수 변경
사실은 포인터를 사용하면 포인터가 가리키는 변수 그 자체의 값을 변경할 수 있도록 되어 있다.
다음의 코드를 살펴보도록 한다.
예제) 포인터를 사용해서 변수의 값 변경
포인터 변수를 이용하여 값을 변경하는 코드를 작성해 보도록 한다.
#include <iostream>
using namespace std;
int main()
{
int a;
int* pA;
a = 5;
pA = &a;
cout << "변수 a의 값은 " << a << "입니다.\n";
*pA = 50;
cout << "*pA에 50을 대입하였습니다.\n";
cout << "변수 a의 값은 " << a << "입니다.\n";
return 0;
}
이 코드에서는 변수 a의 값이 도중에 변경되어 있다. 그러나 이것은 변수를 이용해 'a = 50;'이라는 대입문으로 기술해서 변수 a를 변경한 것은 아니다. 가리킬 때 '*pA = 50;'이라는 코드를 기술한 것이다. 이것은 포인터 pA가 a를 가리킬 때 *pA와 a가 같다는 의미를 이용하는 것이다. 즉,
*pA ↔ a
(같다)
이므로
*pA = 50; ↔ a = 50;
(같다)
라고 한다.
'a = 50;'과 '*pA = 50;'의 양쪽 모두 변수 a에 값을 저장하는 처리를 수행하는 것이다.
그러나 변수 a의 값을 변경할 목적이라면 *pA에 값을 대입하는 것보다 a에 직접 대입하는 쪽이 좀 더 간단하다.
왜 이런 번거로운 방법이 준비되어 있는지는 다음절에서 살펴보기로 한다.
출처: 박흥복, 서정희. 2015. C++ 프로그래밍 (초보자를 위한). 문운당