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

C++智能指针(三)——unique_ptr初探

与共享指针shared_ptr用于共享对象的目的不同,unique_ptr是用于独享对象。

文章目录

  • 1. unqiue_ptr的目的
  • 2. 使用 unique_ptr
    • 2.1 初始化 unique_ptr
    • 2.2 访问数据
    • 2.3 作为类的成员
    • 2.4 处理数组
  • 3. 转移所有权
    • 3.1 简单语法
    • 3.2 函数间转移所有权
      • 3.2.1 转移至函数体内
      • 3.2.2 转移出函数体
  • 4. Deleter
    • 4.1 default_delete<>
    • 4.2 其他相关资源的Deleters
  • 5. unique_ptr与shared_ptr性能的简单分析
  • 6. 附录
  • 7. 参考文献


1. unqiue_ptr的目的

首先,如果我们在函数中使用普通指针,会有许多问题,比如下面的函数 void f()

void f()
{ClassA* ptr = new ClassA; // create an object explicitly... // perform some operationsdelete ptr; // clean up(destroy the object explicitly)
}

抛开使用普通指针容易忘记使用 delete 导致内存泄漏不谈,如果我们在执行一些操作的时候,出现错误了,那么也会导致内存泄漏。

那么就要引入异常处理操作,比如 try...catch...,那么程序会看起来冗余且复杂。

之前已经讨论过智能指针中的两个 shared_ptrweak_ptr 带来的便捷性,而与共享对象的场景不同,unique_ptr 主要用于独享对象所有权的场景,即程序只有一个unique_ptr拥有该对象的所有权,只能转移所有权,不能共享所有权。与 shared_ptr 没有拥有该对象的智能指针后,释放资源与空间。

比如我们用 unique_ptr 改写上面的代码:

// header file for unique_ptr
#include <memory>
void f()
{// create and initialize an unique_ptrstd::unique<ClassA> ptr(new ClassA);... // perform some operations
}

that’s all,这样我们就省去了delete 与异常处理部分,是不是非常滴nice~


2. 使用 unique_ptr

2.1 初始化 unique_ptr

unique_ptr 不允许使用赋值语法去使用一个普通指针初始化,只能直接将值代入构造函数进行初始化:

std::unique_ptr<int> up = new int; // ERROR
std::unique_ptr<int> up(new int); // OK

当然,一个unique_ptr可以不用一定拥有一个对象,即可以是空的。那就使用默认构造函数进行初始化:

std::unique_ptr<std::string> up;

可以使用 nullptr 赋值,或调用 reset(),这是等价的:

up = nullptr;
up.reset();

2.2 访问数据

与普通指针类似,可以使用 * 解引用出指向对象,也可以使用 -> 访问一个对象的成员:

// create and initialize (pointer to) string:
std::unique_ptr<std::string> up(new std::string("nico"));
(*up)[0] = ’N’; // replace first character
up->append("lai"); // append some characters
std::cout << *up << std::endl; // print whole string

unique_ptr 不允许指针算术运算,比如 ++,这可以看作是 unique_ptr 的优势,因为指针算术运算容易出错。

如果想在访问数据前,检查一个 unique_ptr 是否拥有一个对象,可以调用操作符 bool()

if (up) { // if up is not empty
std::cout << *up << std::endl;
}

当然也可以将 unique_ptr 与 nullptr 比较,或者将 get() 得到的原生指针与 nullptr 比较,以达到检查的目的:

if (up != nullptr) // if up is not empty
if (up.get() != nullptr) // if up is not empty

2.3 作为类的成员

我们将 unique_ptr 应用到类,也可以避免内存泄漏。并且,如果使用 unique_ptr 而不是普通指针,类将不需要析构器,因为对象删除时,其成员也会被自动删除。

除此之外,正常情况下,析构器只有在构造函数完成的情况下才会调用。如果在一个构造函数内部出现异常,析构器只会给那些完全完成构造的对象调用(所以下面的ClassB在构造函数中出现异常时,此时不会调用析构函数)。这就会导致如果使用原生指针,那么构造函数中第一个new成功,第二个new失败就会产生内存泄漏:

class ClassB {
private:
ClassA* ptr1; // pointer members
ClassA* ptr2;
public:
// constructor that initializes the pointers
// - will cause resource leak if second new throws
ClassB (int val1, int val2)
: ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
}
// copy constructor
// - might cause resource leak if second new throws
ClassB (const ClassB& x)
: ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
}
// assignment operator
const ClassB& operator= (const ClassB& x) {
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
}
~ClassB () {
delete ptr1;
delete ptr2;
}
...
};

可以使用 unique_ptr 替换原生指针来避免上述情况:

class ClassB {
private:
std::unique_ptr<ClassA> ptr1; // unique_ptr members
std::unique_ptr<ClassA> ptr2;
public:
// constructor that initializes the unique_ptrs
// - no resource leak possible
ClassB (int val1, int val2)
: ptr1(new ClassA(val1)), ptr2(new ClassA(val2)) {
}
// copy constructor
// - no resource leak possible
ClassB (const ClassB& x)
: ptr1(new ClassA(*x.ptr1)), ptr2(new ClassA(*x.ptr2)) {
}
// assignment operator
const ClassB& operator= (const ClassB& x) {
*ptr1 = *x.ptr1;
*ptr2 = *x.ptr2;
return *this;
}
// no destructor necessary
// (default destructor lets ptr1 and ptr2 delete their objects)
...
};

需要注意,如果不提供拷贝和赋值函数,对于默认的拷贝和赋值拷贝构造函数,显然是不可能的(不可能用一个独享指针拷贝构造一个独享指针),所以,默认只提供移动构造函数。

2.4 处理数组

默认情况下,unique_ptrs 在失去对象的所有权之后,调用 delete,但对于数组,应该是调用 delete[],所以下面的代码是可以编译的,但是有运行时错误的:

std::unique_ptr<std::string> up(new std::string[10]); // runtime ERROR

但其实我们不必像 shared_ptr 一样,在这种情况下,要通过自定义 deleter 来实现 delete[]。C++标准库已经对 unique_ptr 处理数组进行了特化,所以仅需要声明如下就可以:

std::unique_ptr<std::string[]> up(new std::string[10]); // OK

但是需要注意的是,这个特化会导致,我们不能使用 *-> 访问数组,而是要使用 [],这其实就和我们正常访问数组是一样的了:

std::unique_ptr<std::string[]> up(new std::string[10]); // OK
...
std::cout << *up << std::endl; // ERROR: * not defined for arrays
std::cout << up[0] << std::endl; // OK

:最后需要注意,索引的合法性是编码人员需要保证的,错误的索引会带来未定义的结果。


3. 转移所有权

3.1 简单语法

根据前面的讲解,我们知道需要保证没有两个unique_ptrs使用同一个指针初始化:

std::string* sp = new std::string("hello");
std::unique_ptr<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp); // ERROR: up1 and up2 own same data

因为只能独有,不能共享,所以肯定也无法进行一般的拷贝和赋值操作。但C++11有了新的语法——移动语义,这可以让我们使用构造函数和赋值操作来在unique_ptrs中转移对象所有权:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
// copy the unique_ptr
std::unique_ptr<ClassA> up2(up1); // ERROR: not possible
// transfer ownership of the unique_ptr
std::unique_ptr<ClassA> up3(std::move(up1)); // OK

上面是使用移动构造函数,下面使用赋值运算符有类似表现:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2; // create another unique_ptr
up2 = up1; // ERROR: not possible
up2 = std::move(up1); // assign the unique_ptr
// - transfers ownership from up1 to up2

当然,如果up2之前拥有另一个对象的所有权,那么在将up1的所有权转移给up2之后,up2之前拥有的对象就会被释放:

// initialize a unique_ptr with a new object
std::unique_ptr<ClassA> up1(new ClassA);
// initialize another unique_ptr with a new object
std::unique_ptr<ClassA> up2(new ClassA);
up2 = std::move(up1); // move assign the unique_ptr
// - delete object owned by up2
// - transfer ownership from up1 to up2

当然,不能是将普通指针赋值给 unique_ptr,我们可以构造一个新的 unique_ptr来赋值:

std::unique_ptr<ClassA> ptr; // create a unique_ptr
ptr = new ClassA; // ERROR
ptr = std::unique_ptr<ClassA>(new ClassA); // OK, delete old object
// and own new

