Programming/C, C++

[C++] shared_ptr (1) - 필요성에 대해 알아보기

변화의 물결1 2025. 6. 20. 14:46

 

 

 

안녕하세요.

 

 C++에서 메모리 관리는 중요한 요소 중 하나입니다. 특히 new와 delete를 사용하여 직접 메모리를 할당하고 해제하는 방식은 유연성을 제공하지만, 동시에 다양한 문제점을 야기할 수 있습니다.

 이번 글에서는 이러한 문제를 해결하기 위한 방법 중 std::shared_ptr에 대해 알아보겠습니다.

 


 

1. shared_ptr란 무엇일까요?

 

 std::shared_ptr는 C++11부터 표준 라이브러리에 포함된 스마트 포인터(Smart Pointer)의 한 종류입니다.

 스마트 포인터는 이름 그대로 일반적인 "날 것의 포인터(raw pointer)"가 가지고 있는 단점을 보완하기 위해 만들어진 객체 래퍼(object wrapper)입니다.

 

 shared_ptr의 핵심은 참조 횟수(reference count)를 통해 객체의 생명 주기를 관리한다는 점입니다.

 여러 shared_ptr 객체가 하나의 자원을 공유할 때마다 내부적으로 참조 횟수를 증가시키고, shared_ptr 객체가 소멸되거나 더 이상 자원을 참조하지 않게 되면 참조 횟수를 감소시킵니다.

 

 참조 횟수가 0이 되는 순간, shared_ptr는 자신이 관리하던 메모리를 자동으로 해제합니다.

 

 

2. shared_ptr의 동작 원리

 

 shared_ptr는 RAII(Resource Acquisition Is Initialization) 원칙을 따릅니다. 이는 자원(메모리)이 획득되는 즉시 초기화되고, 자원을 소유하는 객체가 범위를 벗어나거나 소멸될 때 자동으로 자원이 해제되는 방식입니다.

 

<기존 new, delete 방식과의 차이점>

 

(1)명시적인 delete 호출 불필요

 shared_ptr는 참조 횟수에 따라 자동으로 메모리를 해제합니다.

 

(2) nullptr 초기화 불필요

 shared_ptr는 기본적으로 nullptr로 초기화되거나, reset()을 통해 초기화할 수 있습니다.

 

(3) 할당 및 해제 방식  

 shared_ptr는 std::make_shared 함수를 통해 안전하고 효율적으로 생성하는 것이 권장됩니다. 또한, shared_ptr::reset() 멤버 함수를 사용하여 새로운 자원을 할당하거나 현재 자원의 참조 횟수를 감소시킬 수 있습니다

 

 

3. 기존 포인터 방식의 문제점 및 스마트 포인터를 사용해야 하는 이유

 

3.1) 원시 포인터 사용의 위험성

 

 아래 코드는 new와 delete를 사용하여 int 타입의 메모리를 할당하고 해제하는 기본적인 예시입니다.

 

#include <stdio.h>

void main() {
    int* ip = nullptr;
    ip = new int;
    *ip = 10;
    printf(" %d\n", *ip);
    delete ip; 
}

 

 

 

 

위 코드는 단순해 보이지만, 실제 복잡한 시스템에서는 메모리 누수(memory leak)나 이중 해제(double free)와 같은 문제가 발생할 수 있습니다.

 

 특히 여러 곳에서 하나의 메모리 영역을 공유하여 사용하는 경우, 한 곳에서 delete를 호출하면 다른 곳에서는 이미 해제된 메모리에 접근하려 시도하는 댕글링 포인터(dangling pointer) 문제가 발생할 수 있습니다.

 

 

 

3.2) 메모리 접근 문제 상황 발생하는 경우

 

  예시를 통해 문제를 더 자세히 살펴보겠습니다. CRef 클래스는 int* 타입의 멤버 변수를 가지고 있어 외부에서 할당된 int 포인터를 참조합니다.

 

#include <stdio.h>

class CRef
{
    int* ipRef;
public :
    CRef() { ipRef = nullptr; }
    void setData(int* _ip) { ipRef = _ip; }
    int getData() const { return *ipRef; }
};

