본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍 : 중급] 11. 스마트 포인터

by GDNGY 2023. 6. 12.

Chapter 11. 스마트 포인터

스마트 포인터는 C++에서 동적 메모리 관리를 단순화하는 도구입니다. 그들은 기본 포인터와 비슷하게 동작하지만, 적절한 시점에 자동으로 메모리를 해제하여 메모리 누수를 방지합니다. 스마트 포인터의 기본 개념과 함께, 다양한 스마트 포인터 유형(auto_ptr, unique_ptr, shared_ptr, weak_ptr)에 대해 알아보고, 사용자 정의 스마트 포인터 구현 방법에 대해 학습합니다.

 

반응형

 


[Chapter 11. 스마트 포인터]

 

11.1. 스마트 포인터 이해하기

11.1.1. 스마트 포인터란 무엇인가

11.1.2. 스마트 포인터의 필요성과 사용 이유

11.1.3. 스마트 포인터의 작동 원리

 

11.2. C++ 스마트 포인터 종류

11.2.1. std::auto_ptr (deprecated)

11.2.2. std::unique_ptr

11.2.3. std::shared_ptr

11.2.4. std::weak_ptr

 

11.3. std::unique_ptr에 대해

11.3.1. std::unique_ptr의 정의와 특징

11.3.2. std::unique_ptr의 기본 사용법

11.3.3. std::unique_ptr와 소유권

11.3.4. std::unique_ptr의 예제코드와 활용법

11.3.5. std::unique_ptr 예외 처리

 

11.4. std::shared_ptr에 대해

11.4.1. std::shared_ptr의 정의와 특징

11.4.2. std::shared_ptr의 기본 사용법

11.4.3. std::shared_ptr와 참조 카운팅

11.4.4. std::shared_ptr의 예제코드와 활용법

11.4.5. std::shared_ptr 예외 처리

 

11.5. weak_ptr에 대해

11.5.1. weak_ptr의 정의와 특징

11.5.2. weak_ptr의 기본 사용법

11.5.3. weak_ptr와 순환 참조 문제

11.5.4. weak_ptr의 예제코드와 활용법

11.5.5. weak_ptr 예외 처리

 

11.6. 사용자 정의 스마트 포인터

11.6.1. 사용자 정의 스마트 포인터 구현하기

11.6.2. 사용자 정의 스마트 포인터의 활용 방안

11.6.3. 사용자 정의 스마트 포인터 예제코드와 활용법


11.1. 스마트 포인터 이해하기

스마트 포인터는 C++의 특별한 클래스 타입으로, 포인터처럼 동작하지만 스스로 메모리를 관리합니다. 이는 메모리 누수를 방지하고, 메모리 관리의 복잡성을 줄여줍니다. 스마트 포인터는 객체가 더 이상 필요하지 않을 때 자동으로 삭제하며, 이러한 기능 때문에 '스마트'라고 부릅니다. 스마트 포인터의 필요성, 사용 이유, 그리고 작동 원리에 대해 자세히 알아보도록 하겠습니다. 이 섹션을 통해 스마트 포인터의 핵심 개념을 이해하게 될 것입니다.

11.1.1. 스마트 포인터란 무엇인가

스마트 포인터(smart pointer)는 C++에서 제공하는 포인터의 한 형태입니다. 그러나 일반 포인터와는 다르게 '스마트'한 기능을 가지고 있습니다. 그렇다면 이 '스마트'한 기능이란 무엇일까요? 이것이 바로 자동 메모리 관리입니다.

 

일반적으로 C++에서 동적 메모리를 할당하면 프로그래머는 그 메모리를 직접 해제해야 합니다. 이를 잊어버리면 메모리 누수가 발생하게 되고, 이는 프로그램의 성능을 저하시키는 주요한 원인이 됩니다. 또한, 메모리를 해제한 후에 그 메모리를 다시 사용하려고 하는 '잘못된 해제(dangling reference)' 문제도 발생할 수 있습니다. 이런 문제들은 프로그래밍을 복잡하게 만드는 요인 중 하나입니다.

 

이를 해결하기 위해 등장한 것이 스마트 포인터입니다. 스마트 포인터는 포인터가 가리키는 메모리를 자동으로 해제하는 역할을 합니다. 즉, 프로그래머는 메모리 관리에 대해 크게 신경 쓸 필요가 없습니다.

 

[예제]

#include <memory>

int main() {
    std::unique_ptr<int> smart_ptr(new int(10));
    return 0;
}

 

위 예제에서 std::unique_ptr<int>는 int 타입의 스마트 포인터입니다. 이 스마트 포인터는 new int(10)을 통해 동적으로 할당된 메모리를 가리키고 있습니다. 그러나 가장 중요한 점은 main 함수가 끝나고 smart_ptr이 사라질 때, smart_ptr이 가리키는 메모리도 자동으로 해제된다는 것입니다. 이렇게 해서 프로그래머는 메모리 해제를 신경 쓸 필요가 없게 됩니다. 

 

스마트 포인터는 이러한 메모리 관리 외에도 소유권(ownership) 개념을 도입하여 자원을 안전하게 공유하거나 이동시키는 데도 사용됩니다. 이에 대해서는 다음 섹션에서 자세히 다루도록 하겠습니다. 

 

스마트 포인터는 이처럼 C++에서 메모리 관리를 단순화하고 안전하게 하는 데 큰 도움을 주는 도구입니다. 이것이 바로 '스마트' 포인터라 불리는 이유입니다. 다음 섹션에서는 이 스마트 포인터의 종류와 각각의 특징에 대해 알아보겠습니다.

 

11.1.2. 스마트 포인터의 필요성과 사용 이유

스마트 포인터의 필요성과 사용 이유를 이해하려면 먼저 C++의 메모리 관리 체계와 그에 따른 문제점을 이해해야 합니다. C++에서는 동적 메모리 할당과 해제를 직접 관리해야 합니다. new를 통해 동적으로 메모리를 할당하면 반드시 delete를 통해 그 메모리를 해제해야 합니다.

 

[예제]

int* ptr = new int(10); // 메모리 할당
delete ptr; // 메모리 해제

 

그런데 이런 방식에는 두 가지 큰 문제가 있습니다.

 

첫 번째는 메모리 누수입니다. 동적으로 할당한 메모리를 해제하지 않고 잊어버리면 그 메모리는 계속 시스템에 할당된 상태로 남게 됩니다. 이렇게 되면 메모리 사용량이 증가하고, 결국은 시스템의 성능을 저하시키는 문제가 발생합니다.

 

두 번째 문제는 해제 후 사용(dangling pointer)입니다. 이미 해제된 메모리를 계속 사용하려는 경우에 발생합니다. 이는 데이터 손실이나 시스템 충돌을 일으킬 수 있는 심각한 문제입니다.

 

[예제]

int* ptr = new int(10);
delete ptr;
*ptr = 20; // 잘못된 해제 후 사용

 

이처럼 동적 메모리 관리는 프로그래밍의 복잡성을 증가시키고, 실수로 인한 오류 가능성을 높입니다. 이를 해결하기 위해 등장한 것이 바로 스마트 포인터입니다. 스마트 포인터는 포인터가 가리키는 메모리를 자동으로 해제해 주기 때문에 위와 같은 문제를 효과적으로 방지할 수 있습니다.

 

[예제]

std::unique_ptr<int> smart_ptr(new int(10)); // 메모리 할당

// 메모리 해제는 필요 없음

 

스마트 포인터를 사용하면 메모리를 직접 관리하는 수고를 덜 수 있습니다. 또한 메모리 누수나 해제 후 사용과 같은 실수를 예방할 수 있습니다. 이러한 이유로, C++에서는 가능한 한 스마트 포인터를 사용하는 것이 권장됩니다.

 

이외에도 스마트 포인터는 메모리 이외의 리소스를 관리하는 데에도 사용할 수 있습니다. 파일, 네트워크 연결, 뮤텍스 등의 리소스를 안전하게 관리하기 위해 스마트 포인터를 사용할 수 있습니다.

 

이제 스마트 포인터의 필요성과 사용 이유에 대해 알았으니, 다음 섹션에서는 스마트 포인터의 작동 원리에 대해 알아보겠습니다.

 

11.1.3. 스마트 포인터의 작동 원리

스마트 포인터는 포인터처럼 동작하지만, 메모리를 직접 관리하는 것이 아니라 RAII(Resource Acquisition Is Initialization)이라는 원칙을 사용해 메모리를 자동으로 관리합니다. RAII는 객체의 수명이 그 객체가 소유한 자원의 수명과 동일하게 관리되는 것을 의미합니다. 즉, 객체가 생성될 때 자원을 할당하고, 객체가 소멸될 때 자원을 해제합니다.

 

스마트 포인터는 기본적으로 템플릿 클래스로, 포인터와 같은 방식으로 사용할 수 있습니다. 그러나 스마트 포인터 객체가 소멸될 때, 스마트 포인터가 가리키는 메모리를 자동으로 해제합니다. 이렇게 해서 프로그래머가 메모리 해제를 잊어버리는 문제를 예방할 수 있습니다.

 

[예제]

std::unique_ptr<int> smart_ptr(new int(10)); // 스마트 포인터 생성
// 스마트 포인터가 소멸되면 메모리는 자동으로 해제됩니다.

 

스마트 포인터의 작동 원리를 이해하기 위해서는 C++의 생성자와 소멸자에 대한 이해가 필요합니다. 생성자는 객체가 생성될 때 호출되고, 소멸자는 객체가 소멸될 때 호출됩니다. 스마트 포인터의 경우, 생성자에서 메모리를 할당하고, 소멸자에서 메모리를 해제합니다.

 

스마트 포인터는 내부적으로 '원시 포인터(raw pointer)'를 보관하고 있습니다. 이 원시 포인터는 스마트 포인터가 가리키는 실제 메모리를 가리킵니다. 그러나 사용자는 이 원시 포인터에 직접 접근할 수 없으며, 스마트 포인터가 제공하는 인터페이스를 통해서만 메모리에 접근할 수 있습니다.

 