一个特殊的语法,我们可以使用 release()unique_ptr 拥有的对象所有权还给普通指针,当然也可以用于创建新的智能指针:

std::unique_ptr<std::string> up(new std::string("nico"));
...
std::string* sp = up.release(); // up loses ownership

3.2 函数间转移所有权

分为将所有权转入函数体内,以及从函数内转移出所有权两种情况。

3.2.1 转移至函数体内

下面的代码中,我们使用 sink(std::move(up)) 将函数体外部的up的所有权转移至函数 sink 内:

void sink(std::unique_ptr<ClassA> up) // sink() gets ownership
{
...
}
std::unique_ptr<ClassA> up(new ClassA);
...
sink(std::move(up)); // up loses ownership
...

3.2.2 转移出函数体

比如下面的代码中,source() 返回一个 unique_ptr,我们用 p 接收,就能获取对应对象的所有权:

std::unique_ptr<ClassA> source()
{std::unique_ptr<ClassA> ptr(new ClassA); // ptr owns the new object...return ptr; // transfer ownership to calling function
}
void g()
{std::unique_ptr<ClassA> p;for (int i=0; i<10; ++i) {p = source(); // p gets ownership of the returned object// (previously returned object of f() gets deleted)...}
} // last-owned object of p gets deleted

当然,每一次获取新的对象的所有权,都会把老对象给释放掉,在 g() 函数结束,也会释放最后获得的对象。

这里,不需要再 source() 函数中,使用移动语义,是因为根据C++11语法规则,编译器会自动尝试进行一个移动


4. Deleter

unique_ptr<> 针对初始指针引用对象的类别以及deleter的类型进行模板化:

namespace std {
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
typedef ... pointer; // may be D::pointer
typedef T element_type;
typedef D deleter_type;
...
};
}

对于数组的特化,其有相同的默认deleter,即 default_delete<T[]>

namespace std {
template <typename T, typename D>
class unique_ptr<T[], D>
{
public:typedef ... pointer; // may be D::pointertypedef T element_type;typedef D deleter_type;...};
}

4.1 default_delete<>

下面我们深入研究 unique_ptr 的声明:

namespace std {
// primary template:
template <typename T, typename D = default_delete<T>>
class unique_ptr
{
public:
...
T& operator*() const;
T* operator->() const noexcept;
...
};
// partial specialization for arrays:
template<typename T, typename D>
class unique_ptr<T[], D>
{
public:
...
T& operator[](size_t i) const;
...
}
}

其中,std::default_delete<> 内容如下,对于 T 调用 delete,对于 T[],调用 delete[]

namespace std {
// primary template:
template <typename T> class default_delete {
public:
void operator()(T* p) const; // calls delete p
...
};
// partial specialization for arrays:
template <typename T> class default_delete<T[]> {
public:
void operator()(T* p) const; // calls delete[] p
...
};
}

4.2 其他相关资源的Deleters

这个在 shared_ptr 的讲解中有提到,与 shared_ptr 一致在我们可以自定义 deleter,不同的是,shared_ptr 需要模板中提供deleter的类别。比如使用一个函数对象,传入类的名称:

class ClassADeleter
{
public:
void operator () (ClassA* p) {
std::cout << "call delete for ClassA object" << std::endl;
delete p;
}
};
...
std::unique_ptr<ClassA,ClassADeleter> up(new ClassA());

再比如使用一个函数或lambda表达式,我们可以指定为类似 void(*)(T*)std::function<void(T*)> 或使用 decltype

std::unique_ptr<int,void(*)(int*)> up(new int[10],
[](int* p) {
...
delete[] p;
}); // 1std::unique_ptr<int,std::function<void(int*)>> up(new int[10],
[](int* p) {
...
delete[] p;
}); // 2auto l = [](int* p) {
...
delete[] p;
};
std::unique_ptr<int,decltype(l)>> up(new int[10], l); // 3

还有一个骚操作:使用别名模板以避免指定deleter的类型

template <typename T>
using uniquePtr = std::unique_ptr<T,void(*)(T*)>; // alias template
...
uniquePtr<int> up(new int[10], [](int* p) { // used here
...
delete[] p;
});

5. unique_ptr与shared_ptr性能的简单分析

