深入理解 C++ 智能指针

news2024/9/20 0:48:48

文章目录

  • 一、引言
  • 二、 原始指针的问题
    • 1、原始指针的问题
    • 2、智能指针如何解决这些问题
  • 三、智能指针的类型
  • 四、std::shared_ptr
    • 1、shared_ptr使用
    • 2、shared_ptr的使用注意事项
    • 3、定制删除器
    • 4、shared_ptr的优缺点
    • 5、shared_ptr的模拟实现
  • 五、std::unique_ptr
    • 1、unique_ptr的使用
    • 2、unique_ptr的使用注意事项
    • 3、定制删除器
    • 4、unique_ptr的优缺点
    • 5、unique_ptr的模拟实现
  • 六、std::weak_ptr
  • 七、RAII模式:资源获取即初始化
  • 八、再谈删除器

一、引言

智能指针是现代 C++ 编程中的重要概念,它们为程序员提供了一种更安全、更方便地管理动态内存的方式。在传统的 C++ 编程中,手动管理内存通常会导致一系列问题,例如内存泄漏、悬空指针以及释放已释放的内存等。智能指针的出现解决了这些问题,使得 C++ 编程更加健壮、安全和高效。

智能指针本质上是一种对象,它模拟了指针的行为,但具有自动管理内存的功能。通过使用智能指针,可以避免手动调用 newdelete 来分配和释放内存,从而减少了出错的可能性。


二、 原始指针的问题

1、原始指针的问题

在使用原始指针(raw pointers)时,程序员需要手动管理内存的生命周期,这包括分配和释放内存。这种管理方式非常直接但也很容易出错,常见的问题有:

  1. 内存泄漏(Memory Leaks)

    • 当动态分配的内存不再需要时,程序员需要显式地释放它。如果忘记释放内存,或者由于某种原因(如异常)导致内存释放的代码没有被执行,那么这块内存就会被永久占用,导致内存泄漏。
    • 内存泄漏在长时间运行的程序中尤为严重,因为它们会逐渐消耗所有可用的内存,最终导致程序崩溃。
  2. 悬空指针(Dangling Pointers)

    • 如果一个指针被赋予了动态分配的内存地址,并且随后该内存被释放了,但是指针的值并没有被置为nullptr或重新分配其他地址,那么这个指针就被称为悬空指针。
    • 使用悬空指针会导致不可预测的行为,因为这块内存可能已经被操作系统分配给其他部分使用,或者已经被其他代码覆盖。
  3. 双重释放(Double Deletion)

    • 如果同一块内存被释放了两次,这通常会导致运行时错误,因为第二次释放尝试会试图操作一个已经被标记为“已释放”的内存块。

2、智能指针如何解决这些问题

智能指针是C++标准库提供的一种自动管理内存的机制,它们通过封装原始指针并提供额外的功能来自动处理内存的生命周期。

  1. 自动内存释放

    • 智能指针在析构时会自动释放它们所指向的内存,从而避免了内存泄漏的问题。
    • 例如,std::unique_ptr在析构时会调用delete来释放内存,而std::shared_ptr则使用引用计数来确保当最后一个shared_ptr被销毁时,内存才会被释放。
  2. 防止悬空指针

    • 智能指针在释放内存后会将其置为nullptr,从而避免了悬空指针的问题。
    • 这意味着即使尝试访问一个已经被销毁的智能指针,它也会安全地返回,而不会导致未定义的行为。
  3. 防止双重释放

    • 由于智能指针在析构时只释放一次内存,因此它们可以防止双重释放的问题。
    • 当将一个智能指针赋值给另一个智能指针时(例如,通过赋值或移动操作),原始的智能指针会自动放弃对内存的所有权,从而确保同一块内存不会被多次释放。

总的来说,智能指针通过自动管理内存的生命周期和提供额外的安全性检查来解决了原始指针常见的问题。然而,它们并不是万能的,仍然需要程序员谨慎使用以避免其他类型的错误。


三、智能指针的类型

当谈到C++中的智能指针时,通常指的是std::unique_ptrstd::shared_ptrstd::weak_ptr这三种类型。它们在管理动态内存分配和资源所有权方面提供了更安全和方便的方法。这三种类型都定义在memory头文件中。

  1. std::unique_ptr
    • 特点:std::unique_ptr提供了独占所有权的智能指针。这意味着同一时间只能有一个std::unique_ptr指向同一个资源,当指针超出范围或被销毁时,它所指向的资源会被自动释放。
    • 适用场景:当需要确保资源只有一个所有者时,std::unique_ptr是一个很好的选择。比如,当在函数中分配了一个资源,但是需要在函数返回后释放资源时,使用std::unique_ptr可以确保资源在函数退出时被正确释放。
  2. std::shared_ptr
    • 特点:std::shared_ptr允许多个指针共享同一个资源。它使用引用计数来跟踪资源的所有者数量,并在没有所有者时释放资源。
    • 适用场景:当需要多个指针共享同一资源,并且不清楚哪个指针会最后释放资源时,std::shared_ptr是一个很好的选择。比如,当需要在多个地方引用同一个对象,但不想手动跟踪所有权时,使用std::shared_ptr可以简化管理。
  3. std::weak_ptr
    • 特点:std::weak_ptr是一种弱引用智能指针,它不增加资源的引用计数,指向std::shared_ptr所管理的对象。它用于解决std::shared_ptr可能导致的循环引用问题。
    • 适用场景:当需要引用std::shared_ptr所管理的资源,但不希望增加资源的引用计数时,可以使用std::weak_ptr。比如,在观察者模式中,观察者可能需要引用被观察者,但不应该影响被观察者的生命周期。

总的来说,选择哪种智能指针类型取决于需求和设计。如果需要确保资源只有一个所有者,使用std::unique_ptr;如果需要多个所有者,使用std::shared_ptr;如果需要避免循环引用,使用std::weak_ptr


四、std::shared_ptr

在这里插入图片描述

std::shared_ptr 是 C++11 引入的一个智能指针,用于管理动态分配的对象。它的主要特点是可以共享所有权,并通过引用计数来管理资源的释放,它具有以下特点:

  • 共享所有权std::shared_ptr 允许多个指针共享对同一资源的所有权。这意味着当最后一个指向资源的 std::shared_ptr 被销毁时,资源才会被释放。

  • 引用计数std::shared_ptr 内部维护一个引用计数器,用于跟踪有多少个 std::shared_ptr 指向相同的资源。每当创建或销毁一个 std::shared_ptr 时,引用计数都会相应地增加或减少。

