본문으로 건너뛰기 C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe

C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe

C++ Smart Pointers: unique_ptr, shared_ptr & Memory-Safe

이 글의 핵심

C++ smart pointers explained: unique_ptr for exclusive ownership, shared_ptr for shared ownership, weak_ptr for cycles—examples, make_unique/make_shared, and production tips.

Introduction

Smart pointers are RAII-style wrappers that provide automatic memory management. They help avoid leaks and dangling pointers from raw new/delete.

// ❌ Raw pointer (unsafe)
int* ptr = new int(10);  // Allocate on heap
// ... use ...
delete ptr;  // Manual delete (forget it → leak!)
// Issues:
// 1. Forgetting delete → leak
// 2. Exception may skip delete
// 3. Double delete → crash
// 4. Use after delete → UB (dangling)
// ✅ Smart pointer (safe)
// std::make_unique: factory for unique_ptr
// RAII: acquire in constructor, release in destructor
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// delete runs automatically at end of scope
// Still safe if exceptions occur

1. unique_ptr — exclusive ownership

Basic usage

#include <memory>
#include <iostream>
int main() {
    // Create
    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    
    // Use
    std::cout << *ptr << std::endl;  // 10
    *ptr = 20;
    std::cout << *ptr << std::endl;  // 20
    
    // nullptr check
    if (ptr) {
        std::cout << "valid" << std::endl;
    }
    
    // Array
    std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
    arr[0] = 1;
    arr[1] = 2;
    std::cout << arr[0] << ", " << arr[1] << std::endl;  // 1, 2
    
    return 0;
}  // automatic delete

Move only (no copy)

#include <memory>
#include <iostream>
// Pass by value: transfer ownership
// Memory freed when function returns
void process(std::unique_ptr<int> ptr) {
    std::cout << "value: " << *ptr << std::endl;
}  // ptr destroyed → memory freed
int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10);
    
    // ❌ No copy: unique_ptr is exclusive
    // std::unique_ptr<int> ptr2 = ptr1;  // compile error
    // Copy constructor is deleted
    
    // ✅ Move: transfer ownership with std::move
    // Ownership moves from ptr1 to ptr2
    // After move, ptr1 is nullptr
    std::unique_ptr<int> ptr2 = std::move(ptr1);
    
    // Check ptr1
    if (!ptr1) {
        std::cout << "ptr1 is nullptr" << std::endl;
    }
    // Check ptr2
    if (ptr2) {
        std::cout << "ptr2 valid: " << *ptr2 << std::endl;
    }
    
    // Pass to function: transfer ownership
    // std::move(ptr2) transfers ownership into process
    // After call, ptr2 is nullptr
    process(std::move(ptr2));
    
    if (!ptr2) {
        std::cout << "ptr2 also nullptr" << std::endl;
    }
    
    return 0;
}

Output:

ptr1 is nullptr
ptr2 valid: 10
value: 10
ptr2 also nullptr

2. shared_ptr — shared ownership

Basic usage

#include <memory>
#include <iostream>
int main() {
    // std::make_shared: preferred way to make shared_ptr
    // One allocation for control block + object (efficient)
    std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
    // use_count(): current reference count
    std::cout << "ref count: " << ptr1.use_count() << std::endl;  // 1
    
    {
        // shared_ptr copies: ref count increases
        // ptr1 and ptr2 share the same object
        std::shared_ptr<int> ptr2 = ptr1;  // copy ok
        // Ref count 2: two owners
        std::cout << "ref count: " << ptr1.use_count() << std::endl;  // 2
        std::cout << "ptr1: " << *ptr1 << std::endl;  // 10
        std::cout << "ptr2: " << *ptr2 << std::endl;  // 10
    }  // ptr2 destroyed → count 2 → 1
       // ptr1 still alive → storage kept
    
    std::cout << "ref count: " << ptr1.use_count() << std::endl;  // 1
    
    return 0;
}  // ptr1 destroyed → count 0 → freed

Output:

ref count: 1
ref count: 2
ptr1: 10
ptr2: 10
ref count: 1