[예제]

std::unique_ptr<int> smart_ptr(new int(10));
*smart_ptr = 20; // 스마트 포인터를 통해 메모리에 접근

 

스마트 포인터는 또한 '소유권(ownership)' 개념을 도입하여, 동일한 메모리에 대한 접근을 관리합니다.

예를 들어,  std::unique_ptr는 이름에서도 알 수 있듯이, 단 하나의 스마트 포인터만이 메모리를 소유할 수 있습니다. 이는 메모리 해제를 보다 안전하게 만들어줍니다.

 

스마트 포인터는 이러한 방식으로 메모리 관리의 부담을 줄여줍니다. 이제 이해하기 어려웠던 포인터의 개념을 좀 더 쉽게 다룰 수 있게 될 것입니다.

 

이번 섹션에서는 스마트 포인터의 기본적인 작동 원리에 대해 알아보았습니다. 다음 섹션에서는 다양한 종류의 스마트 포인터에 대해 알아보겠습니다. 다양한 스마트 포인터가 있지만, 그중에서도 가장 주요한 몇 가지를 중심으로 살펴보겠습니다.

 

std::unique_ptr, std::shared_ptr, std::weak_ptr 등의 주요 스마트 포인터를 살펴보겠습니다. 각 스마트 포인터는 상황에 따라 적합한 사용법과 특성을 가지고 있습니다. 이들을 이해하고 올바르게 사용하면 C++에서의 메모리 관리가 보다 간편해집니다.

 

그럼에도 불구하고, 스마트 포인터를 사용하더라도 메모리 관리에 대한 주의는 필요합니다. 스마트 포인터는 메모리 누수를 방지하는 데 매우 효과적이지만, 모든 메모리 관리 문제를 해결하지는 못합니다. 예를 들어, 스마트 포인터를 사용하더라도 순환 참조와 같은 문제가 발생할 수 있습니다. 이러한 문제는 프로그래머의 주의를 필요로 합니다.

 

이 섹션에서 배운 내용을 바탕으로, 다음 섹션에서는 각 스마트 포인터의 상세한 사용법과 특성에 대해 알아보겠습니다. 이를 통해 여러분은 C++의 메모리 관리에 대한 이해를 더욱 높일 수 있을 것입니다.

 

이번 섹션에서 배운 내용을 잘 이해하셨다면, 다음 섹션에서는 각 스마트 포인터의 특징과 사용법에 대해 살펴보면서 실제 코드 예제를 통해 스마트 포인터를 어떻게 활용할 수 있는지 알아보는 시간을 가져보겠습니다.


11.2. C++ 스마트 포인터 종류

C++의 스마트 포인터는 다양한 종류가 있으며, 각각이 메모리 관리를 지원하는 독특한 방식을 가지고 있습니다. 이번 장에서는 네 가지 주요 스마트 포인터 - std::auto_ptr, std::unique_ptr, std::shared_ptr, std::weak_ptr - 를 살펴보겠습니다. std::auto_ptr은 C++11 이후로 deprecated 되었지만, 이해를 돕기 위해 간략히 소개합니다. 나머지 세 가지 스마트 포인터는 메모리 소유권을 다루는 방식이 서로 다르므로 상황에 맞게 적절히 사용하는 것이 중요합니다.

11.2.1. std::auto_ptr (deprecated)

std::auto_ptr은 현재 C++11부터 deprecated(사용이 권장되지 않는) 상태이며, 이후에 배울 std::unique_ptr이 그 기능을 대체하게 되었습니다. 하지만 여전히 이해를 돕기 위해 std::auto_ptr에 대해 간략히 설명하고자 합니다.

 

std::auto_ptr은 이름에서도 알 수 있듯이 자동 포인터를 의미합니다. 이것은 자동 메모리 관리를 지원하는 첫 번째 스마트 포인터로, 스코프가 끝날 때 자동으로 소멸자를 호출하여 메모리를 해제합니다.

 

[예제]

#include <memory>

void auto_ptr_example() {
    std::auto_ptr<int> ptr(new int);
    *ptr = 10;
    std::cout << *ptr << std::endl;
} // ptr goes out of scope here, and is destroyed. Memory is freed automatically.

위의 예제 코드에서 볼 수 있듯이, std::auto_ptr 객체 ptr는 new로 생성된 동적 메모리를 가리킵니다. ptr가 가리키는 메모리에는 10이라는 값이 저장되어 있습니다. 그러나 함수가 끝나면서 ptr의 스코프가 종료되면 ptr이 가리키는 메모리는 자동으로 해제됩니다.

 

그러나 std::auto_ptr에는 치명적인 문제가 있습니다. 그것은 복사 연산을 허용한다는 점입니다. 이러한 복사 연산은 동일한 메모리를 가리키는 두 개의 스마트 포인터를 만들 수 있음을 의미하는데, 이는 두 포인터가 동일한 메모리를 해제하려고 할 때 문제를 야기할 수 있습니다. 이러한 이유로 std::auto_ptr은 C++11에서 deprecated 되었으며, 대신 std::unique_ptr이 도입되었습니다.

 

[예제]

#include <memory>

void auto_ptr_problem() {
    std::auto_ptr<int> ptr1(new int);
    *ptr1 = 10;

    std::auto_ptr<int> ptr2 = ptr1; // ptr1 is copied to ptr2

    std::cout << *ptr2 << std::endl; // this works fine
    std::cout << *ptr1 << std::endl; // undefined behavior!
}

 

위의 코드에서 볼 수 있듯이, ptr1이 ptr2로 복사되면서 문제가 발생합니다. ptr1은 복사된 후에 null로 설정되며, ptr2가 이제 동일한 메모리를 가리키게 됩니다. 이것은 두 개의 포인터가 동일한 메모리를 관리하고 있는 상황을 만듭니다. 이 상황에서 ptr1을 사용하려고 하면 예상하지 못한 동작이 발생하게 됩니다.

 

즉, std::auto_ptr은 소유권을 복사하는 대신 이전 소유자에서 새로운 소유자로 이동합니다. 이것은 다른 스마트 포인터, 특히 std::unique_ptr에서 볼 수 있는 복사 대신 이동(move)의 아이디어를 반영하는 초기의 시도였습니다.

 

이러한 문제점 때문에, std::auto_ptr은 더 이상 사용되지 않으며, 대신 std::unique_ptr 또는 std::shared_ptr가 사용됩니다. std::unique_ptr는 유일한 소유권을 보장하며, std::shared_ptr는 참조 카운팅을 통해 여러 개의 스마트 포인터가 동일한 자원을 안전하게 공유할 수 있도록 합니다.

 

그래도 std::auto_ptr에 대해 알아두면 스마트 포인터의 작동 원리와 발전 과정을 이해하는 데 도움이 될 수 있습니다. 이것이 C++의 첫 번째 스마트 포인터였고, 후속 버전에서 개선되고 확장된 개념을 제공하므로, 이러한 관점에서 std::auto_ptr를 이해하면 도움이 될 것입니다. 그러나 실제 코드에서는 std::auto_ptr를 사용하지 말고, 대신 std::unique_ptr나 std::shared_ptr를 사용하도록 권장드립니다.

 

11.2.2. std::unique_ptr

std::unique_ptr은 이름에서 알 수 있듯이, '유일한' 포인터입니다. 즉, 동일한 메모리를 가리키는 두 개의 std::unique_ptr 인스턴스가 동시에 존재할 수 없습니다. 이러한 특성은 메모리 누수와 같은 일반적인 문제를 방지하는 데 도움이 됩니다.

 

이것을 이해하는 한 가지 좋은 방법은 예제를 통해 보는 것입니다.

 

[예제]

std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = ptr1; // 컴파일 에러!

 

위의 코드에서, 우리는 ptr1을 ptr2로 복사하려고 했습니다. 하지만, 이것은 std::unique_ptr의 핵심 원칙에 위반되므로 컴파일러는 이를 허용하지 않습니다.

 

그러나 std::unique_ptr은 '이동'이라는 개념을 사용하여 포인터의 소유권을 이전할 수 있습니다. 이것이 어떻게 작동하는지 보겠습니다.

 

[예제]

std::unique_ptr<int> ptr1(new int(5));
std::unique_ptr<int> ptr2 = std::move(ptr1); // 이제 괜찮습니다!

 

이 경우, std::move 함수를 사용하여 ptr1에서 ptr2로 소유권을 '이동'했습니다. 이로 인해 ptr1은 이제 null이 되고, ptr2는 이전에 ptr1이 가리켰던 메모리를 가리키게 됩니다.

 

이러한 특성은 std::unique_ptr이 자원의 '유일한' 소유자임을 보장합니다. 이것은 메모리 관리를 더욱 안전하고 예측 가능하게 만드는 중요한 속성입니다.

 

std::unique_ptr은 이러한 속성 덕분에 다양한 유스케이스에서 유용하게 사용됩니다. 예를 들어, 함수에서 동적으로 할당된 메모리를 반환할 때, std::unique_ptr를 사용하면 호출자가 반환된 메모리의 소유권을 명확하게 이해할 수 있으며, 메모리 누수의 위험을 크게 줄일 수 있습니다.

 

[예제]

std::unique_ptr<int> createInt(int value) {
    return std::unique_ptr<int>(new int(value));
}

void useInt() {
    std::unique_ptr<int> ptr = createInt(5);
    // 여기서 ptr은 createInt에서 반환된 메모리를 소유합니다.
    // ptr이 범위를 벗어날 때, 메모리는 자동으로 해제됩니다.
}

 

이처럼, std::unique_ptr는 C++에서 안전하고 효과적인 메모리 관리를 가능하게 하는 강력한 도구입니다. 다음 섹션에서는 다른 종류의 스마트 포인터인 std::shared_ptr에 대해 알아보겠습니다.

 

11.2.3. std::shared_ptr