使用场景:

  • 多个所有者:当需要多个对象共享同一资源的所有权时,std::shared_ptr 是一个很好的选择。比如,在设计图形用户界面(GUI)时,多个对象可能需要访问同一块内存或同一个文件资源。
  • 循环引用std::shared_ptr 可以用于解决循环引用的问题,因为它会自动处理对象之间的引用计数,确保在没有被引用时能够正确释放资源。

1、shared_ptr使用

make_shared<T>(args):返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。

shared_ptr<T>p(q)pshared_ptr q的拷贝,此操作会递增q中的计数,因为 pq 现在都指向了相同的资源。这种操作允许多个智能指针共享同一块内存,同时确保在最后一个指针超出作用域时释放资源。q中的指针必须能转换为T*,即 q 所管理的资源类型能够隐式转换为 T 类型的指针。这通常是因为 q 的类型本身是 shared_ptr,并且 T 类型是 q 中指针的类型或者可以从 q 中指针的类型隐式转换为 T*p=qpq都是shared_ptr,所保存的指针必须能相互转换。此操作会递减p的引用计数,递增q的引用计数;若p的引用计数变为0,则将其管理的原内存释放。

p.unique:若p.use count()为1,返回true;否则返回false

p.use_count():返回与p共享对象的智能指针数量;可能很慢,主要用于调试。

std::shared_ptrreset 函数用于重新分配被管理的资源,或者将其置为空(不管理任何资源)。reset 函数接受一个可选的参数,用于指定新的资源。如果不提供参数,则该 std::shared_ptr 将置为空。

以下是 std::shared_ptrreset 函数的一般语法:

void reset(); // 重置为 nullptr,不管理任何资源
void reset(T* ptr); // 重置为指定的指针 ptr,开始管理该指针指向的资源
void reset(nullptr_t); // 重置为 nullptr,不管理任何资源

其中 T* ptr 是指向被管理的资源的原始指针,nullptr_t 是空指针类型。使用 reset 函数可以安全地在不同的 std::shared_ptr 之间转移资源的所有权,或者在不再需要资源时释放它。

以下是一些示例说明了 reset 函数的用法:

#include <iostream>
#include <memory>

int main() {
    // 创建一个 shared_ptr 来管理动态分配的整数
    std::shared_ptr<int> ptr(new int(42));
    // 重新分配资源为一个新的整数
    ptr.reset(new int(100));

    // 释放资源,置为空指针
    ptr.reset();

    return 0;
}

在这个示例中,我们首先创建了一个 std::shared_ptr 来管理动态分配的整数。然后,我们使用 reset 函数将该 std::shared_ptr 重新分配为指向一个新的整数。最后,我们再次调用 reset 函数,这次没有传递任何参数,将该 std::shared_ptr 置为空指针。

2、shared_ptr的使用注意事项

当使用 new 创建对象时,可以将返回的指针包装在 shared_ptr 中,以确保对象的安全共享和自动内存管理。但是若补初始化一个智能指针,它就会被初始化成一个空指针。

shared_ptr<double> p1;
shared_ptr<int> p2(new int(1));

需要注意的是,接受指针参数的智能指针的构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化智能指针:

shared_ptr<double> p1 = new int(1024); 	//错误
shared_ptr<double> p2(new int(1024));	//正确

p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr
由于我们不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

shared_ptr<int>clone(int p){
    return new int(p); 
}//错误:不存在从 "int *" 转换到 "std::shared_ptr<int>" 的适当构造函数

我们必须将其显式绑定到一个想要返回的指针上:

shared_ptr<int>clone(int p){
    return shared_ptr<int>(new int(p));
}//正确:显式地用int*创建shared ptr<int>    

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 释放它所关联的对象。我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete

**混合使用智能指针和普通指针可能导致内存管理问题。**智能指针会自动管理其所指向的内存资源,而普通指针则需要手动管理内存。混合使用时,可能会导致重复释放内存或者内存泄漏等问题。

重复释放内存:

#include <memory>

int main() {
    int* rawPtr = new int(5);
    {
        std::shared_ptr<int> smartPtr(rawPtr);
        // 这里会发生问题,因为当 unique_ptr 离开作用域时,它会尝试释放内存。
        // 而 rawPtr 本身并不知道 smartPtr 已经释放了内存,因此可能导致重复释放。
    }
    delete rawPtr; // 这里会导致重复释放内存,造成未定义行为。
    return 0;
}

不能使用get初始化另一个智能指针或为智能指针赋值。这是因为智能指针的设计初衷是为了自动管理资源。使用get方法获得底层指针,并且将其用于初始化另一个智能指针或者直接赋值给另一个智能指针,会导致资源的所有权问题。因为这样做会使得两个智能指针都认为自己拥有资源,从而可能导致重复释放资源或者其他未定义行为。

在混合使用智能指针和原始指针时,get 函数提供了一种将指针传递给无法接受智能指针的代码的方法。但是,强调了这并不意味着可以安全地将 get 返回的指针传递给另一个智能指针,因为这可能导致内存所有权混乱和未定义行为。举例说明,当使用 get 返回的指针来初始化另一个智能指针时,每个智能指针都认为自己拥有该资源,这可能导致同一块内存被重复释放,或者在使用时发生未定义行为。因此,为了避免这种情况,强调了永远不要将 get 返回的指针用于初始化另一个智能指针或为另一个智能指针赋值。

#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> ptr1(new int(42));
    // 使用 get 返回的原始指针来初始化另一个智能指针
    std::shared_ptr<int> ptr2(ptr1.get()); // 错误的做法!
    
    // 此时,ptr1 和 ptr2 都认为自己拥有该资源,这会导致问题
    // 当程序结束时,ptr1 和 ptr2 都尝试释放相同的内存,导致未定义行为
    return 0;
}

在这个示例中,我们尝试使用 ptr1 的原始指针来初始化 ptr2,这是一种错误的做法。现在两个 std::shared_ptr 都认为它们拥有相同的资源。

这样做可能导致内存重复释放的问题或者更糟糕的未定义行为。所以,对于 std::shared_ptr,同样要避免使用 get 返回的指针来初始化另一个智能指针或为另一个智能指针赋值。

3、定制删除器

为了为 std::shared_ptr 定制删除器,可以在创建 std::shared_ptr 对象时,提供一个自定义的删除器函数对象。这个删除器函数对象将在 std::shared_ptr 的引用计数变为0时被调用,以释放所管理的资源。以下是一个简单的示例,演示了如何为 std::shared_ptr 提供自定义的删除器:

#include <iostream>
#include <memory>

// 自定义删除器函数对象
struct CustomDeleter {
    void operator()(int* p) const {
        std::cout << "Custom deleter is called.\n";
        delete p; // 自定义的删除操作
    }
};

