当前位置: 首页 > news >正文

C++——智能指针剖析

参考:

恋恋风辰官方博客

动态内存管理 - cppreference.com

SRombauts/shared_ptr: 一个最小的 shared/unique_ptr 实现,用于处理 boost/std::shared/unique_ptr 不可用的情况。

C++智能指针_c++ 智能指针-CSDN博客

当我们谈论shared_ptr的线程安全性时,我们在谈论什么 - 知乎

C++ 智能指针 - 全部用法详解-CSDN博客

文章目录

    • @[toc]
  • 序言
  • 1. shared_ptr
    • 1.1 shared_ptr 内存模型
    • 1.2 shared_ptr的使用
      • 1.2.1 make_shared
      • 1.2.2 shared_ptr与new结合
      • 1.2.3 reset()
      • 1.2.4 通过智能指针共享数据
      • 1.2.5 shared_ptr是线程安全的吗?
  • 2. weak_ptr
    • 2.1 循环引用问题
    • 2.2 其他成员函数
      • 2.2.1 use_count()
      • 2.2.2 expired()
      • 2.2.3 lock()
  • 3. enable_from_this_shared
  • 4. unqiue_ptr

序言

传统的手动管理内存方式(如 newdelete)虽然灵活,但也容易引发内存泄漏(new的对象在作用域结束后没有被及时释放)、悬空指针(指针的指向对象已被删除或释放,但仍有其他指针保留了对该内存位置的引用)和重复释放(一个指针指向的内存被多次重复释放)等问题。随着项目规模的扩大和代码复杂性的增加,这些问题不仅让程序员疲于奔命,也直接影响了软件的可靠性和可维护性。

智能指针就是为了实现类似于Java中的垃圾回收机制。Java的垃圾回收机制使程序员从繁杂的内存管理任务中彻底的解脱出来,在申请使用一块内存区域之后,无需去关注应该何时何地释放内存,Java将会自动帮助回收。但是出于效率和其他原因(可能C++设计者不屑于这种傻瓜氏的编程方式),C++本身并没有这样的功能,其繁杂且易出错的内存管理也一直为广大程序员所诟病。

更进一步地说,智能指针的出现是为了满足管理类中指针成员的需要。包含指针成员的类需要特别注意复制控制和赋值操作,原因是复制指针时只复制指针中的地址,而不会复制指针指向的对象(一块内存地址可能被多个对象所指向)。当类的实例在析构的时候,可能会导致垂悬指针问题(即指针的指向对象已被删除或释放,但仍有其他指针保留了对该内存位置的引用)。

**管理类中指针成员的方法一般有两种方式:**一种是采用值型类,这种类是给指针成员提供值语义(value semantics),当复制该值型对象时,会得到一个不同的新副本。这种方式典型的应用是string类。另外一种方式就是智能指针,实现这种方式的指针所指向的对象是共享的。

智能指针不仅提供了内存管理的自动化,还增强了代码的安全性和可读性,是现代 C++ 中推荐的内存管理方式之一。本篇文章旨在系统地介绍 C++ 标准库中的三种常用智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

智能指针并不是指针,而是行为类似于指针的类对象,这种对象具有指针不包含的其他功能。简单来说,智能指针能帮助我们管理动态分配的内存,它会帮助我们自动释放new出来的内存,从而避免 newdelete引发的一系列问题,比如内存泄漏、悬空指针和重复释放等。

C++里面有四个智能指针:auto_ptr、share_ptr、unique_ptr、weak_ptr。其中后三个是C++11支持的,并且第一个已经在C++11弃用,这里我们只学习后三个。

1. shared_ptr

这三类智能指针模板都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象,当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,将无需记住稍后释放这些内存:在智能指针过期时,这些内存会自动释放(RAII)

我们可以回顾一下我们在并发编程(10)这篇文章中提到的问题:shared_ptr是线程安全的吗?我只简单做了回答,在这篇文章中,我将在下面详细分析。

1.1 shared_ptr 内存模型

shared_ptr有以下两个作用:

  1. std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。只有在最后一个shared_ptr副本对象析构的时候,内存才会被释放。
  2. sharedd_ptr共享被管理的对象,同一时刻可以有多个shared_ptr拥有对象的所有权(多线程中,不同线程可以对指向同一内存的sharedd_ptr副本中的数据进行安全访问或修改,但多个线程不能对同一个sharedd_ptr对象中的数据进行修改),当最后一个shared_ptr对象销毁时,被管理对象自动销毁。

简单来说,shared_ptr实现包含了两个部分:

  • 一个指向堆上创建的对象的裸指针 raw_ptr
  • 一个指向内部隐藏的、共享的管理对象 shared_count_object。其中use_count是当前这个堆上对象被多少对象引用了,简单来说就是引用计数。

在这里插入图片描述

图片来源:https://github.com/SRombauts/shared_ptr

如上图所示,shared_ptr内部包含了两个指针,一个Ptr to T指向目标管理对象T object,另一个Ptr to Control Block指向控制块Control Block。控制块包含了一个引用计数(reference count)、一个弱计数(weak count)其他数据(other data)(比如删除器、分配器等)。

引用计数会累加共享同一块资源(内存)的shared_ptr对象数量,是shared_ptr的核心,在任何情况下都是线程安全的(因为引用计数的实现过程是原子操作)。

为了满足线程安全的要求,引用计数通常使用类似于 std::atomic::fetch_add 的操作并结合 std::memory_order_relaxed 进行递增(递减操作则需要更强的内存排序,以确保控制块能够安全销毁)。

简单举一个例子:

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2=p1;

shared_ptr有很多构造函数,这里使用的构造函数原型为:

template< class Y >
explicit shared_ptr( Y* ptr );template< class Y >
shared_ptr& operator=( const shared_ptr<Y>& r ) noexcept;

二者的内存模型如下所示:

在这里插入图片描述

图片来源:https://blog.csdn.net/LCZ411524/article/details/143648637

很明显,p1和p2都指向同一内存空间T Object,而且引用计数为2,只有当p1和p2都被释放后,引用计数减为0的同时,智能指针管理的对象才会被释放。

1.2 shared_ptr的使用

上面多次说过,通过newdelete创建的对象存在很多隐患,但我还要在这里重复提醒:

  1. 当一个函数返回局部变量的指针(非new创建)时,外部使用该指针可能会造成崩溃或逻辑错误。因为局部变量随着函数的右}释放了。
  2. 当在作用域内使用new创建的对象在作用域结束后仍未被delete,那么该内存不存在任何对象指向(内存泄漏)。
  3. 如果多个指针指向同一个堆空间,其中一个释放了堆空间,使用其他的指针时会造成崩溃(悬空指针)。
  4. 对一个指针多次delete,会造成double free问题(重复释放)。
  5. 两个类对象A和B,分别包含对方类型的指针成员,互相引用时如何释放是个问题。

1.2.1 make_shared

