设计模式之单例模式(C++)

news2024/11/27 20:31:49

作者:翟天保Steven
版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处

一、单例模式是什么?

       单例模式是一种创建型的软件设计模式,在工程项目中非常常见。通过单例模式的设计,使得创建的类在当前进程中只有一个实例,并提供一个全局性的访问点,这样可以规避因频繁创建对象而导致的内存飙升情况。

       实现单例模式的三个要点:

1)私有化构造函数:这样外界就无法自由地创建类对象,进而阻止了多个实例的产生。

2)类定义中含有该类的唯一静态私有对象:静态变量存放在全局存储区,且是唯一的,供所有对象使用。

3)用公有的静态函数来获取该实例:提供了访问接口。

       单例模式一般分为懒汉式和饿汉式。

1)懒汉式:在使用类对象(单例实例)时才会去创建它,不然就懒得去搞。

2)饿汉式:单例实例在类装载时构建,有可能全局都没使用过,但它占用了空间,就像等着发救济粮的饿汉提前排好队等吃的一样。

二、懒汉式实现

2.1 懒汉基础实现

       最基本的懒汉实现方法。

//Singleton.h
/****************************************************/
#include <iostream>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton* getInstance(){
		// 若为空则创建
		if (instance == nullptr) {
			cout << "实例为空,开始创建。" << endl;
			instance = new Singleton();
			cout << "创建结束。" << endl;
		}
		else {
			cout << "已有实例,返回。" << endl;
		}
		return instance;
	}
private:
	// 私有构造函数
	Singleton(){
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton(){
		cout << "析构函数启动。" << endl;
	};
private:
	// 静态私有对象
	static Singleton* instance;
};

// 初始化
Singleton* Singleton::instance = nullptr;
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main() 
{
	cout << "main开始" << endl;
	Singleton* s1 = Singleton::getInstance();
	Singleton* s2 = Singleton::getInstance();
	Singleton* s3 = Singleton::getInstance();
	cout << "main结束" << endl;
	return 0;
}

       执行代码,让我们看看结果。

       从结果中可以看出这样设计主要有两个问题,一个是线程安全,另一个是内存泄漏。

       线程安全是因为在多线程场景下,有可能出现多个线程同时进行new操作的情况,没通过加锁来限制。

       内存泄漏是因为使用了new在堆上分配了资源,那么在程序结束时,也应该进行delete,确保堆中数据释放。

       接下来,我们先解决线程安全问题,对懒汉式实现进行改进。

2.2 基于双重检测锁的懒汉实现

       通过双重检测锁,可以确保线程安全。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton* getInstance(){
		// 若为空则创建
		if (instance == nullptr) {
			// 加锁保证线程安全
			// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
			// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
			lock_guard<mutex> l(m_mutex);
			if (instance == nullptr) {
				cout << "实例为空,开始创建。" << endl;
				instance = new Singleton();
				cout << "地址为:" << instance << endl;
				cout << "创建结束。" << endl;
			}
		}
		else {
			cout << "已有实例,返回。" << endl;
		}
		return instance;
	}
private:
	// 私有构造函数
	Singleton(){
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton(){
		cout << "析构函数启动。" << endl;
	};
private:
	// 静态私有对象
	static Singleton* instance;
	// 锁
	static mutex m_mutex;
};

// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main() 
{
	cout << "main开始" << endl;
	thread t1([] {
		Singleton* s1 = Singleton::getInstance();
	});
	thread t2([] {
		Singleton* s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	Singleton* s3 = Singleton::getInstance();
	cout << "地址为:" << s3 << endl;
	cout << "main结束" << endl;
	return 0;
}

       执行代码,让我们看看结果。

       这样看来没有问题,那如果取消双重检测锁,在多线程下看看会发生什么。将代码部分函数修改为下。把锁注释掉,再查看地址信息。

// 公有接口获取唯一实例
static Singleton* getInstance(){
	// 若为空则创建
	if (instance == nullptr) {
		// 加锁保证线程安全
		// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
		// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
		//lock_guard<mutex> l(m_mutex);
		if (instance == nullptr) {
			cout << "实例为空,开始创建。" << endl;
			instance = new Singleton();
			cout << "地址为:" << instance << endl;
			cout << "创建结束。" << endl;
		}
	}
	else {
		cout << "已有实例,返回。" << endl;
	}
	return instance;
}

       此时结果中可以看出,两个线程进行了两次new操作,但是最后只能捕捉到最后一次new的地址信息了,前面的那个丢失了。。。。。

       这个测试也是让大家直观地感受下双重检测锁的用处。

       接下来,我们再解决内存泄漏(资源释放)问题,对懒汉式实现进行进一步的改进。

2.3 基于双重检测锁和资源管理的懒汉实现

       在2.2的基础上,我们加入资源管理机制,以达到对资源的释放的目的,解决方法有两个:智能指针&静态嵌套类。

2.3.1 智能指针方案

       将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static shared_ptr<Singleton> getInstance(){
		// 若为空则创建
		if (instance == nullptr) {
			// 加锁保证线程安全
			// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
			// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
			lock_guard<mutex> l(m_mutex);
			if (instance == nullptr) {
				cout << "实例为空,开始创建。" << endl;
				instance.reset(new Singleton(), destoryInstance);
				cout << "地址为:" << instance << endl;
				cout << "创建结束。" << endl;
			}
		}
		else {
			cout << "已有实例,返回。" << endl;
		}
		return instance;
	}
	// 毁灭实例
	static void destoryInstance(Singleton* x) {
		cout << "自定义释放实例" << endl;
		delete x;
	}
private:
	// 私有构造函数
	Singleton(){
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton(){
		cout << "析构函数启动。" << endl;
	};
private:
	// 静态私有对象
	static shared_ptr<Singleton> instance;
	// 锁
	static mutex m_mutex;
};

// 初始化
shared_ptr<Singleton> Singleton::instance;
mutex Singleton::m_mutex;
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main() 
{
	cout << "main开始" << endl;
	thread t1([] {
		shared_ptr<Singleton> s1 = Singleton::getInstance();
	});
	thread t2([] {
		shared_ptr<Singleton> s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	shared_ptr<Singleton> s3 = Singleton::getInstance();
	cout << "地址为:" << s3 << endl;
	cout << "main结束" << endl;
	return 0;
}

       应用智能指针后,在程序结束时,它自动进行资源的释放,解决了内存泄漏的问题。

2.3.2 静态嵌套类方案

       类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton* getInstance() {
		// 若为空则创建
		if (instance == nullptr) {
			// 加锁保证线程安全
			// 如果两个线程同时进行到这一步,一个线程继续向下执行时,另一个线程被堵塞
			// 等锁解除后,被堵塞的线程就会跳过下面的if了,因为此时实例已经构建完毕
			lock_guard<mutex> l(m_mutex);
			if (instance == nullptr) {
				cout << "实例为空,开始创建。" << endl;
				instance = new Singleton();
				cout << "地址为:" << instance << endl;
				cout << "创建结束。" << endl;
			}
		}
		else {
			cout << "已有实例,返回。" << endl;
		}
		return instance;
	}
private:
	// 私有构造函数
	Singleton() {
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton() {
		cout << "析构函数启动。" << endl;
	};
	// 定义一个删除器
	class Deleter {
	public:
		Deleter() {};
		~Deleter() {
			if (instance != nullptr) {
				cout << "删除器启动。" << endl;
				delete instance;
				instance = nullptr;
			}
		}
	};
	// 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
	static Deleter m_deleter;
private:
	// 静态私有对象
	static Singleton* instance;
	// 锁
	static mutex m_mutex;
};

// 初始化
Singleton* Singleton::instance = nullptr;
mutex Singleton::m_mutex;
Singleton::Deleter Singleton::m_deleter;

      main.h同2.2中的一致,结果如下,可以看出,当嵌套类Deleter对象销毁时,其析构函数执行的实例删除操作也完成了。

2.4 基于局部静态对象的懒汉实现

       C++11后,规定了局部静态对象在多线程场景下的初始化行为,只有在首次访问时才会创建实例,后续不再创建而是获取。若未创建成功,其他的线程在进行到这步时会自动等待。注意C++11前的版本不是这样的。

       因为有上述的改动,所以出现了一种更简洁方便优雅的实现方法,基于局部静态对象实现。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton& getInstance() {
		cout << "获取实例" << endl;
		static Singleton instance;
		cout << "地址为:" << &instance << endl;
		return instance;
	}
private:
	// 私有构造函数
	Singleton() {
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton() {
		cout << "析构函数启动。" << endl;
	};
};
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main()
{
	cout << "main开始" << endl;
	thread t1([] {
		Singleton &s1 = Singleton::getInstance();
	});
	thread t2([] {
		Singleton &s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	cout << "main结束" << endl;
	return 0;
}

       从结果中可以看出,构造函数启动了一次,另一个线程直接获取了地址。并且当程序结束时,进行了自动释放。

三、饿汉式实现

3.1 饿汉基础实现

       饿汉和懒汉的差别就在于,饿汉提前进行了创建,所以它的基础实现也不是很复杂,如下所示。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton* getInstance() {
		cout << "获取实例" << endl;
		cout << "地址为:" << instance << endl;
		return instance;
	}
private:
	// 私有构造函数
	Singleton() {
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton() {
		cout << "析构函数启动。" << endl;
	};
private:
	// 静态私有对象
	static Singleton* instance;
};

// 初始化
Singleton* Singleton::instance = new Singleton();
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main()
{
	cout << "main开始" << endl;
	thread t1([] {
		Singleton* s1 = Singleton::getInstance();
	});
	thread t2([] {
		Singleton* s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	cout << "main结束" << endl;
	return 0;
}

       输出结果中可知,main还没开始,实例就已经构建完毕,获取实例的函数也不需要进行判空操作,因此也就不用双重检测锁来保证线程安全了,它本身已经是线程安全状态了。

       但是内存泄漏的问题还是要解决的,这点同懒汉是一样的。

3.2 基于资源管理的饿汉实现

       内存泄漏解决方法有两个:智能指针&静态嵌套类。

3.2.1 智能指针方案

       将实例指针更换为智能指针,另外智能指针在初始化时,还需要人为添加公有的毁灭函数,因为析构函数私有化了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static shared_ptr<Singleton> getInstance() {
		cout << "获取实例" << endl;
		cout << "地址为:" << instance << endl;
		return instance;
	}
	// 毁灭实例
	static void destoryInstance(Singleton* x) {
		cout << "自定义释放实例" << endl;
		delete x;
	}
private:
	// 私有构造函数
	Singleton() {
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton() {
		cout << "析构函数启动。" << endl;
	};
private:
	// 静态私有对象
	static shared_ptr<Singleton> instance;
};

// 初始化
shared_ptr<Singleton> Singleton::instance(new Singleton(), destoryInstance);
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main()
{
	cout << "main开始" << endl;
	thread t1([] {
		shared_ptr<Singleton> s1 = Singleton::getInstance();
	});
	thread t2([] {
		shared_ptr<Singleton> s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	cout << "main结束" << endl;
	return 0;
}

       加入了智能指针后,不出意外地进行了自动的资源释放。

3.2.2 静态嵌套类方案

       类中定义一个嵌套类,初始化该类的静态对象,当程序结束时,该对象进行析构的同时,将单例实例也删除了。

//Singleton.h
/****************************************************/
#include <iostream>
#include <mutex>

using namespace std;

// 单例模式演示类
class Singleton
{
public:
	// 公有接口获取唯一实例
	static Singleton* getInstance() {
		cout << "获取实例" << endl;
		cout << "地址为:" << instance << endl;
		return instance;
	}
private:
	// 私有构造函数
	Singleton() {
		cout << "构造函数启动。" << endl;
	};
	// 私有析构函数
	~Singleton() {
		cout << "析构函数启动。" << endl;
	};
	// 定义一个删除器
	class Deleter {
	public:
		Deleter() {};
		~Deleter() {
			if (instance != nullptr) {
				cout << "删除器启动。" << endl;
				delete instance;
				instance = nullptr;
			}
		}
	};
	// 删除器是嵌套类,当该静态对象销毁的时候,也会将单例实例销毁
	static Deleter m_deleter;
private:
	// 静态私有对象
	static Singleton* instance;
};

// 初始化
Singleton* Singleton::instance = new Singleton();
Singleton::Deleter Singleton::m_deleter;
//main.h
/****************************************************/
#include <iostream>
#include "Singleton.h"

using namespace std;

int main()
{
	cout << "main开始" << endl;
	thread t1([] {
		Singleton* s1 = Singleton::getInstance();
	});
	thread t2([] {
		Singleton* s2 = Singleton::getInstance();
	});
	t1.join();
	t2.join();
	cout << "main结束" << endl;
	return 0;
}

       同懒汉的一样,不多做阐述。

四、总结

       上述讲了这么多关于单例模式的内容,我尽可能地将测试的结果也同步展示了,目的就是帮助大家更好地理解。文中所有的代码都是完整的,可以直接复制到自己的项目中测试验证下。

       最后,如果说让我选择用什么样的实现,那我选择用局部静态对象的方法,代码简洁,线程安全,内存无泄漏,有什么理由说不呢?除非你是C++11之前的版本。。。。

       如果文章帮助到你了,可以点个赞让我知道,我会很快乐~加油!

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

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

相关文章

具有大部分相似的项目之间的项目整合

1.将多个项目文件合并&#xff0c;如&#xff1a;c2文件夹和c3文件夹不同&#xff0c;其余文件都是可以一起用的 2. router/index.js (1) 声明 公用路由&#xff0c;如 const common [{// :xxxx 代表任意匹配&#xff08;输啥都可以匹配&#xff09;path: /:channel/login,c…

html 浏览器存储方式

浏览器有三种本地存储方式&#xff1a; 1、localstorage 2、sessionStorage 3、cookie 浏览器 F12 打开调试模式&#xff0c;可以看到&#xff1a; 点击对应域名&#xff0c;可以看到当前域名下存储的数据&#xff0c;是以key&#xff0c;value形式存储的。 三种方式的共同…

某程序员去华为面试,因为错了一道题而被淘汰

题目有一道数学的逻辑题&#xff0c;这种提一般智商测试或者公务员考试中经常见到&#xff0c;传说华为有道面试题是这样的&#xff0c;求出下划线的数字应该是多少&#xff1f;请准备好纸和笔&#xff0c;思考10分钟&#xff0c;看看你能否得出正确答案。1分钟后。。。2分钟后…

MySQl学习(从入门到精通11)

MySQl学习&#xff08;从入门到精通11&#xff09;第 14 章_视图1. 常见的数据库对象2. 视图概述2. 1 为什么使用视图&#xff1f;2. 2 视图的理解3. 创建视图3. 1 创建单表视图3. 2 创建多表联合视图3. 3 基于视图创建视图4. 查看视图5. 更新视图的数据5. 1 一般情况5. 2 不可…

餐饮企业数据可视化大屏(智慧餐饮)

随着信息技术的深入发展&#xff0c;数据大屏的适用场景日益广泛&#xff0c;集工作汇报、实时监控和预测分析等功能于一身。 数据可视化的本质是视觉对话&#xff0c;数据可视化将数据分析技术与图形技术结合&#xff0c;清晰有效地将分析结果信息进行解读和传达。 当前很多餐…

第八天字符串

344.反转字符串力扣题目链接(opens new window)编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。你可以假设数组中…

数据结构|绪论

&#x1f525;Go for it!&#x1f525; &#x1f4dd;个人主页&#xff1a;按键难防 &#x1f4eb; 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x1f4d6;系列专栏&#xff1a;数据结构与算法 &#x1f52…

基于STM32设计的音乐播放器

一、项目背景与设计思路 1.1 项目背景 时代进步,科学技术的不断创新,促进电子产品的不断更迭换代,各种新功能和新技术的电子产品牵引着消费者的眼球。人们生活水平的逐渐提高,对娱乐消费市场需求日益扩大,而其消费电子产品在市场中的占有份额越来越举足轻重。目前消费电…

FPGA纯verilog代码读写N25Q128A QSPI Flash 提供工程源码和技术支持

目录1、N25Q128A芯片解读2、N25Q128A读写时序3、整体设计思路架构4、verilog读写Flash驱动设计5、verilog读写Flash控制器设计6、FIFO缓存设计7、串口输出Flash读取数据8、vivado工程介绍9、上板调试验证并演示10、福利&#xff1a;工程源码获取1、N25Q128A芯片解读 N25Q128A的…

一篇文章学懂C++和指针与链表

指针 目录 指针 C的指针学习 指针的基本概念 指针变量的定义和使用 指针的所占的内存空间 空指针和野指针 const修饰指针 指针和数组 指针和函数 指针、数组、函数 接下来让我们开始进入学习吧&#xff01; C的指针学习 指针的基本概念 指针的作用&#xff1a;可…

K8S集群管理平台Rancher(2):安装与使用

Rancher 是一个容器管理平台。Rancher 简化了使用 Kubernetes 的流程。 下面记录一下手动安装Rancher的步骤 一、docker安装rancher 拉取rancher镜像 docker pull rancher/rancher运行rancher容器 sudo docker run -d --restartalways \ -v /mydata/docker/rancher_data…

基于Apache Maven构建多模块项目

title: 基于Apache Maven构建多模块项目 date: 2022-04-10 00:00:00 tags: Apache Maven多模块 categories:Maven 介绍 多模块项目由管理一组子模块的聚合器 POM 来构建。在大多数情况下聚合器位于项目的根目录中&#xff0c;并且必须是 pom 类型的项目。子模块是常规的 Mave…

excel数据整理:合并计算快速查看人员变动

相信大家平时在整理数据时&#xff0c;都会对比数据是否有重复的地方&#xff0c;或者该数据与源数据相比是否有增加或者减少。数据量不大还好&#xff0c;数据量大的话&#xff0c;对比就比较费劲了。接下来我们将进入数据对比系列课程的学习。该系列一共有两篇教程&#xff0…

ESP8266 + STC15+ I2C OLED带网络校时功能的定时器时钟

ESP8266 + STC15+ I2C OLED带网络校时功能的定时器时钟 📍相关篇《ESP8266 + STC15基于AT指令通过TCP通讯协议获取时间》 📌ESP8266 AT固件基于安信可AT固件,相关刷AT固件可以参考《NodeMCU-刷写AT固件》 🔖STC15 单片机采用的是:STC15F2K60S2 晶振频率采用内部:22.11…

Java面试——多线程并发篇

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

滑动窗口算法

滑动窗口 滑动窗口有俩种&#xff1a;定窗口和不定窗口。滑动窗口说白了就是双指针的运用。 定窗口说明是一个特定窗口大小&#xff0c;通常用来解决相邻的元素&#xff0c;最大值&#xff0c;最小值。 不定窗口说的是&#xff0c;先由右指针去找到第一个能够符合题意的位置…

【微信小程序】在WXML文件中显示JS文件中全局变量的方法

前言我们知道在wxml中可以通过数据绑定的方法来获取到js文件中data里面的数据&#xff0c;并且显示到wxml界面&#xff0c;那么我们该如何在wxml中显示js文件里面的全局变量呢&#xff1f;显示data里面数据的方法在wxml种我们可以显示js代码中data代码段中的变量。具体的操作是…

《跃迁:从技术到管理的硅谷路径》读后感

一、技术管理 1.技术管理包含两层含义&#xff1a; 一层是管理自己和团队的技术&#xff0c;进行技术选型&#xff0c;在正确的场景使用最适合的技术&#xff0c;保证程序简捷、强壮、可维护&#xff0c;最终完成产品的上线另一层是管理技术团队&#xff0c;帮助团队成员成长…

【面试题】2023 vue高频面试知识点汇总

一、MVVM原理在Vue2官方文档中没有找到Vue是MVVM的直接证据&#xff0c;但文档有提到&#xff1a;虽然没有完全遵循MVVM模型&#xff0c;但是 Vue 的设计也受到了它的启发&#xff0c;因此在文档中经常会使用vm(ViewModel 的缩写) 这个变量名表示 Vue 实例。为了感受MVVM模型的…

十二款硬盘数据恢复软件!恢复数据,最适合你的是这一款

硬盘数据发生丢失&#xff0c;通过快捷键、回收站、备份都无法恢复回来。对于电脑小白来说&#xff0c;最好的方法就是通过专业软件恢复数据。市面上很多硬盘数据恢复软件&#xff0c;我们大多不清楚它们有哪些功能&#xff0c;好不好用。 为此&#xff0c;我尝试了十二款数据…