C++ < 자원 관리(스택 메모리, 힙 메모리, 스마트 포인터) >

2026. 3. 9. 16:46TIL

메모리는 프로그램이 실행되면 데이터와 정보가 저장되는데, 공간에 한계가 있기 때문에 무한정 데이터를 담을 수는 없다.

 

이 예시로 주차장을 들 수 있다.

차량을 세울 수 있는 공간은 정해져 있는데, 처음에 빈 자리가 많아 차가 들어와도 쉽게 주차를 할 수 있지만, 시간이 지나면서 차량이 계속 들어오면 주차 공간이 점점 줄어들게 된다. 결국 모든 차가 채워지면 더 이상 차를 세울 수가 없게 된다.

이 때 새로운 차가 들어오게 되면 주차할 공간이 없고, 억지로 들어오려고 하면 통로가 막히거나 다른 차량의 이동을 방해하게 된다.

그래서 어떤 차는 밖에서 기다리거나 기존에 있던 차량이 나가야만 새로운 차량으 들어올 수 있게 된다.

 

그래서 프로그램을 설계할 때 필요한 만큼만 메모리를 사용하고, 사용이 끝난 메모리는 적절히 비워 주는 관리 과정이 매우 중요하다.

오늘은 이 자원 관리에 대해서 알아보려고 한다.

 


스택 메모리

스택 메모리는 프로그램이 실행될 때 함수와 지역 변수들이 임시로 저장되는 메모리 영역이다.

특히 함수 안에서 선언되는 변수들은 대부분 이 스택 영역에 만들어진다.

 

스택 메모리의 가장 큰 특징은 자동 관리이다. 변수가 선언되면 메모리가 자동으로 할당되고, 해당 영역의 실행이 끝나면 자동으로 메모리가 사라진다. 그래서 직접 메모리를 해제할 필요가 없다.

 

기본 원리

이름 그대로 접시를 쌓는 구조와 비슷하게 동작한다.

LIFO구조라고 부르는데, Last In First Out의 약자로 나중에 들어온 것이 먼저 나간다.

  • 새로운 함수가 실행되면 메모리가 그 위에 쌓인다.
  • 함수 실행이 끝나면 가장 위에 있는 메모리부터 제거된다.

힙 메모리

힙 메모리는 프로그램 실행 중 필요할 때 직접 메모리를 생성해서 사용하는 공간이다.

스택 메모리와 달리 자동으로 생성되거나 자동으로 사라지지 않는다. 개발자가 필요할 때 메모리를 만들어 사용이 끝나면 직접 해제해야 한다.

 

이러한 방식으로 메모리를 사용하는 것을 동적 메모리 할당이라고 한다.

 

기본 개념

int* ptr = new int(10);

delete ptr;

이 코드는 힙 메모리에 정수 하나를 생성하고 해제하는 코드이다.

 

Stack 영역          Heap 영역
      
ptr  		 	10

메모리 구조를 간단하게 표현하면 다음과 같다.

ptr이라는 포인터 변수는 스택에 있고 실제 데이터 10은 힙 메모리에 저장되어 있다.

그리고 사용이 끝나면 반드시 delete ptr;로 메모리를 해제해야한다.

 

힙 메모리는 이렇게 메모리를 해제하지 않으면 프로그램이 끝날 때까지 메모리가 계속 남아 있게 된다.

이를 메모리 누수라고 한다. 메모리 누수란, 메모리를 만들었지만 아무도 사용하지 않는 상태를 말한다. 그래서 꼭 delete로 메모리를 해제해 줘야 한다.

 


Dangling Pointer

Dangling Pointer는 이미 해제된 메모리를 계속 가리키고 있는 포인터를 의미한다. 즉, 메모리는 사라졌는데 포인터만 남아 있는 상태이다.

 

포인터는 단순히 메모리 주소만 저장하는 변수이기 때문에, 해당 메모리가 삭제되었는지 자동으로 알 수 없다. 그래서 이미 삭제된 메모리를 사용하려 하면 프로그램에서 오류가 발생할 수 있다.

 

대표적으로 

int* ptr = new int(40);
int* ptr2 = ptr;

delete ptr;

이런 상황에서 ptr2를 사용하면 Dangling Pointer가 발생한다.

 

Dangling Pointer의 위험성

Dangling Pointer는 프로그램의 안정성을 크게 떨어뜨리는 원인이 된다.

이는 다음과 같은 문제를 발생시킬 수 있다.

  • 프로그램이 갑자기 종료될 수 있다.
  • 예상하지 못한 값이 출력될 수 있다.
  • 다른 메모리 영역을 침범할 수 있다.

