프로그래밍 지식/C++

C++문법 / 포인터, 메모리 해제

게임이 더 좋아 2021. 11. 10. 14:39
반응형
728x170

 

포인터는 C에서도 배웠겠지만 값 자체가 아니라 값의 주소를 가지고 있는 변수다.

어려워하는 것은 이해가 가지만 배워보면 그냥 그렇다고 생각이 된다.

 

https://www.geeksforgeeks.org/pointers-c-examples/

 

변수 -> 주소를 담는 변수 + 값을 담는 변수

다시 말해서

포인터는 주소를 담는 변수(장소)

변수는 값을 담는 장소

 


 

일반적으로 포인터변수는 주소를 저장하므로

흔히 주소를 알아내기 위해서 앰퍼샌드,&를 써서 해당 변수의 주소를 알아낸다.

 

int donuts = 4;
int* ptr = &donuts;

//*은 포인터라는 뜻이다. 
//출력해보면
/*

donuts
ptr을 출력해보면 
ptr의 가지고 있는 주소 값이 아니라
가리키고 있는 값을 출력하고 싶을 때가 있다.
그렇다면 ptr 자체를 출력하는 것이 아니라
*ptr를 출력하면 ptr이 가리키고 있는 값이 출력된다.

*/

 

이렇게 간접적으로 주소를 가져서 값을 가지는 변수를 가리키는 포인터를

간접값, indirect value, 또는 간접 참조, dereferencing 를 통해 해당 변수의 value를 얻어낼 수 있다.

위에 말했던 것처럼 포인터에 *을 붙이면 주소가 가리키는 변수의 value가 되는 것이다.

 

즉, 위에 선언했던 donuts 와 *ptr은 같은 것이다. -> 값을 가리킴


& donuts와 ptr은 같은 것이다. -> 주소를 가리킴

 


 

하지만 포인터가 여기선 간단하지만 어려워지는 이유가 있다.

포인터는 주소를 저장하기 위해 메모리를 대입한다.

그 포인터가 지시하는 데이터를 저장하기 위한 메모리가 아니라는 것을 알아야 한다.

다시 말해서

 

long * fellow; //포인터를 생성해놓고

*fellow = 223323; // 어딘지 알 수 없는 곳에 값을 저장하면..?

 

fellow는 포인터임에도 불구하고 포인터가 어디를 지시하는지를 알 수 없다.

왜냐하면 초기화를 시키지 않았기 때문이다.

하지만 위의 문장은 실행된다??

그게 문제다.. 실행되는데.. 포인터에 대한 메모리, 변수에 대한 메모리가 쓰이고 있지만 사용할 수 없다.

즉, 포인터를 사용할 때는 반드시 적절한 주소로 초기화를 시켜서 해당 포인터에 접근할 수 있도록 해야 한다.

 


 

일반적으로 컴퓨터에선 주소를 정수로 표현하지만

실제로 포인터는 정수형이 아니다.

포인터는 정수형이 아니기 때문에 산술 연산자로 연산이 되지 않는다.

**포인터 연산은 따로 정해져있다.

포인터는 그저 위치를 나타내는 것이다.

즉, 포인터에 직접 정수를 대입할 수는 없다.

 

int * ptr;

pt = 0xB8111000;

 

는 말이 안되는 문장이다.

우리가 주소를 출력하면 나오는 16진수로 표현한다고 해도 저렇게 대입을 하는 것은 허용하지 않는다.

** 데이터 형이 다르다고 보는 것이 더 맞을 것이다. (C99 이전에는 저렇게 대입하는 것이 허용되었다고 한다)

 

즉, 데이터형이 맞지 않으니 형변환,Type Casting을 해줘야 가능하다.

int * ptr;
ptr = (int *) 0xB1111000;

이것은 정수형을 주소형으로 형변환을 해주었기에 실제 ptr에 대입될 수 있다.

 


 

이제 더 나아가서 포인터가 동작하는 방법을 알았다면 