Reference counting

#include <memory>
#include <iostream>
#include <vector>
class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " created" << std::endl;
    }
    
    ~Resource() {
        std::cout << "Resource " << id_ << " destroyed" << std::endl;
    }
    
    int getId() const { return id_; }
    
private:
    int id_;
};
int main() {
    std::vector<std::shared_ptr<Resource>> resources;
    
    {
        auto r1 = std::make_shared<Resource>(1);
        resources.push_back(r1);
        resources.push_back(r1);
        resources.push_back(r1);
        
        std::cout << "ref count: " << r1.use_count() << std::endl;  // 4
    }  // r1 gone but vector still holds refs
    
    std::cout << "vector size: " << resources.size() << std::endl;  // 3
    std::cout << "ref count: " << resources[0].use_count() << std::endl;  // 3
    
    resources.clear();  // drop refs → destroy Resource
    
    return 0;
}

Output:

Resource 1 created
ref count: 4
vector size: 3
ref count: 3
Resource 1 destroyed

3. weak_ptr — breaking circular references

The circular reference problem

#include <memory>
#include <iostream>
class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
    std::shared_ptr<A> a_ptr;  // circular ref!
    ~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        
        a->b_ptr = b;
        b->a_ptr = a;  // circular reference
        
        std::cout << "a ref count: " << a.use_count() << std::endl;  // 2
        std::cout << "b ref count: " << b.use_count() << std::endl;  // 2
    }  // a,b dtor never runs — not freed!
    
    std::cout << "block end" << std::endl;
    
    return 0;
}

Output:

a ref count: 2
b ref count: 2
block end

Problem: A and B destructors never run (leak).

Fixing it with weak_ptr

#include <memory>
#include <iostream>
class B;
class A {
public:
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
    std::weak_ptr<A> a_ptr;  // use weak_ptr
    ~B() { std::cout << "B destroyed" << std::endl; }
};
int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();
        
        a->b_ptr = b;
        b->a_ptr = a;  // weak_ptr does not bump strong count
        
        std::cout << "a ref count: " << a.use_count() << std::endl;  // 1
        std::cout << "b ref count: " << b.use_count() << std::endl;  // 2
    }  // A and B destroy normally
    
    std::cout << "block end" << std::endl;
    
    return 0;
}

Output:

a ref count: 1
b ref count: 2
B destroyed
A destroyed
block end

Using weak_ptr

#include <memory>
#include <iostream>
int main() {
    // weak_ptr: non-owning weak reference
    std::weak_ptr<int> weak;
    
    {
        // create shared_ptr
        std::shared_ptr<int> shared = std::make_shared<int>(42);
        // assign to weak_ptr: no strong count bump
        // does not extend shared lifetime
        weak = shared;
        
        // strong count still 1
        std::cout << "shared ref count: " << shared.use_count() << std::endl;  // 1
        
        // use weak_ptr: get shared_ptr via lock()
        // lock(): shared if alive, else nullptr
        if (auto locked = weak.lock()) {
            // locked: temporary shared (count++)
            std::cout << "value: " << *locked << std::endl;  // 42
            // ref count 2: shared + locked
            std::cout << "ref count: " << locked.use_count() << std::endl;  // 2
        }  // locked destroyed → back to 1
    }  // shared gone → count 0 → freed
    
    // check weak_ptr expiry
    // expired(): was object destroyed?
    if (weak.expired()) {
        std::cout << "weak_ptr expired" << std::endl;
    }
    
    // lock() on expired weak_ptr
    if (auto locked = weak.lock()) {
        std::cout << "value: " << *locked << std::endl;
    } else {
        // object gone — lock fails
        std::cout << "lock failed" << std::endl;
    }
    
    return 0;
}

Output:

shared ref count: 1
value: 42
ref count: 2
weak_ptr expired
lock failed

4. Practical examples

Example 1: Resource management