shared_ptr 类是使用一种非侵入的方式实现的,意味着这个类管理的对象不需要满足一个特定的需求,比如必须一个公共的基类等。这带来的巨大优势就是这个共享指针可以被用于任意类型,包括基础数据类型。因而产生的代价是,shared_ptr 对象内部需要多个成员:

  • 一个指向引用对象的普通指针
  • 一个所有共享指针引用相同的对象的计数器
  • 由于weak_ptr的存在,需要另一个计数器

因此,shared 和 weak 指针内部需要额外的helper对象,内部有指针引用它。这意味着一些特定的优化是不可能的(包括空基类优化,这允许消除任何内存开销)。

unique_ptr 不需要这些开销。它的“智能”是基于特有的构造函数和析构函数,以及拷贝语义的去除。对于一个有着无状态的活空的deleter的unique指针,将会消耗与原生指针相同大小的内存。而且比起使用原生指针和进行手动delete,没有额外的运行时开销。

但是,为了避免不必要开销的引入,你应该使用对于deleters使用函数对象(包括lambda表达式)以带来理想情况下0开销的最佳优化。


6. 附录

A. unique_ptr 操作表


7. 参考文献

《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

相关文章:

C++智能指针(三)——unique_ptr初探

与共享指针shared_ptr用于共享对象的目的不同&#xff0c;unique_ptr是用于独享对象。 文章目录 1. unqiue_ptr的目的2. 使用 unique_ptr2.1 初始化 unique_ptr2.2 访问数据2.3 作为类的成员2.4 处理数组 3. 转移所有权3.1 简单语法3.2 函数间转移所有权3.2.1 转移至函数体内3.…...

Composition Api 与 Options Api 有什么区别?

Vue 3.0采用的Composition API与Vue 2.x使用的Options API在编写Vue组件时有一些区别。 区别&#xff1a; 组织代码的方式不同&#xff1a; Options API&#xff1a;按照选项进行组织&#xff0c;将数据、计算属性、方法等声明在一个对象中。Composition API&#xff1a;按照逻…...

紫光同创FPGA实现UDP协议栈网络视频传输,基于YT8511和RTL8211,提供4套PDS工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的以太网方案紫光同创FPGA精简版UDP方案紫光同创FPGA带ping功能UDP方案 3、设计思路框架OV7725摄像头配置及采集OV5640摄像头配置及采集UDP发送控制视频数据组包数据缓冲FIFOUDP协议栈详解RGMII转GMII动态ARPUDP协议IP地址、端口…...

深度学习简述

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ 🐴作者:秋无之地 🐴简介:CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作,主要擅长领域有:爬虫、后端、大数据开发、数据分析等。 🐴欢迎小伙伴们点赞👍🏻、收藏⭐️、…...

【从零开始学习Redis | 第二篇】Redis中的数据类型和相关命令

前言&#xff1a; Redis是一种快速、高效的开源内存数据库&#xff0c;被广泛用于构建各种类型的应用程序。其被设计成支持多种数据类型&#xff0c;这使得Redis在处理各种场景的数据存储和操作中非常灵活。Redis的数据类型提供了对不同数据结构的直接支持&#xff0c;包括字符…...

数据结构 - 3(链表12000字详解)

一&#xff1a;LinkedList的使用 1.1 ArrayList的缺陷 上篇文章我们已经基本熟悉了ArrayList的使用&#xff0c;并且进行了简单模拟实现。由于其底层是一段连续空间&#xff0c;当在ArrayList任意位置插入或者删除元素时&#xff0c;就需要将后序元素整体往前或者往后搬移&am…...

Jmeter性能测试插件jpgc的安装

一、获取插件包 1.访问官网获取 官网地址&#xff1a; 2.百度网盘下载 链接&#xff1a;百度网盘 请输入提取码 提取码&#xff1a;blmn 二、安装路径 将下载到的plugins-manager.jar插件存放到%JMETER_HOME%/lib/ext目录下 ​ 三、安装插件 1.重启Jmeter 如果已启动了…...

关于safari浏览器浏览html video标签无法正常播放的问题