void main() {
    int* ip = nullptr;
    ip = new int;

    CRef cRef;
    *ip = 10;
    cRef.setData(ip);
    printf(" %d\n", cRef.getData()); 

    delete ip; 
    ip = nullptr;

    // 이미 해제된 메모리에 접근하려 시도 (댕글링 포인터)
    printf(" %d\n", cRef.getData()); 
}

 

 

 

위 코드에서 delete ip;가 실행된 후에는 ip가 가리키던 메모리 공간은 더 이상 유효하지 않습니다. 하지만 cRef 객체 내부의 ipRef는 여전히 이 해제된 메모리 주소를 가리키고 있습니다.

 

 따라서 printf(" %d\n", cRef.getData());를 호출하면 이미 해제된 메모리에 접근하게 되어 프로그램이 크래시(crash)되거나 예측 불가능한 결과를 초래할 수 있습니다. 이것이 바로 원시 포인터로 자원을 공유할 때 발생하는 심각한 문제입니다.

 

3.3) shared_ptr를 이용한 문제 해결

 

 std::shared_ptr를 사용하여 위에서 발생한 문제를 어떻게 해결하는지 살펴보겠습니다.

 

#include <stdio.h>
#include <memory> // shared_ptr를 사용하기 위해 포함

class CRef
{
    std::shared_ptr<int> ipRef; 
public :
    CRef() { }
    void setData(std::shared_ptr<int> _ip) { ipRef = _ip; }
    int getData() const { return *ipRef; }
    bool isRef() const { return (bool)ipRef; } // shared_ptr는 bool 타입으로 캐스팅 가능
    void release() { ipRef.reset(); } 
};

void main() {
    std::shared_ptr<int> ip = nullptr;
    ip.reset(new int); 

    CRef cRef;
    *ip = 10;
    cRef.setData(ip); // shared_ptr를 복사하여 전달 -> 참조 횟수 증가
    printf(" %d\n", cRef.getData()); 

    ip.reset(); 
    
    if (cRef.isRef()) {
        printf(" %d\n", cRef.getData()); 
    }

    cRef.release(); 

    if (cRef.isRef()) {
        printf(" %d\n", cRef.getData());
    }
    else { 
        printf("released \n");
    }
}

 

 

 

 

위 코드를 보면 CRef 클래스의 ipRef가 int* 대신 std::shared_ptr<int>로 선언되었습니다.

 

(1) std::shared_ptr<int> ip = nullptr;로 shared_ptr를 선언하고, ip.reset(new int);로 메모리를 할당합니다.

 (std::make_shared<int>(10);와 같이 사용하는 것이 더 효율적이고 안전합니다.)

 

(2) cRef.setData(ip); 호출 시, ip가 가리키는 객체를 cRef 내부의 ipRef도 함께 가리키게 되며, 이 과정에서 참조 횟수가 증가합니다.

 

(3) ip.reset();을 호출하면 ip가 더 이상 해당 메모리를 관리하지 않게 되므로 참조 횟수가 1 감소합니다.

 하지만 cRef가 여전히 해당 메모리를 참조하고 있으므로 참조 횟수는 0이 되지 않으며, 메모리는 해제되지 않습니다.

 따라서 cRef.getData()는 여전히 유효한 값을 반환합니다.

 

(4) cRef.release();를 호출하면 cRef 내부의 ipRef가 관리하던 메모리의 참조 횟수를 다시 1 감소시킵니다.

 이제 해당 메모리를 참조하는 shared_ptr가 없으므로 참조 횟수가 0이 되고, shared_ptr가 자동으로 메모리를 해제합니다.

 

 

 이처럼 shared_ptr는 참조 횟수를 통해 여러 객체가 동일한 자원을 안전하게 공유하고, 더 이상 아무도 자원을 사용하지 않을 때 자동으로 해제되도록 보장합니다.

 

 이는 개발자가 메모리 해제 시점을 일일이 추적하고 delete를 호출하는 실수를 줄여주며, 안전한 코드를 작성하는 데  도움을 줍니다.

 

 

감사합니다.

 

 

<참고 사이트>

1. shared_ptr in C++

https://www.geeksforgeeks.org/cpp/shared_ptr-in-cpp/

2. std::shared_ptr

https://en.cppreference.com/w/cpp/memory/shared_ptr.html

 

 

반응형