포인터란 무엇이냐 하면... 포인터는 변수이다. (변수가 무엇인지는 알꺼라고 본다.)
아주 중요한 사실이니 꼭 기억하기 바란다.
그럼 포인터가 변수이긴 한데 과연 무엇을 저장하는 변수란 말인가??
int로 정의한 변수는 정수를 저장하는 변수이고,
double로 정의한 변수는 소수점이 있는 수를 저장하는 변수이고,
char로 정의한 변수는 문자를 저장하는 변수인데...
과연 포인터는 무엇을 저장하는 변수인가??
포인터는 바로 변수의 메모리 주소를 저장하는 변수이다. (그러므로 포인터는, 그 포인터가 어떤 종류의 변수의 주소값을 가지느냐에 따라서 변수의 가짓수 만큼의 변형이 있다. 왜 여러 종류의 포인터가 필요한지는 나중에 포인터의 연산(물론 포인터도 변수이므로 연산, 비교, 대입 등 거의 일반변수에서 가능한 모든 것이 가능하다)에서 설명하겠다.)
변수에 어떤 값을 저장한다는 것은 실제로 그 변수에 대응하는 메모리에 저장하는 것이다.
"이 컴퓨터는 메모리가 256MB"이다 라고 할때의 그 메모리를 말이다.
그 256MB나 되는 메모리에서 그 값이 저장되는 위치가 메모리의 주소이다.
256MB나 되는 메모리에서 변수에 대응하는 메모리의 위치가 변수의 메모리 주소이다.
이에 대해서는 하드웨어적인 컴퓨터의 동작원리에 대한 체계적인 이해가 필요하지만 나중에 기술하도록 하겠다.
그렇다면 대체 왜 이다지도 복잡한 포인터를 쓰는가? 내가 알기로는 포인터는 C 와 C++ 언어에서만 존재하는 기능이다. 동시에 사람들이 C 와 C++ 를 어려워하는 이유이기도 하다. 왜 사람들은 C 에만 있는 기능인 포인터를 어려워 하는가. 그건 포인터를 써야만 하는 어떤 '이유' 가 있는게 아닐까? 재미있게도, 사람들이 C 언어를 좋아하는 이유가 '포인터의 강력함' 때문인 경우가 많다.
여러가지로 말이 많았는데 이쯤에서 포인터의 쓰임에 대해 간략하게나마 설명하겠다. 포인터의 이론적인 정의는 무엇인가. 바로 '메모리의 주소를 가지고있는 변수'란 것이다. 그건, 동시에 포인터를 이용하면 그 해당하는 메모리로의 직접적인 접근과 제어가 가능하다는 뜻이다. 그렇다면 포인터의 역활은 메모리로의 직접적인 억세스가 되는것이고, 여기에 포인터의 연산을 통해 거의 어셈블리언어에 가까운정도의 저수준 메모리 제어를 가능케 한다. 바로 그것이 포인터의 존재이유이며 쓰임이다. 이런 이론적인 포인터의 쓰임 말고, 직접적인 강함의 체험을 원한다면, 여러분이 직접 사용해볼것을 권한다.
정수형 변수는 int를 사용해 만든다.
문자형 변수는 char를 사용해 만든다.
~cpp
// 이렇게 말이다..
int a; // 정수형 변수
char b; // 문자형 변수
그렇다면 포인터는 어떻게 만들까?
포인터는 다음과 같이 *만 쓰면 만들 수 있다.
~cpp
int *a; // 정수형 변수의 메모리 주소를 저장하는 변수(정수형 포인터라고 함..)
char *b; // 문자형 변수의 메모리 주소를 저장하는 변수(문자형 포인터)
int *c1, *c2, *c2; // 여러개를 같이 만들땐.. 모두다 *를 붙여야함.
다음과 같은 선언도 유효하다.
~cpp
int* pi;
int * pi;
(int *) pi; /* 이런 식으로 사용하면 좀더 명확하다. 정수의 포인터라는. */
이렇게 만들면, a는(*a가 아니다. 정의할때만 *를 붙여야 한다. 사용할땐 a로 쓴다)
정수형 변수의 메모리 주소를 저장하는 포인터가 되고, b는 문자형 변수의 메모리 주소를
저장하는 포인터가 된다.
이제 포인터가 무엇인지도 알았고, 포인터를 어떻게 만드는지도 알았다.
그럼 포인터를 어떻게 사용하는지 알아보기로 하자.
포인터를 사용하는데는 두가지 연산자가 사용된다.
하나는 &이고, 다른 하나는 *이다.(여기서 *는 포인터를 만들때의 *와는 다른 의미이므로 잘 구분하기 바란다.)
먼저 &에 대해 살펴보기로 하자.
&는 어떤 변수에 주소를 얻어내는데 사용된다. 어떤 변수가 있을때 그 변수 앞에 &를 붙이면 그것은 그 변수의 메모리 주소가 된다.
예를 보면 쉽게 이해가 될거라고 본다.
~cpp
int a; // a는 정수형 변수
int *p; // p는 정수형 변수의 메모리 주소를 저장하는 변수. (정수형 포인터)
p=&a; // a의 메모리 주소를 p에 저장한다. (p는 정수형 변수의 메모리 주소를 저장하는 변수이므로...)
&는 일반적으로 정수형 변수나 문자형 변수 등과 같이 포인터가 아닌 변수들을 대상으로 사용하지만... 포인터를 대상으로 사용할 수도 있다.
처음에 강조했듯이 포인터 역시 변수이기 때문이다. 그러한 경우에 대해서는 나중에 다시 알아보기로 하겠다.
다음 *에 대해 알아보자.
(이해하기 힘들지도 모르니.. 아주 자~알 읽기 바란다.)
*는 포인터를 대상으로만 사용되는 연산자인데, 포인터에 저장되어 있는 메모리 주소에 보관되어 있는 값을 엑세스할때 사용한다.
무슨말인지 이해가 안되면 10번만 다시 잘 읽고, 다음 예를 보길 바란다.
~cpp
#include <iostream>
using namespace std;
void main()
{
int a; // 여기 세줄은 앞에서 설명했다
int *p;
p=&a;
a=10; // a에 10을 넣는다.
cout << a << " " << *p << "\n"; // a의 값과 p에 저장되어있는 메모리 주소에 보관되어있는 값을 출력한다.
*p=3; // p에 저장되어있는 메모리 주소에 보관되어있는 값을 3으로 바꾼다.
cout << a << " " << *p << "\n"; // a의 값과 p에 저장되어있는 메모리 주소에 보관되어있는 값을 출력한다.
}
이걸 실행하면 무엇이 출력될까?
이걸 실행하면 다음과 같이 출력이 될 것이다.
~cpp
10 10
3 3
여기서 *연산자를 이용하여 *p를 출력하는것의 의미는 무엇이냐 하면...
p에 저장되어있는 메모리 주소에 보관되어있는 값을 출력하는 것이고,
p에 저장되어있는 메모리 주소는 a의 주소이므로 결국 a에 보관되어있는 값을 출력하게 되는 것이다.
또한 *연산자를 이용하여 *p의 값을 바꾸는것은...
p에 저정되어있는 메모리 주소에 보관되어있는 값을 바꾸는 것이고,
p에 저장되어있는 메모리 주소는 a의 주소이므료 결국 a에 보관되어있는 값을 바꾸는 것이 된다.
3번째 문장 이후 *p와 a는 같은 의미로 쓰이게 되는 것이다.
그래서 이러한 결과가 나타나게 된다.
- 포인터의 활용 (Call by value, Call by address)
지금까지 포인터에 대하여 쭉 했는데...
그렇다면 과연 이런 포인터를 어디에다가 써먹을수 있을까?
가장 대표적인 것으로 함수 호출의 전달인자에서의 활용을 알아보도록 하자.
먼저 다음 예제를 보고 계속하도록 하겠다.
~cpp
#include <iostream>
using namespace std;
void func(int a)
{
a=10;
}
void main()
{
int a;
a=3;
func(a);
cout << a;
}
이걸 실행하면 결과가 어떻게 나올까?
C++을 제대로 배웠다면 3이 출력된다고 말할 것이다.
main 함수에서 a에 3을 대입하고 func라는 함수를 출력하며 a를 func에 넘겼다.
그리고 func에서는 a에 10을 대입했는데, 그러니까 10이 출력되야 하지 않을까?
그렇다고 생각한다면.. C++을 다시 배우길 바란다.
main 함수에서 a라는 변수와, func 함수에서 a라는 변수는 서로 다른 변수이다.
블럭({로 시작하여 }로 끝나는것) 안에서 static 이라는 키워드 없이 정의된 변수는
지역변수라고 하여 해당 블럭 안에서만 쓸 수 있는 변수이다.
비록 같은 이름이라 할지라도 다른 블럭에 있으면 다른 변수인 것이다.
따라서 main 함수의 a와 func 함수의 a는 다른 것이고, func에서 a를 10으로 바꿔봤자
main 함수의 a는 변함이 없기 때문에 3이 출력된다.
main 함수에서 func 함수를 호출할때는 main 함수의 a 변수의 값을
func 함수의 a 변수로 복사해주는 형식으로 호출이 이루어진다.
이와 같은 호출을 Call by value 라고 한다..
그렇다면 func에서 main 함수의 a의 값을 바꿔줄 수 있는 방법은 없을까?
당연히 있으니까 이런 소릴 하고 있을 것이다.
포인터를 사용하면 이런 것이 가능해진다.
다음 예제를 보기 바란다.
~cpp
#include <iostream>
using namespace std;
void func(int *p)
{
*p=10;
}
void main()
{
int a;
a=3;
func(&a);
cout << a;
}
앞에서 포인터에 대하여 잘 이해했다면, 보자마자 이해가 됬을 것이다.
더이상 설명이 필요없을꺼라 믿는다.
하지만 앞에서 제대루 공부하지 않은 사람들을 위해 설명하겠다.
아까와는 달리 main 함수에서 func 함수를 호출할때 a의 메모리 주소를 넘겨 주었다.
그리고 그 메모리 주소를 func 함수에서는 p라는 포인터로 받고 있다.
또한 func 함수에서 p 포인터가 가지고 있는 메모리 주소의 값을 바꾸고 있다.
p 포인터는 main 함수의 a의 주소를 가지고 있으므로 결국 main 함수의 a가 바뀌게 되는 것이다.
그래서 출력은 3이 아닌 10이 된다.
이와같이 함수 호출에서 주소를 사용해 전달 인자를 넘겨주는 방법을
Call by address 라고 한다.
이것 외에도 Call by reference 라는 방법이 하나 더 있다.
이것은 포인터 말고 참조 라는 것이 있는데, 이 참조를 이용한 방법이므로 참조를 설명할때 하도록 하겠다.