이를 방지하기 위해서는 어떻게 해야 할까?


스마트 포인터

스마트 포인터는 메모리를 자동으로 관리해 주는 포인터 객체이다. 즉, 우리가 delete를 직접 호출하지 않아도 객체의 생명주기가 끝나면 자동으로 메모리를 정리한다.

위에서 나온 Dangling Pointer, 메모리 누수 등 다양한 메모리 문제를 방지하기 위해 사용한다.

 

스마트 포인터는 크게 세 가지 종류로 나뉜다.

  • unique_ptr
  • shared_ptr
  • weak_ptr

각각의 역할이 서로 다르다.

 

unique_ptr

unique_ptr은 하나의 객체를 단 하나의 포인터만 소유할 수 있는 스마트 포인터이다.

사용 예시는 다음과 같다.

unique_ptr<int> ptr = make_unique<int>(10);

이 코드는 힙 메모리에 10을 생성하고 ptr이 그 메모리를 단독으로 관리한다.

 

메모리 구조를 그림으로 보면 다음과 같다.

Stack
----------
>> ptr

Heap
----------
>> 10

그리고 ptr은 복사가 불가능하다.

 

아래는 컴파일 에러가 발생하는 유니크 포인터의 예시이다.

unique_ptr<int> ptr1 = make_unique<int>(10);
unique_ptr<int> ptr2 = ptr1;

한 객체를 두 포인터가 동시에 소유하면 unique(유일하다)의 의미가 깨진다.

이렇게 복사는 불가능하지만 소유권의 이동은 가능하다.

 

unique_ptr<int> ptr1 = make_unique<int>(20);
unique_ptr<int> ptr2 = move(ptr1);

move()를 사용해서 

 

ptr1 >> nullptr

ptr2 >> 20

 

이렇게 결과가 바뀐다.

 

클래스 객체 관리

스마트 포인터는 일반 클래스도 관리할 수 있다.

unique_ptr<MyClass> obj = make_unique<MyClass>(42);

이렇게 하면

클래스 객체 생성 >> unique_ptr이 객체 관리 >> 범위 종료 시 자동 삭제

 

흐름으로 클래스를 unique_ptr로 관리할 수 있다.


shared_ptr

shared_ptr은 여러 포인터가 하나의 객체를 함께 소유할 수 있는 스마트 포인터이다.

 

여기서 핵심 개념은 참조 카운트(reference count)이다.

이 객체를 참조하는 포인터 개수를 카운트하는 것 이다.

 

shared_ptr<int> ptr1 = make_shared<int>(10);
shared_ptr<int> ptr2 = ptr1;

이런식으로 포인터를 설정하고 복사하면 두개가 아래와 같이 같은 메모리를 가리킨다.

Stack
----------------
ptr1 ----\
          \
ptr2 ------> Heap
            -----
              10

Reference count = 2

 

ptr2.reset();
Reference count = 1

하나가 삭제되면 메모리는 하나가 남아있게 되고, 나머지 하나가 사라지면 자동으로 메모리가 삭제된다.

 

shared_ptr 문제점 (순환 참조)

shared_ptr은 순환 참조라는 문제가 있다.

예를 들면

 

A >> B

B >> A

 

처럼 서로를 참조하는 경우이다.

 

이 상황에서는

  • 참조 카운트가 0이 될 일이 없음.
  • 프로그램이 끝나도 메모리가 삭제되지 않음
  • 메모리 누수 발생

이런 문제가 발생한다.

 

이 문제를 해결하기 위해 weak_ptr이 존재한다.


weak_ptr

weak_ptr은 객체를 소유하지 않는 포인터이다.

즉, 참조는 하지만 reference count를 증가시키지 않는다.

 

shared_ptr과 다른 점은

shared_ptr >> 소유 + 참조
weak_ptr   >> 참조만

이런 특징이 있다.

 

사용 방법은 다음과 같다.

if (auto sp = weak.lock())
{
    sp->doSomething();
}

 

이는  다음 의미와 같다.

 

객체가 아직 살아있다면

if          shared_ptr 생성 후 사용 

else    객체가 이미 삭제되었다면 nullptr 반환

 

사용 예시는 다음과 같다.

class A {
    shared_ptr<B> b_ptr;
};

class B {
    weak_ptr<A> a_ptr;
};

이렇게 사용하면 정상적으로 프로그램이 끝날 때 메모리가 해제된다.

 


얕은 복사와 깊은 복사

얕은 복사

얕은 복사는 데이터 자체를 복사하는 것이 아니라 주소값만 복사하는 방식이다.

즉, 두 포인터가 같은 메모리를 가리키게 된다.

 