#include <memory>
#include <iostream>
#include <fstream>
class FileHandler {
private:
    std::unique_ptr<std::ofstream> file;
    std::string filename;
    
public:
    FileHandler(const std::string& filename) : filename(filename) {
        file = std::make_unique<std::ofstream>(filename);
        if (!file->is_open()) {
            throw std::runtime_error("failed to open file: " + filename);
        }
        std::cout << "file opened: " << filename << std::endl;
    }
    
    ~FileHandler() {
        if (file && file->is_open()) {
            file->close();
            std::cout << "file closed: " << filename << std::endl;
        }
    }
    
    void write(const std::string& data) {
        if (file && file->is_open()) {
            *file << data << std::endl;
        }
    }
};
int main() {
    try {
        FileHandler handler("output.txt");
        handler.write("Hello");
        handler.write("World");
        // file still closed on exception
    } catch (const std::exception& e) {
        std::cerr << "error: " << e.what() << std::endl;
    }
    
    return 0;
}

Example 2: Factory pattern

#include <memory>
#include <iostream>
#include <string>
class Animal {
public:
    virtual void speak() = 0;
    virtual ~Animal() {}
};
class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof!" << std::endl;
    }
    ~Dog() {
        std::cout << "Dog destroyed" << std::endl;
    }
};
class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Meow!" << std::endl;
    }
    ~Cat() {
        std::cout << "Cat destroyed" << std::endl;
    }
};
std::unique_ptr<Animal> createAnimal(const std::string& type) {
    if (type == "dog") {
        return std::make_unique<Dog>();
    } else if (type == "cat") {
        return std::make_unique<Cat>();
    }
    return nullptr;
}
int main() {
    auto animal1 = createAnimal("dog");
    if (animal1) {
        animal1->speak();
    }
    
    auto animal2 = createAnimal("cat");
    if (animal2) {
        animal2->speak();
    }
    
    auto animal3 = createAnimal("bird");
    if (!animal3) {
        std::cout << "unknown animal" << std::endl;
    }
    
    return 0;
}

Output:

Woof!
Meow!
unknown animal
Cat destroyed
Dog destroyed

Example 3: Cache (shared_ptr)

#include <memory>
#include <unordered_map>
#include <iostream>
#include <string>
class Resource {
private:
    std::string name;
    
public:
    Resource(std::string n) : name(n) {
        std::cout << "load resource: " << name << std::endl;
    }
    
    ~Resource() {
        std::cout << "unload resource: " << name << std::endl;
    }
    
    void use() {
        std::cout << name << " in use" << std::endl;
    }
    
    std::string getName() const { return name; }
};
class ResourceCache {
private:
    std::unordered_map<std::string, std::shared_ptr<Resource>> cache;
    
public:
    std::shared_ptr<Resource> getResource(const std::string& name) {
        if (cache.find(name) == cache.end()) {
            cache[name] = std::make_shared<Resource>(name);
        }
        return cache[name];
    }
    
    void printCacheSize() {
        std::cout << "cache size: " << cache.size() << std::endl;
    }
    
    void clear() {
        cache.clear();
        std::cout << "cache cleared" << std::endl;
    }
};
int main() {
    ResourceCache cache;
    
    {
        auto r1 = cache.getResource("texture1");
        auto r2 = cache.getResource("texture1");  // same object
        r1->use();
        
        std::cout << "r1 ref count: " << r1.use_count() << std::endl;  // 3 (r1, r2, cache)
        std::cout << "r2 ref count: " << r2.use_count() << std::endl;  // 3
    }  // r1,r2 gone but cache holds ref
    
    cache.printCacheSize();  // 1
    
    cache.clear();  // clear cache → unload
    
    return 0;
}

Output:

load resource: texture1
texture1 in use
r1 ref count: 3
r2 ref count: 3
cache size: 1
cache cleared
unload resource: texture1

5. Common pitfalls

Pitfall 1: Avoiding make_unique / make_shared