런타임 도중 메모리를 어떻게 할당하는지 알아보려고 한다.

포인터들을 변수의 주소로 초기화해서 이름으로 직접 접근할 수 있게 했지만

프로그램을 실행하는 동안에도 이름이 없는(unnamed) 메모리를 대입하는 것이 진짜 필요하다.

다시 말해서 pointer가 해당 메모리를 접근할 수 있게 해주는 것이다.

** C에서는 memory allocation이란 뜻으로 malloc을 썼지만 C++에서는 new를 쓴다.

 

즉, 프로그램을 실행하는 동안에 int형 값을 저장할 수 있는 이름이 없는 메모리를 대입할 수 있게하고

해당 메모리 영역을 포인터가 가리킬 수 있게하여 사용하는 것이다.

여기서 우리는 new를 쓸 수 있다.

 

쉽게 말하면

어떤 데이터 형의 메모리를 원하는지 new에게 알려주고

new 연산자는 그에 맞는 메모리 블록을 찾아내어 해당 블록의 주소를 return 하게 된다.

해당 주소를 pointer에 할당하고 그 메모리를 pointer를 통해 사용할 수 있게 된다.

이것이 런타임에서도 일어난다는 것이다.

 

예를 들면

 

int * ptr = new int;

 

앞서 말한 것과 같이 포인터를 쓸 때에는 포인터 변수 단독이 아닌 초기화를 해준 맥락과 같다.

 

?? 근데 뭔가 좀 이상하다..?

앞서 말하자면 어떠한 변수의 주소를 가리키는 것이 포인터라고 했는데...?

변수가 없는데 어떻게 해당 주소를 가리킬 수 있는 것이지???

 

메모리 블록 자체는 변수가 아니다.

즉, 앞서 말한 포인터는 이해하기 쉽게 설명하기 위해 많이 설명되는 개념이지 정확하지는 않다.

다시 말해서 포인터는 Data Object를 가리킬 수 있는 것이다.

** 여기서 말하는 객체는 우리가 OOP에서 말하는 객체가 아니라 정말 어떠한 것, thing이라고 한다.

변수도 물론 Data Object이다. 하지만 변수에는 이름이 주어져 있지만

여기선 이름이 없는 Data Object를 다루는 것이다.

이렇게 함으로써 포인터는 런타임에서도 메모리에 자유롭게 접근할 수 있다.

 

때문에 일반적으로 메모리를 확보하고 해당 메모리에 접근하는 방식은 아래와 같다.

 

typeName * pointer_name = new typeName;
//실제로 typeName엔 원하는 데이터형을, pointer_name엔 원하는 이름을 넣으면 되겠다.

 

앞의 typeName은 적절한 포인터를 선언하기 위함이고

뒤의 typaName은 어떻게 메모리를 확보해야 할 지 쓰기 위함이다.

 

뭐 이미 초기화하기 전에 포인터 변수를 미리 선언해놓았다면

typeName * ptr;
ptr = new typeName;

이렇게 써도 무방하다.

 

**만약 new 가 가용할 메모리 블럭을 찾지 못한다면 0을 return하게 된다.

pointer가 값이 0이면 해당 포인터는 null pointer라고 불린다.

null pointer 자체는 어떠한 데이터도 가리키지 않는다.

즉, 디버깅 하기 위해 null check를 하기도 한다.

 

**참고로 new는 0값 뿐만 아니라 bad_alloc exception을 리턴할 수도 있다.

 


new로 메모리를 접근할 수 있다면

해당 메모리를 재활용하기 위해 다시 가용할 메모리 블럭에 올려놓을 수도 있어야 한다.

그것이 delete 다.

 

즉, delete는 new로 대입한 메모리 블럭을 가리키는 포인터와 같이 사용된다.

**new로 할당한 것은  delete로만 해제시켜야 한다.

int * ps = new int;
...
delete ps;