问题&#xff1a; 前端demo使用一个video标签包含一个非静态资源的mp4文件。在chrome浏览器下可以正常展示&#xff0c;但是safari却不可以。 原因&#xff1a; 1. mp4文件必须用ffmpeg合成的&#xff0c;其他压缩的mp4文件是不可能展示的。请确定mp4文件并用正常的ffmpeg进…...

【C++代码】最大二叉树,合并二叉树,二叉搜索树中的搜索,验证二叉搜索树--代码随想录

题目&#xff1a;最大二叉树 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最大值。递归地在最大值 左边 的 子数组前缀上 构建左子树。递归地在最大值 右边 的 子数组后缀上 构建右子树。 …...

母婴用品会员商城小程序的作用是什么

随着政策放松&#xff0c;母婴行业相比以前迎来了更高的发展空间&#xff0c;由于可以与多个行业连接&#xff0c;因此市场规模也是连年上升&#xff0c;母婴用品是行业重要的分支&#xff0c;近些年从业商家连年增加&#xff0c;但在实际经营中&#xff0c;商家所遇经营痛点也…...

c++初阶--内存管理

目录 c/c 内存分布c内存管理方式new/delete操作内置类型new和delete操作自定义类型 operator new与operator delete函数new和delete的实现原理内置类型自定义类型 malloc/free和new/delete的区别内存泄露什么是内存泄漏&#xff0c;内存泄露的危害如何避免内存泄漏 在c语言中我…...

Burstormer论文阅读笔记

这是CVPR2023的一篇连拍图像修复和增强的论文&#xff0c;一作是阿联酋的默罕默德 本 扎耶得人工智能大学&#xff0c;二作是旷视科技。这些作者和CVPR2022的一篇BIPNet&#xff0c;同样是做连拍图像修复和增强的&#xff0c;是同一批。也就是说同一个方向&#xff0c;22年中了…...

Apifox 学习笔记 - 前置操作之:动态更新请求体中的时间戳

Apifox 学习笔记 - 前置操作之&#xff1a;动态更新请求体中的时间戳 1. 在前置操作中添加一个&#xff1a;自定义脚本或公共脚本2. 定义我们所需的环境变量。3. 在请求参数中使用【时间戳】4. 检验参考资料 1. 在前置操作中添加一个&#xff1a;自定义脚本或公共脚本 2. 定义我…...

2023年9月 青少年软件编程等级考试Scratch二级真题

202309 青少年软件编程等级考试Scratch二级真题&#xff08;电子学会等级考试&#xff09; 试卷总分数&#xff1a;100分 试卷及格分&#xff1a;60 分 考试时长&#xff1a;60 分钟 第 1 题 点击绿旗&#xff0c;运行程序后&#xff0c;舞台上的图形是?( ) A&#xff1a;画…...

12.验证码以及付费代理

文章目录 一、验证码的处理1、验证码概述1、2 什么是图片验证码&#xff1f;1、2 验证码的作用1、3 图片验证码使用场景1、4 图片验证码的处理方案 2、图片在网页页面中的形式2、1 如何进行图片形式的转化 3、打码平台 二、代理的使用2、1 付费代理2、1、1 找付费代理服务站点2…...

使用Plotly可视化

显示项目受欢迎程度 改进图表 设置颜色&#xff0c;字体...

【C语言】结构体、位段、枚举、联合(共用体)

结构体 结构&#xff1a;一些值的集合&#xff0c;这些值称为成员变量。结构体的每个成员可以是不同类型的变量&#xff1b; 结构体声明&#xff1a;struct是结构体关键字&#xff0c;结构体声明不能省略struct&#xff1b; 匿名结构体&#xff1a;只能在声明结构体的时候声…...

“Python+”集成技术高光谱遥感数据处理与机器学习深度应用

涵盖高光谱遥感数据处理的基础、python开发基础、机器学习和应用实践。重点解释高光谱数据处理所涉及的基本概念和理论&#xff0c;旨在帮助学员深入理解科学原理。结合Python编程工具&#xff0c;专注于解决高光谱数据读取、数据预处理、高光谱数据机器学习等技术难题&#xf…...

Excel 转为 PDF,PNG,HTML等文件

1.安装 Spire.XLS for Java,下载jar包 下载地址 2.引入方式一&#xff08;我这里这种方式一直无法引入&#xff0c;都是失败&#xff0c;所以用的方式二&#xff09; <repositories><repository><id>com.e-iceblue</id><name>e-iceblue</na…...