int main() {
    // 使用自定义删除器创建 shared_ptr
    std::shared_ptr<int> ptr(new int(42), CustomDeleter());
    
    // 手动重置 shared_ptr
    ptr.reset(new int(100));

    return 0;
}

在这个例子中,我们定义了一个名为 CustomDeleter 的结构体,它重载了调用运算符 operator(),以执行我们自定义的删除操作。然后,在创建 std::shared_ptr 对象时,我们通过在括号中提供 CustomDeleter 的一个临时实例来指定这个自定义的删除器。当 std::shared_ptr 的引用计数为0时,CustomDeleter 中的 operator() 将被调用来释放所管理的资源。

需要注意的是,使用自定义删除器时,确保删除器能够正确释放所管理的资源,并且与 std::shared_ptr 的资源类型兼容。

std::shared_ptrreset 函数在传递自定义删除器时的行为与不传递删除器时略有不同。当使用自定义删除器创建 std::shared_ptr 时,必须显式地指定新资源,以便 reset 函数能够知道要使用哪个删除器。

让我们看看如何使用自定义删除器来重新分配资源:

#include <iostream>
#include <memory>

// 自定义删除器函数对象
struct CustomDeleter {
    void operator()(int* p) const {
        std::cout << "Custom deleter is called.\n";
        delete p; // 自定义的删除操作
    }
};

int main() {
    // 创建 shared_ptr,并传递自定义删除器
    std::shared_ptr<int> ptr(new int(42), CustomDeleter());

    // 使用 reset 重新分配资源,并传递自定义删除器
    ptr.reset(new int(100), CustomDeleter());

    return 0;
}
/*
运行结果:
Custom deleter is called.
Custom deleter is called.
*/

在这个示例中,我们使用自定义删除器创建了一个 std::shared_ptr,然后使用 reset 函数重新分配了资源,并且仍然传递了相同的自定义删除器。这确保了在资源管理转移到新分配的整数时,仍然使用相同的删除器来释放旧资源。

4、shared_ptr的优缺点

  • 优点
    • 自动管理资源生命周期,无需手动释放。
    • 允许多个指针共享所有权,灵活性高。
    • 可以避免循环引用导致的内存泄漏。
  • 缺点
    • 额外的开销:std::shared_ptr 内部需要维护引用计数,可能会带来额外的开销。
    • 不能解决循环依赖:当存在 A 指向 B,B 指向 A 的情况时,即使使用了 std::shared_ptr,仍然会导致资源无法释放的问题。

循环依赖是指两个或多个对象之间相互依赖,形成一个环形结构。在 C++ 中,使用 std::shared_ptr 来管理资源的所有权时,循环依赖可能导致资源无法正确释放的问题,这被称为“循环引用”或“循环依赖”。

考虑以下情况:对象 A 拥有一个指向对象 B 的 shared_ptr,而对象 B 同样拥有一个指向对象 A 的 shared_ptr。这样一来,当没有其他对象持有 A 和 B 时,它们之间的引用计数永远不会降为零,因为彼此持有对方的指针,导致它们的析构函数永远不会被调用,从而资源无法释放,造成内存泄漏。

这种情况下,使用 std::weak_ptr 可以打破循环依赖。weak_ptr 是一种弱引用,它允许观察 shared_ptr 指向的对象,但不会增加其引用计数。通过在循环依赖中使用 weak_ptr,可以防止引用计数永远不会降为零的情况发生,从而正确释放资源。下面是一个示例:

class B; // 前向声明
class A {
public:
    std::weak_ptr<B> b_ptr;

    A() {
        std::cout << "A constructor\n";
    }
    
    ~A() {
        std::cout << "A destructor\n";
    }
};

class B {
public:
    std::weak_ptr<A> a_ptr;

    B() {
        std::cout << "B constructor\n";
    }
    
    ~B() {
        std::cout << "B destructor\n";
    }
};

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;

    // 此时引用计数仍然为 1,但是资源可以正确释放
    return 0;
}

在这个例子中,类 A 持有类 B 的 std::weak_ptr,而类 B 则持有类 A 的 std::weak_ptr。这样一来,资源就可以正确释放。

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called." << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor called." << std::endl;
    }
};

int main() {
    // 创建一个智能指针,共享一个 MyClass 实例的所有权
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();

    {
        // 创建另一个智能指针,共享相同的 MyClass 实例的所有权
        std::shared_ptr<MyClass> ptr2 = ptr1;

        // 此时引用计数为 2
        std::cout << "Reference count: " << ptr1.use_count() << std::endl;
    } // ptr2 超出作用域,引用计数减少为 1

    // ptr1 仍然指向相同的 MyClass 实例,引用计数为 1
    std::cout << "Reference count: " << ptr1.use_count() << std::endl;

    // ptr1 超出作用域,引用计数减少为 0,MyClass 实例被销毁
    return 0;
}

在上面的示例中,std::shared_ptr 被用来管理 MyClass 的实例。ptr1ptr2 共享对相同 MyClass 实例的所有权。当 ptr2 超出作用域时,引用计数减少为 1,但资源不会被释放,因为仍然有一个 std::shared_ptr 持有它。最后,当 ptr1 也超出作用域时,引用计数减少为 0,MyClass 实例被销毁。

5、shared_ptr的模拟实现

template<class T>
class shared_ptr {
public:
    // RAII
    shared_ptr(T* ptr = nullptr)
        :_ptr(ptr)
            , _pcount(new int(1))
        {}
    template<class D>
        shared_ptr(T* ptr ,D del)
        :_ptr(ptr)
            , _pcount(new int(1))
            ,_del(del)
        {}

    ~shared_ptr() {
        if (_ptr) {
            release();
        }
    }

    shared_ptr(const shared_ptr<T>& sp) {
        _ptr = sp._ptr;
        _pcount = sp._pcount;
        ++(*_pcount);
    }

    void release() {
        if (--(*_pcount) == 0) {
            cout << "delete:" << _ptr << endl;
            delete _pcount;
            _del(_ptr);
            //delete _ptr;
        }
    }
    shared_ptr<T>& operator=(const shared_ptr<T>& sp) 
    {
        //if (&sp != this) //分析 为什么不行  sp1 = sp2;  sp2是sp1构造的。
        if (sp._ptr != _ptr) {
            release();
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            ++(*_pcount);
        }

        return *this;
    }

    int use_count() { return *_pcount; }

    T& operator*() { return *_ptr; }

    T* operator->() { return _ptr; }

    T* get()const { return _ptr; }
private:
    T* _ptr;
    int* _pcount;
    std::function<void(T*)> _del = [](T* ptr) {delete ptr;  };
};