#include <memory>
void func(std::unique_ptr<int> p1, std::unique_ptr<int> p2) {
    // ...
}
int main() {
    // ❌ unsafe: exception safety issue
    // func(std::unique_ptr<int>(new int(1)), std::unique_ptr<int>(new int(2)));
    // evaluation order not guaranteed → possible leak:
    // 1. new int(1)
    // 2. new int(2)
    // 3. unique_ptr ctor (exception can leak 1,2)
    
    // ✅ safe
    func(std::make_unique<int>(1), std::make_unique<int>(2));
    
    // ✅ or
    auto p1 = std::make_unique<int>(1);
    auto p2 = std::make_unique<int>(2);
    func(std::move(p1), std::move(p2));
    
    return 0;
}

Pitfall 2: shared_ptr cycles

#include <memory>
#include <iostream>
// ❌ cycle
class Node {
public:
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // cycle!
    int value;
    
    Node(int v) : value(v) {
        std::cout << "Node " << value << " created" << std::endl;
    }
    
    ~Node() {
        std::cout << "Node " << value << " destroyed" << std::endl;
    }
};
void testCircular() {
    auto n1 = std::make_shared<Node>(1);
    auto n2 = std::make_shared<Node>(2);
    
    n1->next = n2;
    n2->prev = n1;  // circular ref — dtors never run
    
    std::cout << "n1 ref count: " << n1.use_count() << std::endl;  // 2
    std::cout << "n2 ref count: " << n2.use_count() << std::endl;  // 2
}
// ✅ use weak_ptr
class NodeFixed {
public:
    std::shared_ptr<NodeFixed> next;
    std::weak_ptr<NodeFixed> prev;  // weak_ptr
    int value;
    
    NodeFixed(int v) : value(v) {
        std::cout << "NodeFixed " << value << " created" << std::endl;
    }
    
    ~NodeFixed() {
        std::cout << "NodeFixed " << value << " destroyed" << std::endl;
    }
};
void testFixed() {
    auto n1 = std::make_shared<NodeFixed>(1);
    auto n2 = std::make_shared<NodeFixed>(2);
    
    n1->next = n2;
    n2->prev = n1;  // weak_ptr does not bump strong count
    
    std::cout << "n1 ref count: " << n1.use_count() << std::endl;  // 1
    std::cout << "n2 ref count: " << n2.use_count() << std::endl;  // 2
}
int main() {
    std::cout << "=== circular ref test ===" << std::endl;
    testCircular();
    std::cout << "end function (dtors NOT called!)" << std::endl;
    
    std::cout << "\n=== weak_ptr test ===" << std::endl;
    testFixed();
    std::cout << "end function (dtors called)" << std::endl;
    
    return 0;
}

Output:

=== circular ref test ===
Node 1 created
Node 2 created
n1 ref count: 2
n2 ref count: 2
end function (dtors NOT called!)
=== weak_ptr test ===
NodeFixed 1 created
NodeFixed 2 created
n1 ref count: 1
n2 ref count: 2
NodeFixed 2 destroyed
NodeFixed 1 destroyed
end function (dtors called)

Pitfall 3: Passing unique_ptr to functions

#include <memory>
#include <iostream>
// Option 1: transfer ownership
void takeOwnership(std::unique_ptr<int> ptr) {
    std::cout << "ownership transfer: " << *ptr << std::endl;
}
// Option 2: pass by const& (keep ownership)
void borrow(const std::unique_ptr<int>& ptr) {
    std::cout << "borrow: " << *ptr << std::endl;
}
// Option 3: raw pointer (non-owning)
void observe(int* ptr) {
    if (ptr) {
        std::cout << "observe: " << *ptr << std::endl;
    }
}
int main() {
    auto ptr = std::make_unique<int>(10);
    
    // ❌ compile error
    // takeOwnership(ptr);  // no copy
    
    // ✅ transfer ownership
    // takeOwnership(std::move(ptr));  // ptr becomes nullptr
    
    // ✅ pass by const&
    borrow(ptr);  // ptr still valid
    
    // ✅ raw .get()
    observe(ptr.get());  // ptr still valid
    
    std::cout << "ptr valid: " << (ptr ? "yes" : "no") << std::endl;
    
    return 0;
}

