2025-03-26 00:02:56 +08:00

12 KiB
Raw Blame History

#qt

简介

C++智能指针shared_ptr是一种可以自动管理内存的智能指针它是C++11新增的特性之一。与传统指针不同shared_ptr可以自动释放所管理的动态分配对象的内存并避免了手动释放内存的繁琐操作从而减少了内存泄漏和野指针的出现。

shared_ptr是一个模板类通过引用计数器实现多个智能指针共享对一个对象的所有权。每次复制一个shared_ptr对象时该对象的引用计数器会增加1当最后一个shared_ptr对象被销毁时引用计数器减1如果引用计数器变为0则释放所管理的对象的内存。

使用shared_ptr需要包含头文件<memory>,并且可以通过以下方式创建:
std::shared_ptr<int> p(new int(10));

上面的代码创建了一个shared_ptr对象p它指向一个动态分配的int类型对象初始值为10。

在使用shared_ptr时需要注意以下几点

不要使用裸指针来初始化shared_ptr否则可能导致多次删除同一个对象的情况。

避免在shared_ptr中存储数组因为shared_ptr只能处理单个对象的释放而不能正确地处理数组的销毁。

可以通过自定义删除器deleter来实现对对象的特定方式的释放。

shared_ptr可以作为函数参数传递但要注意避免循环引用的问题否则会导致内存泄漏。

shared_ptr是一种方便且安全的内存管理工具能够有效地避免内存泄漏和野指针的出现。

二、底层原理

!Pasted image 20240804164523.png

2.1、引用计数 shared_ptr的核心是引用计数技术。在每个shared_ptr对象中都有一个指向所管理对象的指针和一个整型计数器。这个计数器统计有多少个shared_ptr对象指向该所管理对象。当一个新的shared_ptr对象指向同一块内存时该内存的引用计数就会增加1。当一个shared_ptr对象不再指向该内存时该内存的引用计数就会减少1。当引用计数为0时说明没有任何shared_ptr对象指向该内存此时该内存将会被自动释放。

2.2、shared_ptr的构造和析构 shared_ptr的构造函数需要一个指针作为参数该指针指向要被管理的对象。当一个新的shared_ptr对象被创建时它会尝试增加所管理对象的引用计数。如果该对象还未被其他shared_ptr对象管理则会创建一个新的引用计数并将其设置为1。否则它会与已经存在的shared_ptr对象共享同一个引用计数。

shared_ptr的析构函数会尝试减少所管理对象的引用计数。如果引用计数变成0则会自动释放所管理对象的内存。

shared_ptr的控制块包含引用计数和删除器等信息会在最后一个指向所管理对象的shared_ptr析构时被释放。当引用计数减为0时就说明没有任何shared_ptr对象指向该所管理对象了此时shared_ptr会自动调用删除器并释放掉控制块。由于shared_ptr可以共享同一个控制块因此只有所有shared_ptr对象都析构后控制块才能被释放。如果一个shared_ptr对象使用reset()方法手动解除与所管理对象的关联也会相应地减少引用计数当引用计数变成0时控制块也会被释放。

2.3、shared_ptr的共享和拷贝 shared_ptr可以与其他shared_ptr对象共享同一个指向对象的指针。当一个shared_ptr对象被复制时它所管理的对象的引用计数也会增加1。因此任何一个持有相同指针的shared_ptr对象都可以通过更改其所管理对象的状态来影响所有其他shared_ptr对象。

2.4、循环引用问题 如果一个对象A包含指向另一个对象B的shared_ptr而对象B也包含指向对象A的指针则这两个对象将形成循环引用。在这种情况下可能会出现内存泄漏。

shared_ptr 循环引用问题是指两个或多个对象之间通过shared_ptr相互引用导致对象无法被正确释放从而造成内存泄漏。

常见的情况是两个对象A和B它们的成员变量互相持有了对方的shared_ptr。当A和B都不再被使用时它们的引用计数不会降为0无法被自动释放。

解决这个问题的方法有以下几种:

打破循环引用可以通过将shared_ptr改为weak_ptr来解决。weak_ptr是一种弱引用不会增加对象的引用计数在对象释放时会自动设置为nullptr。可以使用weak_ptr.lock()方法来获取对象的shared_ptr当对象已经释放时会返回一个空shared_ptr。

使用std::enable_shared_from_this如果其中一个对象A需要获取对另一个对象B的shared_ptr可以让对象B继承std::enable_shared_from_this并在A中使用shared_from_this()方法获取B的shared_ptr这样就不会形成循环引用。

手动析构如果无法修改代码结构或者无法使用前两种方法解决问题可以使用手动析构的方式来释放对象。通过调用reset()方法手动释放shared_ptr确保引用计数降为0对象会被正确释放。

使用weak_ptr和shared_ptr组合将两个对象的循环引用中的一个改为weak_ptr另一个仍使用shared_ptr。这样可以避免循环引用导致的内存泄漏。

使用示例

#include <memory>
#include <iostream>

using namespace std;

class MyClass {
public:
    MyClass() { cout << "MyClass constructor" << endl; }
    ~MyClass() { cout << "MyClass destructor" << endl; }
    void printInfo() { cout << "This is MyClass" << endl; }
};