这个是一个简单的 shared_ptr 模板类的模拟实现。让我们来分析一下:

  1. 构造函数: 类中定义了两个构造函数。第一个构造函数接受一个指向 T 类型对象的指针,将其作为初始资源,并为计数器分配一个新的 int 对象来跟踪引用计数。第二个构造函数类似于第一个,但还接受一个可调用对象作为删除器,用于释放资源。

  2. 析构函数: 析构函数释放资源。如果引用计数器为 0,则调用删除器释放资源。

  3. 拷贝构造函数: 拷贝构造函数复制指针和计数器,并增加引用计数。

  4. release 函数: 减少引用计数,并在引用计数为 0 时释放资源。

  5. 赋值操作符重载: 赋值操作符重载实现了浅拷贝语义。如果两个 shared_ptr 指向不同的资源,则释放当前资源并复制新的资源,并增加新资源的引用计数。

  6. use_count 函数: 返回当前引用计数的值。

  7. 重载 * 和 -> 运算符: 使得 shared_ptr 可以像指针一样操作。

  8. get 函数: 返回指向的原始指针。

这个模拟实现中考虑了资源管理和拷贝语义,并且使用引用计数来跟踪共享资源的引用情况。


五、std::unique_ptr

在这里插入图片描述

std::unique_ptr 是 C++11 中引入的智能指针之一,它具有以下特点:

  1. 独占所有权(Exclusive Ownership)unique_ptr 确保在任意时间点只有一个 unique_ptr 实例可以指向一个特定的对象。当 unique_ptr 被销毁时,它所管理的对象也会被销毁。即不允许拷贝。
  2. 移动语义(Move Semantics)unique_ptr 支持移动语义,因此可以在不复制实际对象的情况下将所有权从一个 unique_ptr 转移到另一个。这使得 unique_ptr 在资源管理和传递所有权时非常高效。
  3. 自动释放资源(Automatic Resource Release):通过使用 unique_ptr,可以确保在不再需要对象时自动释放资源,避免了手动管理内存的复杂性和潜在的内存泄漏。

一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定的对象。当unique_ptr被销毁时,它所指向的对象也被销毁。

1、unique_ptr的使用

对于unique_ptr,初始化不必使用直接初始化形式。它允许使用直接初始化形式,也可以使用拷贝初始化形式,因为 unique_ptr 有移动构造函数。这意味着可以通过赋值运算符或者其他可以转换为 unique_ptr 类型的函数返回值初始化 unique_ptr

std::unique_ptr<int> ptr(new int(42));
std::unique_ptr<int> ptr = std::make_unique<int>(42);

在第二种情况下,我们使用 std::make_unique 来创建一个新的 int 对象,并将返回的 std::unique_ptr 直接初始化为 ptrmake_unique是c++14引入的。

unique_ptr<double>pl;			//可以指向一个double的unique_ptr
unique_ptr<int>p2(new int(42));	//p2指向一个值为 42的int

由于一个 unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝构造和赋值构造操作:

unique_ptr<string> p1(new string("stegosaurus"));
unique_ptr<string>p2(p1);	//错误:unique ptr不支持拷贝
unique_ptr<string>p3;
p3 = p2;					//错误:unique ptr不支持赋值

下面我会举一个例子来说明std::unique_ptr的移动构造和移动赋值,以及它不支持拷贝构造和拷贝赋值。

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass constructor called." << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass destructor called." << std::endl;
    }
};

int main() {
    // 使用移动构造
    std::unique_ptr<MyClass> ptr1(new MyClass); // 创建 ptr1
    std::unique_ptr<MyClass> ptr2(std::move(ptr1)); // 移动构造 ptr2,ptr1变为空指针
    // 使用移动赋值
    std::unique_ptr<MyClass> ptr3(new MyClass); // 创建 ptr3
    ptr1 = std::move(ptr3); // 移动赋值,ptr3变为空指针

    // 以下代码会导致编译错误,因为 std::unique_ptr 不支持拷贝构造和拷贝赋值
    // std::unique_ptr<MyClass> ptr4(ptr1); // 拷贝构造
    // std::unique_ptr<MyClass> ptr5 = ptr3; // 拷贝赋值
    return 0;
}

在这个例子中,我们首先创建了两个std::unique_ptr对象ptr1ptr3,并分别通过移动构造和移动赋值将它们的所有权转移给了ptr2ptr1。然后,我们尝试使用拷贝构造和拷贝赋值来创建新的std::unique_ptr对象,但是编译器会报错,因为std::unique_ptr不支持拷贝操作。

当使用std::unique_ptr时,有几个函数和操作值得详细解释:

  1. reset()

    • reset()函数用于释放unique_ptr当前持有的指针,并将unique_ptr置为空指针。这意味着它不再拥有任何资源。
    • 例如:
      std::unique_ptr<int> ptr(new int(42));
      ptr.reset(); // 释放资源并将ptr置为空指针
      
  2. reset(nullptr)

    • reset(nullptr)reset()函数的一种特例,它释放当前持有的指针,并将unique_ptr置为空指针。
    • 例如:
      std::unique_ptr<int> ptr(new int(42));
      ptr.reset(nullptr); // 释放资源并将ptr置为空指针
      
  3. release()

    • release()函数放弃unique_ptr对指针的控制权,并返回指针,但不会释放资源。这意味着在调用release()之后,unique_ptr将不再管理该资源,需要手动释放。该函数仅切断了这原来管理对象之间的联系。
    • 例如:
      std::unique_ptr<int> ptr(new int(42));
      int* rawPtr = ptr.release(); // 放弃控制权并返回指针
      // 现在ptr为空指针,需要手动释放rawPtr指向的资源
      delete rawPtr;
      
  4. reset(q)

    • reset(q)函数允许将unique_ptr重新指向一个新的指针q,释放当前持有的指针。如果q不为空,则unique_ptr开始管理q指向的资源。
    • 例如:
      std::unique_ptr<int> ptr(new int(42));
      int* newPtr = new int(100);
      ptr.reset(newPtr); // 释放原始资源,开始管理newPtr指向的资源
      

