C++智能指针

376 阅读6分钟

内存泄漏的原因

智能指针自动销毁new出来的对象,智能指针可以自动完成垃圾清理这个工作

内存泄漏(memory leak)是指程序在动态分配内存后未能正确释放,导致这些内存无法被回收和重用。

1.忘记释放内存,动态分配了内存但是没有调用delete或free释放内存

2.丢失指针,动态分配内存的指针被覆盖或丢失,导致无法释放内存

int main(){
    int *ptr = new int(5);
    ptr = new int(6);
    delete ptr;
    return 0;
}

3.循环引用,发生在两个或多个对象相互引用对方,最终形成一个环,导致无法被正确释放

#include <iostream>
using namespace std;
​
#include <iostream>
class B;
class A {
public:
    B* b;  
    ~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
    A* a;  
    ~B() { std::cout << "B destroyed\n"; }
};
void createCycle() {
    A* a = new A();
    B* b = new B();
    a->b = b; 
    b->a = a; 
 
}
int main() {
    createCycle();
    return 0;
}

解决内存泄漏

1.正确使用内存的分配与回收

2.使用智能指针

  • std::unique_ptr:独占所有权,超出作用域时自动释放内存。
  • std::shared_ptr:共享所有权,引用计数为零时自动释放内存。
  • std::weak_ptr:与 std::shared_ptr 搭配使用,解决循环引用问题。

智能指针案例

下面的程序有内存泄漏

#include <iostream>
using namespace std;
​
int div(){
    int a, b;
    cin >> a >> b;
    if (b == 0){
        throw invalid_argument("除0错误");
    }
    return a / b;
}
void Func(){
    int* p1 = new int[10];
    int* p2 = new int[10];
    cout << div(1,0) << endl;
    delete[] p1;
    delete[] p2;
}
int main(){
    try{
        Func();
    }
    catch (exception& e){
        cout << e.what() << endl;
    }
    return 0;
}

如果div()函数抛出异常,代码将不会继续执行,导致 delete[] p1delete[] p2不会被调用,从而产生内存泄漏。

为了避免内存泄漏,可以使用智能指针来管理内存。例如 std::unique_ptr

#include <memory>
void Func(){
    std::unique_ptr<int[]> p1(new int[10]);
    std::unique_ptr<int[]> p2(new int[10]);
    cout << div() << endl;
}

无论是否发生异常,智能指针都会自动释放内存,确保没有内存泄漏

智能指针使用及原理

RAII思想

RAII是一种利用对象生命周期来控制程序资源(例如内存、文件句柄、网络连接、互斥量等)的简单技术.

获取到资源以后去初始化一个对象,将资源交给这个对象管理:资源获取即初始化

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。 借此,我们实际是把管理一份资源的责任托管给了一个对象,这种做法有两大好处

  • 不需要显式的释放资源
  • 采用这种方式,对象所需的资源在其生命周期内始终有效

使用RAII的思想设计一个智能指针SmartPtr

#include <iostream>
#include <memory>
using namespace std;
​
template <typename T>
class SmartPtr{
private:
    T *_ptr;
public:
    SmartPtr(T *ptr = nullptr):_ptr(ptr){}
    ~SmartPtr(){
        cout<<"delete"<<_ptr<<endl;
        if (_ptr){
            delete _ptr;
            _ptr = nullptr;
        }
    }
};
int div1(int a, int b){
    if (b==0){
        throw invalid_argument("div 0 error"); 
    }
    return a/b;
}
int main(){
    SmartPtr<int> p1(new int);
    SmartPtr<int> p2(new int);
    try{
        cout<<div1(1,0)<<endl;
    }catch(exception& e){
        cout<<e.what()<<endl;
    }
    return 0;
}
​
div 0 error
delete0x26124b0
delete0xfe3f80

在此基础上还需要加上指针解引用,通过->访问指向空间的内容,以及解决浅拷贝的问题。

C++函数库里的智能智能指针

unique_ptr

提供独占式所有权的内存管理,它不允许拷贝构造,但支持移动语义move

避免了多个指针指向同一块内存的情况,并在指针超出作用域时自动释放内存。

创建unique_ptr

#include <iostream>
#include <memory>
int main() {
    std::unique_ptr<int> ptr(new int(10)); 
    std::cout << "value: " << *ptr << std::endl; 
    return 0; 
}

