【C++】总结10--C++11第二篇

文章目录

  • RAII
  • C++11新特性

RAII

  • RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等)的简单技术,在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确获取和释放
  • 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这样做的两大好处:
    • 不需要显式的释放资源
    • 采用这种方式,保证了对象所需的资源在其生命周期内始终保持有效

C++11新特性

  • 部分新特性如列表初始化、变量类型推导(auto)、默认成员函数控制、右值引用等在这篇博文中详细介绍:【C++】C++11 第一篇_林深方见鹿的博客-CSDN博客

  • 范围for循环

    用于遍历容器中的元素或者数组中的元素,语法如下:

  for (auto element : container) {// 循环体,使用element访问容器中的元素}

范围for循环会自动遍历容器或数组中的每一个元素,并将当前元素的值赋值给element,然后执行循环体中的代码。循环会在容器或数组的每个元素上执行一次,直到遍历完所有元素为止。

范围for循环在遍历容器或数组时,避免了使用迭代器或下标的复杂语法,使得代码更加简洁和易读。它是C++11中非常实用和方便的特性之一。

  • final和override

  • 智能指针,C++库的智能指针都定义在memory头文件中

    • 智能指针原理:RAII特性;重载operator* 和 operator->,具有指针行为

    • unique_ptr:用unique_ptr替换已弃用的C++98的auto_ptr,auto_ptr的实现思想是管理权转移,auto_ptr弃用的原因是它非常容易造成访问空指针,引发程序崩溃,因为当对象拷贝或赋值后,前面的对象就悬空了,即原对象将失去资源的所有权,如果此时再意外访问原对象资源,就可能会导致意外的行为。例如,下面的代码可能会导致问题:

      std::auto_ptr<int> ptr1(new int(42));
      std::auto_ptr<int> ptr2 = ptr1; // ptr1将变为空指针
      std::cout << *ptr1 << std::endl; 
      // 未定义的行为,因为ptr1已经失去了对资源的所有权
      

      unique_ptr的实现思想:简单粗暴的防拷贝,使用独占所有权语义,确保每个指针只能管理一个对象,并在拷贝时禁止所有权转移,从而避免了auto_ptr所产生的问题,模拟一份简单的unique_ptr:

      template<class T>
      class UniquePtr {UniquePtr(T* ptr = nullptr) :_ptr(ptr){}~UniquePtr() {if (_ptr)delete _ptr;}
      private:UniquePtr(UniquePtr<T> const &) = delete;UniquePtr& operator=(UniquePtr<T> const &) = delete;
      private:T *_ptr;
      };
      
    • shared_ptr

      shared_ptr的原理是通过引用计数的方式来实现多个share_ptr对象之间共享资源

      shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享

      在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一

      如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

      简单模拟share_ptr,代码如下:

      template<class T>
      class SharePtr {
      public:SharePtr(T* ptr = nullptr):_ptr(ptr),_pRefCount(new int(1)),_pMutex(new mutex){}~SharePtr() {Release();}//拷贝构造SharePtr(const SharePtr<T>& sp) :_ptr(sp._ptr),_pRefCount(sp._pRefCount),_pMutex(sp._pMutex){AddRefCount();}//赋值语句SharePtr<T>& operator=(const SharePtr<T>& sp) {if (_ptr != sp._ptr) {//释放之前的管理资源Release();//共享管理新对象的资源,增加引用计数_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pMutex = sp._pMutex;AddRefCount();}return *this;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }int UseCount() { return *_pRefCount; }T* Get() { return _ptr; }void AddRefCount() {//加锁_pMutex->lock();++(*_pRefCount);_pMutex->unlock();}
      private:void Release() {bool deleteflag = false;//释放资源时引用计数减一,如果减到0,就释放资源_pMutex->lock();if (--(*_pRefCount) == 0) {delete _ptr;delete _pRefCount;deleteflag = true;}_pMutex->unlock();//如果deleteflag为true了,则意味着_ptr、_pRefCount已经被释放了//我们就需要进行_pMutex的释放if (deleteflag == true) {delete _pMutex;}}
      private:T* _ptr;//指向所管理资源的指针int* _pRefCount;//引用计数mutex* _pMutex;//互斥锁
      };
      

      shared_ptr的线程安全问题:

      shared_ptr的线程安全问题分为两方面,首先是引用计数是多个智能指针对象所共享的,两个线程中智能指针的引用计数同时加加或减减,这个操作不是原子的,可能会导致引用计数的错乱,最终导致资源未释放或程序崩溃的问题,所以智能指针中引用计数加加和减减是需要加锁的,也就是说引用计数的操作是线程安全的。

      另一方面是智能指针管理的对象存放在堆上,两个线程同时去访问,可能会导致线程安全问题

      shared_ptr的循环引用问题:

      智能指针指向的空间中也保存有智能指针,且两个智能指针指向的空间中保存的智能指针还指向对方的空间,这称为循环引用。

      为了解决这个问题。C++中增加了weak_ptr。可以使用 weak_ptr 来取得 shared_ptr 的临时共享所有权,在引用计数的场景下,把节点中的 _prev 和 _next改成weak_ptr就可以了。weak_ptr只是单纯的赋值,不会使引用计数++,析构后也不负责释放空间。

    • weak_ptr:结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例拥有的对象的访问,但不参与引用计数。 如果你想要观察某个对象但不需要其保持活动状态,请使用该实例。 在某些情况下,需要断开 shared_ptr 实例间的循环引用。
  • 新增加容器——静态数组array、forward_list以及unordered系列

  • lambda表达式

    lambda表达式式实际是一个匿名函数

    lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

    lambda表达式各部分说明:

    • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用
    • (parameters):参数列表,与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
    • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
    • ->returntype:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分 可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
    • {statement}:函数体,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

    注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体不可以为空。 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情

    lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量

    捕获列表说明:捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

    • [var]:表示值传递方式捕捉变量var
    • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
    • [&var]:表示引用传递捕捉变量var
    • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
    • [this]:表示值传递方式捕捉当前的this指针

    注意:

    • 父作用域指包含lambda函数的语句块
    • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
    • 捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
    • 在块作用域以外的lambda函数捕捉列表必须为空
    • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
    • lambda表达式之间不能相互赋值,即使看起来类型相同

    函数对象与lambda表达式:

    函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象

    class Rate{
    public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
    private:double _rate;
    };
    int main(){// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lambda表达式auto r2 = [=](double monty, int year)->double {return monty * rate*year; };r2(10000, 2);return 0;
    }
    

    从使用方式上来看,函数对象与lambda表达式完全一样,函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

    实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一 个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

  • 线程库

    函数名功能
    thread()构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
    thread(fn, args1,…)构造一个线程对象,并关联线程函数fn,args1,…为线程函数的参数
    get_id()获取线程id
    jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程
    jion()该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行
    detach()在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程 变为后台线程,创建的线程的"死活"就与主线程无关

    注意:

    • 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态

    • 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

    • get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类,该类中包含了一个 结构体:

      typedef struct
      { /* thread identifier for Win32 */void *_Hnd; /* Win32 HANDLE */unsigned int _Id;
      } _Thrd_imp_t;
      
    • 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一 般情况下可按照以下三种方式提供:函数指针、lambda表达式、函数对象

    • thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行

    • 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效:采用无参构造函数构造的线程对象、线程对象的状态已经转移给其他线程对象、线程已经调用jion或者detach结束

    线程函数参数:

    • 线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。 如果想要通过形参改变外部实参时,必须借助std::ref()函数

    • 注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数

    join与detach:

    启动了一个线程后,当这个线程结束的时候,如何去回收线程所使用的资源呢?

    • join()方式 :主线程被阻塞,当新线程终止时,join()会清理相关的线程资源,然后返回,主线程再继续向下执行,然后销毁线程对象。由于join()清理了线程的相关资源,thread对象与已销毁的线程就没有关系 了,因此一个线程对象只能使用一次join(),否则程序会崩溃。

    • detach()方式:该函数被调用后,新线程与线程对象分离,不再被线程对象所表达,就不能通过线程对象控制线程了,新线程会在后台运行,其所有权和控制权将会交给C++运行库。同时,C++运行库保证,当 线程退出时,其相关资源的能够正确的回收。

      detach()函数一般在线程对象创建好之后就调用,因为如果不是jion()等待方式结束,那么线程对象可能会在新线程结束之前被销毁掉而导致程序崩溃。因为std::thread的析构函数中,如果线程的状态是 jionable,std::terminate将会被调用,而terminate()函数直接会终止程序。

    线程对象销毁前,要么以jion()的方式等待线程结束,要么以detach()的方式将线程与线程对象分 离。

    原子性操作库(atomic)

    • C++11引入的原子操作类型,使得线程间数据的同步变得非常高效,所谓原子操作:即不可被中断的一个或一系列操作
    • 在C++11中,我们不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问;可以使用atomic类模板,定义出需要的任意原子类型
    • 注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及operator=等,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算符重载默认删除掉了。

    lock_guard与unique_lock

    • std::lock_gurad 是 C++11 中定义的模板类,定义如下:

      template<class _Mutex>
      class lock_guard
      {
      public:// 在构造lock_gard时,_Mtx还没有被上锁explicit lock_guard(_Mutex& _Mtx): _MyMutex(_Mtx){_MyMutex.lock();}// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁lock_guard(_Mutex& _Mtx, adopt_lock_t): _MyMutex(_Mtx){}~lock_guard() _NOEXCEPT{_MyMutex.unlock();}lock_guard(const lock_guard&) = delete;lock_guard& operator=(const lock_guard&) = delete;
      private:_Mutex& _MyMutex;
      };
      

      lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用任意互斥量实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题。

      lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了unique_lock

    • 与lock_gard类似,unique_lock类模板也是采用RAII的方式对锁进行了封装,并且也是以独占所有权的方式 管理mutex对象的上锁和解锁操作,即其对象之间不能发生拷贝。在构造(或移动(move)赋值)时, unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。使用以上类型互斥量实例化unique_lock的对象时,自动调用构造函数上锁, unique_lock对象销毁时自动调用析构函数解锁,可以很方便的防止死锁问题

    • 与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

      • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
      • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放 (release:返回它所管理的互斥量对象的指针,并释放所有权)
      • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool() (与owns_lock()的功能相同)、 mutex(返回当前unique_lock所管理的互斥量的指针)

    Mutex的种类:

    • std::mutex:C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动
    • std::recursive_mutex:其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,释放互斥量时需要调用与该锁层次深度相同次数的unlock(),除此之外,std::recursive_mutex 的特性和 std::mutex 大致相同
    • std::timed_mutex 比 std::mutex 多了两个成员函数,try_lock_for(),try_lock_until()
      • try_lock_for() :接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与 std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回 false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返 回false。
      • try_lock_until() 接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住,如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指定时间内还是没有获得锁),则返回 false

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

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

相关文章

深度学习torch基础知识

torch. detach()拼接函数torch.stack()torch.nn.DataParallel()np.clip()torch.linspace()PyTorch中tensor.repeat()pytorch索引查找 index_select detach() detach是截断反向传播的梯度流 将某个node变成不需要梯度的Varibale。因此当反向传播经过这个node时&#xff0c;梯度…

IDEA用Gradle构建项目时,lombok插件无效的解决办法

Lombok 可用来帮助开发人员消除 Java 的重复代码&#xff0c;尤其是对于简单的 Java 对象&#xff08;POJO&#xff09;&#xff0c;比如说getter/setter/toString等方法的编写。它通过注解实现这一目的。 正确使用姿势 一、安装Lombok插件 菜单栏File -> Settings ->…

死锁的发生原因和怎么避免

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 例如&#xff1a;项目场景&#xff1a;示例:通过蓝牙芯片(HC-05)与手机 APP 通信&#xff0c;每隔 5s 传输一批传感器数据(不是很大) 问题描述 死锁&#xff0c;简单来说就是两个或者两个以上的线程在…

翻转卡片游戏(力扣)

题目 在桌子上有 n 张卡片&#xff0c;每张卡片的正面和背面都写着一个正数&#xff08;正面与背面上的数有可能不一样&#xff09;。 我们可以先翻转任意张卡片&#xff0c;然后选择其中一张卡片。 如果选中的那张卡片背面的数字 x 与任意一张卡片的正面的数字都不同&#…

【C语言进阶】数据的存储----整型篇

​ &#x1f341; 博客主页:江池俊的博客 &#x1f4ab;收录专栏&#xff1a;C语言——探索高效编程的基石 &#x1f4bb; 其他专栏&#xff1a;数据结构探索 ​&#x1f4a1;代码仓库&#xff1a;江池俊的代码仓库 &#x1f3aa; 社区&#xff1a;GeekHub &#x1f341; 如果觉…

洛谷P1525 关押罪犯(种类并查集)

S 城现有两座监狱&#xff0c;一共关押着 $N$ 名罪犯&#xff0c;编号分别为 $1-N$ 。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久&#xff0c;如果客观条件具备则随时可能爆发冲突。我们用“怨气值”&#xff08;一个正整数值&#xff09;来表示某两名罪犯之间的…

Liunx环境下git的详细使用(gitee版)

Liunx环境下git的详细使用&#xff08;gitee版&#xff09; 1.git是什么2.git操作2.1在gitee创建一个仓库2.2.gitignore2.3.git 3.git三板斧3.1add3.2 commit3.3push 4.git其他命令4.1查看当前仓库状态4.2查看提交日志4.3修改git里面文件名称4.4删除文件4.5修改远端仓库内容 1.…

postgresql表膨胀处理之pgcompacttable部署及使用

环境&#xff1a; 1&#xff09;redhat-release&#xff1a;CentOS Linux release 7.6.1810 (Core) 2&#xff09;database version&#xff1a;postgresql 14.6 一、添加pgstattuple pgcompacttable工具使用过程中需要依赖pgstattuple&#xff0c;因此需先添加pgstattuple…

【SEO基础】百度权重是什么意思及网站关键词应该怎么选?

百度权重是什么意思及网站关键词应该怎么选&#xff1f; 正文共&#xff1a;3253字 20图 预计阅读时间&#xff1a;9分钟 ​ 1.什么是网站权重&#xff1f; 这段时间和一些朋友聊到网站权重以及关键词&#xff0c;发现蛮多人对于这两个概念的认知还是存在一些错误的&#xf…

数组的使用(逆序、冒泡)

内存连续数据类型相同从0开始索引 找出数组中的最大值 #include <iostream> #include <stdlib.h> //随机数所在文件 using namespace std;int main() {int arr[5]{104,134,145,129,89};//初始化没有填的为0 int max0;for(int i0;i<5;i){if(arr[i]>max){ma…

矩阵计算之求解

文章目录 已知 A*BC一、矩阵计算 -求A一、矩阵计算 -求B 已知 A*BC 一、矩阵计算 -求A AC*invB一、矩阵计算 -求B BinvA*C

203. 移除链表元素

203. 移除链表元素 题目方法1递归方法2迭代 题目 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 方法1递归 class Solution { public:ListNode* removeElements(ListNode* head, in…

winform使用SetParent 嵌入excel,打开的excel跟随dpi 25%*125%缩放了两次,目前微软官方没有好的解决方案,为什么

双重缩放问题在将 Excel 嵌入到 WinForm 中时确实可能会出现&#xff0c;这是因为两个不同的应用程序&#xff08;WinForm 和 Excel&#xff09;之间的 DPI 缩放逻辑不一致&#xff0c;导致双重缩放的结果。 在 Windows 操作系统中&#xff0c;DPI 缩放是一种全局的设置&#…

RabbitMQ快速入门

文章目录 1、RabbitMQ的概述1.1、什么是消息队列&#xff1f;1.2、为什么要使用消息队列&#xff1f;1.3、RabbitMQ的特点&#xff1a; 2、RabbitMQ的安装2.1 下载与安装2.2 常用命令 3、RabbitMQ消息发送和接受3.1 消息发送和接受机制3.2 AMQP的消息路由3.3 Exchange(交换机)的…

Datax 数据同步-使用总结(一)

1&#xff0c;实时同步&#xff1f; datax 通常做离线数据同步使用。 目前能想到的方案 利用 linux 的定时任务时间戳的方式做增量同步。 2&#xff0c;同步速度快不快&#xff1f; 单表同步速度还是挺快的 但是如果遇到复杂的 sql 查询&#xff0c;其同步效率&#xff0c…

中国信通院发布《高质量数字化转型产品及服务全景图(2023)》

2023年7月27日&#xff0c;由中国信息通信研究院主办的2023数字生态发展大会暨中国信通院铸基计划年中会议在北京成功召开。 本次大会发布了中国信通院《高质量数字化转型产品及服务全景图&#xff08;2023&#xff09;》&#xff0c;中新赛克海睿思受邀出席本次大会并成功入选…

Golang中空结构体的使用

空结构体的使用 何为空结构体 在结构体中&#xff0c;可以包裹一系列与对象相关的属性&#xff0c;但若该对象没有属性呢&#xff1f;那它就是一个空结构体。   空结构体&#xff0c;和正常的结构体一样&#xff0c;可以接收方法函数。 eg&#xff1a; type Lamp struct{}f…

秋招算法备战第38天 | 动态规划理论基础、509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯

动态规划理论基础 什么是动态规划 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划是最有效的。 所以动态规划中每一个状态一定是由上一个状态推导出来的&#xff0c;这一点就…

能源电力工程师专属Python学习资料

随着我国新型电力系统的建设&#xff0c;一方面电源侧各类新能源装机快速增长&#xff0c;对于新能源出力的功率预测需求日益增长&#xff1b;另一方面&#xff0c;我国电力市场经过 8 年建设&#xff0c;关于电力商品价格影响因素的研究亟待深入。超过 90% 的业务小伙伴都具备…

viewerjs 如何新增下载图片功能(npm包补丁)

文章目录 先实现正常的效果实现下载图片改变viewerjs的build函数源码改变之后&#xff0c;执行npm i 之后node_modules源码又变回了原样 1、viwerjs所有功能都很完善&#xff0c;但唯独缺少了图片的下载 2、需求&#xff1a;在用viwerjs旋转图片后&#xff0c;可以直接下载旋转…