int* A = new int(30);
int* B = A;

예시 코드이다.

A와 B는 같은 메모리를 공유한다는 의미이다.

 

여기서 A의 메모리를 해제하면

B는 여전히 그 주소를 가지고 있으므로 오류가 발생하며 Dangling Pointer가 발생한다.

 

얕은 복사의 특징으로는 다음과 같다.

  • 메모리 주소만 복사된다
  • 두 객체가 같은 데이터를 공유한다
  • 한쪽에서 메모리를 삭제하면 문제가 발생할 수 있다
  • dangling pointer가 발생할 위험이 있다.

 

깊은 복사

깊은 복사는 데이터를 새 메모리에 완전히 복사하는 방식이다.

예시는 다음과 같다.

int* A = new int(30);
int* B = new int(*A);

new int로 B는 새 메모리를 생성하고 A의 값을 복사한다는 의미이다.

 

A와 B는 서로 다른 메모리를 가지고 있기 때문에 A가 삭제되어도 B는 문제 없이 동작한다.

 


오늘은 조금 어렵지만 개발을 할 때 가장 중요한 메모리의 정리를 공부해봤다.

오늘 배운 핵심 내용들을 다시 정리해보면 다음과 같다.

 

스택 메모리

  • 함수나 지역 변수가 저장되는 메모리의 영역이다.
  • 변수가 선언되면 자동으로 메모리가 할당되고, 범위를 벗어나면 자동으로 해제된다.
  • 개발자가 직접 메모리를 관리할 필요가 없다는 특징이 있다.
  • LIFO(Last In First Out)구조로 동작한다.

 

힙 메모리

  • 프로그램 실행 중 필요한 메모리를 직접 생성해서 사용하는 영역이다.
  • new로 메모리를 할당하고 delete로 직접 해제해야 한다.
  • 메모리를 해제하지 않으면 메모리 누수(memory leak)가 발생한다.
  • 동적 데이터나 크기가 유동적인 데이터를 저장할 때 사용된다.

 

Dangling Pointer

  • 이미 해제된 메모리를 가리키고 있는 포인터를 의미한다.
  • 메모리는 삭제되었지만 포인터는 여전히 그 주소를 가지고 잇는 상태이다.
  • 접근할 경우 예상하지 못한 값이 출력되거나, 프로그램 오류, 충돌이 발생할 수 있다.
  • 메모리를 해제한 뒤 포인터를 nullptr로 초기화하거나 스마트 포인터를 사용하여 사전에 방지하는 것이 안전하다.

 

스마트 포인터

  • 메모리를 자동으로 관리해 주는 포인터 객체이다.
  • delete를 직접 호출하지 않아도 객체의 생명주기가 끝나면 메모리가 자동 해제된다.
  • unique_ptr : 하나의 포인터만 객체를 소유할 수 있다.
  • shared_ptr : 여러 포인터가 하나의 객체를 공유하며 reference count를 사용한다.
  • weak_ptr : 객체를 소유하지 않고 참조만 하며, 순환 참조 문제를 해결할 때 사용된다.

 

얕은 복사

  • 데이터가 아닌 메모리 주소만 복사하는 방식이다.
  • 두 객체가 동일한 메모리를 공유하게 된다.
  • 한쪽에서 메모리를 해제하면 다른 포인터가 Dangling Pointer가 될 수 있다.
  • 동적 메모리를 사용하는 객체에서는 문제가 발생할 가능성이 높다.

 

깊은 복사

  • 데이터를 새로운 메모리에 완전히 복제하는 방식이다.
  • 각 객체가 서로 다른 메모리 공간을 사용한다.
  • 한 객체가 삭제디어도 다른 객체에는 영향을 주지 않는다.
  • 동적 메모리를 다루는 클래스에서는 안전한 메모리 관리 방법이다.

아직 메모리 관리에 대해 완전히 익숙해진 것은 아니지만, 이번 내용을 정리하면서 기본적인 개념들을 조금씩 이해할 수 있었다.

앞으로 실제 코드 작성과 다양한 예제를 통해 스택, 힙, 스마트 포인터 등을 더 깊이 익혀가며 안정적인 메모리 관리 방법을 계속 공부해 볼 예정이다.

'TIL' 카테고리의 다른 글

C++ < STL (기초) >  (1) 2026.03.10
C++ < Template(템플릿) >  (0) 2026.03.10
코딩테스트 연습문제 <프로그래머스 - 자릿수 더하기, 약수 더하기 >  (1) 2026.03.09
C++ < 상속, 다형성 >  (0) 2026.03.06
C++ < Class 개념 >  (0) 2026.03.05