使用make_unique

make_unique 是一种更安全的创建 unique_ptr 的方法,避免了使用 new 关键字

#include <iostream>
#include <memory>
int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(10); 
    std::cout << "Value: " << *ptr << std::endl; 
    return 0; 
}

使用std::move转移所有权

unique_ptr 不允许复制,但可以通过 std::move 转移所有权

#include <iostream>
#include <memory>
int main() {
    auto ptr1 = std::make_unique<int>(10); 
    std::unique_ptr<int> ptr2 = std::move(ptr1); 
    if (!ptr1) {
        std::cout << "ptr1 is null" << std::endl; 
    }
    std::cout << "Value: " << *ptr2 << std::endl; 
    return 0; 
}

管理动态数组

#include <iostream>
#include <memory>
int main() {
    std::unique_ptr<int[]> arr(new int[5]); 
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 10; 
    }
 
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " "; 
    }
    std::cout << std::endl;
    return 0; 
}

unique_ptr的单一所有权

#include <iostream>
#include <memory>
 int main() {
    std::unique_ptr<int> ptr1 = std::make_unique<int>(10); 
    std::cout << "Value: " << *ptr1 << std::endl; 
    // std::unique_ptr<int> ptr2 = ptr1; // 错误,unique_ptr 不可复制
    return 0; 
}

shared_ptr

多个 shared_ptr 对象可以指向同一个对象,当最后一个 shared_ptr 释放时,该对象的内存才会被释放

创建和共享shared_ptr

#include <iostream>
#include <memory>
int main() {
    std::shared_ptr<int> ptr1 = std::make_shared<int>(30);
    std::shared_ptr<int> ptr2 = ptr1;
 
    std::cout << "Value from ptr1: " << *ptr1 << std::endl; 
    std::cout << "Value from ptr2: " << *ptr2 << std::endl; 
 
    std::cout << "Use count: " << ptr1.use_count() << std::endl; 
 
    return 0;
}

避免循环引用

shared_ptr 可能导致循环引用,从而造成内存泄漏。使用 std::weak_ptr 可以打破循环引用

#include <iostream>
#include <memory>
 
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; 
    ~Node() {
        std::cout << "Node destroyed" << std::endl;
    }
};
 
int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;
    node2->prev = node1;
 
    std::cout << "Use count of node1: " << node1.use_count() << std::endl; // 输出 1
    std::cout << "Use count of node2: " << node2.use_count() << std::endl; // 输出 1
 
    return 0;
}

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期。也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。

解决方案就是,使用 std::weak_ptr 来代替其中任意一个方向上的 std::shared_ptr

它提供了一种不参与对象引用计数的弱引用,这意味着它不会影响所管理对象的生命周期。weak_ptr 只能从shared_ptr或另一个 weak_ptr 构造

也就是说,weak_ptr是为了弥补shared_ptr循环引用而生的,它没有RAII的特性,不直接管理资源,只是shared_ptr的跟班,这也是weak_ptr支持使用shared_ptr构造的原因。

在使用上,weak_ptr支持指针所有的操作。它不是一个功能型的智能指针,而是辅助型,它的使命是解决shared_ptr造成的循环引用问题。

shared_ptr 不同,weak_ptr 不能直接访问所指向的对象。要访问对象,需要先调用 lock() 方法将其转换为 shared_ptr。如果所指向的对象已经被销毁,则 lock() 方法返回空指针。 即便转换为shared_ptr,也不占用引用次数

#include <iostream>
#include <memory>
 
int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(100);
    std::weak_ptr<int> weakPtr = sharedPtr;
 
    if (auto lockedPtr = weakPtr.lock()) { 
        std::cout << "Value: " << *lockedPtr << std::endl; // 输出 100
    } else {
        std::cout << "Pointer is expired" << std::endl;
    }
​
    std::cout<< sharedPtr.use_count()<<std::endl;   //1
    sharedPtr.reset(); // 手动释放 shared_ptr 所管理的对象
    std::cout<< sharedPtr.use_count()<<std::endl;   //0
​
    //检查 weakPtr 观察的对象是否已被销毁
    if (weakPtr.expired()) {
        std::cout << "Pointer is expired" << std::endl; // 输出 Pointer is expired
    }
 
    return 0;
}