docker中使用GPU+rocksdb

配置环境 delldell-Precision-3630-Tower  ~  lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.6 LTS Release: 20.04 Codename: focaldelldell-Precision-3630-Tower  ~  nvcc --version nvcc: NVIDIA (R) Cuda comp…...

好用的跨平台同步笔记工具,手机和电脑可同步的笔记工具

在这个快节奏的工作环境中&#xff0c;每个人都在寻找一种方便又高效的方式来记录工作笔记。记录工作笔记可以帮助大家统计工作进展&#xff0c;了解工作进程&#xff0c;而如果工作中常在一个地方办公&#xff0c;直接选择电脑或者手机中笔记工具来记录即可&#xff0c;但是对…...

【Python 千题 —— 基础篇】浮点数转换为整数

题目描述 题目描述 整数转换为浮点数。 输入描述 输入一个整数。 输出描述 程序将整数转换为浮点数并输出。 示例 示例 ① 2输出&#xff1a; 2.0代码讲解 下面是本题的代码&#xff1a; # 描述: 整数转换为浮点数。 # 输入: 输入一个整数。 # 输出: 程序将整数转换…...

金融科技论文D部分

总结 以每周为例&#xff0c; 动量因子定义每种货币为前一周的回报率 价值因子定义为当前市值与其区块链中过去 7 天平均链上交易价值 利差因子定义为前 7 天硬币发行总量的负数除以在7天期限开始时未偿还的硬币量。 因素定义 为了避免过拟合&#xff0c;我们试图定义每一…...

Apache Tomcat下载安装配置使用超详细

下载安装 tomcat官网 在此我们以Tomcat 9.0.81为例&#xff0c;点击下载压缩包&#xff0c;解压到自己的文件夹。 tar.gz是linux操作系统下的安装版本。zip是windows系统下的压缩版本。Windows Service Installer是windows操作系统下的exe安装版本。 检查是否配置JDK 1.…...

基于Seata的分布式事务方案

在Seata中&#xff0c;有4种分布式事务实现方案 XA、AT、TCC、Saga 其中XA利用了数据库的分布式事务特性&#xff0c;AT相当于框架去控制事务回滚。TCC手写三个方法&#xff0c;saga手写两个方法。 AT的性能和编写比较折中&#xff0c;是最常用的一种。TCC一些视频教程中介绍…...

指令跳转:原来if...else就是goto

目录 CPU 是如何执行指令的&#xff1f; 从 if…else 来看程序的执行和跳转 如何通过 if…else 和 goto 来实现循环&#xff1f; 小结 你平时写的程序中&#xff0c;肯定不只有 int a 1 这样最最简单的代码或者指令。我们总是要用到 if…else 这样的条件判断语句、while 和…...

【数据库系统概论】第四章数据库安全性

数据库的安全性&#xff1a;保护数据库以防止不合法使用所造成的数据泄露、更改或破坏 grant和revoke语法...

如何正确的关闭Redis服务器

Redis官方原生版本是在Linux平台上开发和测试的&#xff0c;但是大多数初学者都是使用Windows系统来学习如何开发的。因此&#xff0c;官方提供了一个叫做“Microsoft Open Tech Redis”的项目&#xff0c;该项目专门为Windows平台提供了一个官方支持的Redis版本&#xff0c;但…...

MySQL日志管理和权限管理(重点)

目录 一、日志管理1.错误日志2.二进制日志3.慢查询日志 二、权限管理(重点)1.用户登录管理2.创建用户及授权3.刷新权限4.权限简介5.查看权限7.修改密码8、删除用户9、查看密码复杂度 一、日志管理 日志类型 1、错误日志&#xff1a;启动&#xff0c;停止&#xff0c;关闭失败报…...

Maven 使用教程(二)

一、如何创建JAR并将其安装在本地存储库中&#xff1f; 制作JAR文件非常简单&#xff0c;可以通过执行以下命令来完成&#xff1a; mvn package现在可以查看${project.basedir}/target目录&#xff0c;您将看到生成的JAR文件。 现在&#xff0c;您需要将生成的工件&#xff0…...