std::shared_ptr는 그 이름에서 알 수 있듯이 메모리에 대한 '공유' 소유권을 제공하는 스마트 포인터입니다. 이는 여러 std::shared_ptr 인스턴스가 동일한 메모리를 가리킬 수 있음을 의미합니다. 그렇다면 어떻게 메모리 누수를 방지할까요? 이것이 std::shared_ptr의 핵심적인 부분입니다: 내부적으로 레퍼런스 카운팅을 수행하여, 메모리를 가리키는 std::shared_ptr 인스턴스의 수를 추적합니다.

 

다음은 간단한 예시입니다.

 

[예제]

std::shared_ptr<int> ptr1(new int(5));
std::shared_ptr<int> ptr2 = ptr1; // 이제 괜찮습니다!

 

// ptr1과 ptr2 모두 동일한 메모리를 가리키며, 레퍼런스 카운트는 2입니다.
이 예제에서는 ptr1을 ptr2로 복사하였고, 둘 다 동일한 메모리를 가리킵니다. 각각의 std::shared_ptr는 이 메모리에 대한 공유 소유권을 가지며, 레퍼런스 카운트를 통해 이를 관리합니다. 이 경우, 레퍼런스 카운트는 2가 됩니다. 

 

std::shared_ptr의 중요한 특징은, 레퍼런스 카운트가 0이 되면 즉시 메모리가 해제된다는 점입니다. 이는 다음과 같이 작동합니다.

 

[예제]

{
    std::shared_ptr<int> ptr1(new int(5));
    {
        std::shared_ptr<int> ptr2 = ptr1;
        // ptr1과 ptr2는 모두 동일한 메모리를 가리키며, 레퍼런스 카운트는 2입니다.
    } // ptr2가 범위를 벗어나므로, 레퍼런스 카운트는 1로 감소합니다.
    // ptr1이 여전히 메모리를 가리키고 있으므로, 메모리는 유지됩니다.
} // ptr1이 범위를 벗어나면, 레퍼런스 카운트가 0이 되고 메모리가 해제됩니다.

 

따라서 std::shared_ptr은 여러 객체가 동일한 리소스를 안전하게 공유할 수 있도록 해줍니다. 그러나 이것은 반드시 필요한 경우에만 사용해야 합니다. 불필요한 레퍼런스 카운팅은 성능을 저하시킬 수 있습니다. 또한 std::shared_ptr은 순환 참조 문제를 야기할 수 있습니다. 이 문제는 std::weak_ptr를 통해 해결할 수 있습니다. 다음 섹션에서는 std::weak_ptr에 대해 자세히 알아보겠습니다.

 

11.2.4. std::weak_ptr

std::weak_ptr는 std::shared_ptr의 중요한 동반자이며, 스마트 포인터가 순환 참조와 같은 문제에 대처하는 데 큰 도움을 줍니다. 기본적으로 std::weak_ptr는 std::shared_ptr와 유사하지만, 가리키는 객체의 수명에 영향을 주지 않는 '약한' 참조를 제공한다는 점에서 다릅니다. 이는 순환 참조를 피하는 데 유용합니다.

 

순환 참조는 두 객체가 서로를 참조하고, 둘 다 std::shared_ptr를 사용하여 참조를 유지하는 경우에 발생합니다. 이 경우, 두 객체 모두 레퍼런스 카운트가 절대 0이 되지 않아 메모리 누수가 발생합니다.

 

[예제]

struct B;
struct A {
    std::shared_ptr<B> b_ptr;
};

struct B {
    std::shared_ptr<A> a_ptr;
};

std::shared_ptr<A> a(new A());
std::shared_ptr<B> b(new B());

a->b_ptr = b;
b->a_ptr = a; // 순환 참조 생성

 

위 예제에서, A와 B 객체는 서로를 참조하므로 순환 참조가 발생합니다. 레퍼런스 카운트가 절대로 0이 되지 않으므로 메모리가 누수됩니다. 이 문제를 해결하려면 std::weak_ptr를 사용합니다.

 

[예제]

struct B;
struct A {
    std::shared_ptr<B> b_ptr;
};

struct B {
    std::weak_ptr<A> a_ptr; // 약한 참조 사용
};

std::shared_ptr<A> a(new A());
std::shared_ptr<B> b(new B());

a->b_ptr = b;
b->a_ptr = a; // 이제 순환 참조가 발생하지 않습니다.

 

이제 B는 std::weak_ptr를 사용하여 A를 참조하므로 순환 참조가 발생하지 않습니다. A가 파괴되면 std::weak_ptr는 자동으로 nullptr로 설정됩니다. 또한, std::weak_ptr은 lock() 함수를 사용하여 std::shared_ptr로 변환할 수 있습니다. 이 함수는 해당 객체가 여전히 존재하는 경우에만 std::shared_ptr를 반환하므로, 메모리를 안전하게 접근할 수 있습니다.

 

[예제]

if (std::shared_ptr<A> a_locked = b->a_ptr.lock()) {
    // 객체가 여전히 존재하므로 안전하게 접근할 수 있습니다.
} else {
    // 객체가 이미 파괴되었습니다.
}

 

따라서, std::weak_ptr은 순환 참조와 같은 복잡한 문제를 해결하는 데 있어 std::shared_ptr와 함께 중요한 역할을 합니다. 다음 섹션에서는 C++ 스마트 포인터의 고급 주제에 대해 더 자세히 알아보겠습니다.


11.3. std::unique_ptr에 대해

std::unique_ptr는 C++11부터 제공되는 스마트 포인터의 한 종류입니다. 이 스마트 포인터는 단일 소유권 모델을 구현합니다. 즉, std::unique_ptr는 메모리 블록을 독점적으로 소유하고, 복사는 허용되지 않습니다. 이러한 특성은 메모리 누수를 예방하고, 자원을 안전하게 관리하는 데 매우 유용합니다. 또한, std::unique_ptr는 비용이 거의 없으며, 일반 포인터와 거의 동일한 성능을 제공합니다. 따라서, 단일 소유가 필요한 경우에는 std::unique_ptr를 사용하는 것이 좋습니다.

11.3.1. std::unique_ptr의 정의와 특징

std::unique_ptr는 C++11에서 도입된 스마트 포인터 중 하나로, 이 포인터는 "유일한 소유권" 모델을 따릅니다. 이 말은, 한 번에 한 std::unique_ptr만이 특정 객체를 소유할 수 있다는 뜻입니다. 이러한 속성은 단일 소유가 보장되어야 하는 시스템 리소스 관리에 적합합니다.

 

[예제]

#include <memory>  // For std::unique_ptr
#include <iostream> // For std::cout

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructed\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
};

int main() {
    std::unique_ptr<MyClass> ptr1(new MyClass);
}

 

위 예제 코드에서 std::unique_ptr은 MyClass 인스턴스를 소유합니다. 프로그램이 main 함수의 범위를 벗어나면,  std::unique_ptr는 자동으로 소유한 객체를 삭제합니다. 이 때문에 명시적으로 delete를 호출할 필요가 없습니다.

 

특히 중요한 점은 std::unique_ptr은 복사를 허용하지 않는다는 것입니다. 즉, 다른 std::unique_ptr로부터 소유권을 가져올 수 없습니다. 대신, std::move 함수를 사용해 소유권을 이전(transfer)할 수 있습니다.

 

[예제]

std::unique_ptr<MyClass> ptr2 = std::move(ptr1);

 

위 코드는 ptr1이 가지고 있던 소유권을 ptr2에게 이전합니다. 이제 ptr1은 더 이상 객체를 소유하지 않으며, ptr2는 객체의 새로운 소유자가 됩니다. 이렇게 std::unique_ptr은 런타임에서 안전하게 객체의 소유권을 이전하는 것을 가능하게 합니다. 

 

한편, std::unique_ptr은 성능 비용이 거의 없습니다. 이는 std::unique_ptr이 내부적으로 일반 포인터를 사용해 객체를 추적하기 때문입니다. 따라서, std::unique_ptr은 안전한 리소스 관리와 효율성을 동시에 달성하는 데 도움이 됩니다. 

 

11.3.2. std::unique_ptr의 기본 사용법

 

std::unique_ptr을 사용하는 것은 매우 간단합니다. 다음은 std::unique_ptr의 기본 사용 방법에 대한 몇 가지 예를 보여주는 C++ 코드입니다. 

 

[예제]

#include <memory> // std::unique_ptr
#include <iostream> // std::cout

class MyClass {
public:
    MyClass(int value) : value_(value) { }
    void PrintValue() { std::cout << "Value: " << value_ << "\n"; }
private:
    int value_;
};

int main() {
    std::unique_ptr<MyClass> ptr1(new MyClass(5)); 
    ptr1->PrintValue(); 

    std::unique_ptr<MyClass> ptr2 = std::make_unique<MyClass>(10); 
    ptr2->PrintValue(); 

    // std::unique_ptr<MyClass> ptr3 = ptr2; // This will give a compile error
    std::unique_ptr<MyClass> ptr3 = std::move(ptr2); // ptr2's ownership is moved to ptr3
    ptr3->PrintValue(); 

    return 0;
}

 

첫 번째 예제에서는 std::unique_ptr가 동적으로 할당된 MyClass 객체를 소유합니다. 이 때문에 명시적으로 delete를 호출할 필요가 없습니다. std::unique_ptr가 범위를 벗어나면 자동으로 메모리를 해제합니다.

 

두 번째 예제에서는 std::make_unique 함수를 사용하여 std::unique_ptr를 생성합니다. 이 방법이 좋은 이유는, 이 함수는 객체 생성과 메모리 할당을 하나의 연산으로 결합하므로, 예외 안정성을 높일 수 있기 때문입니다.

 

세 번째 예제에서는 소유권 이전을 시연하고 있습니다. std::unique_ptr는 복사가 허용되지 않지만, std::move를 사용하여 소유권을 다른 std::unique_ptr로 이전할 수 있습니다.

 

