[C++ Memory Management] C++ Smart Pointers

1. unique_ptr

std::unique_ptrIt is a smart pointer, which is mainly used to encapsulate raw pointers in C++ to ensure memory safety. When std::unique_ptrthe life cycle ends, it automatically deletes the object it owns.

1.1 Basic functions and features

  • Unique Ownership : True to its name "unique", this type of pointer holds unique ownership of the object it points to, meaning that at any point in time, only one unique_ptrcan point to a given object.

  • Prevent memory leaks : It automatically manages the life cycle and unique_ptrautomatically deletes the object it points to when the life cycle ends (such as leaving the scope). This ensures that memory leaks will not be caused by forgetting to manually release memory.

  • Non-copyable but movable : unique_ptrIs non-copyable, preventing multiple pointers from taking ownership of the same object. However, ownership can be ported, meaning ownership can be unique_ptrtransferred from one to another.

The following code demonstrates these features:

#include <memory>

// 定义一个函数,用于演示unique_ptr的功能
void Process(std::unique_ptr<int> ptr) {
    
    
    // 在此函数中,ptr是唯一对内存的所有者
    // 在此函数结束时,ptr将被销毁,并且其指向的内存也将被释放
}

int main() {
    
    
    std::unique_ptr<int> ptr1(new int(5));  // 分配一个新的int

    // std::unique_ptr<int> ptr2 = ptr1;  // 编译错误,unique_ptr是不可复制的

    std::unique_ptr<int> ptr3 = std::move(ptr1);  // ptr1的所有权被移动到ptr3,现在ptr1不再拥有任何对象,ptr1变为nullptr

    Process(std::move(ptr3));  // ptr3的所有权被移动到Process函数的参数,然后在函数结束时释放内存

    return 0;
}

1.2 Managing dynamic arrays

unique_ptrCan also be used to manage dynamic arrays, in which case when unique_ptrthe lifetime is over, will be called delete[]to free the memory, rather than individually delete.

int main() {
    
    
    std::unique_ptr<int[]> arr(new int[10]);  // arr指向一个包含10个int的新数组

    for(int i = 0; i < 10; i++) {
    
    
        arr[i] = i;  // 我们可以像使用普通数组一样使用它
    }

    // 当arr离开作用域时,它会自动删除它所拥有的数组,无需手动调用 delete[] arr;
    return 0;
}

1.3 MoveConstructible and MoveAssignable

unique_ptrIt is removable but not copyable. This means you can unique_ptrtransfer ownership of one to another unique_ptr, but you can't create unique_ptra copy of one. This is because unique_ptrthe original design intention is to ensure that there is only one owner of the managed object at any time.

As shown in the following code:

#include <memory>

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1);  // Now p2 owns the int, and p1 is set to nullptr.

// This would be an error:
// std::unique_ptr<int> p3 = p2;  // This is not allowed because unique_ptr is not copyable.

1.4 Transfer of ownership

As always, unique_ptrobjects cannot be copied, but ownership can be std::move()transferred via . Transferring ownership also changes the original unique_ptrobject. When ownership unique_ptris transferred from one to another unique_ptr, the original unique_ptrwill be set to null. This means that any attempt to use the original unique_ptrwill result in undefined behavior because it is now a null pointer.

std::unique_ptr<int> ptr1(new int(10)); 
std::unique_ptr<int> ptr2 = std::move(ptr1);  // 所有权转移
// 此时,ptr1已经不再拥有任何对象,而ptr2拥有了该对象。

1.5 Custom deleter

unique_ptrProvides the ability to use custom deleters, which can be useful in certain situations. For example, you can use a custom deleter when you need a specific cleaner or have specific memory management needs.

auto deleter = [](int* ptr) {
    
    
    std::cout << "Custom deleting. \n";
    delete ptr;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int, deleter);

In the above code, when unique_ptrdestructed, the specified deleter will be called to release the memory.

1.6 Use make_unique to create unique_ptr

To avoid direct use new, C++14 introduced std::make_uniquefunction templates for creation unique_ptr. Using make_uniquenot only makes the code cleaner, but also better avoids memory leaks and provides exception safety.

auto ptr = std::make_unique<int>(10);  // 自动推导出类型为 std::unique_ptr<int>

1.7 Avoid raw pointer operations

Use unique_ptrto avoid direct manipulation of raw pointers. Raw pointers may cause problems such as memory leaks and wild pointers. After use unique_ptr, you can return the function with confidence knowing that no resources will be leaked.

std::unique_ptr<int> safe_func() {
    
    
    return std::make_unique<int>(10);
}

2. shared_ptr shared smart pointer

std::shared_ptrIs a smart pointer used to implement the concept of shared ownership. Multiple shared_ptrcan point to the same object. This object and its related resources will be released when "the last reference is destroyed". To perform this function, shared_ptra counting mechanism is used to ensure that the number of pointers to an object is kept track of.

2.1 Create shared_ptr

shared_ptrThe most straightforward way to create is to use std::make_shared:

auto ptr = std::make_shared<int>(10);

In the above code, we create a shared_ptr, which points to a dynamically allocated integer.

2.2 Shared Ownership

shared_ptrThe most critical feature is that multiple shared_ptrcan point to the same object, but no one is shared_ptrmore dominant than the others. Each shared_ptrhas equal ownership, and when the last one shared_ptrstops pointing to an object, that object is deleted.

