포인터 소개 및 기본적인 사용법 Introduction to Pointer 본문
포인터 소개 (Introduction to pointer)
우리는 변수는 값을 보유하고 있는 메모리 조각의 이름이라는 것을 배웠다.
프로그램이 변수를 인스턴스화 할 때 사용 가능한 메모리 주소가 변수에 자동으로 할당되고,
변수에 할당된 값은 이 메모리 주소에 저장된다.
int x;
CPU가 위 문장을 실행하면 RAM의 메모리 조각이 따로 설정된다.
예를 들어, 변수 x에 메모리 위치 140이 할당되었다고 가정해보자.
프로그램에서 변수 x를 표현식 또는 명령문으로 접근할 때마다 값을 얻으려면 메모리 위치 140을 찾아야 한다.
변수의 좋은 점은 우리가 어떤 특정한 메모리 주소가 할당되는지 걱정할 필요가 없다는 것이다.
지정된 식별자로 변수를 참조하면 컴파일러에서 이 이름을 할당된 메모리 주소로 변환한다.
하지만 이 접근법에는 몇 가지 제한 사항이 있으며, 이에 대해서는 앞으로 배울 내용에서 살펴보겠다.
우리가 어떠한 변수를 선언한다는 것은 변수가 사용할 메모리 공간을 OS로부터 받아서 빌려오는 것이다.
(지역 변수는 '스택'메모리를 사용하고 동적 할당 메모리는 '힙'메모리를 사용한다. 자세한 내용은 추후 다룰예정)
그리고 빌려 올 때 주소를 알고 있을 것이다.
그 주소를 갖고 있는 집에다가 메모리 공간에 5라는 값을 복사해서 넣고
이 변수는 현재 5가 들어있다. 이런 식으로 사용을 하게 된다.
그래서 우리가 실제 이 변수의 값을 사용하게 되면
변수가 갖고 있는 값 5가 출력이 되는 구조이다
우리가 지금까지 사용해온 모든 변수는 내부적으로 다 주소를 갖고 있다.
주소를 갖고 있지 않다면 메모리에 담을 수가 없다.
어떤 변수가 어떤 메모리 주소의 데이터를 담고 있는지 알고 싶다면
변수명 앞에다가 &를 붙여주면 메모리 주소를 출력해 볼 수 있다.
(큰 메모리에 저장되어 있는 데이터 중에서 일부분을 cpu가 사용하기 위하여 메모리로부터 가져올 때는
메모리 전체를 모두 뒤지면서 찾는 것이 아니라 필요한 데이터가 저장되어 있는 ‘주소’를 사용하여 직접 접근하여 가져온다.)
주소 연산자 (&) (Address of Operator (&))
주소 연산자 & 를 사용하면 변수에 할당된 메모리 주소를 확인할 수 있다.
주의 : 주소 연산자는 비트 AND 연산자와 비슷하게 보이지만,
주소 연산자는 단항이고 비트 AND 연산자는 이항 연산자이다.
역참조 연산자 (*) (The de-reference Operator (*))
레퍼런스라는 용어가 컴퓨터 사이언스에서는 좀 의미가 넓게, 광범위하게 쓰인다.
C++ 언어에서는 포인터가 있고 레퍼런스가 따로 있다. 그래서 혼란스러울 수 있다.
여기서 de-reference라고 할 때 reference는 포인터가 " 저 쪽 주소에 가면 이 데이터가 있어요"라고 간접적으로 가리키기만 하는 것에 대해서, 레퍼런스는 "그럼 거기에 진짜 뭐가 있는지 내가 들여다볼게" 같은 의미이다.
직접적으로 접근하겠다는 의미.
변수의 주소를 얻는 것 자체로는 그다지 유용하지 않다.
역참조 연산자 (*)를 사용하면 특정 주소에서 값에 접근할 수 있다.
주의 : 역참조 연산자는 곱셈 연산자처럼 보이지만, 역참조 연산자는 단항이고 곱셈 연산자는 이항 연산자다.
메모리 주소를 갖고 왔다가 다시 그 메모리의 위치로 가서 값을 꺼내오는 거니까 얘네가 상쇄되는 처럼 작동한다.
실제 컴퓨터 내부에서는 그 두 가지 과정을 거친다.
포인터 (Pointer)
주소 연산자(&)와 역참조 연산자(*)와 함께 이제 포인터에 관해 이야기할 수 있다.
포인터는 어떠한 값을 저장하는 게 아닌 메모리 주소를 저장하는 변수이다.
포인터는 C++ 언어에서 가장 혼란스러운 부분 중 하나로 여겨지지만, 알고 보면 놀랍게도 간단하다.
pointing 하면 referencing 이런 의미로 생각하시는데 reference는 따로 있다.
포인터 선언 (Declaring a Pointer)
포인터 변수는 일반 변수처럼 선언되며, 자료형과 변수 이름 사이에 별표(*)가 붙는다.
- 자료형*포인터 이름;
주소를 가져올 때는 &를 붙이면 된다.
이렇게 할 수도 있고
이때 * 을 왼쪽에 붙여야 하는지, 오른쪽에 붙여야 하는지, 양쪽 다 띄워서 붙여야 하나 생각할 수 있는데
비주얼 스튜디오가 파라미터로 넣어줄 때는 양쪽을 다 띄우는 거고
그 외에는 왼쪽에 붙이는 것보다는 오른쪽에 붙이는 것을 권장한다.
양쪽 다 띄울 때는 혹시 프로그래머가 못 볼까 봐 빈칸으로 띄어둔 것이다.
만약 왼쪽에 붙이게 되면
이렇게 ptr_y를 하나 더 선언했을 때 ptr_y는 포인터가 아니게 된다.
이렇게 별이 붙어있는 애 하나만 포인터로 선언이 된다.
요새는 사람의 실수를 줄이기 위해서 빈칸을 두고 변수명 앞에 *를 붙이는 식으로 코딩을 많이 한다.
그리고 typedef로 데이터 타입인 것처럼 쓸 수 있다.
위 경우에는 int의 포인터 타입을 하나 정의해서 사용했기 때문에 *가 붙을 필요가 없다.
대부분의 경우 typedef를 사용해서 돌려서 코딩하는 것보단 윗 예제를 더 많이 사용한다.
가끔 이중 포인터나, 삼중 포인터를 어쩌다 한번 써야 할 경우 typedef로 돌려서 쓰면 좋을 때도 있다.
그리고 함수가 리턴 타입으로 포인터를 가질 수 있다.
그리고 파라미터로 int가 들어갈 수 있다.
그리고 지금 변수의 주소를 포인터에다가 넣어주고 있다.
포인터 *ptr_x에 저장되는 것은 주소다.
메모리 주소는 데이터 타입과는 상관이 없다. 중립적이다.
포인터가 어떤 타입인지 알고 있어야 하는 이유는
de-referencing 할 때 int로 가져올지, char로 가져올지, float로 가져올지 헷갈리니까 알아야 한다.
포인터를 선언할 때도 *를 사용하고,
de-referencing 할 때도 *를 사용한다.
포인터 변수에 담겨있는 주소가 있고
메모리에서 그 주소를 갖고 실제 메모리 위치에 찾아가고
그다음에 그 메모리 주소에 담겨있는 데이터를 갖고 와서
de-referencing을 하는데 int 타입으로 갖고 오게 된다.
왜냐하면 ptr_x가 int의 포인터 타입으로 선언이 되었기 때문이다.
출력해보면
다른 타입으로 포인터를 사용해보자.
예를 들어, int의 포인터에다가 d를 넣으려고 하면 문제가 생긴다.
d를 넣으려면
주소와 de-referencing 된 value 도 찍어보자
사용자 정의 자료형 struct나 class에서도 사용할 수 있다. (추후 설명 예정)
여기서 의문점이 드는 것은 메모리 주소가 나오니까 이걸 복사해서 내가 바로 값을 넣을 수 있지 않을까 생각할 수 있다.
이렇게는 쓰기도 어렵고 허용을 해주지 않는다.
앞에 0x 붙여도 안된다.
그렇다고 아예 안 되는 건 아니다. 사실 이게 해킹의 원리이다.
다른 프로그램이 돌고 있는데 그 프로그램에서 사용하고 있는 메모리의 주소를 가져다가 빼돌린다.
그러면 다른 프로그램에서 이 변수는 이 프로그램에서 선언한 것은 아니지만 메모리 주소를 갖고 바꿔치기할 수 있다.
아무튼 일단 직접적으로는 못하게 막아놨다.
The address of operator returns a pointer
주소 연산자 &는 피연산자의 주소를 리터럴로 반환하지 않는다.
대신 피연산자의 주소가 들어있는 포인터를 반환한다.
이 사실은 다음 예제에서 확인할 수 있다.
#include <typeinfo>를 하고 typeid를 이용해서 출력해보면
포인터의 크기
포인터 자체의 사이즈는 고정이다.
그런데 pointing 하는 데이터의 사이즈는 다르다.
int는 4바이트, double은 8바이트이다.
나중에 array를 pointer로 다룰 때 이 한 칸이 4바이트인지 8바이트인지 알아야 할 수 있는 연산이 있다.
그걸 알아두기 위해서 포인터도 데이터 타입을 두는 것이다.
포인터는 기본적으로는 그냥 주소 값이고 모든 타입에 대해서 사이즈는 같다.
한번 출력해보자
int는 4바이트.
double은 8바이트,
x의 주소 4바이트, ptr_x는 4바이트,
double의 address도 4바이트, ptr_d도 4바이트이다
주소는 그냥 주소이기 때문에 주소 자체를 저장하는 변수의 크기는 고정인 것이다.
x64로 바꿔서 출력해보면 x86과 다르게 나오는데
그 이유는 64비트에서는 메모리를 더 크게 사용할 수 있어서 주소가 더 길다.
64비트를 가면 주소 자체를 표현하기 위해서 8바이트를 사용한다.
이런 속성 자체도 알아두면 나중에 유용하다.
그다음 structure도 활용해보자
Something이라는 structure 자체는 16바이트인데 포인터는 4바이트 밖에 되지 않는다.
이 경우도 마찬가지로 주소는 사이즈가 고정되어있는 것이다.
주소도 변수다 라는 것 기억하기
A warning about de-referencing invalid pointers
문제가 될 만한 부분 몇 가지 살펴보자
현재 ptr_x가 초기화되지 않은 상태에서 de-referencing을 해보면
debug 모드로 해보면
ptr_x가 주소 값을 갖고 있지 않은데 (쓰레기 값이 들어가 있는데)
그 위치의 주소에 가서 메모리를 꺼내오라고 하니까 OS가 화가 난 것이다.
이 오류가 가장 많이 발생하는 오류이다.
주소가 실제 메모리에 있는 주소가 아니고 엉뚱한 값이 들어있을 때에는 문제가 생길 수 있다.
반대로 생각해서
내 프로그램이 사용하지 않는 메모리 주소도 그냥 슬쩍 갖다 껴놓고
내 거처럼 쓸 수 있다는 게 해킹하는 방법 중 하나다.
C++의 포인터는 본질적으로 안전하지 않으므로 포인터를 부적절하게 사용하면 응용 프로그램을 손상시키기 쉽다.
포인터를 역참조하면 응용 프로그램은 포인터에 저장된 메모리 위치로 이동하여 메모리 내용을 검색한다.
보안상의 이유로 최신 운영체제의 샌드박스 응용 프로그램은
다른 응용 프로그램과의 부적절한 상호 작용을 방지하고
운영체제 자체의 안정성을 보호한다.
응용 프로그램이 운영 체제에 의해 할당되지 않은 메모리 위치에 접근하려고 하면
운영 체제가 응용 프로그램을 종료할 수 있다.
일반 변수를 사용할 수 있는데 굳이 포인터를 사용하는 이유는 무엇일까?
포인터는 여러 가지 경우에서 유용하다.
- 배열은 포인터를 사용하여 구현된다. 포인터는 배열을 반복할 때 사용할 수 있다. (배열 인덱스 대신 사용 가능)
- C++에서 동적으로 메모리를 할당할 수 있는 유일한 방법이다. (가장 흔한 사용 사례)
- 데이터를 복사하지 않고도 많은 양의 데이터를 함수에 전달할 수 있다.
- 함수를 매개 변수로 다른 함수에 전달하는 데 사용할 수 있다.
- 상속을 다룰 때 다형성을 달성하기 위해 사용한다.
- 하나의 구조체/클래스 포인터를 다른 구조체/클래스에 두어 체인을 형성하는 데 사용할 수 있다. 이는 연결리스트 및 트리와 같은 고급 자료구조에서 유용하다.
마지막으로 포인터는 메모리 주소를 저장하는 변수다. * 를 사용해서 저장한 메모리 주소의 값을 알 수 있다.
쓰레기 값이 들어있는 포인터를 역참조하면 응용 프로그램이 중단될 수 있다 는 것 기억하기.
'💘 C++ > 행렬, 문자열, 포인터, 참조' 카테고리의 다른 글
포인터와 정적 배열 (Pointer and Fixed Array) (0) | 2022.07.09 |
---|---|
널 포인터 (Null pointer) (0) | 2022.07.08 |
C언어 스타일의 배열 문자열 C-style strings (0) | 2022.07.06 |
정적 다차원 배열 Multidimensional Arrays (0) | 2022.07.05 |
배열과 선택 정렬 Selection Sort (0) | 2022.07.04 |