마지막으로, std::unique_ptr는 객체의 멤버 함수에 접근하기 위해 일반 포인터처럼 '->' 연산자를 사용할 수 있습니다.

 

std::unique_ptr은 C++의 중요한 특징인 RAII(Resource Acquisition Is Initialization) 패턴의 일부로, 동적 메모리를 안전하게 관리하는 데 도움이 됩니다.

 

11.3.3. std::unique_ptr와 소유권

std::unique_ptr의 핵심적인 특징 중 하나는 소유권(ownership) 개념입니다. std::unique_ptr은 소유하고 있는 동적 메모리에 대한 독점적인 소유권을 가지고 있습니다. 이는 다른 std::unique_ptr 객체가 같은 메모리를 소유하도록 허용하지 않는다는 것을 의미합니다.

 

[예제]

std::unique_ptr<int> ptr1(new int(5)); // ptr1 now owns the memory
std::unique_ptr<int> ptr2 = ptr1; // Compile error! Can't copy unique_ptr

 

이것은 std::unique_ptr이 복사 생성자와 복사 대입 연산자를 삭제(즉, 사용 불가하게 만듬)하기 때문입니다. 이로 인해 std::unique_ptr의 복사가 막히므로, 하나의 std::unique_ptr만이 특정 메모리를 소유할 수 있습니다.

 

하지만, std::unique_ptr은 소유권을 다른 std::unique_ptr로 이전(transfer)시키는 것은 가능합니다. 이는 std::move를 통해 수행됩니다.

 

[예제]

std::unique_ptr<int> ptr1(new int(5)); // ptr1 now owns the memory
std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1 transfers ownership to ptr2

 

위 예제에서 std::move를 사용하면 ptr1은 소유권을 ptr2로 이전시키고, 이제 더 이상 메모리를 소유하고 있지 않습니다.  ptr2가 이제 그 메모리를 소유하고 있으므로, ptr2가 범위를 벗어나면 해당 메모리는 자동으로 해제됩니다.

 

이러한 소유권 이전 특성으로 인해 std::unique_ptr은 자원의 소유와 생명 주기를 명확하게 관리할 수 있으며, 메모리 누수를 방지하는 데 도움이 됩니다. 이러한 소유권 모델은 C++의 자원 관리 전략인 RAII(Resource Acquisition Is Initialization)와 잘 어울립니다.

 

11.3.4. std::unique_ptr의 예제코드와 활용법

std::unique_ptr를 이해하는 데 필요한 핵심 개념과 기본적인 사용법에 대해 설명하겠습니다. std::unique_ptr는 독점적 소유권을 가지는 스마트 포인터입니다. 이는 하나의 std::unique_ptr만이 특정 메모리를 소유할 수 있음을 의미하며, 이 소유권을 다른 std::unique_ptr로 이전(또는 이동)시킬 수 있습니다. 

 

이러한 속성은 std::unique_ptr가 특히 동적 할당된 메모리를 관리하는 데 유용합니다. 이것은 C++에서 자주 발생하는 문제 중 하나인 메모리 누수를 방지하고 자원을 효율적으로 관리하는 데 도움이 됩니다.

 

이제 std::unique_ptr의 활용법을 살펴보겠습니다.

 

1: 기본 사용법

[예제]

#include <memory> // for std::unique_ptr

int main()
{
    std::unique_ptr<int> ptr(new int(5)); // 동적으로 int를 할당하고, ptr이 그것을 소유하게 함

    // 이제 ptr은 동적으로 할당된 int를 가리킵니다.
    // ptr이 범위를 벗어나면 자동으로 메모리를 해제합니다.
}

 

이 예제에서 std::unique_ptr<int> ptr(new int(5));는 동적으로 할당된 int를 소유하는 std::unique_ptr를 생성합니다. std::unique_ptr가 스코프를 벗어나면 소유하고 있는 메모리는 자동으로 해제됩니다. 

 

2: 소유권 이전

[예제]

#include <memory> // for std::unique_ptr

int main()
{
    std::unique_ptr<int> ptr1(new int(5)); // 동적으로 int를 할당하고, ptr1이 그것을 소유하게 함
    std::unique_ptr<int> ptr2; // 아직 아무것도 소유하지 않음

    ptr2 = std::move(ptr1); // ptr1의 소유권을 ptr2로 이전

    // 이제 ptr2는 동적으로 할당된 int를 소유하고, ptr1은 아무것도 소유하고 있지 않습니다.
    // ptr2가 범위를 벗어나면 자동으로 메모리를 해제합니다.
}

 

이 예제에서는 std::unique_ptr의 소유권을 이전하는 방법을 보여줍니다. std::move를 사용하여 ptr1의 소유권을 ptr2로 이전하고 있습니다. 이제 ptr2가 메모리를 소유하고 있으므로, ptr2가 범위를 벗어나면 해당 메모리는 자동으로 해제됩니다.

 

또한, std::unique_ptr는 커스텀 deleters를 지원합니다. 이는 std::unique_ptr가 메모리를 해제하는 방법을 사용자가 정의할 수 있음을 의미합니다. 이러한 기능은 동적으로 할당된 메모리 이외의 자원을 관리하는 데 유용할 수 있습니다. 예를 들어, 파일 핸들이나 네트워크 소켓과 같은 것들입니다.

 

다음 예제에서는 커스텀 deleter를 사용하여 std::unique_ptr가 파일을 관리하도록 하는 방법을 보여줍니다.

 

3: 커스텀 deleter

[예제]

#include <memory> // for std::unique_ptr
#include <cstdio> // for std::FILE, std::fopen, std::fclose

struct FileDeleter
{
    void operator()(std::FILE* file) const
    {
        if (file)
        {
            std::fclose(file);
        }
    }
};



int main()
{
    std::unique_ptr<std::FILE, FileDeleter> filePtr(std::fopen("myfile.txt", "r"));

    // 이제 filePtr은 myfile.txt를 가리키는 파일 핸들을 소유합니다.
    // filePtr이 범위를 벗어나면 커스텀 deleter가 호출되어 파일이 자동으로 닫힙니다.
}

 

이 예제에서 FileDeleter는 std::unique_ptr가 파일 핸들을 제거하는 방법을 정의하는 커스텀 deleter입니다. std::unique_ptr가 범위를 벗어나면, 커스텀 deleter는 파일 핸들을 닫습니다. 

 

이처럼 std::unique_ptr은 자원을 효율적으로 관리하는 데 매우 유용한 도구입니다. C++ 프로그래밍에서 자원 관리는 중요한 주제이며, std::unique_ptr는 이 문제를 해결하는 데 도움이 됩니다.

 

11.3.5. std::unique_ptr 예외 처리

 

C++에서는 예외 처리가 중요한 부분을 차지합니다. 그 중 std::unique_ptr와 예외 처리를 함께 다루는 방법에 대해 이야기하겠습니다. std::unique_ptr는 소유하고 있는 메모리를 자동으로 해제하는 특성 때문에, 이는 예외가 발생했을 때 메모리 누수를 방지하는데 큰 도움이 됩니다.

 

다음은 std::unique_ptr와 예외 처리를 함께 사용하는 방법에 대한 간단한 예제입니다:

 

[예제]

#include <iostream> // for std::cout
#include <memory> // for std::unique_ptr

void MyFunction()
{
    std::unique_ptr<int> ptr(new int(5)); // 동적으로 int를 할당하고, ptr이 그것을 소유하게 함

    // 여기서 예외가 발생한다고 가정합시다
    throw std::runtime_error("An error occurred!");

    // ptr은 스코프를 벗어나는 순간 메모리를 자동으로 해제합니다.
    // 예외가 발생하더라도, 메모리 누수는 발생하지 않습니다.
}

int main()
{
    try
    {
        MyFunction();
    }
    catch (const std::exception& e)
    {
        std::cout << "Caught exception: " << e.what() << '\n';
    }
}

 

이 예제에서, MyFunction 함수 안에서 std::unique_ptr가 생성되었습니다. 이 std::unique_ptr는 동적으로 할당된 int를 소유하고 있습니다. 그리고 함수 안에서 예외가 발생하면, std::unique_ptr는 자동으로 소유하고 있는 메모리를 해제합니다. 이 때문에 예외가 발생하더라도 메모리 누수는 발생하지 않습니다. 

 

그런데 여기서 주의할 점이 있습니다. std::unique_ptr를 다른 함수에 전달하거나, 함수에서 반환받을 때는 소유권 이전에 대한 이해가 필요합니다. std::unique_ptr의 복사 생성자와 복사 대입 연산자는 삭제되어 있으므로, std::unique_ptr를 직접 복사하는 것은 불가능합니다. 대신 std::move를 사용하여 std::unique_ptr의 소유권을 이전해야 합니다. 

 

다음은 std::unique_ptr를 함수에 전달하고 반환하는 방법에 대한 예제입니다:

 

[예제]

#include <memory> // for std::unique_ptr

std::unique_ptr<int> CreateUniquePtr()
{
    // std::unique_ptr를 직접 반환하면, 소유권이 이전됩니다.
    // 이 때문에 함수가 끝난 후에도 메모리 누수는 발생하지 않습니다.
    return std::unique_ptr<int>(new int(5));
}

void UseUniquePtr(std::unique_ptr<int> ptr)
{
    // 함수에 전달된 std::unique_ptr는 이 함수 안에서만 유효합니다.
    // 함수가 끝난 후에는 자동으로 메모리가 해제됩니다.
}

int main()
{
    std::unique_ptr<int> ptr = CreateUniquePtr();
    UseUniquePtr(std::move(ptr)); // std::unique_ptr를 함수에 전달할 때는 std::move를 사용해야 합니다.

    // 이제 ptr는 nullptr입니다. 소유권이 UseUniquePtr 함수에 이전되었기 때문입니다.
}

 

이 예제에서는 CreateUniquePtr 함수가 std::unique_ptr를 반환하고, UseUniquePtr 함수가 std::unique_ptr를 매개변수로 받습니다. 이 두 경우 모두, std::unique_ptr의 소유권이 이전되었습니다. std::unique_ptr를 함수에 전달할 때는 std::move를 사용하였습니다. 

 

