반응형

0. 시작하기

 

 

1. New 키워드에 대해서

앞에서 동적할당에 대해서 간단하게 말씀드렸습니다. 여기서는 메모리와 연결해서 조금 더 자세하게 알아보도록 하겠습니다.

 

int *pt = new int[10];

위의 간단한 코드를 메모리측에서 보면 Heap영역과 Stack영역을 나눠서 봐야합니다.

 

앞서 다른 게시물에서 언급했듯 Heap영역은 동적할당시 사용되는 메모리 영역이며, Stack영역은 지역변수(변수,포인터, 참조자 등 모두해당됩니다.)가 사용하는 메모리 영역입니다.

 

따라서 위의 과정을 메모리 영역에서 보면 아래와 같은 모습을 볼 수 있습니다.

 

 

int *pt = new int[10] 에 대한 메모리 추상화

 

new int[10] 이라는 코드가 heap영역에 공간을 확보하고 해당 공간의 주소값을 반환한다고 했는데, 확인해보겠습니다.

 

#include <iostream>
using namespace std;

int main(void){
    cout << (new int[10]) << endl;

    return 0;
}

실행결과

 

 

위와같이 new 키워드의 결과를 출력하면 주소값을 반환하는 것을 볼 수 있습니다. 때문에 주소값을 저장하기 위해 포인터(*)를 사용하는 것입니다.

 

위에서 heap영역과 stack영역을 구분해서 말씀드린 이유가 있습니다!!

 

가장 중요한 이유, 그것은 stack영역에 저장되는 데이터는 함수가 종료되는 시점까지만 유효한 값을 갖습니다.(지역변수) 반면 heap영역은 할당받은 메모리를 반환할때까지 유효한 값을 유지합니다.

 

이에대한 두가지 예제를 보겠습니다.

 

첫번째는 stack 영역에 저장된 값의 주소를 반환한 경우입니다.

 

#include <iostream>
using namespace std;

int* func(){
    int x=10;
    return &x;
}


int main(void){
    int *pt = func();
    cout << *pt << " : " << pt << endl;

    return 0;
}

경고 메시지
실행결과

 

해당 코드를 실행하면 위와같은 [경고메시지]와 [실행결과]를 볼 수 있습니다.

 

다음으로는 heap 영역에 저장된 값의 주소를 반환하는 경우입니다.

 

#include <iostream>
using namespace std;

int* func(){
    int *x = new int;
    *x = 10;
    return x;
}


int main(void){
    int *pt = func();
    cout << *pt << " : " << pt << endl;

    return 0;
}

실행결과

 

후자에서는 [경고메시지]없이 [실행결과]를 얻을 수 있었습니다.

 

의문이 들 수 있습니다. [경고메시지가 출력되기는 했지만, 실행만 잘 되면 괜찮은거 아닌가요?] 라는 의문.

 

답은 절대로 안됩니다!

 

 

 

여러분이 프로그램을 설계할 때, 심심하면 한번씩 이상한 값을 출력하길 원하는게 아니라면, 절대 위와같은 경고메시지(⚠️)를 무시하시면 안됩니다.

 

위의 경고메시지가 발생하는 원인은 한가지 입니다.

 

-> 유요하지 않은 값, 즉 해당 메모리는 다른 값에 의해서 언제든지 덮에 씌워질 수 있는 상태라는 것입니다.

 

새로운 값이 스택영역에 저장되면 해당 공간에는 다른 값이 저장되고, 그때는 10이 아닌 다른 값을 출력할 것입니다. [경고메시지]의 출력을 제거한 상태라면 위와같은 문제로 프로그램이 꼬였을 때, 문제를 찾기도 힘들죠.

 

반면 후자에서 사용한 heap영역의 주소를 반환했을 때에는 함수가 종료된 후에도 [heap 영역의 데이터] 가 유효한 상태를 유지하기 때문입니다. heap영역의 데이터는 프로그램이 종료되거나, 해당 공간을 os에 반환할 때까지 유효한 값을 갖습니다.

 

 

 

OS로부터 할당받은 공간을 사용 후 반환, 그리고 다시 해당 값을 참조하는 것은?

 