Output:

borrow: 10
observe: 10
ptr valid: yes

6. Example: resource manager

#include <memory>
#include <vector>
#include <iostream>
#include <string>
class ResourceManager {
private:
    std::vector<std::unique_ptr<std::string>> resources_;
    
public:
    // add resource
    void add(std::unique_ptr<std::string> resource) {
        resources_.push_back(std::move(resource));
    }
    
    // create and add
    void create(const std::string& value) {
        resources_.push_back(std::make_unique<std::string>(value));
    }
    
    // take (move out)
    std::unique_ptr<std::string> take(size_t index) {
        if (index >= resources_.size()) return nullptr;
        
        auto resource = std::move(resources_[index]);
        resources_.erase(resources_.begin() + index);
        return resource;
    }
    
    // count
    size_t count() const {
        return resources_.size();
    }
    
    // print
    void print() const {
        std::cout << "Resources (" << resources_.size() << "):" << std::endl;
        for (size_t i = 0; i < resources_.size(); ++i) {
            if (resources_[i]) {
                std::cout << "  [" << i << "]: " << *resources_[i] << std::endl;
            } else {
                std::cout << "  [" << i << "]: (moved)" << std::endl;
            }
        }
    }
};
int main() {
    ResourceManager mgr;
    
    // add resource
    mgr.add(std::make_unique<std::string>("Resource 1"));
    mgr.create("Resource 2");
    mgr.create("Resource 3");
    
    std::cout << "initial state:" << std::endl;
    mgr.print();
    
    // take resource
    auto r = mgr.take(1);
    std::cout << "\ntook resource: " << *r << std::endl;
    
    std::cout << "\nremaining:" << std::endl;
    mgr.print();
    
    return 0;
}

Output:

initial state:
Resources (3):
  [0]: Resource 1
  [1]: Resource 2
  [2]: Resource 3
took resource: Resource 2
remaining:
Resources (2):
  [0]: Resource 1
  [1]: Resource 3

Summary

Key takeaways

  1. unique_ptr: exclusive; no copy; movable
  2. shared_ptr: shared ownership; ref counting
  3. weak_ptr: break cycles; no strong count bump
  4. make_unique/make_shared: exception safety; performance
  5. RAII: automatic cleanup

Smart pointer comparison

Featureunique_ptrshared_ptrweak_ptr
Ownershipexclusivesharednone
Copynoyesyes
Moveyesyesyes
Overheadnoneref countnone
Usedefaultsharingcycles
Arraysyes (T[])limited-

Practical tips

Selection guide:

  • Default: unique_ptr
  • Sharing: shared_ptr
  • Cycles: weak_ptr
  • Arrays: unique_ptr<T[]> or vector Performance:
  • unique_ptr: same cost as raw pointer
  • shared_ptr: small refcount overhead
  • make_shared: often one allocation Caveats:
  • Prefer make_unique / make_shared
  • Watch for cycles
  • Do not use moved-from objects
  • Avoid dangling pointers

Next steps


More articles connected to this topic.

Practical tips

Tips you can apply at work.

Debugging

  • Check compiler warnings first
  • Reproduce with a minimal test

Performance

  • Do not optimize without profiling
  • Define measurable goals first

Code review

  • Anticipate typical review feedback
  • Follow team conventions

Practical checklist

Use this when applying these ideas in production.

Before you code

  • Is this the best fix for the problem?
  • Can teammates maintain this?
  • Meets performance requirements?

While coding

  • All warnings fixed?
  • Edge cases covered?
  • Error handling OK?

At review

  • Intent clear?
  • Tests sufficient?
  • Documented? Use this checklist to reduce mistakes and improve quality.

C++, smart pointers, unique_ptr, shared_ptr, memory management — searches like these should help you find this article.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. C++ smart pointers explained: unique_ptr for exclusive ownership, shared_ptr for shared ownership, weak_ptr for cycles—e…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, smart pointers, unique_ptr, shared_ptr, memory management, RAII.