이처럼 std::unique_ptr는 예외 처리와 함께 사용될 때 특히 유용합니다. 메모리 누수를 방지하고, 코드의 안전성을 향상할 수 있습니다.


11.4. std::shared_ptr에 대해

std::shared_ptr는 C++의 스마트 포인터 중 하나로, 여러 곳에서 동시에 소유할 수 있는 메모리를 가리키는 포인터입니다. 이 포인터가 가리키는 메모리는 참조 횟수를 세어, 마지막 std::shared_ptr이 소멸할 때 해제됩니다. 메모리 해제를 자동으로 처리하므로, 개발자가 직접 메모리를 관리하는 데 드는 시간과 노력을 줄일 수 있습니다. 이로 인해 코드의 안정성이 향상되며, 메모리 누수의 가능성을 줄일 수 있습니다.

11.4.1. std::shared_ptr의 정의와 특징

std::shared_ptr은 C++ 표준 라이브러리의 스마트 포인터 중 하나로서, 참조 카운팅(reference counting) 방식을 사용하여 동적 메모리를 관리합니다. 그 이름에서 알 수 있듯이, std::shared_ptr은 여러 개의 스마트 포인터가 같은 객체를 '공유'할 수 있게 해주는 것이 주요 특징입니다.

 

이 스마트 포인터는 내부적으로 두 가지 주요 구성 요소를 가지고 있습니다. 하나는 가리키는 실제 객체에 대한 포인터이고, 또 다른 하나는 '제어 블록'이라고 불리우는, 참조 카운터와 관련된 정보를 유지하는 부분입니다. std::shared_ptr이 복사되거나 다른 객체로 이동될 때, 이 참조 카운터가 업데이트되며, 참조 카운터가 0이 될 때 실제 객체가 삭제됩니다.

 

[예제]

#include <iostream>
#include <memory>

struct Foo {
    Foo() { std::cout << "Foo::Foo\n"; }
    ~Foo() { std::cout << "Foo::~Foo\n"; }
    void bar() { std::cout << "Foo::bar\n"; }
};

void f(const std::shared_ptr<Foo>& ptr) {
    std::cout << "shared_ptr (use count " << ptr.use_count() << ") at " << ptr.get() << '\n';
}

int main() {
    std::shared_ptr<Foo> ptr1 = std::make_shared<Foo>();
    // 출력: Foo::Foo
    std::cout << "ptr1 (use count " << ptr1.use_count() << ") at " << ptr1.get() << '\n';
    // 출력: ptr1 (use count 1) at 0xADDRESS
    {
        std::shared_ptr<Foo> ptr2 = ptr1;
        // 출력: ptr1 (use count 2) at 0xADDRESS
        f(ptr2);
        // 출력: shared_ptr (use count 2) at 0xADDRESS
    }
    // 여기서 ptr2의 소멸자가 호출되지만, 참조 카운터가 1이므로 아직 Foo 객체는 삭제되지 않습니다.
    std::cout << "ptr1 (use count " << ptr1.use_count() << ") at " << ptr1.get() << '\n';
    // 출력: ptr1 (use count 1) at 0xADDRESS
} // Foo::~Foo가 이 지점에서 호출됩니다.

 

위 코드에서 볼 수 있듯이, std::shared_ptr을 사용하면 동적 메모리의 생명주기를 안전하게 관리할 수 있습니다. 더군다나, 코드가 복잡해지고 여러 함수와 객체가 같은 메모리에 접근해야 할 때, 이러한 스마트 포인터의 이점은 더욱 두드러집니다. 

 

다만, std::shared_ptr를 사용할 때 주의할 점은 순환 참조 문제입니다. 서로가 서로를 가리키는 std::shared_ptr 객체가 생성되면, 참조카운터가 절대 0이 되지 않아 메모리 누수가 발생합니다. 이런 상황을 막기 위해 '약한 포인터'인 std::weak_ptr를 사용할 수 있습니다. 

 

11.4.2. std::shared_ptr의 기본 사용법

std::shared_ptr을 사용하는 기본적인 방법에 대해 알아보겠습니다. 먼저, 이 스마트 포인터의 객체는 다음과 같이 생성할 수 있습니다.

 

[예제]

std::shared_ptr<int> p1(new int(5)); // 방법 1: 일반 포인터를 이용한 초기화
std::shared_ptr<int> p2 = std::make_shared<int>(5); // 방법 2: std::make_shared를 이용한 초기화

 

여기서 std::make_shared를 사용하는 방법이 더 선호되는데, 이유는 두 가지입니다. 첫째, std::make_shared를 사용하면 메모리 할당이 한 번만 이루어져 성능을 향상할 수 있습니다. 둘째, 이 방식은 예외 안전성을 보장합니다.

 

예를 들어, 아래 코드를 보면: 

[예제]

foo(std::shared_ptr<int>(new int(5)), function_may_throw());

 

위 코드에서 function_may_throw()이 예외를 발생시키면, 이미 할당된 new int(5)에 대한 메모리 누수가 발생할 수 있습니다. 반면, std::make_shared를 사용하면 이러한 문제를 방지할 수 있습니다. 

 

[예제]

foo(std::make_shared<int>(5), function_may_throw()); // 더 안전하고 선호되는 방식

 

std::shared_ptr 객체는 다른 스마트 포인터나 일반 포인터로부터 복사하거나 이동할 수 있습니다. 이때 참조 카운터가 적절히 조정됩니다.

 

[예제]

std::shared_ptr<int> p3 = p2; // p3과 p2는 같은 객체를 가리키며, 참조 카운트는 2입니다.

다음으로, std::shared_ptr는 일반 포인터처럼 동작하며, 멤버 접근 연산자(->)와 역참조 연산자(*)를 사용할 수 있습니다.

 

[예제]

std::shared_ptr<std::string> p4 = std::make_shared<std::string>("Hello, world!");
std::cout << *p4 << '\n'; // "Hello, world!"를 출력합니다.
std::cout << p4->size() << '\n'; // "13"을 출력합니다.
std::shared_ptr을 사용하면 메모리 관리에 대한 걱정 없이 공유 소유를 표현할 수 있습니다.

 

11.4.3. std::shared_ptr와 참조 카운팅

std::shared_ptr의 핵심적인 특징 중 하나는 "참조 카운팅"입니다. 참조 카운팅이란 std::shared_ptr 객체가 가리키는 리소스를 몇 개의 std::shared_ptr 객체들이 공유하고 있는지를 추적하는 방식입니다.

 

새로운 std::shared_ptr 객체가 생성되거나 이미 존재하는 std::shared_ptr 객체를 복사하면, 참조 카운트는 증가하며, std::shared_ptr 객체가 소멸되거나 다른 리소스를 가리키게 될 때 참조 카운트는 감소합니다.

 

[예제]

std::shared_ptr<int> p1 = std::make_shared<int>(5);
std::shared_ptr<int> p2 = p1;  // p1을 복사하면 참조 카운트가 증가합니다.

std::cout << "p1 use count: " << p1.use_count() << '\n';  // "p1 use count: 2"를 출력합니다.
std::cout << "p2 use count: " << p2.use_count() << '\n';  // "p2 use count: 2"를 출력합니다.

 

참조 카운트는 std::shared_ptr의 use_count() 메서드를 통해 알 수 있습니다. 하지만 이 메서드는 디버깅이나 학습 목적 외에는 사용을 권장하지 않습니다. 그 이유는 use_count()의 반환 값이 순간적인 상태를 반영하기 때문에 멀티스레드 환경에서는 신뢰할 수 없기 때문입니다. 

 

std::shared_ptr가 참조 카운트를 관리하는 방식 덕분에, 공유하는 모든 std::shared_ptr 객체가 사라지면, 즉 참조 카운트가 0이 되면 자동으로 메모리가 해제됩니다. 이렇게 되면 메모리 누수를 걱정할 필요가 없습니다.

 

[예제]

{
  std::shared_ptr<int> p3 = std::make_shared<int>(10);
  {
    std::shared_ptr<int> p4 = p3;  // p3를 복사하면 참조 카운트가 증가합니다.
  }  // p4가 범위를 벗어나면 참조 카운트가 감소합니다.

  std::cout << *p3 << '\n';  // "10"을 출력합니다. p3는 여전히 유효합니다.
}  // p3가 범위를 벗어나면 참조 카운트가 0이 되고 메모리가 해제됩니다.

 

이것이 바로 std::shared_ptr와 참조 카운팅의 기본적인 원리입니다. 이런 특성 덕분에 std::shared_ptr는 C++에서 자원 공유를 표현하는 강력한 도구입니다. 

 

11.4.4. std::shared_ptr의 예제코드와 활용법

이제 실제로 std::shared_ptr를 어떻게 사용하는지 살펴볼 차례입니다. 아래 예제를 통해 기본적인 사용법을 살펴보겠습니다.

 

[예제]

#include <iostream>
#include <memory>

class MyObject {
public:
  MyObject() { std::cout << "Object created!\n"; }
  ~MyObject() { std::cout << "Object destroyed!\n"; }
};

int main() {
  std::shared_ptr<MyObject> ptr1 = std::make_shared<MyObject>();
  {
    std::shared_ptr<MyObject> ptr2 = ptr1;
  }
  return 0;
}

 

위 예제에서 MyObject라는 간단한 클래스를 생성하였습니다. 이 클래스의 생성자와 소멸자에는 객체의 생성과 소멸을 알리는 메시지를 출력하는 코드가 들어있습니다. 

 

main 함수에서는 std::make_shared 함수를 이용하여 MyObject 객체를 생성하고 std::shared_ptr에 할당하였습니다. 그리고 내부 블록에서 ptr1을 복사하여 ptr2를 생성하였습니다. 이렇게 되면 ptr1과 ptr2는 동일한 MyObject 객체를 가리키며, 참조 카운트는 2가 됩니다. 

 

