深入理解 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_ptrstd::shared_ptr<int> ptr(new int(42), CustomDeleter());// 手动重置 shared_ptrptr.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;// 此时引用计数为 2std::cout << "Reference count: " << ptr1.use_count() << std::endl;} // ptr2 超出作用域,引用计数减少为 1// ptr1 仍然指向相同的 MyClass 实例,引用计数为 1std::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:// RAIIshared_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); // 创建 ptr1std::unique_ptr<MyClass> ptr2(std::move(ptr1)); // 移动构造 ptr2,ptr1变为空指针// 使用移动赋值std::unique_ptr<MyClass> ptr3(new MyClass); // 创建 ptr3ptr1 = 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的资源转移给ptr1return 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 到 ptr2std::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:// RAIIunique_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.rhkb.cn/news/346106.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

讯飞星火大模型个人API账号免费使用申请教程

文章目录 1.登录讯飞星火大模型官网 https://www.xfyun.cn/ 2.下滑找到Spark Lite&#xff0c;点击立即调用 3.星火大模型需要和具体的应用绑定&#xff0c;我们需要先创建一个新应用 https://console.xfyun.cn/app/myapp&#xff0c;应用名称可以按照自己的意愿起。 4.填写应用…

1688商品库存查询

目录 下载安装与运行 功能简介 快速入门&#xff08;视频&#xff09; 当前支持的导出项 常用功能 历史商品是什么意思 粘贴商品有什么要求 导入商品需要什么样的模板 单个商品的查看 查看单个商品详情 下载安装与运行 下载、安装与运行 语雀 功能简介 最近一次测…

kafka-集群-生产消费测试

文章目录 1、集群生产消费测试1.1、消费者消费消息1.2、生产者生产消息 1、集群生产消费测试 1.1、消费者消费消息 [rootlocalhost ~]# kafka-console-consumer.sh --bootstrap-server 192.168.74.148:9095,192.168.74.148:9096,192.168.74.148:9097 --topic my_topic11.2、生…

深度解析地铁票务系统的技术架构与创新应用

在城市交通体系中&#xff0c;地铁作为一种快速、便捷的公共交通方式&#xff0c;已经成为现代都市生活的重要组成部分。而地铁票务系统的技术架构&#xff0c;则是支撑地铁运营的核心之一。本文将深度解析地铁票务系统的技术架构与创新应用&#xff0c;从系统设计、数据管理、…

vuInhub靶场实战系列--Kioptrix Level #3

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

每日复盘-20240607

今日关注&#xff1a; 这几天市场环境不好&#xff0c;一直空仓。 六日涨幅最大: ------1--------605258--------- 协和电子 五日涨幅最大: ------1--------605258--------- 协和电子 四日涨幅最大: ------1--------605258--------- 协和电子 三日涨幅最大: ------1--------0…

Java面向对象-方法的重写、super

Java面向对象-方法的重写、super 一、方法的重写二、super关键字1、super可以省略2、super不可以省略3、super修饰构造器4、继承条件下构造方法的执行过程 一、方法的重写 1、发生在子类和父类中&#xff0c;当子类对父类提供的方法不满意的时候&#xff0c;要对父类的方法进行…

原生小程序一键获取手机号

1.效果图 2.代码index.wxml <!-- 获取手机号 利用手机号快速填写的功能&#xff0c;将button组件 open-type 的值设置为 getPhoneNumber--><button open-type"getPhoneNumber" bindgetphonenumber"getPhoneNumber">获取手机号</button> …

PPT设置为本框的默认格式以及固定文本框

调整文本框固定位置 双击文本框之后勾选如下三个位置 设置文本框为默认 在调整好文本框的基本性质后&#xff0c;设置为默认即可

MySQL数据库的基础:逻辑集合数据库与表的基础操作

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能接…

你还不知道无线PLC?