虽然我们不能拷贝或赋值unique_ptr,但可以使用releasereset将指针的所有权从一个(非constunique_ptr转移给另一个unique_ptr

2、unique_ptr的使用注意事项

unique_ptr作为函数返回值时,涉及到的是C++的移动语义

移动语义允许将临时对象的资源“窃取”(也就是转移)给另一个对象,而不是执行深层的复制操作。这种转移是通过移动构造函数和移动赋值运算符来实现的,它们将资源从一个对象转移到另一个对象,而不是像拷贝构造函数和拷贝赋值运算符那样创建一个资源的完整副本。

unique_ptr的情况下,当函数返回一个unique_ptr时,如果返回值被赋值给另一个unique_ptr,则发生移动语义。例如,考虑以下情况:

std::unique_ptr<int> createInt(int a) {
    return std::make_unique<int>(a);
}

int main() {
    std::unique_ptr<int> ptr1 = createInt(42); 
    // 移动语义:将createInt返回的unique_ptr的资源转移给ptr1

    return 0;
}

在这个例子中,createInt() 返回一个std::unique_ptr<int>,它持有一个动态分配的int对象。当createInt()返回时,它的返回值会被移动到ptr1中,这意味着指针所指向的资源所有权被转移,不会执行资源的深层复制。这样可以避免额外的内存分配和释放,提高程序的性能和效率。

返回unique_ptr的函数允许有效地管理资源的所有权,同时通过移动语义来避免不必要的资源复制。

3、定制删除器

当使用 unique_ptr 时,可以提供一个自定义的删除器,以便在释放指针时执行特定的操作。删除器是一个函数或函数对象,它接受指针并释放它所指向的资源。

与重载关联容器(set)的比较操作类似,必须在指定unique_ptr指向的类型后,提供删除器类型。在创建或重置unique_ptr对象时,必须提供一个特定类型的可调用对象。这样做允许控制unique_ptr销毁其持有对象时的行为,非常类似于重载关联容器的比较操作以控制排序行为。

  1. unique_ptr<T> u1;unique_ptr<T, D> u2;

    • u1是一个使用默认删除器 deletestd::default_delete<T>) 的 unique_ptr。这意味着当 u1 超出作用域或被显式释放时,它所管理的指针将被 delete 释放。
    • u2 是一个使用自定义删除器 Dunique_ptr。这意味着当 u2 被释放时,它所管理的指针将被传递给 D 所指定的自定义删除函数或函数对象来释放。
  2. unique_ptr<T, D> u(d);

    • 在这种情况下,通过构造函数参数 d,创建了一个带有自定义删除器 Dunique_ptr 对象 u。这意味着当 u 被释放时,它所管理的指针将被传递给 D 所指定的自定义删除函数或函数对象来释放。

通过提供自定义删除器,可以更灵活地控制 unique_ptr 如何管理其所拥有的资源的生命周期。

std::unique_ptr 中,可以通过以下方式来定制删除器:

  1. 函数指针或函数对象: 最简单的方式是通过函数指针或函数对象来指定删除器。这个删除器会在 std::unique_ptr 对象超出作用域时被调用,用于释放指针指向的资源。例如:

    void customDeleter(int* ptr) {
        delete ptr;
    }
    
    std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(42), customDeleter);
    std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), customDeleter);
    
    //注意:
    std::cout << typeid(&customDeleter).name() << std::endl;
    std::cout << typeid(customDeleter).name() << std::endl;
    /*
    输出结果:
    void (__cdecl*)(int * __ptr64)
    void __cdecl(int * __ptr64)
    */
    

    或者使用 lambda 表达式:

    std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), [](int* ptr) {
        delete ptr;
    });
    
  2. 函数对象(仿函数): 也可以创建一个函数对象(仿函数),其中实现了 operator(),并将其传递给 std::unique_ptr 的模板参数中。例如:

    struct CustomDeleter {
        void operator()(int* ptr) const {
            delete ptr;
        }
    };
    
    std::unique_ptr<int, CustomDeleter> ptr(new int(42));
    std::unique_ptr<int, CustomDeleter> ptr(new int(42), CustomDeleter());
    
  3. Lambda 表达式: 可以使用 lambda 表达式作为删除器,这种方式非常方便。例如:

    std::unique_ptr<int, std::function<void(int*)>> ptr(new int(42), [](int* ptr) {
        delete ptr;
    });
    

    或者更简洁地:

    auto deleter = [](int* ptr) { delete ptr; };
    std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
    

unique_ptrreset() 函数不接受删除器作为参数,因此在调用 reset() 时,不会更改 std::unique_ptr 对象的删除器。原来定义 std::unique_ptr 对象时所指定的删除器会一直保留,除非销毁了原有的 std::unique_ptr 对象并重新创建一个新的对象,并在创建时使用新的删除器。

因此,如果想要更改 std::unique_ptr 的删除器,需要手动销毁原有的对象,并创建一个新的对象,并在创建新对象时指定新的删除器。

4、unique_ptr的优缺点

优点:

  1. 独占所有权: unique_ptr 确保每个指针只有一个所有者,这意味着它独占了指向的资源。当 std::unique_ptr 被销毁或者被赋予新的指针时,它会自动释放之前所指向的资源。这有助于避免资源泄漏。
  2. 轻量级: unique_ptr 是一种轻量级智能指针,它不需要额外的运行时开销,因为它的功能主要是通过编译器支持的语言特性实现的。
  3. 移动语义支持: unique_ptr 支持移动语义,因此可以通过移动而非复制来传递所有权。这可以提高性能,并且在某些情况下,移动语义可以避免不必要的资源复制或者资源转移。

缺点:

  1. 独占性质限制: unique_ptr 的独占性质也可能是它的一个缺点,因为它不能共享所有权。如果需要在多个地方共享指针所有权,那么 unique_ptr 就不适用。

  2. 不支持复制: unique_ptr 不能进行复制,因为它的拷贝构造函数和拷贝赋值运算符被删除了。这意味着您不能直接将 unique_ptr 传递给函数,除非您使用了移动语义或者显示地将其转移所有权。

  3. 使用限制: 对于一些复杂的场景,如循环引用的管理,unique_ptr 可能不够灵活。在这种情况下,可能需要使用 shared_ptr 或者其他更复杂的智能指针。

总体而言,unique_ptr 是一种非常有用的智能指针,特别适用于管理动态分配的资源,并且在性能和资源管理方面提供了很多优势。然而,它也有其使用上的限制,需要根据具体情况来选择最合适的智能指针类型。

下面是一个示例代码,演示了 unique_ptr 的基本用法:

class MyClass {
public:
    MyClass() { std::cout << "MyClass Constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
    void someMethod() { std::cout << "Some Method of MyClass" << std::endl; }
};

int main() {
    // 创建一个 std::unique_ptr,管理 MyClass 的对象
    std::unique_ptr<MyClass> ptr1(new MyClass());

    // 调用对象的方法
    ptr1->someMethod();

    // 移动 ptr1 到 ptr2
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);

    // 此时 ptr1 不再拥有对象的所有权
    if (!ptr1) {
        std::cout << "ptr1 is nullptr" << std::endl;
    }

