深入理解 C++ 智能指针

文章目录

  • 一、引言
  • 二、 原始指针的问题
    • 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 都支持定制删除器,但由于它们管理资源的方式不同,因此删除器的行为也会有所不同。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java_中间件——Redis

Redis 介绍&#xff1a; Redis是一个基于内存的key-value结构数据库&#xff08;MySQL是通过数据文件方式存储在磁盘上&#xff0c;数据结构是二维表&#xff09; 特点&#xff1a; 更改配置文件&#xff1a; 使用密码&#xff1a; redis默认是不需要密码的&#xff0c;如果…

spring-data-mongodb版本兼容问题

spring-data-mongodb与mongodb驱动有兼容性问题&#xff0c;不匹配会报NoSuchMethod异常&#xff0c;mongodb的java驱动包在4.0之后由mongodb-java-driver更名为mongodb-driver-sync。 spring-data-mongodb包依赖中有mongodb-driver-core&#xff0c;但缺诸如MongoCollection等…

虚拟机调用摄像头设备一直 select timeout问题的解决

在VMware里面调用v4l2-ctl捕获图像&#xff0c;或者opencv的VideoCapture(0)捕获图像&#xff0c;或者直接调用v4l2的函数&#xff0c;在streamon后&#xff0c;调用select读取数据&#xff0c;均会一直提示select timeout的问题&#xff0c;大概率是由于USB版本的兼容性造成的…

【Oracle】Oracle导入导出dmp文件

文章目录 前言一、什么是dmp&#xff1f;二、imp/impdp、exp/expdp对比及示例1.区别2.imp/impdp对比及示例a. impb. impbp 3.exp/expdp对比及示例a. expb.expdp 3.其他事项 三、执行导入导出前置条件1.创建角色并授权2.创建目录映射 前言 在工作中&#xff0c;经常会遇到需要备…

【Linux】网络配置(静态/动态/手动/nmcli)

目录 一、手动修改网络配置文件&#xff1a;静态 二、手动修改网络配置文件&#xff1a;动态 三、nmcli工具命令修改网络配置文件&#xff1a;静态 四、nmcli工具命令修改网络配置文件&#xff1a;动态 错误排查分析&#xff1a;编辑虚拟网络编辑器不生效 1、排除VMware启…

Excel 将同一分类下的值依次填进分类格右边的格中

表格的第2列是分类&#xff0c;第3列是明细&#xff1a; ABC1S.noAccountProduct21AAAQatAAG32BAAQbIAAW43BAAQkJAAW54CAAQaAAP65DAAQaAAX76DAAQbAAX87DAAQcAAX 需要将同一分类下的值依次填入分类格右边的格中&#xff1a; ABCD1S.noAccountProduct21AAAQatAAG32BAAQbIAAWkJ…

[数据集][目标检测]厨房积水检测数据集VOC+YOLO格式88张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;88 标注数量(xml文件个数)&#xff1a;88 标注数量(txt文件个数)&#xff1a;88 标注类别数…

笔记-Python pip配置国内源

众所周知&#xff0c;Python使用pip方法安装第三方包时&#xff0c;需要从 https://pypi.org/ 资源库中下载&#xff0c;但是会面临下载速度慢&#xff0c;甚至无法下载的尴尬&#xff0c;这时&#xff0c;你就需要知道配置一个国内源有多么重要了&#xff0c;通过一番摸索和尝…

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

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

文案策划背后的秘密 | 职场高手养成记

要想在文案策划这个行当里混&#xff0c;首先得对自己的文字功底有足够的信心&#xff0c;那种“文章独步天下”的气势不可或缺。 要是没有这份自信&#xff0c;我建议你还是另寻他路。 要想跨入文案策划的大门&#xff0c;可以从以下几个方面入手&#xff1a; 1. 学习文案基…

4000亿薪酬被驳回!马斯克再次讨薪