내부 블록이 종료되면 ptr2가 소멸되고 참조 카운트는 1로 줄어듭니다. 하지만 여전히 ptr1이 MyObject 객체를 가리키고 있으므로 객체는 소멸되지 않습니다. main 함수가 종료되면 ptr1이 소멸되고 참조 카운트가 0이 되어, 이 시점에서 MyObject 객체가 소멸됩니다.

 

이처럼 std::shared_ptr를 사용하면 자동으로 참조 카운트를 관리해 주기 때문에, 우리는 명시적으로 메모리를 해제해 주는 코드를 작성할 필요가 없습니다. 또한, 참조 카운트가 0이 되는 시점에 객체가 소멸되므로 메모리 누수를 방지할 수 있습니다.

 

이 외에도 std::shared_ptr는 원시 포인터처럼 사용할 수 있습니다. 즉, * 연산자를 이용하여 객체에 접근할 수 있고, -> 연산자를 이용하여 객체의 멤버에 접근할 수 있습니다. 아래 예제를 살펴보면, std::shared_ptr이 원시 포인터와 유사하게 동작함을 알 수 있습니다.

 

[예제]

#include <iostream>
#include <memory>

class MyObject {
public:
  MyObject(int val) : value(val) { }
  int value;
};

int main() {
  std::shared_ptr<MyObject> ptr = std::make_shared<MyObject>(10);
  std::cout << "Value: " << ptr->value << "\n";
  return 0;
}

 

위 예제에서는 MyObject 객체에 value라는 멤버 변수를 추가하였습니다. 그리고 std::make_shared 함수를 이용하여 MyObject 객체를 생성할 때 value를 초기화하였습니다. 이후 ptr->value를 이용하여 value에 접근하였습니다.

 

따라서 std::shared_ptr를 이용하면 메모리 관리를 자동화할 수 있으면서도 원시 포인터와 유사한 방식으로 코드를 작성할 수 있습니다. 이러한 특성 덕분에 std::shared_ptr는 C++에서 자주 사용되는 스마트 포인터 중 하나입니다.

 

11.4.5. std::shared_ptr 예외 처리

C++에서 예외 처리는 중요한 부분입니다. 이는 메모리 관리와 밀접한 관련이 있는데, std::shared_ptr은 이러한 문제를 처리하는데 유용한 도구입니다.

 

예외와 메모리 누수

먼저, 예외와 메모리 누수에 대한 문제를 이해해보겠습니다. 아래는 예외 처리를 사용하지 않았을 때 발생할 수 있는 문제를 보여주는 예제입니다.

 

[예제]

#include <iostream>
#include <stdexcept>

class MyObject {
public:
  MyObject() { std::cout << "Object created!\n"; }
  ~MyObject() { std::cout << "Object destroyed!\n"; }
};

void riskyFunction() {
  MyObject* obj = new MyObject();
  throw std::runtime_error("Exception occurred!");
  delete obj; // 이 코드는 실행되지 않습니다.
}

int main() {
  try {
    riskyFunction();
  } catch (const std::exception& e) {
    std::cerr << "Caught exception: " << e.what() << '\n';
  }
  return 0;
}

 

위의 riskyFunction에서는 MyObject를 동적으로 생성하고, 이후에 예외를 발생시킵니다. 예외가 발생하면 delete obj; 코드는 실행되지 않고, 이로 인해 메모리 누수가 발생합니다. 

 

std::shared_ptr를 사용한 안전한 코드

std::shared_ptr을 사용하면 위와 같은 문제를 쉽게 해결할 수 있습니다. std::shared_ptr을 사용하여 객체를 생성하면, std::shared_ptr이 자동으로 메모리를 관리하므로 명시적으로 delete를 호출할 필요가 없습니다. 이를 통해 메모리 누수를 방지할 수 있습니다.

 

[예제]

#include <iostream>
#include <stdexcept>
#include <memory>

class MyObject {
public:
  MyObject() { std::cout << "Object created!\n"; }
  ~MyObject() { std::cout << "Object destroyed!\n"; }
};

void safeFunction() {
  std::shared_ptr<MyObject> obj = std::make_shared<MyObject>();
  throw std::runtime_error("Exception occurred!");
  // shared_ptr은 자동으로 메모리를 해제합니다.
}

int main() {
  try {
    safeFunction();
  } catch (const std::exception& e) {
    std::cerr << "Caught exception: " << e.what() << '\n';
  }
  return 0;
}

 

위의 코드에서 safeFunction은 std::shared_ptr을 사용하여 MyObject를 생성합니다. 이후 예외가 발생하더라도 std::shared_ptr이 소멸될 때 MyObject가 자동으로 삭제되므로 메모리 누수가 발생하지 않습니다. 

 

이처럼 std::shared_ptr은 예외 안전성을 강화하는 데 유용한 도구입니다. 이 도구를 사용하면 예외 상황에서도 메모리 누수를 방지하고, 코드를 더 안전하게 만들 수 있습니다. 


11.5. weak_ptr에 대해

std::weak_ptr는 C++ 스마트 포인터 중 하나로, std::shared_ptr와 같이 참조 카운팅을 사용하지만, 참조 카운트에 영향을 주지 않습니다. 이는 순환 참조 문제를 방지하기 위해 사용됩니다. 순환 참조란 서로가 서로를 참조하는 상황으로, 스마트 포인터가 자동으로 메모리를 해제하지 못하게 만듭니다. std::weak_ptr를 사용하면 이러한 문제를 피할 수 있습니다.

11.5.1. weak_ptr의 정의와 특징

C++의 스마트 포인터 중 하나인 std::weak_ptr는 이름에서도 알 수 있듯이, '약한 포인터'를 의미합니다. 이 포인터는 std::shared_ptr와 연동되어 작동하며, 가장 큰 특징은 참조 카운트에 영향을 미치지 않는다는 것입니다. 즉, std::weak_ptr이 가리키는 객체를 참조하는 동안에는 해당 객체가 메모리에서 사라지지 않지만, 참조 카운트에는 반영되지 않아 shared_ptr처럼 메모리 해제에 영향을 미치지 않습니다.

 

이 특징은 순환 참조 문제를 해결하는데 아주 중요한 역할을 합니다. 순환 참조는 두 객체가 서로를 참조할 때 발생하는 문제로, 이런 상황에서는 각 객체의 참조 카운트가 0이 되지 않아 영원히 메모리에서 해제되지 않는 문제가 발생합니다. std::weak_ptr를 사용하면, 해당 객체를 참조하지만 참조 카운트에는 반영하지 않으므로, 다른 모든 std::shared_ptr이 소멸되었을 때 객체는 메모리에서 해제됩니다.

 

다음은 std::weak_ptr의 기본적인 사용법을 보여주는 예제 코드입니다.

 

[예제]

#include <iostream>
#include <memory>

struct MyClass {
    std::weak_ptr<MyClass> ptr;
    
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
};

int main() {
    std::shared_ptr<MyClass> sp = std::make_shared<MyClass>();
    sp->ptr = sp;
    
    return 0;
}

 

이 코드에서는, MyClass 객체가 자신을 가리키는 weak_ptr을 멤버로 가지고 있습니다. 메인 함수에서는 shared_ptr을 생성하고, 이 shared_ptr이 자신을 가리키게 합니다. 그러나 이 경우에도 MyClass 객체는 프로그램이 종료될 때 정상적으로 해제됩니다. 이것은 weak_ptr이 참조 카운트에 반영되지 않기 때문입니다.

 

이렇게 std::weak_ptr은 std::shared_ptr과 함께 사용하여 순환 참조 문제를 해결하는 데 아주 중요한 도구입니다. 다음 섹션에서는 std::weak_ptr의 사용법과 예제 코드를 더 자세히 살펴보겠습니다.

 

참조 카운트에 반영되지 않는 std::weak_ptr의 특성 때문에, weak_ptr가 가리키는 객체를 직접 접근할 수는 없습니다. 이를 위해서는 std::shared_ptr로 변환해야 하는데, 이 과정을 '락(lock)'이라고 합니다. std::weak_ptr의 lock 메서드를 호출하여 std::shared_ptr를 얻을 수 있습니다.

 

다음은 lock 메서드를 사용하는 예제 코드입니다.

 

[예제]

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> sp = std::make_shared<int>(123);
    std::weak_ptr<int> wp = sp;
    
    if (std::shared_ptr<int> locked = wp.lock()) { // Use lock() to check whether the object still exists
        std::cout << "*locked = " << *locked << '\n';
    }
    else {
        std::cout << "wp is expired\n";
    }
    
    return 0;
}

 

이 코드에서는, 먼저 shared_ptr을 생성하고, 이를 weak_ptr로 복사합니다. 그런 다음 lock 메서드를 사용하여 weak_ptr에서 shared_ptr를 얻고, 이를 통해 객체에 접근합니다. 이 방법으로 객체가 아직 메모리에 있는지 확인할 수 있습니다. 만약 객체가 메모리에서 해제되었다면, lock 메서드는 빈 shared_ptr를 반환합니다.

 

따라서, std::weak_ptr는 객체가 아직 메모리에 있는지 확인하는 데 사용할 수 있는 훌륭한 도구입니다. std::shared_ptr와 함께 사용하면, 메모리 누수를 방지하고, 객체의 생명 주기를 안전하게 관리할 수 있습니다.

 

다음 섹션에서는 std::weak_ptr의 보다 고급스러운 사용법과 예제를 살펴보겠습니다. 주목할 점은, std::weak_ptr는 객체에 접근하기 전에 항상 lock 메서드를 호출하여 객체가 아직 메모리에 있는지 확인해야 한다는 것입니다. 이는 std::weak_ptr의 핵심적인 특성이자 사용 방법입니다.

 

11.5.2. weak_ptr의 기본 사용법

