일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- gold5
- AWS
- PYTHON
- gold2
- 배포
- 9252
- Gold4
- LEVEL1
- glod4
- error
- LCS
- leetcode 69
- 오류
- spring
- CSS
- 구현
- HTML
- mysql
- Kakao
- jpa
- glod5
- leetcode
- 백엔드
- 백준
- siver3
- Thymeleaf
- 프로그래머스
- LEVEL2
- 개념
- java
- Today
- Total
이 험난한 세상에서어어~
포인터 확실하게 알고 가기! (with c++) 본문
서론
개발에 관해 조금이라도 관심이 있다면 포인터의 악명에 관해 적어도 한 번 쯤은 들어봤을 것이다. 포인터의 산을 넘지 못해 개발과 멀어져야 했던 온갖 썰들과 함께 말이다.
나는 고등학교 시절 C를 처음 배울 때 포인터를 처음 접했다. 그 당시 어떤 심정이었냐고 하면... 기억 안 난다. 그냥 아, 주소를 가리키는 거구나 하고 말았다. 한 4년 뒤에 컴퓨터 공학을 복수 전공하면서 다시 포인터를 보게 되었는데, 그때도 비슷한 감정이었다. 솔직히 재귀 구현이 어려우면 어려웠지 포인터는 개념이 명료해서 크게 힘들지는 않았기 때문이다.
지금부터 내가 어떻게 포인터를 이해했는지를 이야기해보려고 한다. 포인터 때문에 골머리를 앓고 있는 분이라면 이 글이 도움이 되었으면 한다.
포인터 개념
포인터란 뭘까? 책에서는 보통 해당 변수의 주소를 가리키는 변수라고 설명을 한다. 이해를 돕기 위해 주소가 그려진 아파트와 화살표를 그려가면서 말이다.
정말로 맞는 말이다. 우리가 선언한 모든 변수는 메모리에 저장이 되기 때문이다. 그렇기에 포인터에 관해 본격적으로 설명하기 전에 변수와 메모리에 관해 잠깐 설명을 하겠다.
메모리와 변수
메모리는 단어 그대로 진행되고 있는 프로세스의 데이터를 기억하는 곳이다. 그리고 이는 하드웨어적으로 RAM에 저장이 된다. 때문에 우리가 어떤 프로그램에서 변수를 선언하면 해당 변수의 값이 메모리 어딘가에 저장이 되는 것이다.
아래의 코드를 보자.
#include <iostream>
int main() {
int x = 5;
return 0;
}
이렇게 되면 변수 x에는 5가 들어가고 메모리 어디엔가에는 5가 들어간다.
전통적인 예시로 메모리 아파트를 그려봤다.
그림에서 보듯이 특정한 메모리 안에 값이 할당이 되면 해당 값을 접근할 수 있어야 한다. 당연한 말 아닌가? 우리가 5를 x라는 변수로 선언한 이유는 x에 들어있는 값 5를 여기저기서 꺼내서 쓰기 위함이니까.
그렇다면 5라는 값을 접근하기 위해서 우리는 어떻게 할까? 아주 간단하다! 우리가 선언한 변수의 이름인 x를 불러오면 된다.
하지만 실제 메모리에서는? 실제 5가 저장된 메모리 주소가 x라는 이름을 가지고 있을까? 아니다, 아마 정학하지는 않지만 메모리에는 5라는 값이 0x000... 뭐 이런 주소에 저장되어 있을 것이다.
즉, 겉보기에는 x라는 변수 이름을 가지고 와서 5를 사용하는 것처럼 느껴지지만, 실제로는 x라는 변수 이름이 호출될 때마다 해당되는 메모리 주소를 찾아 그곳에 있는 값을 가지고 오는 것이다.
사실 이 부분을 이해했다면 포인터는 금방 알 수 있다.
포인터의 선언과 개념
포인터는 말 그대로 무언가를 가리킨다는 것이다. 여기서는 보통 변수가 들어있는 주소를 가리킨다.
하지만, 단순히 가르키는 변수라고 이해를 한다면 조금 어려울 수 있다. 그러므로 이제부터는 가르킨다는 수식어를 붙이지 않고 포인터를 특수한 종류의 변수라고 볼 것이다.
포인터는 일반적으로 아래처럼 선언한다.
int main(){
int x = 5;
int* xPoint = &x;
return 0;
}
여기서 생소한 문자가 보일 것이다. 바로 '*'과 '&'이다. 일단 직관적으로 이 둘을 설명하자면, '*'는 해당 주소에 저장되어 있는 실제 값을 의미하고 '&'는 해당 주소를 의미한다.
'*' : 해당 메모리 주소에 저장되어 있는 실제 값
'&' : 변수의 실제 값이 저장된 메모리 주소
이렇게만 일단 이해하고 차례대로 코드를 보자.
int x = 5;
먼저 우리는 정수형 변수 x에 5를 할당해줬다. 이렇게 되면 메모리 어딘가에는 5라는 값이 저장이 될 것이다. 아래처럼 말이다.
int* xPoint = &x;
좋다. 그리고 우리는 int* xPoint로 xPoint라는 변수가 포인터임을 명시해줬다. 그리고 xPoint 변수에 '&x'를 넣어준다. 여기서 잠깐! 우리는 방금 '&'가 변수의 실제 값이 저장된 메모리 주소라고 했다. 그렇다면 xPoint에는 무슨 값이 저장될까? 어떤 값을 할당해줬지? 바로 x의 주소이다. x의 주소를 '&' 를 이용해 불러와 xPoint 변수에 넣어준 것이다.
믿기 어렵다고? 그렇다면 직접 xPoint 안에 들어 있는 값과 x 변수가 메모리에 실제로 저장된 주소를 확인해보자.
#include <iostream>
int main() {
int x = 5;
int* xPoint = &x;
std::cout << "xPoint 변수 안에 들어 있는 값 : " << xPoint << '\n';
std::cout <<"x변수가 실제로 저장된 메모리 주소 : " << &x << '\n';
return 0;
}
둘의 값이 같다는 걸 알 수 있다. 이게... 포인터의 개념이다. 굉장히 간단하다.
포인터 응용
1.
사실 '*'과 '&'는 굉장히 독립된 요소처럼 보이지만 두 개를 같이 사용할 수 있다.
바로 아래처럼 말이다.
std::cout << *&x << '\n';
위의 코드를 실행하면 어떤 값이 나올까? 답을 보기 전에 먼저 한 번 생각해보기를 바란다.
흠... 일단 우리가 위에서 배운 개념들을 가지고 찬찬히 코드를 살펴보자.
먼저 '&'는 x의 변수가 저장된 실제 메모리 주소를 의미한다고 했다. 그러므로 만일 x의 실제 메모리 주소가 0x00000001이라면 해당 주소가 반환이 될 것이다.
그리고 '*'는 해당 주소에 들어가 있는 값을 의미한다고 했다. 그러므로 메모리 주소 0x00000001에 들어가 있는 값이다. 여기에는 뭐가 들어가 있지? 바로 5이다! 우리가 x에서 할당해줬던 5!
그러니까 x와 *&x는 같은 값을 의미한다.
아래의 코드는 어떨까?
std::cout << *&xPoint << '\n';
우리는 제일 첫 번째 개념 부분에서 xPont라는 포인터 변수 안에 x라는 주소를 저장해줬다. 때문에 xPoint 변수에는 x의 주소가 들어가있다는 걸 잊어서는 안 된다.
그리고 &xPoint를 이용해서 xPoint 변수 자체의 메모리 주소를 불러온다. 이때 &x와 혼돈해서는 안 된다! &x는 x라는 변수가 메모리에서 갖는 주소이고 &xPoint는 xPoint라는 변수가 메모리에서 갖는 주소이다. 둘은 독립적이다!
아무튼... 다시 돌아가서 *&xPoint를 이용하면 xPoint의 주소 안에 들어가 있는 값을 반환하라는 의미가 되고 이는 곳 우리가 아까 저장해줬던 x의 주소가 된다.
여기서 한 번 더 나아가서 **&xPoint를 하면 어떻게 될까. 일단 '*&xPont'는 x의 주소라고 했다. x의 주소에다가 '*'를 붙여줬으니 x의 주소가 가지고 있는 값인 5가 나올 것이다.
std::cout << "*&*xPoint: " << *&*xPoint << '\n';
정리
간단하게 말해서 포인터란 특수한 변수로 다른 변수의 주소를 저장한다고 말할 수 있다. 그리고 변수의 메모리 주소는 '&'로 불러올 수 있으며 해당 메모리에 저장되어 있는 값은 '*'로 가지고 올 수 있다.
사실 투 포인터라고 해서 '**'를 연속해서 사용할 수도 있다(사실 위에서 한 번 연속으로 사용해봤다). 그리고 배열을 포인터로 접근할 수도 있다. 이러한 내용들은 글이 너무 길어지니 두 번째 포인터 글에서 다루도록 하겠다.