int main() {
    shared_ptr<MyClass> p1(new MyClass()); // 创建一个shared_ptr指向MyClass对象
    shared_ptr<MyClass> p2 = p1; // p1和p2都指向同一个MyClass对象

    p1->printInfo(); // 访问MyClass对象的成员函数
    p2.reset(); // 释放p2所指向的MyClass对象
    p1->printInfo(); // 由于p1仍然指向MyClass对象所以此处输出"This is MyClass"

    return 0;
}

上述代码中通过调用shared_ptr<MyClass>构造函数创建了两个指针p1和p2并且它们都指向一个MyClass对象。我们调用reset()函数来释放p2所指向的MyClass对象但是由于p1仍然指向该对象所以在调用p1->printInfo()时仍然输出"This is MyClass"。当程序结束时p1所指向的MyClass对象会被自动释放。

可以看到使用shared_ptr可以很方便地避免内存泄漏和悬空指针等问题。另外需要注意的是shared_ptr指针之间的赋值和拷贝操作都会增加指向对象的引用计数即使一个指针已经释放了它所指向的对象只要其他指针还在使用该对象该对象就不会被自动删除。因此在使用shared_ptr时需要注意对象的生命周期避免产生意外的副作用。

shared_ptr循环引用问题的解决方法

假设存在这样的循环引用:

#include <memory>

class B; //前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A类持有B类的shared_ptr

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> a_ptr; // B类持有A类的shared_ptr

    ~B() {
        std::cout << "B destructor" << 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;
    
    // 此时a和b之间形成了循环引用导致其引用计数一直不为0

    return 0;
}

解决方法:

1使用weak_ptr打破循环引用 将A类和B类中的shared_ptr改为weak_ptr将对象引用改为弱引用这样不会增加对象的引用计数从而避免循环引用导致的内存泄漏。在需要使用对象的地方可以通过lock()方法将weak_ptr转换为shared_ptr来进行使用如果对象已被释放则返回空shared_ptr。

#include <memory>

class B; //前向声明

class A {
public:
    std::weak_ptr<B> b_ptr; // A类持有B类的weak_ptr

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // B类持有A类的weak_ptr

    ~B() {
        std::cout << "B destructor" << 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;
    
    // 此时a和b之间形成了循环引用但由于使用了weak_ptr不会形成内存泄漏

    return 0;
}

2修改对象的引用关系 考虑是否需要A类和B类之间互相持有shared_ptr的引用关系。如果某一方只需要单向引用可以将其引用改为裸指针或者weak_ptr。这样可以避免形成循环引用。

#include <memory>

class B; //前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A类持有B类的shared_ptr

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // B类持有A类的weak_ptr

    ~B() {
        std::cout << "B destructor" << 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;
    
    // 此时a和b之间形成了循环引用但由于a->b_ptr改为shared_ptrb->a_ptr改为weak_ptr不会形成内存泄漏

    return 0;
}

3手动析构 如果无法修改代码结构或者无法使用前两种方法解决问题可以使用手动析构的方式来释放对象。通过调用reset()方法手动释放shared_ptr确保引用计数降为0对象会被正确释放。

#include <memory>

class B; //前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A类持有B类的shared_ptr

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> a_ptr; // B类持有A类的shared_ptr

    ~B() {
        std::cout << "B destructor" << 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;
    
    a.reset(); // 手动释放A对象
    b.reset(); // 手动释放B对象

    return 0;
}

4使用weak_ptr和shared_ptr组合 将两个对象的循环引用中的一个改为weak_ptr另一个仍使用shared_ptr。这样可以避免循环引用导致的内存泄漏。

#include <memory>

class B; //前向声明

class A {
public:
    std::shared_ptr<B> b_ptr; // A类持有B类的shared_ptr

    ~A() {
        std::cout << "A destructor" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // B类持有A类的weak_ptr

    ~B() {
        std::cout << "B destructor" << 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;
    
    // 此时a和b之间形成了循环引用但由于a->b_ptr使用shared_ptrb->a_ptr使用weak_ptr不会形成内存泄漏

    return 0;
}

总结

智能指针是C++中一种重要的语言机制其中shared_ptr是最常用和最经典的智能指针之一。

shared_ptr是一种引用计数的智能指针可以共享同一个对象。使用shared_ptr时需要包含头文件<memory>。

创建shared_ptr对象时可以直接将原始指针作为参数传递给构造函数也可以使用make_shared函数进行创建。

对象的引用计数会在shared_ptr对象初始化、复制、释放时自动更新。当某个shared_ptr对象被销毁时它所指向的对象的引用计数会减少如果引用计数为0则该对象会被自动删除。

通过get函数可以获取shared_ptr对象所管理的原始指针。

通过reset函数可以重新绑定shared_ptr对象所管理的原始指针。

可以使用unique函数判断shared_ptr对象是否唯一拥有原始指针。

通常情况下shared_ptr对象应该在栈上创建而不是使用new运算符在堆上创建。

在多线程环境下使用shared_ptr时需要注意需要采取线程安全措施比如使用锁来保证引用计数的正确性。

shared_ptr是C++11中STL的一部分它是一个模板类用于管理动态地分配对象的内存。shared_ptr可以自动完成内存管理确保内存被正确释放并且非常易于使用。