随着技术的不断发展&#xff0c;工业控制系统也在经历着革新。无线PLC&#xff08;Programmable Logic Controller&#xff0c;可编程逻辑控制器&#xff09;是一种结合了无线通讯技术和传统PLC系统的创新型技术。它为工业自动化提供了一种更灵活、更便捷的解决方案&#xff0c…

大模型基础——从零实现一个Transformer(2)

大模型基础——从零实现一个Transformer(1) 一、引言 上一章主要实现了一下Transformer里面的BPE算法和 Embedding模块定义 本章主要讲一下 Transformer里面的位置编码以及多头注意力 二、位置编码 2.1正弦位置编码(Sinusoidal Position Encoding) 其中&#xff1a; pos&…

jmeter性能优化之mysql配置

一、连接数据库和grafana 准备&#xff1a;连接好数据库和启动grafana并导入mysql模板 大批量注册、登录、下单等&#xff0c;还有过节像618&#xff0c;双11和数据库交互非常庞大&#xff0c;都会存在数据库的某一张表里面&#xff0c;当用户在登录或者查询某一个界面时&…

第十二届蓝桥杯单片机国赛练习代码

文章目录 前言一、问题重述二、主函数总结 前言 第十五蓝桥杯国赛落幕已有十天&#xff0c;是时候总结一下&#xff0c;这个专栏也将结束。虽然并没有取得预期的结果&#xff0c;但故事结尾并不总是美满的。下面是赛前练习的第十二届国赛的代码。 一、问题重述 二、主函数 完整…

渗透测试模拟实战-tomexam网络考试系统

渗透测试&#xff0c;也称为“pentest”或“道德黑客”&#xff0c;是一种模拟攻击的网络安全评估方法&#xff0c;旨在识别和利用系统中的安全漏洞。这种测试通常由专业的安全专家执行&#xff0c;他们使用各种技术和工具来尝试突破系统的防御&#xff0c;如网络、应用程序、主…

高德地图简单实现点标,和区域绘制

高德地图开发文档:https://lbs.amap.com/api/javascript-api/guide/abc/quickstart 百度搜索高德地图开发平台 注册高德地图开发账号 在应用管理中 我的应用中 添加一个Key 点击提交 进入高德地图开发文档:https://lbs.amap.com/api/javascript-api/guide/abc/quickstart …

定个小目标之刷LeetCode热题(15)

这道题直接就采用两数相加的规则&#xff0c;维护一个进阶值&#xff08;n&#xff09;即可&#xff0c;代码如下 class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 新建一个值为0的头结点ListNode newHead new ListNode(0);// 创建几个指针用于…

从零开始,手把手教你文旅产业策划全攻略

如果你想深入了解文旅策划的世界&#xff0c;那么有很多途径可以获取知识和灵感。 首先&#xff0c;阅读一些专业书籍也是一个不错的选择。书店或图书馆里有许多关于文旅策划的书籍&#xff0c;它们通常涵盖了策划的基本理论、方法和实践案例。通过阅读这些书籍&#xff0c;你…

对接专有钉钉(浙政钉)登陆步骤

背景 因为项目需要对接浙政钉&#xff0c;我想应该和之前对接阿里云的钉钉登陆钉钉登陆类似&#xff0c;就上网搜索看看&#xff0c;出现了个专有钉钉的概念&#xff0c;就一时间搞不清楚&#xff0c;钉钉&#xff0c;专有钉钉&#xff0c;浙政钉的区别&#xff0c;后续稍微理…

20240606更新Toybrick的TB-RK3588开发板在Android12下的内核

20240606更新Toybrick的TB-RK3588开发板在Android12下的内核 2024/6/6 10:51 0、整体编译&#xff1a; 1、cat android12-rk-outside.tar.gz* | tar -xzv 2、cd android12 3、. build/envsetup.sh 4、lunch rk3588_s-userdebug 5、./build.sh -AUCKu -d rk3588-toybrick-x0-a…