std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1;  // 复制 ptr1;引用计数增加

// ptr1 和 ptr2 都销毁时,内存被释放

2.3 Reference counting

As mentioned earlier, shared_ptrshared ownership is achieved through reference counting. Whenever a new shared_ptrobject is pointed to, the reference count is incremented; when shared_ptrit is destroyed (or repointed to another object), the reference count is decremented. When the reference count reaches 0, the object is deleted.

We can use_countview the current reference count through the method:

auto ptr1 = std::make_shared<int>(10);
std::cout << ptr1.use_count() << '\n';  // 输出:1

auto ptr2 = ptr1;
std::cout << ptr1.use_count() << '\n';  // 输出:2

Note, use_countprimarily for debugging purposes, it is not a constant time operation. And, even though we can see use_countthe value of , in a multi-threaded environment we can't use it to reliably tell whether there is other shared_ptr, because after you check use_count, there may be other shared_ptrcreated or destroyed.

2.4 Custom deleter

Like unique_ptr, shared_ptrcustom deleters are also supported:

auto deleter = [](int* ptr) {
    
    
    std::cout << "Custom deleting. \n";
    delete ptr;
};
std::shared_ptr<int> ptr(new int, deleter);

In the above code, we have provided a custom deleter function. When shared_ptrit needs to delete the object it manages, it will call this deleter.

2.5 Direct use of make_shared vs shared_ptr

When creating shared_ptr, it is recommended for std::make_sharedtwo reasons: performance and security. std::make_sharedMemory is allocated once to store objects and control blocks (containing information such as reference counts), which helps improve memory usage efficiency. shared_ptrUsing the constructor directly requires allocating memory twice, once for the object and once for the control block. In addition, std::make_sharedmemory leaks due to exceptions can be better prevented.

2.6 Purpose of weak_ptr

std::shared_ptrThere is a related smart pointer type: std::weak_ptr. weak_ptrIt exists to solve shared_ptrthe circular reference problem that may occur in . We discuss this in detail below weak_ptr.

3. Shared_ptr circular reference problem

Although shared_ptrprovides the facility to automatically manage and delete dynamically allocated objects, it can also cause problems if used inappropriately. A typical problem is circular references.

Circular references occur when two or more objects hold each other's shared_ptr. In this case, every object has a reference pointing to it shared_ptr, so their reference count never reaches zero, even if they are actually no longer accessible.

The following code shows an example of this:

class B;  // 预声明 B 类

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

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

int main() {
    
    
    {
    
    
        std::shared_ptr<A> a(new A());
        std::shared_ptr<B> b(new B());
        a->ptr = b;
        b->ptr = a;
    }  // 当离开这个块时,a 和 b 的 shared_ptr 将被销毁,但是由于循环引用,A 和 B 对象并未被删除

    return 0;
}

In the above code, we create two objects A and B, and let A hold B's shared_ptrand B hold A's shared_ptr. This creates a circular reference, so when we try to delete a and b, the A and B objects are not deleted because their reference count has not reached zero.

This is the circular reference problem. The result of this problem is a memory leak: although our intention is shared_ptrto delete the object when it is destroyed, the object's memory is not released due to circular references.

4. weak_ptr solves circular references

std::weak_ptris a special smart pointer that points to an std::shared_ptrobject managed by , but it does not increment the object's reference count. Since std::weak_ptrdoes not increment the reference count, it does not prevent the pointed-to object from being deleted. This is weak_ptrhow to solve the circular reference problem: shared_ptrreplace the one that caused the cycle with weak_ptr.

The following code example shows how to use weak_ptrto solve the circular reference problem mentioned earlier:

class B;  // 预声明 B 类

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

class B {
    
    
public:
    std::weak_ptr<A> ptr;  // 注意这里我们使用了 weak_ptr 而不是 shared_ptr
    ~B() {
    
     std::cout << "B deleted\n"; }
};

int main() {
    
    
    {
    
    
        std::shared_ptr<A> a(new A());
        std::shared_ptr<B> b(new B());
        a->ptr = b;
        b->ptr = a;  // B 现在持有 A 的 weak_ptr
    }  // 当离开这个块时,A 和 B 对象被成功删除

    return 0;
}

In the above code, we create two objects A and B, but this time B holds A weak_ptr's shared_ptr. Therefore, even though A holds B's shared_ptr, B does not hold A's shared_ptr, so there is no circular reference, and when we try to delete a and b, both A and B objects will be deleted successfully.

Note: When you need to weak_ptrget from shared_ptr, you can use the function weak_ptrof lock. But this doesn't always succeed because the pointed object may have been deleted. Therefore, the returned shared_ptrmay be empty.

std::weak_ptr<int> weak;
{
    
    
    auto shared = std::make_shared<int>(42);
    weak = shared;
}
// 此时,shared 已经销毁,所以从 weak 中获取 shared_ptr 会得到一个空的 shared_ptr。
auto sharedFromWeak = weak.lock();  // 返回一个空的 shared_ptr
if (sharedFromWeak) {
    
    
    // 这里的代码不会被执行,因为 sharedFromWeak 是空的。
}

Guess you like

Origin blog.csdn.net/weixin_52665939/article/details/131485245