乐闻世界logo
搜索文章和话题

How to use C++ smart pointers

2月18日 17:41

C++ Smart Pointers Deep Dive

Smart pointers are important features introduced in C++11 for automatically managing dynamically allocated memory, avoiding memory leaks and dangling pointer issues.

Smart Pointer Overview

Why do we need smart pointers?

  • Automatically release memory, avoiding memory leaks
  • Prevent dangling pointers and double deletion
  • Provide exception-safe memory management
  • Clearly express ownership semantics

RAII Principle:

  • Resource Acquisition Is Initialization
  • Resources are acquired in constructors and released in destructors
  • Use object lifecycle to manage resources

unique_ptr

Basic usage:

cpp
#include <memory> // Create unique_ptr std::unique_ptr<int> ptr1(new int(42)); std::unique_ptr<int> ptr2 = std::make_unique<int>(42); // C++14 // Access object *ptr1 = 100; std::cout << *ptr1 << std::endl; // Reset ptr1.reset(); // Release memory ptr1.reset(new int(200)); // Reallocate // Release ownership int* rawPtr = ptr1.release(); // ptr1 no longer manages memory delete rawPtr; // Manual deletion // Check if empty if (ptr1) { std::cout << "ptr1 is not empty" << std::endl; }

Move semantics:

cpp
// unique_ptr can only be moved, not copied std::unique_ptr<int> ptr1 = std::make_unique<int>(42); std::unique_ptr<int> ptr2 = std::move(ptr1); // Move construction // ptr1 is now empty if (!ptr1) { std::cout << "ptr1 is empty after move" << std::endl; } // Move assignment std::unique_ptr<int> ptr3; ptr3 = std::move(ptr2); // ptr2 becomes empty

Custom deleters:

cpp
// Array deleter struct ArrayDeleter { void operator()(int* p) const { delete[] p; } }; std::unique_ptr<int, ArrayDeleter> arr(new int[10]); // Use lambda deleter auto deleter = [](FILE* f) { fclose(f); }; std::unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "w"), deleter); // Use default array deleter std::unique_ptr<int[]> arr2(new int[10]); arr2[0] = 1; arr2[1] = 2;

Using in containers:

cpp
std::vector<std::unique_ptr<int>> vec; // Use make_unique vec.push_back(std::make_unique<int>(1)); vec.push_back(std::make_unique<int>(2)); // Use emplace_back vec.emplace_back(std::make_unique<int>(3)); // Iterate for (const auto& ptr : vec) { std::cout << *ptr << std::endl; }

shared_ptr

Basic usage:

cpp
// Create shared_ptr std::shared_ptr<int> ptr1(new int(42)); std::shared_ptr<int> ptr2 = std::make_shared<int>(42); // Recommended // Copy construction std::shared_ptr<int> ptr3 = ptr1; // Reference count +1 // Assignment std::shared_ptr<int> ptr4; ptr4 = ptr1; // Reference count +1 // Check reference count std::cout << "use_count: " << ptr1.use_count() << std::endl; // Reset ptr1.reset(); // Reference count -1

Reference counting mechanism:

cpp
class MyClass { public: MyClass() { std::cout << "Constructor" << std::endl; } ~MyClass() { std::cout << "Destructor" << std::endl; } }; { std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); std::cout << "Count: " << ptr1.use_count() << std::endl; // 1 { std::shared_ptr<MyClass> ptr2 = ptr1; std::cout << "Count: " << ptr1.use_count() << std::endl; // 2 } std::cout << "Count: " << ptr1.use_count() << std::endl; // 1 } // ptr1 destructor, object is deleted

Custom deleters:

cpp
// Use function pointer void customDeleter(int* p) { std::cout << "Custom deleter called" << std::endl; delete p; } std::shared_ptr<int> ptr(new int(42), customDeleter); // Use lambda auto deleter = [](int* p) { std::cout << "Lambda deleter called" << std::endl; delete p; }; std::shared_ptr<int> ptr2(new int(42), deleter); // Array deleter std::shared_ptr<int> arr(new int[10], [](int* p) { delete[] p; });

get() and reset():

cpp
std::shared_ptr<int> ptr = std::make_shared<int>(42); // Get raw pointer int* rawPtr = ptr.get(); *rawPtr = 100; // Reset ptr.reset(); // Release current object ptr.reset(new int(200)); // Manage new object

weak_ptr

Basic usage:

cpp
// Create weak_ptr std::shared_ptr<int> shared = std::make_shared<int>(42); std::weak_ptr<int> weak = shared; // Check if expired if (!weak.expired()) { std::cout << "Object still exists" << std::endl; } // Lock to get shared_ptr if (auto ptr = weak.lock()) { std::cout << *ptr << std::endl; }

Solving circular references:

cpp
class Node { public: std::shared_ptr<Node> next; std::weak_ptr<Node> prev; // Use weak_ptr to avoid circular references ~Node() { std::cout << "Node destroyed" << std::endl; } }; void createCycle() { auto node1 = std::make_shared<Node>(); auto node2 = std::make_shared<Node>(); node1->next = node2; node2->prev = node1; // weak_ptr doesn't increase reference count // node1 and node2 will be properly released }

Observer pattern:

cpp
class Subject { private: std::vector<std::weak_ptr<Observer>> observers; public: void addObserver(std::shared_ptr<Observer> obs) { observers.push_back(obs); } void notify() { for (auto it = observers.begin(); it != observers.end(); ) { if (auto obs = it->lock()) { obs->update(); ++it; } else { // Observer has been deleted, remove it = observers.erase(it); } } } };

Smart Pointer Best Practices

1. Prefer make_unique and make_shared

cpp
// Recommended auto ptr1 = std::make_unique<int>(42); auto ptr2 = std::make_shared<int>(42); // Not recommended std::unique_ptr<int> ptr1(new int(42)); std::shared_ptr<int> ptr2(new int(42));

2. Avoid using raw pointers to manage smart pointers

cpp
// Not recommended int* raw = new int(42); std::unique_ptr<int> ptr(raw); // Recommended auto ptr = std::make_unique<int>(42);

3. Don't return raw pointers from functions

cpp
// Not recommended int* getPtr() { auto ptr = std::make_unique<int>(42); return ptr.get(); // Dangerous! } // Recommended std::shared_ptr<int> getPtr() { return std::make_shared<int>(42); }

4. Use smart pointers to manage arrays

cpp
// unique_ptr array std::unique_ptr<int[]> arr(new int[10]); arr[0] = 1; // shared_ptr array (needs custom deleter) std::shared_ptr<int> arr(new int[10], [](int* p) { delete[] p; });

5. Use smart pointers in function parameters

cpp
// Pass by value (increases reference count) void process(std::shared_ptr<int> ptr) { // Use ptr } // Pass by reference (doesn't increase reference count) void process(const std::shared_ptr<int>& ptr) { // Use ptr } // Pass raw pointer (if function doesn't need ownership) void process(int* ptr) { // Use ptr }

Common Pitfalls

1. Circular references

cpp
class A { public: std::shared_ptr<B> b; }; class B { public: std::shared_ptr<A> a; // Circular reference! }; // Solution: use weak_ptr class B { public: std::weak_ptr<A> a; // Correct };

2. this pointer issue

cpp
class MyClass { public: std::shared_ptr<MyClass> getShared() { return shared_from_this(); // Needs enable_shared_from_this } }; class MyClass : public std::enable_shared_from_this<MyClass> { // ... };

3. Mixing raw pointers and smart pointers

cpp
std::shared_ptr<int> ptr = std::make_shared<int>(42); int* raw = ptr.get(); delete raw; // Wrong! Will cause double deletion

4. Passing this in constructor

cpp
class MyClass { public: MyClass() { // Wrong! Object not fully constructed yet registerObserver(this); } }; // Solution: use two-phase construction class MyClass { public: static std::shared_ptr<MyClass> create() { auto ptr = std::shared_ptr<MyClass>(new MyClass()); ptr->init(); return ptr; } private: MyClass() = default; void init() { registerObserver(shared_from_this()); } };

Performance Considerations

1. shared_ptr overhead

  • Reference counting: two atomic integers (control block)
  • Memory allocation: control block and object may be allocated separately
  • Thread safety: reference count operations are atomic

2. Advantages of make_shared

  • Single memory allocation: object and control block allocated together
  • Better cache locality
  • Reduced memory fragmentation

3. weak_ptr overhead

  • Needs additional weak reference count
  • lock() operation requires atomic operations

Real-world Use Cases

1. Factory pattern

cpp
class Factory { public: template <typename T, typename... Args> static std::shared_ptr<T> create(Args&&... args) { return std::make_shared<T>(std::forward<Args>(args)...); } }; auto obj = Factory::create<MyClass>(arg1, arg2);

2. Dependency injection

cpp
class Service { public: Service(std::shared_ptr<Database> db) : db_(db) {} void execute() { db_->query("SELECT * FROM table"); } private: std::shared_ptr<Database> db_; };

3. Resource management

cpp
class ResourceManager { public: void addResource(std::unique_ptr<Resource> resource) { resources_.push_back(std::move(resource)); } Resource* getResource(size_t index) { return resources_[index].get(); } private: std::vector<std::unique_ptr<Resource>> resources_; };

Notes

  • Smart pointers cannot manage stack objects
  • Avoid creating many shared_ptr in loops
  • Be aware of thread safety of smart pointers
  • Pay attention to atomic operations of reference counts when using shared_ptr in multithreaded environments
  • Consider using custom allocators to optimize performance
  • Understand ownership semantics of smart pointers, choose appropriate types
标签:C++