C++의 std::weak_ptr는 공유된 소유권 (std::shared_ptr)의 문제를 해결하기 위해 도입된 스마트 포인터입니다. 이 스마트 포인터는 참조 카운팅을 증가시키지 않아 순환 참조 문제를 방지할 수 있습니다. 하지만, std::weak_ptr는 객체에 대한 '약한' 참조만을 제공하므로, 객체에 직접 접근하려면 먼저 std::shared_ptr로 변환해야 합니다. 이를 '락(lock)'이라고 부릅니다. 이제 기본 사용법을 알아보겠습니다.

 

먼저, std::weak_ptr를 생성하는 방법입니다. std::weak_ptr는 기본적으로 std::shared_ptr에서 생성됩니다.

 

[예제]

std::shared_ptr<int> sp(new int(10));  // shared_ptr 생성
std::weak_ptr<int> wp(sp);  // shared_ptr로부터 weak_ptr 생성

 

위의 코드에서 wp는 sp가 소유한 객체를 가리키지만, 이는 '약한' 참조이므로 sp의 참조 카운트를 증가시키지 않습니다. 즉, wp는 객체의 소유권을 가지지 않습니다.

 

객체에 접근하려면, 먼저 std::shared_ptr로 변환해야 합니다. 이를 위해 std::weak_ptr의 lock 메서드를 사용합니다.

 

[예제]

if (std::shared_ptr<int> sp = wp.lock()) {  // lock 메서드로 shared_ptr 획득
    // 객체에 접근
}

 

lock 메소드는 해당 객체가 아직 메모리에 있을 때 std::shared_ptr를 반환하며, 그렇지 않을 때는 빈 std::shared_ptr를 반환합니다. 따라서 lock 메서드를 사용하여 객체가 메모리에 있는지 확인할 수 있습니다.

 

또한 std::weak_ptr는 expired 메소드를 제공하여 객체가 메모리에 있는지 확인할 수 있습니다.

 

[예제]

if (wp.expired()) {
    // 객체는 더 이상 메모리에 없음
}

 

하지만, 이 메소드는 객체의 상태를 확인하고 객체에 접근하는 사이에 객체가 해제될 수 있으므로, 안전한 객체 접근을 위해서는 항상 lock 메서드를 사용해야 합니다.

 

11.5.3. weak_ptr와 순환 참조 문제

스마트 포인터는 메모리 관리를 편리하게 해주지만, std::shared_ptr를 사용할 때 주의해야 할 주요한 문제가 하나 있습니다. 바로 '순환 참조(circular reference)' 문제입니다. 이 문제는 서로가 서로를 참조하는 두 개의 std::shared_ptr 객체가 있을 때 발생합니다. 이렇게 되면 참조 카운트는 결코 0이 될 수 없으므로, 이러한 객체들은 메모리에서 절대 해제되지 않게 됩니다. 이것이 바로 메모리 누수(memory leak)를 일으키는 원인이 되는 것입니다.

 

이 문제를 해결하기 위해서 std::weak_ptr가 등장한 것입니다. std::weak_ptr는 std::shared_ptr와 달리 참조 카운트를 증가시키지 않기 때문에 순환 참조를 방지할 수 있습니다.

 

이를 이해하기 위해 다음과 같은 예를 들어 보겠습니다.

 

[예제]

class B;  // 선언

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A deleted\n"; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B deleted\n"; }
};

void func() {
    std::shared_ptr<A> a(new A);
    std::shared_ptr<B> b(new B);
    a->b_ptr = b;
    b->a_ptr = a;
}

 

위 코드에서 클래스 A와 B는 서로를 참조합니다. 따라서 func 함수가 종료될 때 a와 b의 참조 카운트는 각각 1로 남아있게 됩니다. 그 결과 A와 B 객체는 메모리에서 삭제되지 않습니다.

 

이 문제를 해결하기 위해 한쪽의 std::shared_ptr를 std::weak_ptr로 바꿔 보겠습니다.

 

[예제]

class B;

class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A deleted\n"; }
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // weak_ptr로 변경
    ~B() { std::cout << "B deleted\n"; }
};

void func() {
    std::shared_ptr<A> a(new A);
    std::shared_ptr<B> b(new B);
    a->b_ptr = b;
    b->a_ptr = a;
}

 

이제 func 함수가 종료될 때 b의 참조 카운트는 1이지만 a의 참조 카운트는 0이 되므로 A와 B 객체는 모두 메모리에서 삭제됩니다. 따라서 std::weak_ptr를 사용하면 순환 참조 문제를 효과적으로 해결할 수 있습니다. 그러나 std::weak_ptr를 사용할 때는 해당 객체가 여전히 메모리에 있는지 확인하고 접근해야 하므로 주의가 필요합니다. 

 

11.5.4. weak_ptr의 예제코드와 활용법

std::weak_ptr는 std::shared_ptr와 함께 사용되어 순환 참조 문제를 해결하고, 특정 객체가 여전히 메모리에 존재하는지 판단할 수 있는 방법을 제공합니다. 그렇다면 이제 std::weak_ptr의 기본적인 사용법과 예제 코드를 통해 실제로 어떻게 활용할 수 있는지 알아봅시다. 

 

먼저, std::weak_ptr는 기본적으로 std::shared_ptr에서 생성됩니다.

 

[예제]

std::shared_ptr<int> sp(new int(10)); 
std::weak_ptr<int> wp = sp;

 

위의 코드에서 wp는 sp로부터 생성된 std::weak_ptr입니다. 이때 wp는 sp가 가리키는 메모리에 대한 약한 참조(weak reference)를 가지게 됩니다. 그러나 wp는 참조 카운트를 증가시키지 않으므로, sp가 소멸되면 wp는 더 이상 유효한 참조를 가지지 않게 됩니다. 

 

그럼에도 불구하고 wp를 통해 안전하게 메모리에 접근하려면 어떻게 해야 할까요? 여기서는 std::weak_ptr의 lock 함수를 사용할 수 있습니다. lock 함수는 std::shared_ptr를 반환하며, 이 std::shared_ptr를 통해 안전하게 메모리에 접근할 수 있습니다. 

 

[예제]

std::shared_ptr<int> sp(new int(10)); 
std::weak_ptr<int> wp = sp;

if(auto p = wp.lock()) {  // wp가 유효한지 확인하고, 유효하다면 shared_ptr를 얻습니다.
    // 안전하게 메모리에 접근할 수 있습니다.
    std::cout << *p << std::endl;
} else {
    // wp가 더 이상 유효하지 않습니다.
    std::cout << "wp is no longer valid" << std::endl;
}

 

위의 코드에서 wp.lock()은 std::shared_ptr를 반환하며, 이 std::shared_ptr는 wp가 가리키는 메모리를 안전하게 참조할 수 있게 해 줍니다. 만약 wp가 더 이상 유효하지 않다면, lock 함수는 nullptr를 가리키는 std::shared_ptr를 반환하므로 안전하게 예외 상황을 처리할 수 있습니다. 

 

이처럼 std::weak_ptr는 메모리 관리를 더욱 안전하게 할 수 있게 해주는 중요한 도구입니다. 이를 잘 활용하면 메모리 누수와 같은 문제를 효과적으로 방지할 수 있습니다.

 

11.5.5. weak_ptr 예외 처리

C++에서 std::weak_ptr를 사용하면서 예외 처리를 적절히 하는 것은 매우 중요합니다. 이는 std::weak_ptr가 가리키는 메모리에 접근할 수 없을 때 생기는 예외 상황을 처리하기 위함입니다.

 

std::weak_ptr는 std::shared_ptr가 가리키는 메모리에 대한 약한 참조를 가지므로, std::shared_ptr가 소멸되면 std::weak_ptr는 더 이상 유효한 참조를 가지지 않게 됩니다. 이렇게 std::weak_ptr가 더 이상 유효하지 않은 상태에서 메모리에 접근하려고 하면 문제가 발생합니다.

 

std::weak_ptr에서 제공하는 lock 함수를 이용하면 안전하게 메모리에 접근할 수 있습니다. lock 함수는 std::shared_ptr를 반환하며, 이 std::shared_ptr를 통해 메모리에 접근할 수 있습니다. 만약 std::weak_ptr가 유효하지 않다면 lock 함수는 nullptr를 가리키는 std::shared_ptr를 반환합니다. 

 

다음은 lock 함수를 이용한 예제 코드입니다:

 

[예제]

std::shared_ptr<int> sp(new int(10)); 
std::weak_ptr<int> wp = sp;

sp.reset();  // sp를 reset 하여 메모리를 해제합니다.

if(auto p = wp.lock()) {  // wp가 유효한지 확인하고, 유효하다면 shared_ptr를 얻습니다.
    // wp가 유효하다면 이 블록은 실행되지 않습니다.
    std::cout << *p << std::endl;
} else {
    // wp가 더 이상 유효하지 않으므로, 이 블록이 실행됩니다.
    std::cout << "wp is no longer valid" << std::endl;
}

 

위의 코드에서 wp.lock()은 std::shared_ptr를 반환합니다. 만약 wp가 유효하다면 이 std::shared_ptr를 통해 메모리에 안전하게 접근할 수 있습니다. 반면 wp가 더 이상 유효하지 않다면, lock 함수는 nullptr를 가리키는 std::shared_ptr를 반환하므로 안전하게 예외 상황을 처리할 수 있습니다.

 

이처럼 std::weak_ptr는 lock 함수를 통해 안전하게 메모리에 접근할 수 있게 해 주며, 더 이상 유효하지 않은 참조에 대한 예외 상황을 적절히 처리할 수 있습니다. 이러한 기능은 메모리 관리를 더욱 안전하고 효율적으로 만들어줍니다.

 

마지막으로, std::weak_ptr는 순환 참조와 같은 문제를 방지하면서도 동시에 안전하게 메모리를 관리할 수 있게 해주는 매우 유용한 도구입니다. 이를 적절히 활용하여 프로그램의 안정성을 높일 수 있습니다.


11.6. 사용자 정의 스마트 포인터