    // ptr2 拥有对象的所有权
    ptr2->someMethod();

    // 当 ptr2 超出作用域时,对象会被销毁
    return 0;
}

在这个示例中,unique_ptr 确保了 MyClass 对象的自动释放,无论是因为指针超出作用域还是因为移动指针。

5、unique_ptr的模拟实现

template<class T>
class unique_ptr {
public:
    // RAII
    unique_ptr(T* ptr)
        :_ptr(ptr)
        {}

    ~unique_ptr() {
        reset();
    }

    // 删除拷贝构造函数和拷贝赋值运算符,确保只有一个 unique_ptr 可以管理资源
    unique_ptr(const unique_ptr<T>&) = delete;
    unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;

    // 移动构造函数
    unique_ptr(unique_ptr<T>&& other) noexcept
        : _ptr(other.release())
    {}

    // 移动赋值运算符
    unique_ptr<T>& operator=(unique_ptr<T>&& other) noexcept {
        if (this != &other) {
            reset(other.release());
        }
        return *this;
    }

    T& operator*() const { return *_ptr; }
    T* operator->() const { return _ptr; }

    // 返回指向被管理资源的原始指针
    T* get() const noexcept { return _ptr; }

    // 释放资源所有权
    T* release() noexcept {
        T* releasedPtr = _ptr;
        _ptr = nullptr;
        return releasedPtr;
    }

    // 重置 unique_ptr,释放当前资源并接管新资源
    void reset(T* ptr = nullptr) noexcept {
        if (_ptr != ptr) {
            delete _ptr;
            _ptr = ptr;
        }
    }

private:
    T* _ptr;
};

让我们逐个解释这个类的各个部分:

  1. 构造函数和析构函数:构造函数接受一个原始指针作为参数,用于初始化 unique_ptr,并且在析构函数中释放资源。这是 RAII(资源获取即初始化)的一个例子,确保资源在 unique_ptr 生命周期结束时被正确释放。

  2. 删除拷贝构造函数和拷贝赋值运算符:通过将拷贝构造函数和拷贝赋值运算符声明为 delete,禁止了 unique_ptr 的拷贝,从而确保了资源的独占所有权。

  3. 移动构造函数和移动赋值运算符:通过移动构造函数和移动赋值运算符,unique_ptr 可以从另一个 unique_ptr 实例中获取资源的所有权,而不进行资源的复制。这提高了效率,并避免了资源的重复释放。

  4. 解引用和成员访问运算符重载:这些重载允许像使用原始指针一样使用 unique_ptr,使得用户可以像操作普通指针一样访问所管理的资源。

  5. get() 函数:返回指向被管理资源的原始指针,使得用户可以在需要时直接操作原始指针。

  6. release() 函数:释放 unique_ptr 对资源的所有权,并返回指向该资源的原始指针。这允许用户在不删除资源的情况下放弃对资源的所有权,常见于需要将资源传递给 C API 或延迟释放资源的情况。

  7. reset() 函数:重置 unique_ptr,释放当前资源并接管新资源。如果传递了新的原始指针,则 unique_ptr 会释放当前资源并获取新资源的所有权,如果未传递任何指针,则 unique_ptr 会释放当前资源而不获取新资源,相当于将 unique_ptr 重置为空指针。


六、std::weak_ptr

在这里插入图片描述

std::weak_ptr 是 C++ 中用于解决 std::shared_ptr 循环引用问题的工具。它是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。

循环引用通常发生在两个或多个对象相互持有对方的 shared_ptr 实例时,导致它们的引用计数永远不会归零,从而无法释放它们的内存,造成内存泄漏。因为它们会增加资源的引用计数,导致资源无法被正确释放。

weak_ptr 允许我们观测由 shared_ptr 管理的对象,但不会增加引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。因此这种智能指针”弱“共享对象。

下面是一个示例代码,演示了如何使用 weak_ptr

class B; // 提前声明 B 类
class A {
public:
    void setB(std::shared_ptr<B> b) {
        _b = b;
    }
private:
    std::weak_ptr<B> _b;
};

class B {
public:
    void setA(std::shared_ptr<A> a) {
        _a = a;
    }
private:
    std::weak_ptr<A> _a;
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->setB(b);
    b->setA(a);
    // 使用 a 和 b,它们彼此共享资源,但不会导致循环引用
  	return 0;
}

在这个示例中,类 AB 相互引用,但是其中的一个使用了 std::weak_ptr。这样做可以确保在没有循环引用的情况下共享资源,并且不会导致内存泄漏。


七、RAII模式:资源获取即初始化

RAII(Resource Acquisition Is Initialization)是一种C++编程范式,它利用对象的生命周期来管理资源的获取和释放。其核心思想是:在对象的构造函数中获取资源,在析构函数中释放资源。这样做的好处是,只要对象在作用域内存在,资源就会被正确地管理,不会出现资源泄漏。

RAII 的原理和概念可以总结为以下几点:

  1. 资源获取即初始化:通过在对象的构造函数中获取资源,利用C++的对象生命周期机制来确保资源在对象生命周期内有效。

  2. 资源的释放由析构函数负责:在对象的析构函数中释放资源,无论对象是因为正常结束作用域而销毁还是因为异常而销毁,都会确保资源得到正确释放。

  3. 异常安全性:RAII能够保证在发生异常时资源能够被正确释放,不会出现资源泄漏。

RAII与智能指针有着密切的联系,智能指针本身就是 RAII 的一种实现。智能指针通过将资源(如内存、文件句柄等)的所有权与指针绑定,利用对象的析构函数来确保资源在适当时机释放。

例如,unique_ptr它在对象被销毁时自动释放内存。通过将动态分配的内存与unique_ptr对象绑定,可以很容易地实现 RAII。当unique_ptr对象超出作用域时,其析构函数会被调用,自动释放所管理的内存。

{
    std::unique_ptr<int> ptr(new int(42)); // 获取资源(动态分配的内存)
    // 在此作用域内,ptr对象存在,资源有效

    // 使用ptr指向的内存
    std::cout << *ptr << std::endl;
} // ptr对象超出作用域,资源被释放

八、再谈删除器

删除器必须保存为一个指针或一个封装了指针的类。

