C++学习记录——삼십 智能指针
文章目录
- 1、为什么需要智能指针?
- 2、内存泄漏
- 3、智能指针的使用及原理
- 1、RAII思想
- 2、拷贝问题
- 1、unique_ptr
- 2、shared_ptr
- 1、多线程
- 2、循环引用
- 3、定制删除器
1、为什么需要智能指针?
看一个场景
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
new是可能开辟失败,抛异常的。上述代码中,如果p1抛异常,那么可以外面的catch可以捕获到,打印出消息;如果p1异常,p2也要抛异常,那么在这之前,应当销毁p1,再去抛;同理,到了div()那里如果也抛异常,那么得销毁p1和p2,整体就得这样写
void Func()
{// 1、如果p1这里new 抛异常会如何?// 2、如果p2这里new 抛异常会如何?// 3、如果div调用这里又会抛异常会如何?int* p1 = new int;try{int* p2 = new int;}catch (...){delete p1;throw;}try{cout << div() << endl;}catch (...){delete p1;delete p2;throw;}delete p1;delete p2;
}
一下子就能看出来,这太麻烦了,如果有多个new呢?
2、内存泄漏
Windows和Linux都有检测内存泄漏的工具,不过Windows下的VLD不太靠谱,Linux中valgrind是比较出名的
Linux下几款C++程序中的内存泄露检查工具
为了预防内存泄漏,常用的办法就是用智能指针或者事后检测。
3、智能指针的使用及原理
1、RAII思想
template <class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << _ptr << endl;delete _ptr;}}private:T* _ptr;
};void Func()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl;
}
和封装锁的思路来类似,都是RAII。用临时变量来构造,出了作用域就自动销毁。
RAII利用对象生命周期来控制程序资源,对象构造时获取资源,析构时释放资源
上面的SmartPtr不像一个指针,它不能解引用数据,不过我们可以写对应的函数。
T& operator*(){return *_ptr;}T* operator->(){return _ptr;}cout << *sp1 << endl;//如果模板参数是自定义类型的话就可以用->了。
2、拷贝问题
智能指针如何拷贝?
int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<int> sp2(sp1);return 0;
}
采用默认拷贝会浅拷贝,导致同一空间重复释放。这里应当如何写拷贝构造?是要用深拷贝吗?其实不是,我们要的浅拷贝,sp1和sp2指向同一个资源,以前的链表等这些迭代器结构不需要释放资源,而智能指针需要管理资源,所以不能单纯地浅拷贝,但是又不能要深拷贝。
C++98时已经有智能指针了,那个版本中有一个auto_ptr,它的方法是管理权转移,我们写到SmartPtr类中
//管理权转移auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<int> sp2(sp1);
这样看就像是用ap指向sp1,然后用sp1的_ptr来初始化sp2的_ptr,然后把sp1的_ptr给置空。虽然看起来是可以的,能解决问题,但有很大隐患,这会导致sp1悬空,如果不知道管理权转移的实际写法,那么下面代码中如果有*sp1就出问题了。程序员用它的时候需要时刻提醒自己,被拷贝对象已经悬空了,不能去解引用它。
在C++11之前,有个可移植的C++库——Boost库,不是标准库,但也胜似标准库,是有C++标准委员会库工作组成员发起的,C++中有很多标准都从Boost中吸收过来,像右值引用,线程库。Boost库有scoped_ptr,weak_ptr,_shared_ptr,C++11中把scoped_ptr改名成unique_ptr。
1、unique_ptr
它的思路是防拷贝。
//防拷贝//C++98思路:只声明不实现,但是还可以在外面强行定义,所以会把它放在私有里//C++11思路:函数后= delete//这里拷贝构造和赋值都写上unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
不需要拷贝的场景就用它。
2、shared_ptr
引用计数的思路。有多少个指针指向一个空间,那么这个空间的引用计数就是多少。当一个指针要释放时,如果引用计数大于0,那就不做操作,如果等于0,那就做一次释放资源,这个空间的指针也都用完了。
引用计数这个变量不能放在静态区,因为如果static修饰后,它属于类的每个对象,但我们要的是指向同一空间的所有指针。定义一个int* pcount。
shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << _ptr << endl;delete _ptr;delete _pcount;}}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}
赋值函数,比如sp1 = sp3,那么sp1的引用计数需要–,因为它要指向新空间了;假设sp1的空间还有别的指针指向,而sp3的空间只有sp3这一个指针,sp3 = sp1,那么就是sp3–。
shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._str){if (--(*_pcount) == 0)//处理空间上只有一个指针的情况{delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}
1、多线程
整体改成这样的形式来配合加锁
shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)){}void Release(){if (--(*_pcount) == 0){if(_ptr)//如果为空那就不需要释放{delete _ptr;}delete _pcount;}}void AddCount(){++(*_pcount);}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){AddCount();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._str){Release();_ptr = sp._ptr;_pcount = sp._pcount;AddCount();}return *this;}
多线程比较常见的场景就是线程安全问题。同一个数会出现多次操作,导致结果不是我们想要的。多线程情况下,像传给接收引用的参数时,要写成ref(…),ref是库中的函数,否则会被认为是传值传参。
template <class T>
class shared_ptr
{
public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void Release(){_pmtx.lock();if (--(*_pcount) == 0){if(_ptr){delete _ptr;}delete _pcount;}_pmtx.unlock();}void AddCount(){_pmtx.lock();++(*_pcount);_pmtx.unlock();}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){AddCount();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._str){Release();_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp->_pmtx;AddCount();}return *this;}//防拷贝//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里//C++11思路:函数后= delete//unique_ptr(const unique_ptr<T>& up) = delete;//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;int* pcount;mutex* _pmtx;
};
在Release那里,到了引用计数减到0时,需要释放引用计数,释放锁。如果是在if里释放锁,那么外面的解锁操作就有问题了。 解决办法是可以设置一个状态位
void Release(){_pmtx.lock();bool deleteFlag = false;if (--(*_pcount) == 0){if(_ptr){delete _ptr;}delete _pcount;deleteFlag = true;}_pmtx.unlock();if (deleteFlag){delete _pmtx;}}
shared_ptr本身是线程安全的,因为计数是加锁保护的,它实例化的对象不是线程安全的,想要线程安全,那么在对对象操作时用锁保护就行。
2、循环引用
写一个场景,还是用上面的shared_ptr
template <class T>
class shared_ptr
{
public:shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void Release(){_pmtx.lock();bool deleteFlag = false;if (--(*_pcount) == 0){if (_ptr){delete _ptr;}delete _pcount;deleteFlag = true;}_pmtx.unlock();if (deleteFlag){delete _pmtx;}}void AddCount(){_pmtx.lock();++(*_pcount);_pmtx.unlock();}~shared_ptr(){Release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){AddCount();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._str){Release();_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp->_pmtx;AddCount();}return *this;}//防拷贝//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里//C++11思路:函数后= delete//unique_ptr(const unique_ptr<T>& up) = delete;//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;int* pcount;mutex* _pmtx;
};struct ListNode
{ListNode* _next;ListNode* _prev;int _val;~ListNode(){cout << "~ListNode" << endl;}
};int main()
{shared_ptr<ListNode> n1 = new ListNode;shared_ptr<ListNode> n2 = new ListNode;return 0;
}
当尝试连接两个节点时就发生了错误
n1->_next = n2;n2->_prev = n1;
n1和n2是智能指针类型,而next和prev是ListNode类型的,无法赋值,那把ListNode里的两个指针换成shared_ptr< ListNode >类型的,但这样还不行,因为我们在定义next和prev时没有传参,是无参构造,所以在智能指针的类里应当写上缺省参数。
shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}struct ListNode
{shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode" << endl;}
};
现在有一个问题
n1->_next = n2;n2->_prev = n1;
如果两句都写,程序会不释放资源,而如果只写一句或者两句都不写,那就会释放资源,就会打印"~ListNode",使用库中的智能指针也是这样,这就是智能指针引起的循环引用问题。
n1和n2都有各自的next和prev,如果不相互连接,也就是什么都不写,那么next和prev随着n1n2销毁而销毁。
写了一句,比如n1->_next = n2,那么n2这个节点除了它本身,还有n1的next指向它,n2析构时,引用计数–,但是空间不销毁,n1析构时,里面的成员变量也会随着析构,那么整体也可以完好地退出。
但是两句都写就出问题了。
n1->_next = n2;n2->_prev = n1;
出了作用域,n2先析构,引用计数–,但是还不能销毁空间,引用计数没有为0,也还有一个指针指向它;n1析构时,n1也是一样,也不能析构,引用计数–,现在这两个空间的引用计数都为1,n1的next指向n2的空间,n2的prev指向n1的空间,那么n1这个空间什么时候析构?要看prev,prev析构,n1这个空间就析构,但是n2这个空间由next指向,next析构,n2才能析构,prev才能析构,所以next和prev已经形成了相互制约的关系,没办法全部析构了。这就是循环引用,会导致内存泄漏。
为了解决这个问题,标准库中有个weak_ptr来辅助shared_ptr,也叫做弱指针。weak_ptr不是RAII的,也就是它不是常规的智能指针,但是支持像指针一样,专门用来解决shared_ptr的辅助引用问题。用weak这样写。
struct ListNode
{std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode" << endl;}
};
weak_ptr不会增加引用计数。标准库中weak_ptr实现得很复杂,我们这里只模拟实现一个简单的
template <class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;mutex* _pmtx;};
struct ListNode
{zyd::weak_ptr<ListNode> _next;zyd::weak_ptr<ListNode> _prev;int _val;~ListNode(){cout << "~ListNode" << endl;}
};int main()
{zyd::shared_ptr<ListNode> n1 = new ListNode;zyd::shared_ptr<ListNode> n2 = new ListNode;n1->_next = n2;n2->_prev = n1;return 0;
}
3、定制删除器
在实例化的时候,传new int[10]这样的话,可能会崩溃,是因为new []会在开辟的空间前再开辟一个存放元素个数的空间,但是delete的时候会从开辟的空间开始释放,而不包含那个存储个数的空间,所以本质上是释放的位置不对。
定制删除器本质上是一个可调用对象,函数指针,仿函数,lambda都可以。
template <class T>
struct DeleteArray
{void operator()(T* ptr){cout << "仿函数" << endl;delete[] ptr;}
};int main()
{//zyd::shared_ptr<ListNode> n1 = new ListNode;//zyd::shared_ptr<ListNode> n2 = new ListNode;//n1->_next = n2;//n2->_prev = n1;std::shared_ptr(int) spa1(new int[10], DeleteArray<int>());//仿函数std::shared_ptr(int) spa2(new int[10], [](int* ptr) {delete[] ptr; });//lambdareturn 0;
}
库中的做法是把这个删除器放到构造函数里,实例化的时候传过来,保存起来,析构时用它去析构。这里的重点在于如何保存这个删除器。一个是我们可以在总的模板参数那里加一个模板参数,那么析构函数就可以直接用,也不用在构造函数那里在写上一个模板参数;或者用包装器。这里写包装器。
template <class D>shared_ptr(const shared_ptr<T>& sp, D del):_ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx), _del(del){AddCount();}void Release(){_pmtx.lock();bool deleteFlag = false;if (--(*_pcount) == 0){if (_ptr){//delete _ptr;_del(_ptr);}delete _pcount;deleteFlag = true;}_pmtx.unlock();if (deleteFlag){delete _pmtx;}}private:T* _ptr;int* _pcount;mutex* _pmtx;functional<void(T*)> _del;
这样写其实会有问题,如果用不到这个删除器就会调用默认构造,删除器没有初始化,到了析构时,删除器就是被编译器默认初始化的,用它来析构就容易出问题。我们可以用缺省
private:T* _ptr;int* _pcount;mutex* _pmtx;functional<void(T*)> _del = [](T* ptr) {cout << "lambda delete:" << ptr << endl;delete ptr;};
定制删除器当作了解,重点在于shared_ptr的实现。
本篇gitee
结束。
相关文章:

C++学习记录——삼십 智能指针
文章目录 1、为什么需要智能指针?2、内存泄漏3、智能指针的使用及原理1、RAII思想2、拷贝问题1、unique_ptr2、shared_ptr1、多线程2、循环引用3、定制删除器 1、为什么需要智能指针? 看一个场景 int div() {int a, b;cin >> a >> b;if (b…...

插件式架构 与 ReSharper、Visual Studio的故事
文章首发地址 ReSharper和Visual Studio的故事 ReSharper是一款由JetBrains公司开发的Visual Studio插件,它主要用于提高Visual Studio的开发效率和改善代码质量。ReSharper在早期的版本中被称为"Omea Code",它最初是JetBrains一个研究项目的…...

Python UDP编程
前面我们讲了 TCP 编程,我们知道 TCP 可以建立可靠连接,并且通信双方都可以以流的形式发送数据。本文我们再来介绍另一个常用的协议--UDP。相对TCP,UDP则是面向无连接的协议。 UDP 协议 我们来看 UDP 的定义: UDP 协议ÿ…...

结构体(个人学习笔记黑马学习)
1、结构体的定义和使用 #include <iostream> using namespace std; #include <string>struct Student {string name;int age;int score; }s3;int main() {//1、struct Student s1;s1.name "张三";s1.age 18;s1.score 100;cout << "姓名&a…...

小白带你学习linux的PXE装机
目录 目录 一、PXE是什么? 二、PXE的组件: 1、vsftpd/httpd/nfs 2、tftp 3、dhcp 三、配置dhcp 1、关闭防火墙与selinux和配置本地yum源 2、安装dhcp服务 3、配置dhcp配置文件 四、配置vsftpd 五、配置tftp 1、安装tftp-server 2、启动tft…...

华为鲲鹏服务器
1.简介 鲲鹏通用计算平台提供基于鲲鹏处理器的TaiShan服务器、鲲鹏主板及开发套件。硬件厂商可以基于鲲鹏主板发展自有品牌的产品和解决方案;软件厂商基于openEuler开源OS以及配套的数据库、中间件等平台软件发展应用软件和服务;鲲鹏开发套件可帮助开发…...

Python金币小游戏
游戏规则:移动挡板接住金币 游戏截图: 详细代码如下: import pygame.freetype import sys import randompygame.init() screen pygame.display.set_mode((600, 400)) pygame.display.set_caption(game) p 0 i1 0 s 0 t 0 f1 pygame.f…...

Modbus转Profinet网关在大型自动化仓储项目应用案例
在自动化仓储项目中,Modbus是一种常见的通信协议,用于连接各种设备,例如传感器、PLC和人机界面。然而,Modbus协议只支持串行通信,并且数据传输速度较慢。为了提高通信效率和整体系统性能,许多大型仓储项目选…...

Java 并发 ThreadLocal 详解
文章首发于个人博客,欢迎访问关注:https://www.lin2j.tech 简介 ThreadLocal 即线程本地变量的意思,常被用来处理线程安全问题。ThreadLocal 的作用是为多线程中的每一个线程都创建一个线程自身才能用的实例对象,通过线程隔离的…...

JWT 技术的使用
应用场景:访问某些页面,需要用户进行登录,那我们如何知道用户有没有登录呢,这时我们就可以使用jwt技术。用户输入的账号和密码正确的情况下,后端根据用户的唯一id生成一个独一无二的token,并返回给前端&…...

机器学习深度学习——NLP实战(自然语言推断——微调BERT实现)
👨🎓作者简介:一位即将上大四,正专攻机器学习的保研er 🌌上期文章:机器学习&&深度学习——针对序列级和词元级应用微调BERT 📚订阅专栏:机器学习&&深度学习 希望文…...

如何在windows下使用masm和link对汇编文件进行编译
前言 32位系统带有debug程序,可以进行汇编语言和exe的调试。但真正的汇编编程是“编辑汇编程序文件(.asm)->编译生成obj文件->链接生成exe文件”。下面,我就来说一下如何在windows下使用masm调试,使用link链接。 1、下载相应软件 下载…...

Golang字符串基本处理方法
Golang的字符串处理 字符串拼接 两种方法:strings.Join方法和’方法 package mainimport ("fmt""strings" )func main() {num : 20strs : make([]string, 0)for i : 0; i < num; i {strs append(strs, "fht")}//string.join拼…...

算法训练营第三十九天(8.30)| 动态规划Part09:购买股票
Leecode 123.买卖股票的最佳时机 III 123.买卖股票的最佳时机III 123.买卖股票的最佳时机III 题目地址:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 题目类型:股票问题 class Solution { public:int maxProfit(vector<…...

renren-fast-vue环境升级后,运行正常打包后,访问页面空白
网上各种环境,路径都找了一遍,也没成功。后来发现升级后打包的dist文件结构发生了变化, 1.最开始正常版本是这样 2.升级后是这样,少了日期文件夹 3.问题:打包后的index.html中引入的是config文件夹,而打…...

Uniapp笔记(三)uniapp语法2
一、本节项目预备知识 1、组件生命周期 1.1、什么是生命周期 生命周期(Life Cycle)是指一个对象从创建-->运行-->销毁的整个阶段,强调的是一个时间段 我们可以把每个uniapp应用运行的过程,也概括为生命周期 小程序的启动,表示生命周…...

windows【ftp-FTP】添加配置流程【iis服务】
第一步:自己安装iis服务和ftp服务【自己百度搜索】 第二步:添加ftp站点【配置主动端口默认为21】 第三方配置:ftp被动端口【这里设置为3000-4000】请在防火墙开放此端口【如果是阿里云请在阿里云的后天也开通此端口】【护卫神一般使用55000…...

mysql视图的创建和选项配置详解
在 MySQL 中,可以使用 CREATE VIEW 语句来创建视图。基本的语法如下: CREATE[OR REPLACE][ALGORITHM {UNDEFINED | MERGE | TEMPTABLE}][DEFINER {user | CURRENT_USER}][SQL SECURITY { DEFINER | INVOKER }]VIEW view_name [(column_list)]AS selec…...

Python正则表达式中re.sub自定义替换方法正确使用方法
大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 话不多说,直接开搞,如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 在使用正则替换时,有时候需要将匹配的结果做对应处理,便可以使用自定义替换方法。 re.sub的用法为&…...

hyperf 十五 验证器
官方文档:Hyperf 验证器报错需要配合多语言使用,创建配置自动生成对应的语言文件。 一 安装 composer require hyperf/validation:v2.2.33 composer require hyperf/translation:v2.2.33php bin/hyperf.php vendor:publish hyperf/translation php bi…...

ssh访问远程宿主机的VMWare中NAT模式下的虚拟机
1.虚拟机端配置 1.1设置虚拟机的网络为NAT模式 1.2设置虚拟网络端口映射(NAT) 点击主菜单的编辑-虚拟网络编辑器: 启动如下对话框,选中NAT模式的菜单项,并点击NAT设置: 点击添加,为我们的虚拟机添加一个端口映射。…...

【一等奖方案】大规模金融图数据中异常风险行为模式挖掘赛题「NUFE」解题思路
第十届CCF大数据与计算智能大赛(2022 CCF BDCI)已圆满结束,大赛官方竞赛平台DataFountain(简称DF平台)正在陆续释出各赛题获奖队伍的方案思路,欢迎广大数据科学家交流讨论。 本方案为【大规模金融图数据中…...

npm install 报错
npm install 报错 npm install 报错 npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: yudao-ui-admin1.8.0-snapshot npm ERR! Found: eslint7.15.0 npm ERR! node_modules/eslint npm ERR! dev eslint&q…...

专业人士使用的3个好用的ChatGPT提示
AI正在席卷世界。从自动化各个领域的任务到几秒钟内协助我们的日常生活,它有着了公众还未理解的巨大价值……除非你是专业人士。 专业人士一般都使用什么提示以及如何使用的?这里是经验丰富的懂ChatGPT的专业人士才知道的3个提示。好用请复制收藏。 为…...

doris系列2: doris分析英国房产数据集
1.准备数据 2.doris建表 CREATE TABLE `uk_price_paid` (`id` varchar(50) NOT NULL,`price` int(20),`date` date...

精准运营,智能决策!解锁天翼物联水利水务感知云
面向智慧水利/水务数字化转型需求,天翼物联基于感知云平台创新能力,提供涵盖水利水务泛协议接入、感知云水利/水务平台、水利/水务感知数据治理、数据看板在内的水利水务感知云服务,构建水利水务感知神经系统新型数字化底座,实现智…...

CleanMyMac最新版4.14Mac清理软件下载安装使用教程
苹果电脑是很多人喜欢使用的一种电脑,它有着优美的外观,流畅的操作系统,丰富的应用程序和高效的性能。但是,随着时间的推移,苹果电脑也会产生一些不必要的文件和数据,这些文件和数据就是我们常说的垃圾。那…...

String.Format方法详解
在Java中,String.format() 方法可以用于将格式化的字符串写入输出字符串中。该方法将根据指定的格式字符串生成一个新的字符串,并使用可选的参数填充格式字符串中的占位符。以下是有关 String.format() 方法的更详细信息: 语法 public stati…...

【Mysql】关联查询1对多处理
关联查询1对多返回 遇见的问题 审批主表,和审批明细表,一张审批对应多张明细数据,每条明细数据的状态是不一样的,现在需要根据明细的状态获取到主单子的状态,状态返回矩阵如下 明细状态返回总状态都是已完成已完成都…...

vue 入门案例模版
vue 入门案例1 01.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> &l…...