C++에서는 사용자 정의 스마트 포인터를 생성하여 프로그램의 특정 요구사항을 충족시킬 수 있습니다. 사용자 정의 스마트 포인터는 기본 제공되는 std::unique_ptr, std::shared_ptr, std::weak_ptr 등으로 충분하지 않은 경우에 유용하게 사용할 수 있습니다. 이를 만들기 위해서는 연산자 오버로딩, 생성자와 소멸자의 적절한 사용, 복사 생성자와 복사 대입 연산자의 관리 등에 대한 깊은 이해가 필요합니다. 사용자 정의 스마트 포인터는 메모리뿐 아니라, 네트워크 연결이나 파일 핸들 등 다양한 자원을 관리하는 데에도 활용할 수 있습니다.

11.6.1. 사용자 정의 스마트 포인터 구현하기

우선, 사용자 정의 스마트 포인터가 필요한 이유를 알아보겠습니다. C++의 STL 라이브러리는 std::unique_ptr, std::shared_ptr 및 std::weak_ptr 등을 제공하지만, 때로는 특정 요구 사항을 충족하기 위해 사용자 정의 스마트 포인터를 만드는 것이 필요합니다. 

 

사용자 정의 스마트 포인터를 만드는 기본적인 원칙은 소유권 정책을 구현하는 것입니다. 즉, 언제 메모리를 할당하고, 언제 메모리를 해제할지에 대한 로직을 만드는 것입니다. 이것은 포인터의 생명 주기를 관리하는 기능을 갖춘 클래스를 구현함으로써 달성할 수 있습니다.

 

가장 기본적인 사용자 정의 스마트 포인터의 구현은 다음과 같습니다.

 

[예제]

template <typename T>
class SmartPointer {
private:
    T* ptr;
public:
    explicit SmartPointer(T* p = nullptr) { ptr = p; }
    ~SmartPointer() { delete(ptr); }
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }
};

 

위 예제에서 SmartPointer 클래스는 메모리의 할당과 해제를 관리합니다. 생성자에서는 포인터를 초기화하고, 소멸자에서는 메모리를 해제합니다. 연산자 오버로딩을 통해 일반 포인터와 같은 방식으로 스마트 포인터를 사용할 수 있습니다. 이러한 간단한 사용자 정의 스마트 포인터는 std::unique_ptr와 비슷한 역할을 수행하지만, 복사와 이동에 대한 로직을 추가하여 std::shared_ptr와 유사한 기능도 만들 수 있습니다.

 

하지만, 사용자 정의 스마트 포인터를 구현할 때는 주의가 필요합니다. 연산자 오버로딩, 메모리 관리 등 복잡한 문제를 정확하게 처리하지 않으면 메모리 누수, 데드락, 다른 예상치 못한 문제가 발생할 수 있습니다. 따라서, 가능하다면 표준 라이브러리의 스마트 포인터를 사용하는 것이 가장 좋으며, 특별한 이유가 있는 경우에만 사용자 정의 스마트 포인터를 구현하는 것을 추천합니다.

 

11.6.2. 사용자 정의 스마트 포인터의 활용 방안

우리가 이전 섹션에서 사용자 정의 스마트 포인터를 만드는 방법에 대해 배웠다면, 이제 이것들이 어떻게 실제 코드에서 활용될 수 있는지 알아보겠습니다. 사용자 정의 스마트 포인터는 C++ 프로그래밍에서 다양한 장점을 제공하며, 특히 동적 메모리 관리와 관련된 문제를 해결하는데 매우 유용합니다.

 

먼저, 사용자 정의 스마트 포인터는 메모리 누수를 방지하는 데 도움이 됩니다. 일반적으로 동적 메모리는 new 연산자를 사용하여 할당되며, delete 연산자를 사용하여 해제됩니다. 하지만 예외가 발생하거나, 중간에 함수가 반환되는 등의 상황에서는 delete를 호출하지 않아 메모리 누수가 발생할 수 있습니다. 사용자 정의 스마트 포인터는 객체가 소멸될 때 자동으로 메모리를 해제함으로써 이러한 문제를 해결합니다.

 

다음으로, 사용자 정의 스마트 포인터는 코드의 안정성을 높입니다. 일반 포인터를 사용할 경우, 포인터를 가리키는 객체가 이미 메모리에서 해제된 상태에서 해당 포인터를 사용하려고 하면 크래시를 유발하는 '행방불명 포인터(dangling pointer)' 문제가 발생할 수 있습니다. 스마트 포인터는 이러한 문제를 해결하기 위해 포인터가 더 이상 유효하지 않게 되면 null로 설정되도록 설계되어 있습니다.

 

또한, 사용자 정의 스마트 포인터는 보다 복잡한 소유권 정책을 구현할 수 있습니다. 예를 들어, std::shared_ptr는 여러 객체가 같은 메모리를 가리키는 것을 허용하는 반면, std::unique_ptr는 단일 소유권을 제공합니다. 사용자 정의 스마트 포인터를 사용하면 이러한 정책 외에도 특정 상황에 맞는 소유권 정책을 구현할 수 있습니다.

 

이제 간단한 예제를 통해 사용자 정의 스마트 포인터의 활용을 보겠습니다.

 

[예제]

int main() {
    SmartPointer<int> sp(new int(10));
    cout << *sp << endl; // 10 출력

    {
        SmartPointer<int> sp2 = sp; // error! 복사 생성자를 구현하지 않았으므로 컴파일 오류 발생
        cout << *sp2 << endl;
    } // sp2의 생명 주기가 끝나므로 메모리가 해제됨

    cout << *sp << endl; // 메모리가 이미 해제되었으므로 예상치 못한 동작 발생
    return 0;
}

 

위의 코드에서는 SmartPointer 클래스의 객체 sp가 메모리를 할당하고, sp2가 sp를 복사하려고 하지만 복사 생성자가 구현되어 있지 않기 때문에 컴파일 오류가 발생합니다. 이를 해결하기 위해 복사 생성자 또는 이동 생성자를 구현하거나, 복사를 금지하는 것이 필요합니다.

 

이처럼 사용자 정의 스마트 포인터를 적절하게 활용하면 코드의 안정성을 높이고 메모리 관리를 보다 효과적으로 할 수 있습니다. 하지만 구현에 있어서 많은 주의가 필요하며, 가능한 한 표준 라이브러리의 스마트 포인터를 사용하는 것이 권장됩니다.

 

11.6.3. 사용자 정의 스마트 포인터 예제코드와 활용법

스마트 포인터를 사용하면 메모리 관리를 보다 쉽게 할 수 있지만, 경우에 따라서는 표준 라이브러리의 스마트 포인터로는 충분하지 않을 때가 있습니다. 이럴 때 사용자 정의 스마트 포인터를 구현하여 사용하는 것이 유용합니다.  간단한 사용자 정의 스마트 포인터를 만드는 방법을 예제코드와 함께 보여드리겠습니다. 이 스마트 포인터는 단일 소유권을 가지며, 메모리가 더 이상 필요하지 않을 때 자동으로 해제됩니다.

 

[예제]

template <class T>
class SmartPointer {
private:
    T* ptr;
public:
    explicit SmartPointer(T* p = NULL) { ptr = p; }

    // 디스트럭터: 메모리 해제
    ~SmartPointer() { delete(ptr); }

    // 오버로딩 연산자들
    T& operator * () { return *ptr; }
    T* operator -> () { return ptr; }
};

 

위의 코드는 간단한 사용자 정의 스마트 포인터의 구현입니다. 이 클래스는 템플릿으로 선언되어 있어 다양한 타입에 대해 동작하며, 포인터를 멤버로 가집니다. 생성자에서는 전달된 원시 포인터를 멤버 포인터에 저장하고, 디스트럭터에서는 포인터가 가리키는 메모리를 해제합니다.

 

이 스마트 포인터를 사용하는 코드는 다음과 같습니다:

 

[예제]

int main() {
    SmartPointer<int> sp(new int(10));
    cout << *sp << endl; // 10 출력

    {
        SmartPointer<int> sp2(new int(20));
        cout << *sp2 << endl; // 20 출력
    } // sp2의 범위를 벗어나며 자동으로 메모리 해제

    cout << *sp << endl; // 10 출력
    return 0;
}

 

sp2가 {}로 구분된 범위를 벗어날 때 자동으로 메모리가 해제된다는 것입니다. 이는 SmartPointer 클래스의 디스트럭터가 호출되어 메모리 해제를 자동으로 처리하기 때문입니다. 따라서 이 코드는 메모리 누수를 방지합니다. 

 

이제 이 스마트 포인터를 활용하는 방법에 대해 알아보겠습니다. 사용자 정의 스마트 포인터는 원시 포인터를 사용하는 곳이라면 어디에서든 사용할 수 있습니다. 예를 들어, 동적으로 할당된 배열이나 클래스의 인스턴스에도 사용할 수 있습니다.

 

[예제]

// 배열에 사용
SmartPointer<int> sp(new int[10]);

// 클래스 인스턴스에 사용
class MyClass { /* 클래스 정의 */ };
SmartPointer<MyClass> sp(new MyClass());

 

이처럼 사용자 정의 스마트 포인터를 활용하면 메모리 관리를 보다 편리하게 할 수 있습니다. 하지만 이 구현은 단순화된 버전이며, 실제로는 복사 생성자와 대입 연산자의 오버로딩, 예외 처리 등을 고려해야 할 부분이 많습니다. 따라서 실제 개발에서는 표준 라이브러리의 스마트 포인터를 사용하는 것이 권장됩니다. 

 

 

 

2023.06.09 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍 : 중급] 10. STL 알고리즘

 

[C/C++ 프로그래밍 : 중급] 10. STL 알고리즘

Chapter 10. STL 알고리즘 C++ 표준 템플릿 라이브러리 (STL)의 핵심 부분인 알고리즘을 배웁니다. 알고리즘의 개념, 특징, 분류부터 다양한 알고리즘 함수들의 사용법까지 자세하게 다룹니다. 알고리

gdngy.tistory.com

 

반응형

댓글