特斯拉CEO埃隆马斯克的一笔巨额财产&#xff0c;将在数日后的特斯拉股东大会上&#xff0c;由股东投票决定何去何从。 事情是这样的。 3.5研究测试&#xff1a;hujiaoai.cn 4研究测试&#xff1a;askmanyai.cn Claude-3研究测试&#xff1a;hiclaude3.com 2018年&#xff0c;特…

QT 信号和槽 通过自定义信号和槽沟通 如何自定义槽和信号的业务,让它们自动关联 自定义信号功能

通过信号和槽机制通信&#xff0c;通信的源头和接收端之间是松耦合的&#xff1a; 源头只需要顾自己发信号就行&#xff0c;不用管谁会接收信号&#xff1b;接收端只需要关联自己感兴趣的信号&#xff0c;其他的信号都不管&#xff1b;只要源头发了信号&#xff0c;关联该信号…

常见机器学习的原理及优略势

有监督 一、线性回归&#xff08;Linear Regression) 1. 算法原理 线性回归&#xff08;Linear Regression&#xff09;是一种基本的回归算法&#xff0c;它通过拟合一个线性模型来预测连续型目标变量。线性回归模型的基本形式是&#xff1a;y w1 * x1 w2 * x2 … wn * …

操作系统真象还原:内存管理系统

第8章-内存管理系统 这是一个网站有所有小节的代码实现&#xff0c;同时也包含了Bochs等文件 8.1 Makefile简介 8.1.1 Makefile是什么 8.1.2 makefile基本语法 make 给咱们提供了方法&#xff0c;可以在命令之前加个字符’&#xff20;’&#xff0c;这样就不会输出命令本身…

作业07 递推算法2

作业&#xff1a; #include <iostream> using namespace std; int main(){int a[110][110]{0},b[110][110]{0},n;cin>>n;for(int i1;i<n;i){for(int j1;j<i;j){cin>>a[i][j];}}for(int in-1;i>1;i--){for(int j1;j<i;j){a[i][j]a[i][j]max(a[i1]…

【SQLAlChemy】Query函数可传入的参数有哪些?

Query 函数的使用 参数种类 一般可以传递的参数有如下三种&#xff1a; 模型名。指定查找这个模型的全部属性&#xff08;对应于数据库查询中的全表查询&#xff09;。模型中的属性。可以用来指定只查询某个模型的几个属性值。使用聚合函数。 func.count():统计行的数量。fu…

理财-商业保险

目录&#xff1a; 一、保险查询 1、金事通APP 2、商业保险APP 二、平安寿险 1、智能星 2、智富人生A 3、总结 三、保险中的掩藏项 一、保险查询 1、金事通APP 中国银行保险信息技术管理有限公司发挥金融基础设施作用&#xff0c;以“切实让数据多跑路、百姓少跑腿”为…

安装MySQL Sample Database

本文安装的示例数据库为官方的Employees Sample Database。 操作过程参考其安装部分。 在安装前&#xff0c;MySQL已安装完成&#xff0c;环境为Linux。 克隆github项目&#xff1a; $ git clone https://github.com/datacharmer/test_db.git Cloning into test_db... remo…

OpenAI与核聚变公司寻求合作,白宫拨款1.8亿美元用于核聚变商业化研究

在当下&#xff0c;由 AI 引发的新一轮能源危机已经不再是一个小概率的「黑天鹅」事件&#xff0c;而是一头正在向我们猛冲而来的「灰犀牛」。 Helion Energy&#xff0c;是一家总部位于美国华盛顿州埃弗雷特的能源创业公司。 3.5研究测试&#xff1a;hujiaoai.cn 4研究测试&am…

kaggle竞赛实战9——模型融合

有三种方法&#xff0c; 第一种&#xff1a;均值融合&#xff0c;代码如下 data pd.read_csv(\ result/submission_randomforest.csv\ ) data[randomforest] data[target].values temp pd.read_csv(\ result/submission_lightgbm.csv\ ) …