//메모리 할당과 해제이다.

 

즉, ps가 가리키는 메모리 블럭을 해제하는 것이다.

**물론 ps가 사라지는 것이 아니라 ps가 가리키는 주소가 사라지는 것이다.

**new를 하고나서 delete를 하지 않으면 사용하지 않는 메모리임에도 다른 곳에 할당할 수 없는 일이 일어나서

memory leaking이 일어나서 더 이상 프로그램이 진행되지 않을 수도 있다.

 

그리고 delete는 new로 대입한 메모리에게만 통한다.

변수의 주소를 통해 집어넣은 포인터에 대해서는 작동하지 않는다.

 

또한 delete는 해당 포인터 변수가 아니라 주소에 사용된다고 하는 것이 옳다.

int * ps = new int; // 첫 번째 포인터
int * pq = ps; // 같은 메모리를 가리키는 두 번째 포인터
delete pq; // 두 번째 포인터에 대한 delete

delete를 사용할 때는 new로 대입한 메모리에만 사용하라는 것이다.

new가 사용한 것과 동일한 포인터 변수에 사용하는 것이 아니라 주소에 대해서 사용하라는 것이다.

같은 메모리 블럭을 두 번 delete하는 경우가 생길 수 있기 때문에 좋지 않을 뿐더러

포인트를 리턴하는 함수에서는 두 번째 포인터를 사용할 수 있기 때문이다.

 


 

아래 내용은 (21.11.17 업데이트)

우선 포인터를 이미 어느정도 알고 있어야 아래 내용이 이해가 가능하다.

 

x 가 변수라면

&x는 x의 주소가 된다.

int* ptrX = &x

ptrX는 x를 가리키는 포인터가 된다. (즉, ptrX는 x의 주소다.)

*ptrX는 x의 값을 가리킨다.

 

일반 변수를 사용할 때와는 반대로

저장된 데이터를 다루는 것은 주소를 이름을 붙여서 취급하고

그 값은 주소에서 파생되는 것으로 취급한다.

-> 기존의 데이터를 수정하기 위해서는 포인터를 이용해야함

 

다시 말해서 포인터의 이름이 주소가 되는 것이다.

위에서는 ptrX가 포인터의 이름이고 

이것이 주소가 되는 것이다.

 

여기서 * 연산자를 쓰자

여기서는 곱하기로 쓰이지 않는다.

간접 참조, 간접값으로 쓰인다.

그렇게 되면 해당 주소가 가리키는 변수의 값을 나타낼 수 있다.

 

앞서 말했듯이 포인터가 필요한 이유는 일반 변수와는 다르게

이미 생성되어 저장되어 있는 값에 무엇인가 작업을 하고 싶을 때다.

 

어떤 함수에서 주소가 필요한 이유가 바로 그것이다.

아래 2개의 함수를 비교해보자

 

1번

bool sorting(pair <int,int> p1, pair <int,int> p2) { 
    if (p1.first == p2.first) { // x 좌표가 같다면
        return p1.second < p2.second; // y 좌표를 오름차순으로
    }
    
    return p1.first < p2.first; // x 좌표가 같지 않다면 x 좌표를 오름차순으로
}

2번

bool comp(const pair<int, int>& a, const pair<int,int>& b){
    if (v[a.first]<v[b.first]) {
        return true;
    }
    else if(v[a.first] == v[b.first] && a.second < b.second){
        return true;
    }
    //first로 내림차순, 같다면 second로 오름차순
    return false;
}

 

1번은 해당 값을 복사해서 비교한다

2번은 해당 값을 참조해서 비교한다

 

1. 해당 값으로 연산을 해봤자 기존의 값은 변경을 못한다(주소를 모르기 때문)

2. 해당 값으로 연산을 하고 기존의 값도 변경 가능하다(주소를 알기 때문)

 

그 주소는 포인터에 담겨있다는 것을 잘 기억하기를 바란다.

 

 

반응형
그리드형