안녕하세요.
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
'Programming > C, C++' 카테고리의 다른 글
[C++] if 조건문과 암시적 bool 형변환에 대해 이해하기 (1) | 2025.06.26 |
---|---|
[C++] Thread Local Storage (TLS)인 thread_local 대해 알아보기 (4) | 2025.06.12 |
[C++]컨테이너(Container)와 이터레이터(Iterator) 기초 2 (2) | 2025.05.23 |
[C++] 컨테이너(Container)와 이터레이터(Iterator) 기초 1 (10) | 2025.05.22 |
[C++] 타입 캐스팅 (static_cast, dynamic_cast) 대하여 (0) | 2025.05.10 |