我们可以通过构造函数来创建一个智能指针,也可以通过make_shared来构造智能指针,但更推荐后者,因为:

  1. std::make_shared 减少了内存分配的次数
    • 使用 new 创建: 当直接使用 std::shared_ptr 时,需要两次内存分配:
      1. 为所管理的对象分配内存。
      2. std::shared_ptr 的控制块(控制引用计数和资源信息)分配内存。
    • 使用 make_shared std::make_shared 会在一次内存分配中同时分配对象和控制块的内存,避免了额外的内存分配。
#include <memory>// 使用 new 创建
std::shared_ptr<int> sp1(new int(10));  // 两次内存分配// 使用 make_shared 创建
std::shared_ptr<int> sp2 = std::make_shared<int>(10);  // 一次内存分配
  1. 直接使用 new 创建 std::shared_ptr 可能引发异常时的资源泄漏问题。
    • 如果在 std::shared_ptr 的构造过程中发生异常,new 分配的资源可能无法正确释放,导致内存泄漏。
    • std::make_shared 是异常安全的,因为其分配和构造过程是一体化的,保证资源不会泄漏。
// 错误代码
void exception_test() {std::shared_ptr<int> sp(new int[100]);  // 动态分配数组throw std::runtime_error("Error occurred");  // 如果异常发生,数组内存泄漏
}// 正确代码
void exception_test() {std::shared_ptr<int> sp = std::make_shared<int[100]>();  // 异常安全,资源会正确管理
}
  1. 当直接使用 new 时,需要确保动态分配的内存与 std::shared_ptr 的删除器匹配。
    • 如果使用默认删除器管理动态分配的数组,会导致未定义行为(数组不会被正确释放)。
    • std::make_shared 自动匹配删除器,避免了这种错误。
// 错误代码
void test() {std::shared_ptr<int> sp(new int[10]);  // 错误!默认删除器无法正确释放数组
}
// 正确代码
void test() {std::shared_ptr<int[]> sp = std::make_shared<int[]>(10);  // 正确,删除器自动匹配
}

但注意,当存在以下情况时,不应该使用make_shared来构造shared_ptr对象,而应直接构造shared_ptr对象:

  1. 需要自定义删除器

    std::make_shared 自动使用 delete 来销毁对象,但如果我们创建对象管理的资源不是通过new分配的内存,那么需要我们自定义一个删除器来销毁该内存;或者我们需要为 std::shared_ptr 提供自定义的删除逻辑(例如释放资源时需要执行额外的操作),那么 std::make_shared 就不适用了。在这种情况下,我们需要通过shared_ptr的构造函数来创建对象,并传递一个自定义的删除器。

  2. 创造对象的构造函数是保护或私有时

    当我们想要创建的对象没有公有的构造函数时,make_shared无法使用!!!

  3. 对象的内存可能无法及时回收

    make_shared 只分配一次内存,这看起来很好,减少了内存分配的开销。问题来了,weak_ptr会保持控制块(强引用,以及弱引用的信息)的生命周期,而因此连带着保持了对象分配的内存,只有最后一个weak_ptr离开作用域时,内存才会被释放。原本强引用减为 0 时就可以释放的内存,现在变为了强引用,若引用都减为 0 时才能释放,意外的延迟了内存释放的时间。这对于内存要求高的场景来说,是一个需要注意的问题。

  4. 需要管理数组

    std::make_shared 不能直接用于创建和管理动态数组。如果你希望管理动态数组,应该使用 std::shared_ptr<T[]> 或者手动管理内存,避免数组越界等问题。

int main() {// 使用 make_shared 管理单个对象auto sp1 = std::make_shared<int>(10);// 错误:不能直接使用 make_shared 管理数组// auto sp2 = std::make_shared<int[]>(10); // 不能这样// 正确的做法std::shared_ptr<int[]> sp2 = std::make_shared<int[]>(10);  // 适用于数组
}

在上段代码中,我们可以使用make_shared来创建shared_ptr管理单个对象,但不能将make_shared用来创建一个数组并返回给auto类型的变量。

因为std::make_shared<int[]>(10) 会返回一个 std::shared_ptr<int[]> 类型的指针,但这里的 auto 推导无法正确推断出数组类型,因为 std::shared_ptr<int[]> 是一个特殊类型,它不是一个普通的 std::shared_ptr<int> 类型。因此,在这种情况下,auto 无法推导出正确的类型。

而后者显式地指定了 std::shared_ptr<int[]> 类型,这样编译器就知道我们正在创建一个指向 int[] 数组的智能指针,并且 std::make_shared<int[]>(10) 会返回一个合适的 std::shared_ptr<int[]> 类型的对象。


简单使用:

//定义一个指向整形5的指针
auto psint2 = make_shared<int>(5);
//判断智能指针是否为空
if (psint2 != nullptr)
{cout << "psint2 is " << *psint2 << endl;
}auto psstr2 = make_shared<string>("hello zack");
if (psstr2 != nullptr && !psstr2->empty())
{cout << "psstr2 is " << *psstr2 << endl;
}

对于智能指针的使用和普通的内置指针没什么区别,通过判断指针是否为nullptr可以判断是否为空指针。通过->可以取指针内部的成员方法或者成员变量。

当我们需要获取内置类型(管理资源)时,可以通过智能指针的方法get()返回其底层管理的内置指针。

注意,通过get()函数返回的内置指针时要注意以下问题:

  1. 我们不能主动通过delete回收该指针,要交给智能指针自己回收,否则会造成double free或者使用智能指针产生崩溃等问题。
  2. 也不能用get()返回的内置指针初始化另一个智能指针,因为两个智能指针引用一个内置指针会出现问题,比如一个释放了另一个不知道就会导致崩溃等问题。 因为get() 方法返回的原始指针(即裸指针),不增加智能指针对对象的引用计数或所有权管理
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
int* raw_ptr = sp1.get();// 错误:使用裸指针初始化另一个 shared_ptr
std::shared_ptr<int> sp2(raw_ptr);  // 错误,sp2 和 sp1 都会管理同一个内存

这里,raw_ptrsp1 管理的对象的裸指针,但 raw_ptr 不会增加对象的引用计数,也不会管理其生命周期。当我们通过 raw_ptr 初始化 sp2 时,sp2 会成为一个新的智能指针,指向相同的内存区域。由于 sp1sp2 都管理同一个内存对象,但它们并没有共享引用计数。裸指针的生命周期与智能指针不同,它不被智能指针的生命周期管理,这可能会导致以下错误:

  • 多次释放同一内存:如果两个智能指针都拥有相同的裸指针,而其中一个智能指针释放了这个指针所管理的资源,另一个智能指针会在其析构时试图释放相同的资源。这会导致“双重释放”错误,通常会导致程序崩溃。
  • 悬挂指针:如果原始智能指针在另一个智能指针之前被销毁,那么另一个智能指针会变成一个悬挂指针。虽然这个智能指针指向有效内存,但该内存已被释放,访问它会导致未定义行为(通常会崩溃)。

get()用来将指针的访问权限传递给代码,只有在确定代码不会delete裸指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。

1.2.2 shared_ptr与new结合

我们可以传给shared_ptr一个new构造的指针对象:

auto psint = shared_ptr<int>(new int(5));
auto psstr = shared_ptr<string>(new string("hello zack"));

原型在上面也说过,是