이건 더 큰 문제를 야기할 수 있습니다. heap영역은 가변가능한 공간입니다. 사용하지 않을경우 OS에 반환(또는 회수)되는 공간이죠.

 

이렇게 반환된 공간은 다른 프로그램에 의해서 사용될 수 있습니다.

 

운이 나쁘다면(?) 반환된 공간을 참조함에따라 다른 프로그램이 사용중인 영역의 값을 프로그램이 요청하게되고, 이것은 운영체제의 프로세스 관리 규칙에 위배됨에따라 강제로 해당 프로그램이 종료되는 문제를 발생시킬 수 있습니다.

 

 

 

2. Delete 키워드에 대해서

사실 New 키워드에 대해서 자세히 다룬 덕분에 delete에 대해서는 크게 말씀드리게 없습니다.

 

단일 객체에 대한 할당을 제거할 시 delete를 사용하고, 배열 객체에 대한 할당 제서기 delete[]를 사용한다는 차이와 delete를 사용한 후에도 해당 영역에 대한 참조가 가능하지만, 실제로 접근하면 new키워드의 마지만 부분에서 언급했던 문제와 같은 일이 생길 수 있다는 것만 말씀드리면 될 듯합니다.

 

 

 


...더보기

해당내용은 사용하는 운영체제(OS)와 컴파일러, IDE에 따라서 차이가 있을 수 있는 내용입니다.

반응형
반응형

0. 시작하기

 

1. 동적할당이란?

C, Java와 같은 언어에 익숙하다면 동적할당이라는 개념에 대해서 익숙하실 수 있습니다. 하지만, Python과 같은 언어에 익숙하다면 많이 생소한 개념일 수 밖에 없습니다.

 

동적할당의 반대는 당연히 '정적할당'이라는 것이 될 것입니다.

 

정적할당은 C언어에서 변수의 크기를 정하고 해당 크기만큼을 메모리에 할당하는 것을 말합니다.

 

int x[20];

 

이라는 코드는 x라는 변수에 20개의 '정수'를 저장할 수 있는 배열공간을 할당한다는 의미입니다. 이것을 정적할당이라고 합니다. 선언과 동시에 변수의 크기를 정하기 때문입니다.(즉, 컴파일되는 과정에서 변수의 크기가 결정됩니다.)

 

int형은 기본적으로 4Byte의 크기를 갖기 때문에 x라는 배열은 80Byte의 공간을 할당받습니다.

 

여기서 문제가 생깁니다. 만약 저장할 데이터가 80Byte의 공간에 다 들어가지 못한다면 어떻게 할까요?

 

만약 80Byte의 공간에 그 이상의 데이터를 넣게되면 프로그램은 컴파일과정에서 [Out Of Index]라는 애러를 내뱉거나, 실행과정에서 [OverFlow]라는 끔찍한 상황을 만듭니다.

 

이러한 문제를 해결하기 위해 등장한 것이 '동적할당'입니다.

 

 

동적할당은 컴파일 과정에서 변수의 크기가 결정되는 것이 아니라, 프로그램이 실행되는 과정에서 필요한만큼 저장공간을 할당받는 것을 의미합니다. 이때 이용하는 데이터영역을 Heap이라고 합니다.

 

프로그램은 실행되는 과정에서 변수에 더 큰 공간을 할당할 필요가 생긴다면, 동적으로 Heap영역에 이 변수를 위한 공간을 마련합니다. 만약 Heap영역이 부족하게된다면, 운영체제로부터 더 많은 메모리에 대해나 할당을 요청하고 운영체제가 메모리를 해당 프로그램에 할당하면 해당영역을 Heap영역으로 지정하여 변수에 더 많은 공간을 할당합니다. 이것이 동적할당의 과정입니다.

 

만약 운영체제가 해당 프로그램이 요구하는 만큼의 메모리공간을 할당하지 못한다면 프로그램은 데이터처리에 문제가 생기고 이에따라 애러를 발생시킬 것입니다.( = Ram 용략이 작은 경우 메모리가 터져 게임이 튕기는 이유 )

 

 

 

2. 동적할당의 이용 ( CPP/NewDelete.cpp )