虽然std::shared_ptrstd::unique_ptr都允许使用自定义删除器,但它们在定制删除器方面有一些不同之处:

  1. unique_ptr

    • unique_ptr 允许通过模板参数来指定删除器,这意味着删除器的类型可以作为 unique_ptr 类模板的一部分。例如:std::unique_ptr<T, Deleter>,其中 Deleter 是一个函数对象或者函数指针,用于在指针超出范围时释放资源。
    • 删除器类型必须与所指向的指针类型匹配,即删除器必须接受指向所指类型的指针作为参数。通常,对于动态分配的单个对象,删除器的函数签名为 void operator()(T*);对于动态分配的数组,删除器的函数签名为 void operator()(T[])void operator()(T*)
    • unique_ptr 的删除器默认使用 std::default_delete,它对应于 deletedelete[],取决于指针的类型。
  2. shared_ptr

    • shared_ptr 不直接支持定制删除器作为模板参数。相反,可以在创建 shared_ptr 时传递一个额外的函数对象(或函数指针)参数作为删除器。这个删除器可以是 std::default_delete 的自定义版本,也可以是完全不同的函数对象。
    • 删除器不需要与所指向的类型匹配,因为 shared_ptr 使用类型擦除技术来存储删除器和引用计数等信息。

总的来说,unique_ptr在设计上更加灵活,因为它允许在编译时指定删除器类型,并且删除器的类型必须与所指向的指针类型匹配。而shared_ptr允许在运行时指定删除器,并且删除器不需要与指针类型匹配。

我们可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道。实际上,在一个shared_ptr的生存期中,我们可以随时改变其删除器的类型。我们可以使用一种类型的删除器构造一个shared_ptr,随后使用reset赋予此 shared_ptr另一种类型的删除器。通常,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。而unique_ptr则不行。我们再次对它们进行对比:

  1. 共享所有权 vs. 独占所有权:
    • std::shared_ptr 允许多个智能指针共享同一块资源,因此资源的生命周期由引用计数来管理。这意味着资源只在最后一个引用计数归零时才会被释放,因此删除器可能不会立即被调用。
    • std::unique_ptr 拥有独占所有权,因此资源在指针被销毁时立即释放。这意味着删除器会在 std::unique_ptr 超出作用域或被重置时立即被调用。
  2. 删除器类型:
    • 对于 std::shared_ptr,删除器类型可以是任何可调用对象,包括函数指针、函数对象和 lambda 表达式,因为 std::shared_ptr 不会在编译时执行删除器类型检查。
    • 对于 std::unique_ptr,删除器类型必须作为模板参数之一,在编译时进行类型检查。这意味着必须在编译时提供删除器的确切类型。
  3. 传递参数给删除器:
    • std::shared_ptr 中,删除器可以接受额外的参数,并且可以将这些参数传递给删除器函数,这样可以更灵活地管理资源。
    • std::unique_ptr 中,如果希望删除器接受额外的参数,则需要将这些参数捕获在 lambda 表达式中,或者使用绑定器或者包装器。

让我们通过示例来说明在std::shared_ptrstd::unique_ptr中如何处理删除器参数:

示例1 - 在 std::shared_ptr 中传递额外参数给删除器:

void customDeleter(int* ptr, int extraParam) {
    std::cout << "Custom deleter called with extra parameter: " << extraParam << std::endl;
    delete ptr;
}

int main() {
    int extraParam = 42;

    // 使用 lambda 表达式捕获额外参数
    auto deleter = [&extraParam](int* ptr) {
        std::cout << "Lambda deleter called with extra parameter: " << extraParam << std::endl;
        delete ptr;
    };

    // 创建 shared_ptr 并传递额外参数给删除器
    std::shared_ptr<int> ptr(new int(10), std::bind(customDeleter, std::placeholders::_1, extraParam));
    std::shared_ptr<int> ptr2(new int(20), deleter);

    return 0;
}

在这个示例中,我们定义了一个自定义的删除器 customDeleter,它接受一个指针和一个额外的参数。然后,我们使用 lambda 表达式或者 std::bind 来捕获额外的参数,并将捕获的参数传递给删除器。最后,我们创建了两个 std::shared_ptr,并将删除器作为参数传递给它们,从而实现了在 std::shared_ptr 中传递额外参数给删除器。

示例2 - 在 std::unique_ptr 中使用 lambda 表达式捕获额外参数:

int main() {
    // 额外参数
    int extra_param = 10;

    // 使用 lambda 表达式作为删除器,并捕获额外参数
    auto customDeleter = [&extra_param](int* ptr) {
        std::cout << "Deleting pointer with extra_param: " << extra_param << std::endl;
        delete ptr;
    };

    // 创建 unique_ptr,并指定删除器
    std::unique_ptr<int, decltype(customDeleter)> ptr(new int(42), customDeleter);

    return 0;
}

在这个示例中,我们使用 lambda 表达式作为删除器,并捕获了外部定义的额外参数 extra_param。这样,我们就可以在 lambda 表达式中使用这个额外参数来实现更灵活的资源管理。

示例3 - 在 std::unique_ptr 中使用 bind表达式捕获额外参数:

// 自定义删除器,接受额外参数
void customDeleter(int* ptr, int extra_param) {
    std::cout << "Deleting pointer with extra_param: " << extra_param << std::endl;
    delete ptr;
}

int main() {
    // 额外参数
    int extra_param = 10;

    // 使用 std::bind 绑定函数和额外参数,创建删除器
    auto deleter = std::bind(customDeleter, std::placeholders::_1, extra_param);

    // 创建 unique_ptr,并指定删除器
    std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);

    return 0;
}

在这个示例中,我们使用 bindcustomDeleter 函数与额外参数 extra_param 绑定,然后将绑定后的函数作为删除器传递给 unique_ptr。这样,当 unique_ptr 被销毁时,删除器会正确地调用 customDeleter 函数,并传递额外参数。

  1. 管理资源的方式:
    • shared_ptr 在超出作用域时不会立即释放资源,而是在引用计数归零时才释放资源。因此,删除器可能不会立即被调用。
    • unique_ptr 在超出作用域时立即释放资源,并在释放资源时调用删除器。

总的来说,虽然 std::shared_ptrstd::unique_ptr 都支持定制删除器,但由于它们管理资源的方式不同,因此删除器的行为也会有所不同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1808943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

端午节到了,祝大家粽子甜甜,生活美满!愿粉丝们心想事成,健康平安,阖家幸福!

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

java+Vue +Spring boot技术开发的UWB高精度定位技术系统源码 uwb定位系统+基站定位

javaVue Spring boot技术开发的UWB高精度定位技术系统源码 uwb定位系统基站定位 系统采用UWB高精度定位技术&#xff0c;可实现厘米级别定位。UWB作为一种高速率、低功耗、高容量的新兴无线局域定位技术&#xff0c;目前应用主要聚焦在室内高精确定位&#xff0c;例如在工业自动…

ABAP - SAP与企业微信集成