template< class Y >
explicit shared_ptr( Y* ptr );

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

//错误,不能用内置指针隐式初始化shared_ptr
// shared_ptr<int> psint2 = new int(5);
//正确,显示初始化
shared_ptr<string> psstr2(new string("good luck"));

除了智能指针之间的赋值(赋值构造函数)外,还以通过一个智能指针构造另一个

shared_ptr<string> psstr2(new string("good luck"));
//可以通过一个shared_ptr 构造另一个shared_ptr
shared_ptr<string> psstr3(psstr2);
shared_ptr<string> psstr4;
psstr4 = psstr2;

以上方法构造的智能指针都共享同一个引用计数

在构造智能指针的同时,可以指定自定义的删除方法替代shared_ptr本身内置的delete操作:

//可以设置新的删除函数替代delete
shared_ptr<string> psstr4(new string("good luck for zack"), delfunc);void delfunc(string *p)
{if (p != nullptr){delete (p);p = nullptr;}cout << "self delete" << endl;
}

我们实现了自己的delfunc函数作为删除器,回收了内置指针,并且打印了删除信息。这样当psstr4执行析构时,会打印”self delete”。

1.2.3 reset()

  • reset()不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针s不是唯一指向该对象的指针,则引用计数减一,同时将s置为空。
  • reset()带参数时,若智能指针s是唯一指向该对象的指针,则释放并指向新的对象。若智能指针s不是唯一指向该对象的指针,则引用计数减一,并指向新的对象。
p.reset() ; //将p重置为空指针,所管理对象引用计数 减1
p.reset(p1); //将p重置为p1(的值),p管控的对象计数减1,p接管对p1指针的管控
p.reset(p1,d); //将p重置为p1(的值),p 管控的对象计数减1并使用d作为删除器

reset()的功能是为shared_ptr重新开辟一块新的内存,让shared_ptr绑定这块内存

shared_ptr<int> p(new int(5));
// p重新绑定新的内置指针
p.reset(new int(6));

上述代码为p重新绑定了新的内存空间。

reset常用的情况是判断智能指针是否独占内存,如果引用计数为1,也就是自己独占内存就去修改,否则就为智能指针绑定一块新的内存进行修改,防止多个智能指针共享一块内存,一个智能指针修改内存导致其他智能指针受影响。

//如果引用计数为1,unique返回true
if (!p.unique())
{//还有其他人引用,所以我们为p指向新的内存p.reset(new int(6));
}
// p目前是唯一用户
*p = 1024;

使用智能指针的另一个好处就是,当程序崩溃时,智能指针也能保证内存空间被回收

void execption_shared()
{shared_ptr<string> p(new string("hello zack"));//此处导致异常int m = 5 / 0;//即使崩溃也会保证p被回收
}

即使运行到 m = 5 / 0处,程序崩溃,智能指针p也会被回收。有时候我们传递个智能指针的指针不是new分配的,那就需要我们自己给他传递一个删除器:

void delfuncint(int *p)
{cout << *p << " in del func" << endl;
}void delfunc_shared()
{int p = 6;shared_ptr<int> psh(&p, delfuncint);
}

如果不传递delfuncint,会造成p被智能指针delete,因为p是栈空间的变量,用delete会导致崩溃。

1.2.4 通过智能指针共享数据

我们定义一个 StrBlob 类,该类通过 shared_ptr 实现智能指针管理,用于共享一个 vector<string> 类型的容器。

class StrBlob
{
public://定义类型,用于表示 StrBlob 中存储的元素的数量typedef std::vector<string>::size_type size_type;StrBlob();//通过初始化列表构造StrBlob(const initializer_list<string> &li);//返回vector大小size_type size() const { return data->size(); }//判断vector是否为空bool empty(){return data->empty();}//向vector写入元素void push_back(const string &s){data->push_back(s);}//从vector弹出元素void pop_back();//访问头元素std::string &front();//访问尾元素std::string &back();private:shared_ptr<vector<string>> data;//检测i是否越界void check(size_type i, const string &msg) const;// 判断容器是否无元素的标志string badvalue;
};

该类只实现了默认构造函数和一个带有初始化列表参数的构造函数,后者允许我们通过初始化列表(例如:{"one", "two", "three"})来初始化 StrBlob 对象。但因为StrBlob未重载赋值运算符,也没有实现拷贝构造函数,所以StrBlob对象之间的赋值是浅拷贝(浅拷贝只赋值对象本身的值或引用,并不会复制对象所引用的内存或对象,也就是浅拷贝只创建了一个新的对象,并将原对象的指针或引用直接复制到新对象中,而没有复制指针所指向的数据),因而内部成员data会随着StrBlob对象的赋值修改引用计数(浅拷贝)。

当然我们也可以实现拷贝构造函数和赋值运算符,但只能用浅拷贝的方式实现,不能实现深拷贝(即拷贝指针指向的对象,而不是拷贝指针本身,这样即使其他指针消失,但仍然不影响该指针指向这块内存)

// 默认构造函数
StrBlob::StrBlob()
{data = make_shared<vector<string>>();
}
// 复制构造函数// 列表初始化
StrBlob::StrBlob(const StrBlob &other)
{data = sb.data;
}
// 列表初始化
StrBlob::StrBlob(const initializer_list<string> &li)
{data = make_shared<vector<string>>(li);
}
// 赋值运算符
StrBlob& operator=(const StrBlob &other)
{if (this != &other) {data = other.data;}return *this;
}

注意,将data = other.data修改为data(std::make_shared<std::vector<std::string>>(*other.data))data = std::make_shared<std::vector<std::string>>(*other.data)即可更正为深拷贝。

浅拷贝:只复制对象的值或引用,多个对象共享同一个动态分配的内存空间,修改其中一个对象的数据会影响到另一个对象。

深拷贝:复制对象本身的值,并且对对象引用的内存进行递归复制,确保每个对象拥有独立的内存,修改其中一个对象的数据不会影响另一个对象。

在这里插入图片描述

图片来源:C++ primer plus:深拷贝

实现检查越界的函数:

//检测i是否越界
void StrBlob::check(size_type i, const string &msg) const
{if (i >= data->size()){throw out_of_range(msg);}
}

实现front,访问首元素:

string& StrBlob::front()
{//不要返回局部变量的引用if (data->size() <= 0){return badvalue;}return data->front();
}

如果队列为空,返回一个空的字符串。但是如果我们直接构造一个空字符串返回,这样就返回了局部变量的引用,局部变量会随着函数结束而释放,造成安全隐患。所以我们可以返回类的成员变量badvalue,作为队列为空的标记。当然如果不能容忍队列为空的情况,可以通过抛出异常来处理,那我们用这种方式改写front:

string &StrBlob::front()
{check(0, "front on empty StrBlob");return data->front();
}

同样实现back()和pop_back():

string &StrBlob::back()
{check(0, "back on empty StrBlog");return data->back();
}
void StrBlob::pop_back()
{check(0, "back on pop_back StrBlog");data->pop_back();
}

这样我们通过定义StrBlob类,达到共享vector的方式。多个StrBlob操作的是一个vector向量。

我们可以通过use_count函数获得当前托管指针的引用计数:

void StrBlob::printCount()
{cout << "shared_ptr use count is " << data.use_count() << endl;
}

1.2.5 shared_ptr是线程安全的吗?

现在我们可以很简单的回答这个问题:并不是

引用计数是线程安全的!!!

shared_ptr仅有引用计数是线程安全的,因为在shared_ptr的控制块中,引用计数变量使用类似于 std::atomic::fetch_add 的操作并结合 std::memory_order_relaxed 进行递增(递减操作则需要更强的内存排序,以确保控制块能够安全销毁)。其关键在于使用了原子操作对引用计数进行增加或减少,所以是线程安全的。 而且,因为引用计数是线程安全的,多个线程可以安全地操作引用计数和访问管理对象,即使这些 shared_ptr 实例是同一对象的副本且共享所有权也是如此,所以管理共享资源的生命周期是线程安全的,不用担心因为多线程操作导致资源提早释放或延迟释放。

shared_ptr本身并不是线程安全的!!!

但是**shared_ptr本身并不是线程安全的**,shared_ptr 对象实例包含一个指向控制块的指针和一个指向底层元素的指针。这两个指针的操作在多个线程中并没有同步机制。因此,如果多个线程同时访问同一个 shared_ptr 对象实例并调用非 const 成员函数(如 resetoperator=),这些操作会导致对这些指针的并发修改,进而引发数据竞争(就像我们在并发编程(10)中说的一样,独立的原子操作当然是线程安全的,但是如果原子操作依赖于非原子操作,那么这个过程可能就是非线程安全的)。举例:

情况一:当多线程操作同一个shared_ptr对象时

// 按指针传入
void fn(shared_ptr<A>* sp) {...
}
// 按引用传入
void fn(shared_ptr<A>& sp) {...if (..) {sp = other_sp;} else if (...) {sp = other_sp2;}
}std::thread t1(fn, std::ref(sp1));
std::thread t2(fn, std::ref(sp1));

如果我们将shared_ptr对象的指针或引用传入给可调用对象,当创建不同线程对shared_ptr进行修改时,比如修改其指向(如 resetoperator=)。sp原先指向的引用计数的值要减去1,other_sp指向的引用计数值要加1。然而这几步操作加起来并不是一个原子操作(并发编程(10)在原子操作中说过,并不是所有原子操作都是线程安全的,如果原子操作依赖于非原子操作,那么这个过程可能就是非线程安全的,这里的条件判断并不是原子操作),如果多少线程都在修改sp的指向的时候,那么可能会出问题。比如在导致计数在操作减一的时候,其内部的指向,已经被其他线程修改过了。引用计数的异常会导致某个管理的对象被提前析构,后续在使用到该数据的时候触发core dump

如果不调用shared_ptr非const成员函数修改shared_ptr,那么就是线程安全的。

情况二:当多线程操作不同shared_ptr对象时

如果不是同一 shared_ptr 对象(管理的数据是同一份,引用计数共享,但shared_ptr不是同一个对象),每个线程读写的指针也不是同一个,引用计数又是线程安全的,那么自然不存在数据竞争,可以安全的调用所有成员函数。

// 按值传递
void fn(shared_ptr<A> sp) {...if (..) {sp = other_sp;} else if (...) {sp = other_sp2;}
}
std::thread t1(fn, std::ref(sp1));
std::thread t2(fn, std::ref(sp1));

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的,也就是线程安全的。

shared_ptr所管理的对象不是线程安全的!!!

尽管前面我们提到了如果是按值捕获(或传参)的shared_ptr对象,那么是该对象是线程安全的。然而话虽如此,但却可能让人误入歧途。因为我们使用shared_ptr更多的是操作其中的数据,对齐管理的数据进行读写,而不是修改shared_ptr的指向。尽管在按值捕获的时候shared_ptr本身是线程安全的,我们不需要对此施加额外的同步操作(比如加解锁、条件变量、call_once 和once_flag ),但是这并不意味着shared_ptr所管理的对象是线程安全的!

shared_ptr本身和shared_ptr管理的对象是两个东西,并不是同一个!!!如果我们要多线程处理shared_ptr所管理的资源,我们需要主动的对其施加额外的同步操作(比如加解锁、条件变量、call_once 和once_flag)

如果shared_ptr管理的数据是STL容器,那么多线程如果存在同时修改的情况,是极有可能触发core dump的。比如多个线程中对同一个vector进行push_back,或者对同一个map进行了insert。甚至是对STL容器中并发的做clear操作,都有可能出发core dump,当然这里的线程不安全性,其实是其所指向数据的类型的线程不安全导致的,并非是shared_ptr本身的线程安全性导致的。尽管如此,由于shared_ptr使用上的特殊性,所以我们有时也要将其纳入到shared_ptr相关的线程安全问题的讨论范围内。

除了STL容器的并发修改操作(这里指的是修改容器的结构,并不是修改容器中某个元素的值,后者是线程安全的,前者不是),protobuf的Message对象也是不能并发操作的,比如一个线程中修改Message对象(set、add、clear),另外一个线程也在修改,或者在将其序列化成字符串都会触发core dump。

STL容器如何解决线程安全可以参考这篇文章:C++ STL容器如何解决线程安全的问题? - 知乎

最后,有很多人可能认为引用计数是通过智能指针的静态成员变量所管理的,但这很明显是错的:

shared_ptr<A> sp1 = make_shared<A>(x);
shared_ptr<A> sp2 = make_shared<A>(y);

两个完全不相干的sp1和sp2,只要模板参数T是同一个类型,即使管理的资源不是同一个,但如果使用静态成员变量管理引用计数,那么二者就会共享同一个计数。

2. weak_ptr

我们在shared_ptr说到了,new和delete可能会引发内存泄漏问题(作用域内new的对象在作用域结束后仍未delete,此时没有任何对象指向这片内存),但是shared_ptr本身也可能会引发内存泄漏问题,即循环引用问题

2.1 循环引用问题

shared_ptr 循环引用问题是指**两个或多个对象之间通过shared_ptr相互引用,导致对象无法被正确释放,从而造成内存泄漏。**常见的情况是两个对象A和B,它们的成员变量互相持有了对方的shared_ptr。当A和B都不再被使用时,它们的引用计数不会降为0,无法被自动释放。比如:

class Girl;class Boy {
public:Boy() {cout << "Boy 构造函数" << endl;}~Boy() {cout << "~Boy 析构函数" << endl;}void setGirlFriend(shared_ptr<Girl> _girlFriend) {this->girlFriend = _girlFriend;}
private:shared_ptr<Girl> girlFriend;
};class Girl {
public:Girl() {cout << "Girl 构造函数" << endl;}~Girl() {cout << "~Girl 析构函数" << endl;}void setBoyFriend(shared_ptr<Boy> _boyFriend) {this->boyFriend = _boyFriend;}
private:shared_ptr<Boy> boyFriend;
};void useTrap() {shared_ptr<Boy> spBoy(new Boy());shared_ptr<Girl> spGirl(new Girl());// 陷阱用法spBoy->setGirlFriend(spGirl);spGirl->setBoyFriend(spBoy);// 此时boy和girl的引用计数都是2cout << "r_count of spBoy is : " << spBoy.use_count() << endl;cout << "r_count of spGirl is : " << spGirl.use_count() << endl;
}int main(void) {useTrap();system("pause");return 0;
}