C언어에서는 문자열을 처리하기 위해서 동적할당을 많이 사용했습니다. 하지만, C++에선 string을 정식으로 제공하기 때문에 문자열처리에서 동적할당을 사용할일이 줄었습니다. 하지만, 동시에 클래스와 객체에 대한 개념이 생기면서 이러한 new와 delete 키워드를 사용할 일 자체는 훨씬 늘었다고 볼 수 있습니다.

 

아직 클래스에대해서 다루지 않았지만, 쉬운 예제를 통해 어떤식으로 new와 delete 키워드가 사용되는지 알아보겠습니다.

 

#include <iostream>
using namespace std;

class MyClass{
public:
    void out(){
        cout << "MyClass -> out" << endl;
    }
};

int main(){
    MyClass *cl1 = new MyClass;
    cl1->out();

    delete cl1;

    return 0;
}

실행결과

위에는 간단한 클래스와 클래스 메서드를 하나 만들고 메인함수에서 해당 클래스를 생성 및 메서드 호출의 과정을 담고있습니다.

 

어렵게 생각하지 말아주시고, main함수 부분에서 new를 이용하여 클래스를 생성하고 delete를 이용해서 생성한 클래스를 제거해주는 과정이라는 것만 알아주면 좋을 것 같습니다.

 

new와 delete 키워드는 아래와 같은 역할을 합니다.

 

  • new : Heap영역에 요청한 만큼의 동적공간을 할당합니다. (해당 공간의 주소를 반환합니다. -> 때문에 포인터로 받음)
  • delete : 할당받은 영역은 OS에게 반환합니다. (반환된 영역을 데이터를 참고하면 문제가 생길 수 있습니다.)

 

new와 delete 두 가지 모두 메모리에 공간을 할당/반환하는 역할을 하기 때문에 사용에 있어서 주의가 필요합니다.

 

다음 게시글에서 이 두 키워드에 대해서 조금더 알아보도록 하겠습니다.(중요~!)

반응형
반응형

C언어가 사용하는 메모리 구조


XCode를 이용하였지만, 보는데에는 큰 지장이 없습니다.


1. C언어가 사용하는 메모리

C언어의 메모리 관리와 관련된 이미지를 검색해보면 정말 다양한 내용들을 볼 수 있었다. 근데 조금씩 다르다. 그래서 위키를 기본으로해서 내용을 증명해가는 방식으로 진행해보도록 해보겠다.


<출처 : https://ko.wikipedia.org/wiki/동적_메모리_할당>



위 이미지가 C언어의 메모리구조라고 나와있는 그림이다. 이미지에 나와있는 예시들을 통하여 실제 데이터의 주소를 알아보도록 하겠다.



2) STACK영역


먼저 지역변수가 저장되는 STACK영역이다.


   

< 좌측 : 코드     우측 : 실행결과 >

(왜인지 변수를 초기화 안해도 쓰레기 값이 아닌 0이 들어가있다. XCode라 그런가...?)


< 변수 cnt의 메모리상 위치 >


위키의 내용에 따르면 현재 cnt라는 지역변수가 저장되어있는 '0x7ffeefbff53c'라는 위치는 STACK영역의 가장 하단부분이 해당될 것이다.


그렇다면 STACK영역보다 아래(더 뒤에있는 영역)는?


         

< 좌 : 코드     우 : 실행결과 >


위 결과와 같이 이후의 영역의 내용을 불러와도 정상적으로 값을 불러오는 것을 보면, 이 부분 역시 해당 프로그램이 사용하는 영역이라는 것을 알 수 있다.


이부분에 대한것은 아쉽게도 위키의 이미지에서는 찾아볼 수 없다. 따라서 이부분은 나중에 확인해보도록 하자.



그럼 다음으로 넘어가기전에 왜 이 영역을 STACK영역이라고 부르는지 알아보도록 하자.


이유는 다음 코드와 메모리의 상태를 보면 알 수 있다.


< 코드 >



< 메모리 공간상 위치 >


위의 내용을 보게되면, cnt1 ~ 3의 순서로 선언했음에도 메모리에는 cnt3이 가장 먼저 보이는 결과를 알 수 있다.


이것은 STACK이라는 영역은 선언되는 변수에게 공간을 할당할 때, STACK영역의 가장 마지막 부분부터 공간을 할당한다는 것을 의미한다.