最近接到一个SAP直接给企业微信推送消息的需求&#xff0c;说实话之前一直没接触过&#xff0c;脑袋空空的&#xff0c;最终通过在百度搜索案例成功解决了&#xff0c;百度虽然一直被诟病&#xff0c;但却无法否认它的神奇。实现效果 实现思路&#xff1a;从需求出发&#xff0…

游戏服务器工程实践一:百万级同时在线的全区全服游戏

我应该有资格写这篇文章&#xff0c;因为亲手设计过可以支撑百万级同时在线的全区全服类型的游戏服务器架构。 若干年前我在某公司任职时&#xff0c;参与研发过一款休闲类型的游戏&#xff0c;由 penguin 厂独代。研发的时候&#xff0c;p 厂要求我们的游戏服务器要能支撑百万…

IO进程线程(十一)进程间通信 消息队列

文章目录 一、IPC(Inter-Process Communication)进程间通信相关命令 &#xff1a;&#xff08;一&#xff09;ipcs --- 查看IPC对象&#xff08;二&#xff09;获取IPC键值&#xff08;三&#xff09;删除IPC对象的命令&#xff08;四&#xff09;获取IPC键值的函数1. 函数定义…

[office] 快速提取出Excel 2010单元格括号内的文字信息 #知识分享#经验分享

快速提取出Excel 2010单元格括号内的文字信息 实例演示 ①我们打开一份Excel电子表格&#xff0c;我们要将C列里面括号内的内容提取到D列里面&#xff0c;单击D2单元格&#xff0c;输入下面的函数公式&#xff1a; MID(C2,FIND("(",C2)1,LEN(C2)-FIND("("…

vue如何使用slot

1. vue2 如何使用slot 1.1. 默认插槽&#xff08;Default Slot&#xff09;1.2. 具名插槽&#xff08;Named Slot&#xff09;1.3. 作用域插槽&#xff08;Scoped Slot&#xff09; 2. vue3 如何使用slot 2.1. 默认插槽&#xff08;Default Slot&#xff09;2.2. 具名插槽&…

Pytorch学习11_神经网络-卷积层

1.创建神经网络实例 import torch import torchvision from torch import nn from torch.nn import Conv2d from torch.utils.data import DataLoaderdatasettorchvision.datasets.CIFAR10("../dataset_cov2d",trainFalse,transformtorchvision.transforms.ToTensor(…

【深度学习】NLP,Transformer讲解,代码实战

文章目录 1. 前言2. Transformer结构训练过程1. 输入嵌入和位置编码2. 编码器层2.1 单头的注意力机制(便于理解)2.2 多头的注意力机制(Transformer真实使用的)2.3 残差连接和层归一化2.4 前馈神经网络&#xff08;FFN&#xff09;2.5 残差连接和层归一化2.6 总结 3. 解码器层 推…

本周重磅日程:美联储决议、中美通胀、苹果AI和英伟达拆股

当周重磅看点颇多&#xff1a;美联储FOMC将公布最新利率“点阵图”&#xff0c;中国5月金融数据、中美通胀数据将出炉&#xff0c;日本央行购债计划是否变动成为市场焦点&#xff0c;苹果2024全球开发者大会一系列AI功能将亮相&#xff1b;特斯拉2024股东大会上马斯克560亿美元…

Linux 内核参数-相关介绍

Linux 内核参数-相关介绍 今天&#xff0c;介绍Linux内核参数相关内容。由于Linux内核优化需要根据具体需求进行具体优化&#xff0c;同时需要具备一定经验&#xff0c;所以这里不涉及优化操作内容。 不过&#xff0c;遇到面试中有相关题目&#xff0c;不至于答不上来&#x…

Android.mk文件生成的so工程文件并Debug调试native code

1.这里主要展示一下从最原始先新建一个工程 2.将hello的子工程文件放入上面新建好的工程里面&#xff0c;直接拷贝放置这里 3.修改根目录下的settings.gradle 加入hello 4.app工程下的build.gradle加入依赖&#xff0c;这样就可以识别hello中的java包文件 5.MainActivity 中来&…

python tushare股票量化数据处理:学习中

1、安装python和tushare及相关库 matplotlib pyplot pandas pandas_datareader >>> import matplotlib.pyplot as plt >>> import pandas as pd >>> import datetime as dt >>> import pandas_datareader.data as web 失败的尝试yf…

vscode侧边栏错乱重制

vscode 重制命令面板 View: Reset View Locations

将AIRNet集成到yolov8中,实现端到端训练与推理

AIRNet是一个图像修复网络,支持对图像进行去雾、去雨、去噪声的修复。其基于对比的退化编码器(CBDE),将各种退化类型统一到同一嵌入空间;然后,基于退化引导恢复网络(DGRN)将嵌入空间修复为目标图像。可以将AIRNet的输出与yolov8进行端到端集成,实现部署上的简化。 本博…

LabVIEW汽车电机测试系统

1. 背景 随着电动汽车的快速发展&#xff0c;汽车电机作为电动汽车的核心部件&#xff0c;其性能评估变得尤为重要。电机的功率、效率、转速等参数直接影响着电动汽车的性能和续航里程。因此&#xff0c;设计一套全面、准确的汽车电机测试系统对于提高电动汽车的性能和安全性具…

基于Java-SpringBoot-VUE-MySQL的企业财务报销系统

基于Java-SpringBoot-VUE-MySQL的企业财务报销系统 登陆界面 联系作者 如需本项目源代码&#xff0c;可扫码或者VX:bob1638 联系作者。 主页-02 系统功能持续更新中。。。 介绍 本系统是采用现代信息技术手段&#xff0c;采用JAVA开发语言&#xff0c;VUE语言&#xff0c;HTML语…

【C++进阶】深入STL之 栈与队列:数据结构探索之旅

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;模拟实现list与迭代器 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀stack和queue &#x1f4…

基础数据结构 -- 堆

1. 简介 堆可以看做是一种特殊的完全二叉树&#xff0c;它满足任意节点的值都大于或小于其子节点的值。 2. 功能 插入元素&#xff1a;插入新元素时&#xff0c;先将元素放至数组末尾&#xff0c;然后通过上浮算法自底向上调整&#xff0c;使堆保持性质。删除堆顶元素&#xff…

Unity DOTS技术(九) BufferElement动态缓冲区组件

文章目录 一.简介二.例子 一.简介 在之前的学习中我们发现Entity不能挂载相同的组件的. 当我们需要用相同的组件时则可以使用.IBufferElementData接口 动态缓冲区组件来实现 二.例子 1.创建IBufferElementData组件 using Unity.Entities; using UnityEngine; //[GenerateAu…