我们通过useTrap()函数创建了两个shared_ptr对象,其中spBoy用于存储一个Boy类,spGirl用于存储一个Girl类;但是,Boy类中有一个智能指针变量用于存储Girl类,而Girl类中有一个智能指针变量用于存储Boy类。如果给智能指针spBoy和spGirl管理对象的成员变量赋值,那么会造成循环引用问题。此时,创建的智能指针无法被销毁,因为引用计数总不为0。

代码输出为:

Boy 构造函数
Girl 构造函数
r_count of spBoy is : 2
r_count of spGirl is : 2

确实,因为二者的引用计数总为2或1,这两个类不能被正确析构。

有没有方法解决这个问题呢?这时候我们就用到了智能指针**weak_ptr**。


weak_ptr是一种弱引用,不会增加对象的引用计数,在对象释放时会自动设置为nullptr。它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少(当然,weak_ptr其实不需要析构函数,因为它不需要管理和释放资源,即没有RAII)。 同时weak_ptr 没有重载operator*operator->(不支持访问资源),但可以使用 weak_ptr.lock() 获得一个可用的 shared_ptr 对象,当对象已经释放时会返回一个空shared_ptr

那么,上面的代码可以修改为:

class Girl;class Boy {
public:Boy() {cout << "Boy 构造函数" << endl;}~Boy() {cout << "~Boy 析构函数" << endl;}void setGirlFriend(shared_ptr<Girl> _girlFriend) {this->girlFriend = _girlFriend;// 在必要的使用可以转换成共享指针shared_ptr<Girl> sp_girl;sp_girl = this->girlFriend.lock();cout << "r_count of spGirl is : " << sp_girl.use_count() << endl;// 使用完之后,再将共享指针置NULL即可sp_girl = NULL;}
private:weak_ptr<Girl> girlFriend;
};class Girl {
public:Girl() {cout << "Girl 构造函数" << endl;}~Girl() {cout << "~Girl 析构函数" << endl;}void setBoyFriend(shared_ptr<Boy> _boyFriend) {this->boyFriend = _boyFriend;}private:shared_ptr<Boy> boyFriend;
};void useTrap() {shared_ptr<Boy> spBoy(new Boy());shared_ptr<Girl> spGirl(new Girl());spBoy->setGirlFriend(spGirl);cout << "r_count of spGirl is : " << spGirl.use_count() << endl;spGirl->setBoyFriend(spBoy);cout << "r_count of spBoy is : " << spBoy.use_count() << endl;cout << "r_count of spGirl is : " << spGirl.use_count() << endl;
}
int main(void) {useTrap();system("pause");return 0;
}

我们将Boy类的私有成员变量类型由shared_ptr更换为了weak_ptr,此时,对该变量赋值不会造成引用计数的增加,自然就解决了循环引用问题。

代码输出为:

Boy 构造函数
Girl 构造函数
r_count of spGirl is : 3
r_count of spGirl is : 1
r_count of spBoy is : 2
r_count of spGirl is : 1
~Girl 析构函数
~Boy 析构函数

spGirl的引用计数之所以为3是因为,在调用setGirlFriend函数时,按值传入一个spGirl对象,在函数内部,拥有一个spGirl副本,此时引用计数为2;当创建了一个shared_ptr<Girl>类型的对象sp_girl,并调用weak_ptr<Girl>的lock函数获取shared_ptr<Girl>赋予给sp_girl时,引用计数变为了3。但它们都是局部变量,所以当函数作用域结束后,都会被自动释放,所以最后引用计数变味了1。

spBoy内部的成员变量类型是shared_ptr而不是weak_ptr,所以引用计数为2,但是,当spGirl被释放后,spBoy的引用计数为1,此时spBoy也可以正常释放。

2.2 其他成员函数

2.2.1 use_count()

weak_ptr同样可以通过调用use_count()方法获取当前观察资源的引用计数

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp1(sp);
// 或者
weak_ptr<int> wp2;
wp2 = sp;cout << wp1.use_count() << endl; //结果为输出1
cout << wp2.use_count() << endl; //结果为输出1

2.2.2 expired()

通过expired()成员函数去检查指向的资源是否过期:判断当前weak_ptr智能指针是否还有托管的对象,有则返回false,无则返回true。

如果返回true,等价于 use_count() == 0,即已经没有托管的对象了;当然,可能还有析构函数进行释放内存,但此对象的析构已经临近(或可能已发生)。

std::weak_ptr<int> gw;void f() {// expired:判断当前智能指针是否还有托管的对象,有则返回false,无则返回trueif (!gw.expired()) {std::cout << "gw is valid\n";	// 有效的,还有托管的指针} else {std::cout << "gw is expired\n";	// 过期的,没有托管的指针}
}int main() {{auto sp = std::make_shared<int>(42);gw = sp;f();}// 当{ }体中的指针生命周期结束后,再来判断其是否还有托管的指针f();return 0;
}

代码输出:

gw is valid
gw is expired

在 { } 中,sp的生命周期还在,gw还在托管着make_shared赋值的指针(sp),所以调用f()函数时打印"gw is valid\n";
当执行完 { } 后,sp的生命周期已经结束,已经调用析构函数释放make_shared指针内存(sp),gw已经没有在托管任何指针了,调用expired()函数返回true,所以打印"gw is expired\n";

2.2.3 lock()

可以通过调用lock()成员函数,获取监视的shared_ptr:

  • 使用lock将资源锁住,lock会将weap_ptr转为shared_ptr,即使weap_ptr指向的shared_ptr资源被释放也不影响使用,当对象已经释放时会返回一个空shared_ptr
  • 如果要访问weap_ptr指向的数据,必须使用lock将weap_ptr转为shared_ptr才能访问到。
  • 在多线程中,要防止一个线程在使用智能指针,而另一个线程删除指针指针问题,可以使用weak_ptr的lock()方法。
auto sp = std::make_shared<int>(42); // 创建一个共享指针;
std::weak_ptr<int> wp(sp);shared_ptr<int> p;
if (!wp.expired())
{p = wp.lock();  // 如果要取到weak_ptr中的,需要先使用lock将weak_ptr转为shard_ptr才能取值;sp.reset();cout << *p << endl;              // 42cout << p.use_count() << endl;   // 1cout << sp.use_count() << endl;  // 0
}

3. enable_from_this_shared

在一个类的成员函数中,我们不能直接将this指针作为shared_ptr返回,而需要通过派生std::enable_shared_from_this类,通过其方法shared_from_this来返回指针。原因std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用shared_from_this()方法其实是调用内部这个weak_ptrlock()方法,将所观察的shared_ptr返回。

class MyClass : public std::enable_shared_from_this<MyClass>
{
public:shared_ptr<MyClass> GetSelf() {//return shared_ptr<MyClass>(this);  直接返回this的共享智能指针,如果直接返回MyClass会被析构两次return shared_from_this();}MyClass() {cout << "MyClass()" << endl;};~MyClass() {cout << "~MyClass()" << endl;};
};int main()
{shared_ptr<MyClass> sp1(new MyClass);cout << sp1.use_count()<<endl;shared_ptr<MyClass> sp2 = sp1->GetSelf();cout << sp1.use_count()<<endl;cout << sp2.use_count()<<endl;
}
  • 在外面创建MyClass对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()std::enable_shared_from_this<MyClass>内部的weak_ptr调用lock()方法之后返回的智能指针。在离开作用域之后,sp1和sp2会自动析构,其引用计数减为0,MyClass对象会被析构,不会出现MyClass对象被析构两次的问题。
  • 需要注意的是,获取自身智能指针的函数仅在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

上述代码的输出为:

MyClass()
1
2
2
~MyClass()

很明显,sp1和sp2共用同一个引用计数,共享资源,但确实两个shared_ptr对象(可以修改指向而不影响其他shared_ptr对象)。

但注意,你不能直接将this指针作为shared_ptr返回回来!!!

我在前面说过,不能将智能指针通过get()函数返回的裸指针用于初始化或reset另一个指针。通过这种方法初始化的智能指针,其实和原本在类内部构造的智能指针是两个独立的对象,它们不共享引用计数,仅仅只是管理的资源相同。如果多次析构,会造成同一个资源被重复析构两次的问题。

所以,不要将this指针作为shared_ptr返回回来,因为this指针本质上是一个裸指针,因此,可能会导致重复析构

class MyClass
{
public:shared_ptr<MyClass> GetSelf() {return shared_ptr<MyClass>(this);//不要这样做}MyClass() {cout << "MyClass()" << endl;};~MyClass() {cout << "~MyClass()" << endl;};
};int main()
{// sp1与sp2都会调用new MyClass的析构函数,一个对象析构两次shared_ptr<MyClass> sp1(new MyClass);shared_ptr<MyClass> sp2 = sp1->GetSelf();return 0;
}

在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

4. unqiue_ptr

unique_ptrshared_ptr不同,unique_ptr不允许所指向的内容被其他指针共享,所以unique_ptr不允许拷贝构造和赋值。

void use_uniqueptr()
{//指向double类型的unique指针unique_ptr<double> udptr;//一个指向int类型的unique指针unique_ptr<int> uiptr(new int(42));// unique不支持copy// unique_ptr<int> uiptr2(uiptr);// unique不支持赋值// unique_ptr<int> uiptr3 = uiptr;
}

尽管unqiue_ptr不能拷贝或赋值,但可以通过调用release()reset()移动语义将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

a. release()

  • release() 会释放 unique_ptr 的所有权,返回原始指针,同时将当前 unique_ptr 置为空。
  • 释放后,需要手动管理返回的裸指针(可以将其转移到另一个 unique_ptr 中)。
  • 使用 release() 后,原来的 unique_ptr 不再管理资源,必须确保资源由新的管理者接管,否则会导致内存泄漏。

b. reset()

  • reset() 会释放当前 unique_ptr 所管理的资源,并接管一个新的指针。
  • 可以通过 reset() 将一个裸指针直接交给新的 unique_ptr
  • 调用 reset() 后,原来的 unique_ptr 被释放,接管新资源。

c. std::move

  • std::unique_ptr 支持移动构造移动赋值,可以安全地将所有权从一个 unique_ptr 转移到另一个。
  • 转移后,原来的 unique_ptr 不再拥有资源(变为 nullptr)。
void use_uniqueptr()
{//定义一个upstrunique_ptr<string> upstr(new string("hello zack"));std::cout << "upstr: " << *upstr << "\n";// upstr.release()返回其内置指针,并将upstr置空// 用upstr返回的内置指针初始化了upstr2unique_ptr<string> upstr2(upstr.release());std::cout << "upstr2: " << *upstr2 << "\n";unique_ptr<string> upstr3(new string("hello world"));std::cout << "upstr3: " << *upstr3 << "\n";//将upstr3的内置指针转移给upstr2// upstr2放弃原来的内置指针,指向upstr3返回的内置指针。upstr2.reset(upstr3.release());std::cout << "upstr2: " << *upstr2 << "\n";// 通过移动语义将upstr2的所有权转移给upstrupstr = std::move(upstr2);std::cout << "upstr: " << *upstr << "\n";// 通过移动语义将upstr的所有权转移给upstr4unique_ptr<string> upstr4(std::move(upstr));std::cout << "upstr4: " << *upstr4 << "\n";
}

输出为:

upstr: hello zack
upstr2: hello zack
upstr3: hello world
upstr2: hello world
upstr: hello world
upstr4: hello world

不能拷贝unique_ptr的规则有一个例外:我们可以“拷贝“或”赋值”一个将要被销毁的(非const)unique_ptr。最常见的例子是从函数返回一个unique_ptr:

std::unique_ptr<int> createUniquePtr() {auto ptr = std::make_unique<int>(42); // 创建局部 unique_ptrreturn ptr; // 返回时自动调用移动构造
}int main() {std::unique_ptr<int> myPtr = createUniquePtr(); // 接收函数返回值std::cout << "Value: " << *myPtr << std::endl;return 0;
}

虽然这个过程好像确实是在调用拷贝构造函数创建了一个副本,但std::unique_ptr 的拷贝构造和拷贝赋值是被明确删除的(= delete),无法直接复制。

因为std::unique_ptr支持移动构造和移动赋值,所以当函数返回一个局部的 std::unique_ptr 时,C++ 会隐式应用移动构造,并不会真的尝试拷贝它。

我在文章中简单说过,编译器会根据传入参数自动选择构造函数:

  • 如果对象的拷贝构造函数可用,但移动构造函数也存在,push_back 会选择合适的构造函数:
    • 如果传递的是一个左值,会调用拷贝构造函数。
    • 如果传递的是一个右值,会调用移动构造函数。
  • 如果对象的拷贝构造函数不可用,但移动构造函数存在,push_back 的选择:
    • 如果传递的是一个左值,会直接报错,编译器无法将左值隐式转换为右值从而调用移动。
    • 如果传递的是一个右值,会调用移动构造函数。

在这里,函数返回一个临时的unique_ptr变量,是右值。而unique_ptr禁止拷贝或复制,但允许移动拷贝或移动赋值,所以编译器会自动调用unique_ptr的移动构造函数。因此,从函数返回时并不会违反 std::unique_ptr 的独占所有权规则。

其他内容可参考我的个人博客:
爱吃土豆的个人博客

相关文章:

C++——智能指针剖析

参考&#xff1a; 恋恋风辰官方博客 动态内存管理 - cppreference.com SRombauts/shared_ptr&#xff1a; 一个最小的 shared/unique_ptr 实现&#xff0c;用于处理 boost/std&#xff1a;&#xff1a;shared/unique_ptr 不可用的情况。 C智能指针_c 智能指针-CSDN博客 当…...

241119.LeetCode——383.赎金信

题目描述 给你两个字符串&#xff1a;ransomNote 和 magazine &#xff0c;判断 ransomNote 能不能由 magazine 里面的字符构成。 如果可以&#xff0c;返回 true &#xff1b;否则返回 false 。 magazine 中的每个字符只能在 ransomNote 中使用一次。 示例 1&#xff1a; 输…...

基于SSM的农家乐管理系统+论文示例参考

1.项目介绍 功能模块&#xff1a;管理员&#xff08;农家乐管理、美食信息管理、住宿信息管理、活动信息、用户管理、活动报名、论坛等&#xff09;&#xff0c;普通用户&#xff08;注册登录、活动报名、客房预订、用户评价、收藏管理、模拟支付等&#xff09;技术选型&#…...

用 Python 从零开始创建神经网络(九):反向传播(Backpropagation)(还在更新中。。。)

反向传播&#xff08;Backpropagation&#xff09; 引言1. 分类交叉熵损失导数&#xff08;Categorical Cross-Entropy loss derivative&#xff09;2. 分类交叉熵损失衍生代码实现3. Softmax激活导数&#xff08;Softmax activation derivative&#xff09;4. Softmax激活函数…...

Flink是如何实现 End-To-End Exactly-once的?

flink 如何实现端到端的 Exactly-once? 端到端包含 Source, Transformation,Sink 三部分的Exactly-once Source&#xff1a;支持数据的replay&#xff0c;如Kafka的offset。Transformation&#xff1a;借助于checkpointSink&#xff1a;Checkpoint 两阶段事务提交 两阶段提…...

【vulhub】nginx解析漏洞(nginx_parsing_vulnerability)

1. nginx解析漏洞原理 fastcgi 在处理’.php’文件时发现文件并不存在,这时 php.ini 配置文件中cgi.fix_pathinfo1 发挥作用,这项配置用于修复路径,如果当前路径不存在则采用上层路径 (1)由于 nginx.conf的配置导致 nginx把以’.php”结尾的文件交给 fastcgi 处理,为此可以构造…...

网络协议之邮件协议(SMTP、POP3与IMAP)

一、引言 在数字化时代&#xff0c;电子邮件已成为人们日常沟通和信息交流的重要工具。电子邮件系统的稳定运行离不开一系列网络协议的支撑&#xff0c;其中SMTP、POP3和IMAP是最为关键的三个协议。它们分别负责邮件的发送、接收和管理&#xff0c;共同构建了一个高效、稳定的…...

python学习笔记(3)运算符

Python 语言支持的运算符: Python 语言支持以下类型的运算符: 算术运算符 比较&#xff08;关系&#xff09;运算符 赋值运算符 逻辑运算符 位运算符 成员运算符 身份运算符 运算符优先级 接下来让我们一个个来学习Python的运算符。 Python算术运算符 运算符描述实例加 - 两…...

_FYAW智能显示控制仪表的简单使用_串口通信

一、简介 该仪表可以实时显示位移传感器的测量值&#xff0c;并可设定阈值等。先谈谈简单的使用方法&#xff0c;通过说明书&#xff0c;我们可以知道长按SET键可以进入参数选择状态&#xff0c;按“↑”“↓”可以选择该组参数的上一个或者下一个参数。 从参数一览中可以看到有…...

激光雷达定位初始化的另外一个方案 通过键盘按键移动当前位姿 (附python代码)

通常使用的是通过在 rviz 中点选指定初始化位置和方向来完成点云的初始化匹配。 但是这种粗略的初始化方法有时候可能不成功,因此需要使用准确的初始化方法,以更好的初始值进行无损检测配准。 为了提供更好的匹配初始值,我使用 Python 脚本获取键盘输入,并不断调整这个匹配…...

从0-1逐步搭建一个前端脚手架工具并发布到npm

前言 vue-cli 和 create-react-app 等 cli 脚手架工具用于快速搭建应用&#xff0c;无需手动配置复杂的构建环境。本文介绍如何使用 rollup 搭建一个脚手架工具。 脚手架工具的工作流程简言为&#xff1a;提供远端仓库各种模版 > 用户通过命令选择模版 > 拉取仓库代码 …...

河道水位流量一体化自动监测系统:航运安全的护航使者

在广袤的水域世界中&#xff0c;航运安全始终是至关重要的课题。而河道水位流量一体化自动监测系统的出现&#xff0c;如同一位强大的护航使者&#xff0c;为航运事业的稳定发展提供了坚实的保障。 水位传感器&#xff1a;负责实时监测河道的水位变化。这些传感器通常采用先进的…...

维护在线重做日志

学习目标 解释在线重做日志文件的目的概述在线重做日志文件的结构控制日志开关和检查点多路复用和维护在线重做日志文件使用OMF管理在线重做日志文件获取在线重做日志文件信息 在线重做日志文件提供了在数据库发生故障时重做事务的方法。 每个事务都同步写入重做日志缓冲区&a…...

ASCB1系列APP操控末端回路智能微断 物联网断路器 远程控制开关 学校、工厂、农场、商业大楼等可用

安科瑞戴婷 Acrel-Fanny ASCB1系列智能微型断路器是安科瑞电气股份有限公司全新推出的智慧用电产品&#xff0c;产品由智能微型断路器与智能网关两部分组成&#xff0c;可用于对用电线路的关键电气因素&#xff0c;如电压、电流、功率、温度、漏电、能耗等进行实时监测&#x…...

Python入门(10)--面向对象进阶

Python面向对象进阶 &#x1f680; 1. 继承与多态 &#x1f504; 1.1 继承基础 class Animal:def __init__(self, name, age):self.name nameself.age agedef speak(self):passdef describe(self):return f"{self.name} is {self.age} years old"class Dog(Anim…...

Makefile 之 自动化变量

作用范围只在这条规则以及连带规则中&#xff0c;所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。 "$" 表示目标的集合&#xff0c;就像一个数组&#xff0c;"$"依次取出目标&#xff0c;并执于命令。 "$<"和"$&qu…...

鸿蒙开发:ForEach中为什么键值生成函数很重要

前言 在列表组件使用的时候&#xff0c;如List、Grid、WaterFlow等&#xff0c;循环渲染时都会使用到ForEach或者LazyForEach&#xff0c;当然了&#xff0c;也有单独使用的场景&#xff0c;如下&#xff0c;一个很简单的列表组件使用&#xff0c;这种使用方式&#xff0c;在官…...

沃丰科技智能外呼机器人:超越人工,重塑外呼体验

随着科技的不断发展&#xff0c;人工智能已经逐渐渗透到各行各业&#xff0c;其中智能外呼机器人的出现&#xff0c;更是给企业带来了全新的客户体验。与传统的人工外呼相比&#xff0c;智能外呼机器人具有更高的效率、更低的成本以及更好的用户体验等优势。 优势一&#xff1…...

百度飞浆:paddle 线性回归模型

学习引用 参考视频&#xff1a; https://www.bilibili.com/video/BV1oRtkeVEVx?spm_id_from333.788.player.switch&vd_sourcec7739de98d044e74cdc74d6e772bed5f&p2 这段代码使用PaddlePaddle深度学习框架来实现一个简单的线性回归模型&#xff0c;旨在从给定的出租车…...

【JavaSE】【网络编程】UDP数据报套接字编程

目录 一、网络编程简介二、Socket套接字三、TCP/UDP简介3.1 有连接 vs 无连接3.2 可靠传输 vs 不可靠传输3.3 面向字节流 vs 面向数据报3.4 双向工 vs 单行工 四、UDP数据报套接字编程4.1 API介绍4.1.1 DatagramSocket类4.1.1.1 构造方法4.1.1.2 主要方法 4.1.2 DatagramPocket…...

45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题

点赞收藏加关注&#xff0c;你也能主打别墅&#xff01; 一、问题描述 Mac上终端运行如下命令&#xff1a; sudo npm install typescript -g //全局安装ts提示成功安装后&#xff0c;我测试tsc -v这个命令时出现如下错误&#xff1a; 也就是说找不到 tsc 命令。 二、解决方…...

20241120-Milvus向量数据库快速体验

目录 20241120-Milvus向量数据库快速体验Milvus 向量数据库pymilvus内嵌向量数据库模式设置向量数据库创建 Collections准备数据用向量表示文本插入数据 语义搜索向量搜索带元数据过滤的向量搜索查询通过主键搜索 删除实体加载现有数据删除 Collections了解更多 个人主页: 【⭐…...

【Golang】——Gin 框架中间件详解:从基础到实战

中间件是 Web 应用开发中常见的功能模块&#xff0c;Gin 框架支持自定义和使用内置的中间件&#xff0c;让你在请求到达路由处理函数前进行一系列预处理操作。这篇博客将涵盖中间件的概念、内置中间件的用法、如何编写自定义中间件&#xff0c;以及在实际应用中的一些最佳实践。…...

量子计算来袭:如何保护未来的数字世界

目录 前言 一、量子计算安全的学习方向 1. 量子物理学基础 2. 量子计算原理与技术 3. 传统网络安全知识 4. 量子密码学 5. 量子计算安全政策与法规 二、量子计算的漏洞风险 1. 加密算法被破解风险 2. 区块链安全风险 3. 量子密钥分发风险 4. 量子计算系统自身风险 …...

VMware虚拟机(Ubuntu或centOS)共享宿主机网络资源

VMware虚拟机(Ubuntu或centOS)共享宿主机网络资源 由于需要在 Linux 环境下进行一些测试工作&#xff0c;于是决定使用 VMware 虚拟化软件来安装 Ubuntu 24.04 .1操作系统。考虑到测试过程中需要访问 Github &#xff0c;要使用Docker拉去镜像等外部网络资源&#xff0c;因此产…...

光伏电站仿真系统的作用

光伏仿真系统有多方面的重要作用&#xff0c;不仅对前期的项目设计评估还是后期的运维效验都有非常重要的作用。 1、优化系统设计 通过输入不同的光伏组件参数、布局方案以及气象条件等&#xff0c;模拟各种设计场景下光伏电站的性能表现。例如&#xff0c;可以比较不同类型光…...

Golang文件操作

写文件 &emsp&#xff1b; os模块可以创建文件&#xff0c;使用fmt可以写入文件。如以下例子&#xff1a; package mainimport ("fmt""os" )func main() {// 学习 golang的文件操作file, err : os.Create("test.txt")if err ! nil {fmt.P…...

爬虫开发工具与环境搭建——使用Postman和浏览器开发者工具

第三节&#xff1a;使用Postman和浏览器开发者工具 在网络爬虫开发过程中&#xff0c;我们经常需要对HTTP请求进行测试、分析和调试。Postman和浏览器开发者工具&#xff08;特别是Network面板和Console面板&#xff09;是两种最常用的工具&#xff0c;能够帮助开发者有效地捕…...

React(二)

文章目录 项目地址七、数据流7.1 子组件传递数据给父组件7.1.1 方式一:給父设置回调函数,传递给子7.1.2 方式二:直接将父的setState传递给子7.2 给props传递jsx7.2.1 方式一:直接传递组件给子类7.2.2 方式二:传递函数给子组件7.3 props类型验证7.4 props的多层传递7.5 cla…...

同步原语(Synchronization Primitives)

同步原语&#xff08;Synchronization Primitives&#xff09;是用于控制并发编程中多个线程或进程之间的访问顺序&#xff0c;确保共享资源的安全访问的一组机制或工具。它们解决了竞争条件&#xff08;Race Condition&#xff09;、死锁&#xff08;Deadlock&#xff09;等并…...

君山区建设局网站/百度广告投放价格

配置阿里云ECS支持IPv6 前几天有个小伙伴的 iOS App 提交给水果审核没通过&#xff0c;水果给出的原因是应用在 IPv6 的环境下无法使用。原来小伙伴用的服务器是阿里云的 ECS &#xff0c;而这玩意并不支持 IPv6 。。。这里就不得不吐槽一下阿里云这么大的云服务厂商&#xff0…...

上海营销网站建设定制服务/热搜词排行榜关键词

C语言练习&#xff1a;第二大整数问题描述编写一个程序&#xff0c;读入一组整数(不超过20个)&#xff0c;当用户输入0时&#xff0c;表示输入结束。然后程序将从这组整数中&#xff0c;把第二大的那个整数找出来&#xff0c;并把它打印出来。说明&#xff1a;(1)0表示输入结束…...

淘宝官网首页入口手机/首页关键词优化价格

速度慢有很多种情况&#xff0c;比如 DNS解析CDN&#xff08;可能用了国外的cdn&#xff0c;也会导致很慢的&#xff09;服务器IO&#xff08;阿里云的io做的不是很好&#xff0c;频繁的操作io&#xff0c;可能很慢&#xff09;数据库当然跟访问用户的网络也有关系 可以仔细排…...

wordpress mu安装/站点

以下是Python中通过cx_Oracle操作数据库的过程中我所遇到的问题总结&#xff0c;感谢我们测试组的前辈朱勃给予的帮助最终解决了下列两个问题&#xff1a;1&#xff09;安装cx_Oracle会遇到的问题&#xff1a;在Windows下安装cx_Oracle不要尝试用pip install的方式了&#xff0…...

个人网站模块/千万不要去电商公司上班

据相关数据显示&#xff0c;从今年2季度开始&#xff0c;成都主城区公寓产品成交情况及销售均价都出现明显上涨&#xff0c;并且这种势头在第3季度得到持续&#xff0c;3季度公寓成交面积约51.51万㎡。其中&#xff0c;SOHO产品占42.35万㎡&#xff0c;LOFT产品占9.16万㎡&…...

长治网站建设/seo查询 站长之家

∣A∪B∣∣A∣∣B∣−∣A∩B∣|A∪B| |A||B| - |A∩B| ∣A∪B∣∣A∣∣B∣−∣A∩B∣ ∣A∪B∪C∣∣A∣∣B∣∣C∣−∣A∩B∣−∣B∩C∣−∣C∩A∣∣A∩B∩C∣证明&#xff1a;∣AUBUC∣∣AUB∣∣C∣−∣(AUB)∩C∣∣A∣∣B∣−∣A∩B∣∣C∣−∣(A∩C)U(B∩C)∣∣A∣∣B∣−∣…...