앞서, 반복문을 다룰 때 프로그래밍의 꽃이라고 말했던 적이 있다.
그 때 이런 비유를 했던 것은, 프로그래밍을 할 때 가장 많이 사용하고,
또 대부분의 알고리즘들이 반복문을 사용해서 이루어지기 때문에 그렇게 말했었다.
사용 빈도와 활용성에 있어서 반복문이 프로그래밍의 꽃이라면,
포인터는 그 개념의 중요성과, 해당 개념을 이해하는데 까지의 과정이 굉장히 험난하다는 이유로
프로그래밍의 꽃이라고 부르고 싶다.
많은 사람들이 포인터의 개념을 이해할 때, 많이 좌절하고 포기한다고들 한다.
이것은 전공자도, 비전공자도 예외는 없는 것 같다.
공부해보니 한번에 이해하기 쉽고 굉장히 복잡하지만 이해되는대로 다루어보려고 한다.
전에 함수에서, 함수에 전달된 매개변수는 함수의 시작과 동시에 할당되고, 함수가 종료되면 사라진다고 했었다.
이를 비유해보자면, 변수를 함수에 매개로서 전달하는 것은 변수의 본체를 전달하는 것이 아니라 분신을 전달하는 것이다.
그래서 매개변수를 전달받은 함수는 변수 본체에 직접 접근하여 값을 바꾸는 것이 불가능하고,
매개변수의 값을 그대로 복사하여 해당 값을 조작하고, 그를 토대로 나온 특정 값을 다시 반환하는 것이다.
하지만 프로그램을 구현함에 있어, main함수의 변수를 혹은 다른 함수의 변수의 값을 직접 바꾸어야 할 필요가 생긴다.
함수가 매개변수의 본체로 접근하여 해당 변수의 값을 조절할 수 있게 해주는 것이 이 포인터이다.
포인터를 설명할 때, 여러가지 비유들이 있다.
내가 알고있는 비유들은 이정표, 분신, 가리키는 것 등이 있는데, 모두 맞는 설명이지만,
분신이라는 비유는 이해하기는 쉬우나 본질을 이해하려 할 때 조금 혼동할 여지가 있다고 느낀다.
변수는 자료형을 선언하여 변수의 데이터가 저장될 메모리 공간의 용량을 정한다.
그 후, 초기화를 통해 메모리 공간 속에 저장할 데이터의 값을 입력받게 되어있다.
그렇게 최종적으로 선언된 변수는 특정한 공간에 메모리를 할당받아 그곳에 저장된다.
알기쉽게 설명하자면, int a = 10 이라는 변수를 가정했을 떄,
int의 집은 4평(4바이트)이라고 변수 부분에서 설명했던 적이 있다.
int는 집의 평수, 10은 거주자, a를 집이라고 설명할 수 있겠다.
10은 a라는 int평 집에서 살고있는 것이다.
합법적으로 지어진 집이라면, 당연히 주소가 있기 마련이다.
물론 10도 합법적으로 살고있기 때문에 a에도 주소가 있다.
10이 살고있는 a의 주소를 주소값이라고 부르고,
포인터는 이 주소값을 저장하는 변수이다.
위의 상황과 같은 int a = 10 이라는 변수를 두고 다른 예를 들자면,
10이 배가 고파서 배달음식을 주문했다.
배달기사는 주문을 접수하고 a라는 집으로 가려고 하는데,
이 배달기사는 경력이 적어 아파트에 잘 와놓고 a호를 찾질 못한다.
보다못한 경비아저씨가 배달기사에게 a의 위치를 정확히 알려주었다
경비아저씨에게 안내받은 배달기사는 무사히 a로 찾아가 10에게 배달음식을 배달하였다.
여기서 나오는 경비아저씨가 바로 포인터이다. 배달기사에게 a의 주소를 알려주었기 때문이다.
그렇다면 주소값을 가지고 무엇을 할 수 있을까?
이에 대해선 나중에 알아보도록 하고, 우선 포인터의 선언부터 알아보자.
#include<iostream>
int main()
{
   int a = 10;
   int* p = &a;
   p = a;	// 이렇게 대입했을 때는 빨간줄이 그어진다.
}변수 선언에 있어 익숙한 기호들이 나왔다.
*는 사칙연산을 다룰 때 나왔고 &은 논리연산을 다뤘을 때 보았다. 물론 둘 다 연산기호가 아니다.
우선 int* 에 대해 알아보자면, 이는 int 자료형의 주소를 받을 것이라는 기호이다.
자료형 마다 용량이 다르기 때문에 크기를 알려주어야 한다.
int의 용량은 4바이트, char의 용량은 1바이트인 것을 예로 들자면,
int의 주소는 0부터 4만큼을 차지하고, char의 주소는 0부터 1만큼을 차지하기 때문이다.
주소값을 저장할 때는 시작지점과 끝지점이 어디인지를 저장하기 때문에
포인터를 선언할 때의 자료형은 굉장히 중요하다.
위의 예를 통해 설명하자면, int*는 0과 4를, char*는 0과 1을 저장한다.
잘못된 자료형으로 선언한다면 다른 집으로 갈 수도 있지 않은가?
그 후에는 포인터의 이름을 선언하고, 포인터를 초기화 시켜준다.
포인터를 초기화 할 때 &라는 기호가 나왔다. 논리연산 때에는 두개가 붙어있었는데, 이번엔 혼자다.
&는 변수 앞에 위치하면 해당 변수의 주소값을 반환해주는 역할을 한다.
즉, &a는 10이 살고있는 집인 a의 주소를 의미한다.
이렇게 해서 a의 주소를 저장한 포인터 변수 p가 탄생했다.
포인터 변수는 오직 주소값만을 대입할 수 있기 때문에,
위 처럼 주소값이 아닌 값을 대입하려고 하면 오류가 생긴다.
#include<iostream>
int main()
{
   int a = 10;
   int* p = &a;
   
   std::cout << "a의 값 : " << a << std::endl;
   std::cout << "a의 주소값 : " << &a << std::endl;
   std::cout << "p의 값 : " << p << std::endl;
   std::cout << "p의 역참조 값 : " << *p << std::endl;
   
   
   // 출력값 : 10 (동일한 임의의 16진수값) (동일한 임의의 16진수값) 10
}위의 값을 출력해보면, a의 주소값과 p의 값이 같은 걸 알 수 있다.
또, p의 역참조값 이라는 새로운 개념이 나왔는데, 이 값이 a의 값과 동일한 것을 알 수 있다.
위에서 p가 a의 주소값을 갖는다는 것을 알 수 있었는데, 우리는 또 새로운 개념을 알아야 한다.
포인터는 데이터가 저장된 곳의 주소값을 가리킨다고 설명했었다.
역참조는 역으로 데이터가 저장된 곳의 주소값을 따라가서 그 데이터에 접근하는 것이다.
참조는 데이터의 주소를 따라가는 것. 역참조는 반대로 주소를 통해 데이터를 따라가는 것.
이렇게 이해할 수 있겠다.
역참조는 포인터 앞에 *를 붙여 사용할 수 있다. 그러면 *p에 특정 값을 대입하면 a에 저장된 값이 바뀔까?
#include<iostream>
int main()
{
   int a = 10;
   int* p = &a;
   
   *p = 20;
   std::cout << *p << ',';
   std::cout << a;
   // 출력값 : 20,20
}
정답이다. 주소를 통해 접근한 데이터 값을 통해 해당 데이터의 값에 직접 접근하여 바꿀 수 있다.
집과 집주소의 예를 들자면,
부동산 업자 p가 a라는 집을 소개하여 10은이사가고, 20이라는 새로운 세입자가 들어서게 된 것이다.
위에서 함수를 통해 전달한 매개변수의 본체를 바꿔야 할 필요가 있다고 말했었다.
포인터를 사용한다면, 우리는 다른 함수에서, 다른 함수의 매개변수의 값을 바꿀 수 있다.
말만 들어서는 잘 이해가 가지 않는다. 직접 활용해보자.
#include<iostream>
void Swap1(int num);
void Swap2(int* num);
int main()
{
   int a = 10;
   Swap1(a);
   std::cout << "a의 값 : " << a << std::endl;
   Swap2(&a);
   std::cout << "Swap2를 거친 a의 값 : " << a ;
}
void Swap1(int num)
{
   num = 20;
   std::cout<< "num의 값 : " << num << std::endl;
}
void Swap2(int* num)
{
   *num = 20;
}
/* 출력값 : 
	num의 값 : 20
	a의 값 : 10
	Swap2를 거친 a의 값 : 20
*/기존에는 Swap1 함수 처럼 매개변수를 전달했었다.
앞서 말했듯 매개변수를 전달할 때는
매개변수가 직접 전달되는 것이 아니라 매개변수의 값을 가지고있는 복사본을 전달하는 것이다.
이를 Call by Value (값에 의한 호출)라고 한다.
값에 의한 호출은 그 값을 가지고있는 복사본을 매개변수로 함수에 전달하여,
그 값을 가지고 함수는 코드를 실행하고, 복사본을 통해 나온 값을 다시 반환하며
함수가 종료되면 복사본을 파기한다.
그렇기 때문에 Swap1에 있는 복사본 a를 20으로 바꾼다 한들, main함수에 있는 본체 a에는 아무런 영향이 없는 것이다.
하지만 Swap2를 거쳤더니 a의 값이 바뀌었다.
Swap2에서는 a의 주소를 참조하여 직접 그 주소로 찾아가 a의 값을 바꾸었기 때문에 본체에 영향이 생긴 것이다.
매개변수를 전달할 때를 좀 더 자세히 살펴보자면, 매개변수를 전달할 때,
int* num = &a; 가 되므로, Swap2함수의 포인터 num는 a의 주소를 가리키고 있다.
따라서 포인터 num에서 *을 통해 역참조를 하게되면 a의 값이 도출되고, *num을 바꾸면,
a의 주소를 따라들어가 a의 값을 바꾸기 때문에 본체 a의 값이 바뀌게 되는 것이다.
좀 더 쉽게 말하자면, Swap2의 매개변수인 포인터 num에 a의 주소를 입력했다.
num은 포인터이기 때문에 다른 값이 아닌 오직 주소값만 입력해야 한다.
그래서 num에는 a의 주소를 입력한다.
위에서 포인터 앞에 *을 달면 그 주소에 들어있는 세입자의 값이 나온다고 설명했었다.
그래서 *num은 세입자 a의 값인 10이 있는 것이다.
여기서 *num의 값을 바꾸면 당연히 세입자 a의 값도 같이 바뀌게 된다.
이를 Call by Address(주소에 의한 호출)라고 한다.
주소에 의한 호출은 해당 주소에 직접 찾아가 값을 바꾸기 때문에 값이 바뀌게 된다.
이에 대한 것은 추후에 Call by Reference를 다룰 때 더 자세히 설명하겠다.
'C++ 일기' 카테고리의 다른 글
| 17. 네임스페이스 (namespace) (0) | 2024.02.28 | 
|---|---|
| 16. Call by Reference, Call by Value, Call by Reference (0) | 2024.02.27 | 
| 14. 사용자 정의 자료형 struct (구조체) (2) | 2024.02.27 | 
| 13. 난수 발생 (0) | 2024.02.27 | 
| 12. 열거형 타입 enum (0) | 2024.02.26 |