만약 STACK이라는 자료구조를 알고있다면, 쉽게 이해가 가능할 것이다.


다만 이게 정해진 규칙은 아니다. 메모리관리는 운영체제에 따라 달라질 수 있다. 지금 내가 작업중인 맥OS(XCode)에서는 위와같이 작동했지만, 다른 운영체제, 다른 IDE에서는 다른 규칙에 따라서 작업을 수행할 것이다.



그런데 문제가 있다. 바로 배열을 선언했을 때인데, 아래 그림을 보면서 생각해보자.


< 코드 >


< 메모리 공간상 위치 >


이제까지 내용을 기본으로하면 [ cnt3 - arr2 - cnt2 - arr1 - cnt1 ]의 순서로 메모리에 데이터들이 쌓여있어야 하는데, 실제 데이터의 위치를보면, [cnt3~1 - arr2 - arr1] 순서로 스택에 데이터가 쌓여있는 것을 볼 수 있다. 또한, 각 데이터들의 사이사이에 불필요해보이는 메모리 공간이 보이는 것을 알 수 있다.


어째서일까? 한번 생각해보도록 하자


3) HEAP 영역

HEAP영역은 [ 동적 메모리 할당 ]이라는 녀석을 다루는데 있어서 빠질 수 없는 장소이다. 왜냐하면 malloc이라는 함수를 통해서 변수가 할당받는 메모리가 HEAP영역이기 때문이다.



< 코드 >


다음과 같은 코드를 작성했다고 해보자, [ int *heap_data ] 라고 선언한걸보면, heap_data 라는 녀석은 특정한 주소값을 갖고 있다는 것을 알 수 있다. 그렇다면 메모리 영역에서 이것들이 어떤식으로 존재하는지 확인해보자.


< heap_data 의 메모리상 위치 >



< *heap_data의 메모리상 위치 >


위에 두 그림 중 위에있는 이미지를 먼저 확인해보자, 


위에있는 녀석은 주소값을 갖고있는 포인터이다. 64bit 컴퓨터에서 메모리 공간을 포인팅하기 위해서는 동일한 64bit가 필요하다. 따라서 총 8byte의 공간이 할당되었다.


리틀엔디언 방식을 따르기때문에 뒤에서부터 두개씩 읽어야하는데, 읽어보면 [ 00 00 00 01 00 64 1A E0 ]  라는 주소값을 갖고있는 것을 알 수 있다. 그럼 해당주소를 확인해보자.


두번째 그림이 해당주소를 확인해본 결과이다. (놀랍게도? 아니면 당연하게도?) 해당 주소에는 코드에서 지정한것과 같은 값이 저장되어있는 것을 알 수 있다.




그럼 정리해보자, C언어에서 데이터를 참조하기 위해서는 변수가 필요하다. 여기서 heap_data라는 녀석은 하나의 포인터 변수이다. 이녀석은 이전에 말했던 STACK영역에 존재한다.


하지만, 이 녀석이 가리키고있는 데이터[ *heap_data ]는 동적할당받은 HEAP영역에 존재한다.




4) STACK과 HEAP의 확장

C언어의 메모리에 대해서 관심을 갖고 찾아보다보면 아래와 같은 그림을 볼 수 있다.


이 그림에서 중점적으로 봐야할 부분은 바로 회색 화살표이다. 그렇다고 'HEAP영역은 위에서 아래로 데이터 공간을 할당하고,  STACK영역은 아래서 위로 데이터 공간을 할당한다.' 라고 하는 것은아니다. 


다만 OS라고하는 운영체제는 미래를 알지는 못해서 프로그램이 수행되는 과정에서 얼마만큼의 동적할당이 발생할지, 스택영역은 정확하게 얼마만큼이 필요한지 정확하게 알지 못한다. 그렇기 때문에 적당한 메모리를 할당하고, 이후 추가적으로 메모리가 필요할 경우 두 위 그림과 같은 방향으로 프로그램이 가용할 수 있는 메모리 크기를 확장한다.


그렇다면 HEAP영역을 무한히 늘리면 언제가는 스택영역을 침범하지 않을까? 하는 의문이 발생한다.


생각해보자.



반응형

+ Recent posts