c++单例实践

news2024/11/24 17:15:41

C++单例实践

在日常开发中,虽然太多的单例调用会让代码的耦合度变高,但是例如日志类这种,单例模式就变得非常有。所以这篇文章为大家介绍static 关键字相关知识以及如何实现自己的C++单例类。

static关键字

首先让我们请出今天的主角: static。C++中有一个关键字——static,用static修饰的变量或者函数,都会变得不同。根据cpp reference中关于static的描述,static主要有几个作用:

  1. 使全局变量变为内部链接性。
  2. 修饰块作用域变量,其静态存储时间跟随程序并且只会初始化一次。
  3. 修饰类成员,使其与类相关,而非对象。

修饰全局变量

首先补充一下全局变量相关的基础:
假设你在头文件中定义了一个变量,此时你在多个文件里都包含了这个头文件,因为你想在这些地方都共用这个变量。然后你编译代码,发现编译器报错并提示你“xxx重定义”。

// 1.h
int GlobalVar = 1;

// 1.cpp
#include "1.h"
int LocalVar = GlobalVar;

// main.cpp
#include "1.h"

int main()
{
    int var = GlobalVar;

    return 0;
}

问题出在哪呢?让我们回顾一下预处理相关的知识,包含一个头文件,编译器在预处理阶段就会将头文件中的内容展开。回到刚刚的问题,你在多个文件中都包含这个头文件,此时编译器发现有多个地方都声明并且定义了一个GlobalVar,所以就会报错。那么怎样能够在别的文件中使用这个变量呢?别担心,C++提供了extern关键字,来帮助你使用全局变量:

// 1.h
extern int GlobalVar;

// 1.cpp
#include "1.h"
int GlobalVar = 1;
int LocalVar = GlobalVar;

// main.cpp
#include "1.h"

int main()
{
    int var = GlobalVar;

    return 0;
}

你需要在1.h中使用extern声明这个变量,然后在1.cpp中定义这个全局变量。此时任何包含1.h的地方都能够正常使用这个全局变量了。
回到正轨,用static修饰这个全局变量,会有什么效果呢?用static修饰变量,那么这个变量将会变成内部链接性,什么叫内部链接性呢?就是说这个变量只在当前源文件才被可见,即使用extern修饰也不行

// 1.h
static int StaticGlobalVar = 2;
void funct();

// 1.cpp
#include "1.h"
int GlobalVar = StaticGlobalVar;
void funct()
{
    std::cout << "address of: " << std::addressof(StaticGlobalValue) << std::endl;
}

// main.cpp
#include "1.h"

int main()
{
    funct();
    std::cout << "address of: " << std::addressof(StaticGlobalValue) << std::endl;

    return 0;
}

虽然这里在多个地方使用没有问题,但是打印变量的地址你就会发现,在不同的文件中使用,实际上就相当于是创建了两个变量。

address of: 00007FF6BABC16F8
address of: 00007FF6BABC16F0

所以,这也就是内部链接,也就是内部可见性,外部不可见

修饰块内局部变量

static修饰局部变量,变量的存储周期将会发生变化:从第一次定义这个变量起,到程序结束。

#include <iostream>

void func()
{
    static int a = 0;
    int b = 0;
    a++;
    b++;

    std::cout << "a = " << a << "; b = " << b << std::endl;
}

int main()
{
    func();
    func();
    
    return 0;
}

上面代码运行输出为:

a = 1; b = 1;
a = 2; b = 1;

从这我们可以得知,static修饰局部变量之后,其存储空间变成了Static Storage Duration,也就是随程序退出而结束。

修饰类成员

static修饰类成员,该成员变成类的静态成员,属于类,而非属于对象。当static修饰类的成员函数时,相比于成员变量会有一些限制:类的静态成员函数,只能访问类的静态成员,不能访问非静态成员。 这里不难理解,毕竟在没有实例化对象的时候,类的非静态成员还没有创建,此时通过静态函数访问非静态成员就会导致未定义行为。

#include <iostream>

class MyClass
{
public:
    static void FuncStatic
    {
        std::cout << staticVar << std::endl;

        // error! 静态函数只能访问静态成员
        //std::cout << var << std::endl;
    }


private:
    static int staticVar;
    int var;
};

创建属于自己的单例类

通过对上面的介绍,我们已经拥有了一把能够解决单例模式的利剑,让我们一步一步来创建一个属于自己的单例类。暂且将这个类命名为MyInstanceClass

饿汉式单例

实现一个单例,有以下几个点要求:

  1. 全局只有一个实例
  2. 提供了一个全局访问点来访问该实例

要实现全局只有一个实例,意味着不允许自己创建对象,聪明的你可以想到,将构造函数声明成private,这样外部就没办法调用构造函数,也就谈不上创建对象了。

class MyInstanceClass
{
private:
    MyInstanceClass();

};

到这里,有的同学会问了:“构造函数私有化了,那还怎么创建唯一实例呢?” 还记得我们前面介绍过static变量吗?现在该到他出场的时候了。☝🤓我们可以定义一个static成员变量,众所周知,类的静态成员属于类,而不属于对象,这也就符合我们的要求:全局唯一实例。

class MyInstanceClass
{
private:
    MyInstanceClass();

private:
    static MyInstanceClass instance;
};

PS: 顺带插一句:关于为什么静态成员变量能够调用私有的构造函数,网上说的是,静态成员变量是属于类的,并且这个静态成员变量是由编译器去进行初始化的,这个操作在main函数运行之前执行(别问,问就是编译器做的)。这一块可以看一本经典的书:《程序员的自我修养——链接、装载与库》中的11.4节:

对于每个编译单元(.cpp),GCC编译器会遍历其中所有的全局对象,生成一个特殊的函数,这个特殊函数的作用就是对本编译单元里的所有全局对象进行初始化。我们可以通过对本节开头的代码进行反汇编得到一些粗略的信息,可以看到GCC在目标代码中生成了一个名为_GLOBAL__I_Hw的函数,由这个函数负责本编译单元所有的全局\静态对象的构造和析构

现在,我们实现第二个点:提供一个全局访问点来访问。
我们通过定义一个public的静态成员函数GetInstance来获取这个全局实例。为什么要是静态成员函数呢?关于这个问题,首先要明确一个点,静态成员变量是属于类的。如果声明的不是static函数,那么需要实例化一个对象才能调用,而由于构造函数私有化又不能实例化对象,所以只能使用静态成员函数。此外static成员变量,需要在cpp文件里面进行定义。

// .h
class MyInstanceClass
{
public:
    static MyInstanceClass* GetInstance()
    {
        return &instance;
    }

private:
    MyInstanceClass();

private:
    static MyInstanceClass instance;
};

//.cpp
// 定义
MyInstanceClass MyInstanceClass::instance;

同时为了防止能够通过拷贝构造函数来生成对象,我们显式的将拷贝构造函数和赋值运算符删除

// .h
class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        return instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static MyInstanceClass instance;
};

通过调用MyInstanceClass::GetInstance()来获取这个实例。
看到这里,恭喜你🥳,你创建了一个饿汉型单例。什么叫“饿汉型单例”呢?顾名思义,饿汉饿汉,就是很饿了,马上就要吃东西,也就对应着这个单例实例在软件运行的时候就会创建

懒汉型单例

也许你是一个十分珍惜内存的开发者,这个单例在你不需要的时候,就占用了内存空间,这显然是不符合你的性格。优化!一定要优化🤬!聪明的你又想到了一个办法☝🤓,将成员变量改成指针,在需要用的时候再去创建不就好了。并且提供一个销毁函数,在程序退出的时候,调用析构函数,将占用的资源适当。像下面这样:

// .h
class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        if (instance == nullptr)
        {
            instance = new MyInstanceClass;
        }
        
        return *instance;
    }

    void Destroy()
    {
        if (instance)
        {
            delete instance;
            instance = nullptr;
        }
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static MyInstanceClass* instance;
};

// .cpp
MyInstanceClass* MyInstanceClass::instance = nullptr;

为了防止忘记手动析构变量(不要相信自己一定会记得),你用上智能指针,自动管理内存。

// .h
#include <memory>

class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        if (instance == nullptr)
        {
            instance.reset(new MyInstanceClass);
        }
    
        return *instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static std::unique_ptr<MyInstanceClass> instance;
};

// .cpp
std::unique_ptr<MyInstanceClass> MyInstanceClass::instance;

恭喜你,创建了一个懒汉型单例(在第一次调用全局访问接口的时候,才初始化单例)。现在你拿着你的单例去应用到你的项目里面,发现十分的好用🤤,你尝试应用到更多的场景。正好你有一个应用多线程的项目,你也打蒜用你的单例,问题随之而来。

Double Check Lock Pattern(DCLP)

考虑一个多线程场景,两个线程A、B,在A线程第一次调用GetInstance,并判断instance == nullptr,满足条件准备构造的时候。线程B也调用了GetInstance,而此时线程A调用的instance = new MyInstanceClass;还没有返回,所以instance == nullptr仍然是满足的,此时又会调用一遍构造函数,此时就会导致内存泄漏(因为创建了两次,但是只保存了一个指针的地址)。image.png
为了解决这种多线程问题,你引入常见的处理多线程同步的机制——锁。在判断instance变量是否为nullptr时,加锁。

// .h 
#include <memory>
#include <mutex>

class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        std::lock_guard<std::mutex> locker(mutex_);
        if (instance == nullptr)
        {
            instance.reset(new MyInstanceClass);
        }
    
        return *instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static std::unique_ptr<MyInstanceClass> instance;
    static std::mutex mutex_;
};

// .cpp
std::unique_ptr<MyInstanceClass> MyInstanceClass::instance;
std::mutex MyInstanceClass::mutex_;

但是细心的又双叒想到了问题(盲生,你发现了华点🕵),其实真正的内存创建只会发生一次,但每一次调用不管内存创建有没有执行,都会执行加锁解锁的操作,这是不必要的浪费。你又会说:优化!一定要优化🤬!
于是你选择在加锁之前,再进行一次判空的操作,如果已经初始化完成,就直接返回instance。就不需要每一次都进行昂贵的加解锁操作。

// .h 
#include <memory>
#include <mutex>

class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        if (instance == nullptr)
        {
            std::lock_guard<std::mutex> locker(mutex_);
            if (instance == nullptr)
            {
                instance.reset(new MyInstanceClass);
            }
        }
        
        return *instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static std::unique_ptr<MyInstanceClass> instance;
    static std::mutex mutex_;
};

// .cpp
std::unique_ptr<MyInstanceClass> MyInstanceClass::instance;
std::mutex MyInstanceClass::mutex_;

这种双重检查的操作,我们称之为:Double Check Lock Pattern(DCLP)。然而,DCLP也不像你想象中的那么稳妥,在多线程场景下,仍然是会有问题的。简单来说就是:
instance = new MyInstanceClass
这句代码分成三个步骤:

  1. 创内存
  2. 调构造
  3. 赋变量

但是编译器可能会把第二、第三两个步骤调换顺序,导致另外一个线程获取的变量是一个没有调用构造函数的变量。具体分析可以看文末参考中的第7条链接。
image.png
那么有没有一种方法能够让你放心大胆的在各种场景去使用这个单例呢?答案当然是有,而且还不止一种。

std::call_once

C++11新增了一个函数std::call_once,函数原型如下:

template< class Callable, class... Args >
void call_once( std::once_flag& flag, Callable&& f, Args&&... args );

这个函数保证你所传入的f只调用一次,那么把你的单例类稍作修改,就可以实现只构造一次的需求。

// .h 
class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        static std::once_flag s_flag;
        std::call_once(s_flag, [&]() {
            instance.reset(new Singleton);
        });

        return *instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;

private:
    static std::unique_ptr<MyInstanceClass> instance;
};

// .cpp
std::unique_ptr<MyInstanceClass> MyInstanceClass::instance;

Meyer’s Singleton

第二种方法采用在函数中创建一个静态局部变量的方式,利用函数内的静态变量只有在第一次调用,才会初始化的特性,你可以以一种非常简单的方式来实现单例模式。

// .h 
class MyInstanceClass
{
public:
    static MyInstanceClass& GetInstance()
    {
        static MyInstanceClass instance;
        return instance;
    }

private:
    MyInstanceClass();
    MyInstanceClass(const MyInstanceClass&) = delete;
    MyInstanceClass& operator=(const MyInstanceClass&) = delete;
};

拓展:

Storage Duration & Linkage

此部分内容参考Storage class specifiers - cppreference.com,想要详细了解的同学请到这个链接去查看,本文只做简单介绍。

Storage Duration(存储周期)

存储周期,也就是变量什么时候被销毁,根据不同情况主要分为四种:

  1. 自动存储周期(Automatic Storage Duration)

    这种存储周期一般结束于当前的程序块。例如块作用域中的局部变量在结束当时块时就自动销毁。又比如函数中的形参,在函数结束后,就自动销毁。

  2. 静态存储周期(Static Storage Duration)

    此类存储周期跟随程序的退出而结束。例如全局变量或者static修饰的局部变量。

  3. 线程存储周期(Thread Storage Duration)

    此类存储周期跟随线程的退出而结束。注意,这个仅在C++11及之后版本才存在,因为C++11引入了thread_local修饰符。

  4. 动态存储周期(Dynamic Storage Duration)

    此类存储周期的存储周期取决于使用者。例如手动调用new和delete创建的对象。

Linkage(链接性)

同样,链接性也分成三种:

  1. 无链接性(No Linkage)

    此类链接性代表仅仅只能在同一作用域才能访问。例如函数内的局部变量(没有被explicit修饰),局部类和其成员函数等。

  2. 内部链接性(Internal Linkage)

    能够被当前翻译单元访问称之为内部链接性。例如用static修饰的变量、函数等;

  3. 外部链接性(External Linkage)

    能够被其他翻译单元访问的称之为外部链接性。例如有名命名空间下的类、枚举等,使用extern声明的变量等都具有外部链接性。

Static Members

在声明类成员(成员变量和成员函数)时,在前面加上一个static,即可将此成员定义成一个类的静态成员。静态成员拥有静态存储周期以及内部链接性。

基础

当static修饰类成员时,这个类成员就不再与类的对象(object)相关,而是与类相关。直白一点说就是不管你定义多少个对象,类的静态成员始终只有一个,它是与类相关的。

class MyStaticClass
{
public:
    static void StaticFunc()
    {
        
    }

private:
    static int StaticVar;
};
Static Data Members

将类的成员变量用static修饰,它就成为了一个静态数据成员。需要注意的是,静态数据成员不能是mutable

如何定义以及初始化静态成员

根据给变量添加的不同的修饰符,同样也分几种情况:

  1. 普通静态成员

    这种成员不能在类里进行定义,需要在类外进行定义。但是从C++17开始,在static前面加上inline即可实现在类内定义。在类外的定义语法为:
    类型 类名::变量名;

    // .h
    class MyStaticClass
    {
    public:
        static void StaticFunc()
        {
            
        }
    
    private:
        static int StaticVar;
        inline static int InlineStaticVar = 1; 	// since C++17
    };
    
    //.cpp
    int MyStaticClass::StaticVar = 0;
    
  2. const静态成员

    如果一个整形变量或者枚举类型变量用const进行修饰时,其能够直接在类中进行定义。如果 LiteralType 的静态数据成员声明为 constexpr,则必须在类定义中使用初始化器对其进行初始化,初始化器中的每个表达式都是常量表达式。

    struct X
    {
        const static int n = 1;
        const static int m{2}; // since C++11
        const static int k;
    };
    
如何使用静态成员

访问类的静态成员有两种方式:

  1. 通过限定符(qualified)

    MyStaticClass::Func();

  2. 通过成员访问表达式(.、->)

    MyStaticClass().Func();

最后

通过一步一步的对所写的代码进行优化,我们最后实现了比较完美的单例:代码简单、线程安全、用时初始化,这都是这个单例的优点。希望看到这里,各位看官朋友能够有所收获,谢谢。

创作不易,如果对您有帮助,烦请点赞、收藏、关注支持一下,也欢迎各位大佬指点,谢谢。

参考

  1. Storage class specifiers - cppreference.com
  2. static members - cppreference.com
  3. 类的私有private构造函数 ,为什么要这样做 - onewayheaven - 博客园 (cnblogs.com)
  4. c++ - How static function is accessing private member function(constructor) of a class - Stack Overflow
  5. Initialization - cppreference.com
  6. C++11实现线程安全的单例模式(使用std::call_once)_c++11 线程安全-CSDN博客
  7. C++和双重检查锁定模式(DCLP)的风险_dclp认证-CSDN博客

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

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

相关文章

Brave浏览器:开启隐私保护新时代

今天电脑天空要向大家介绍一款专注隐私安全的浏览器——BraveBrowser&#xff0c;它不仅仅是一个浏览工具&#xff0c;更是你在线隐私的守护者&#xff01; BraveBrowser是一款免费且开源的网络浏览器&#xff0c;它的核心优势在于其强大的广告和跟踪器阻止功能。想象一下&…

数据要素流通交易的场景概述

00前言 数据要素作为国家重点发展方向&#xff0c;如何发挥其数据要素价值是目前研究的重点。核心的观点是流动的数据才能产生价值&#xff0c;如果数据并没有开放、共享那么价值一定是有限的。目前&#xff0c;数据流通类型主要包括数据开放、数据共享和数据交易三种。 数据…

【Python学习手册(第四版)】学习笔记16-函数基础

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文主要介绍Python中函数的基本概念&#xff0c;作用域以及参数传递&#xff0c;函数语法以及def和return语句的操作&#xff0c;函数调用表达式的行为&#xff…

AWS DMS遇到 Error : no handler found for uri

问题描述&#xff1a; 当我按照文档[1]配置AWS DMS 目标端为OpenSearch, 并进行数据迁移的时候&#xff0c;我遇到了如下报错&#xff1a; 00015696: 2024-07-31T03:26:57 [TARGET_LOAD ]E: Elasticsearch:FAILED SourceTable:test TargetIndex:test Operation:INSERT_E…

Mysql80主从复制搭建;遇到问题 Slave_IO_Running: Connecting和Slave_SQL_Running以及解决过程

总结主要步骤 1.配置一个提供复制的账号&#xff1b; 创建用户 CREATE USER replication% IDENTIFIED BY your_password; GRANT REPLICATION SLAVE ON *.* TO replication%; FLUSH PRIVILEGES;2.修改配置 选择模式 主库配置&#xff1b; windows的得话是my.ini文件 默认这个目…

Vue项目学习(1)

1、进入cmd命令行——> vue ui ——>等等操作 2、 3、src目录下 4、vue项目的启动 &#xff08;1&#xff09; &#xff08;2&#xff09; 5、如何更改前端vue项目的端口号&#xff1f;——>去vue.config.js里配置应一个对象

云端医疗解决方案:互联网医院系统的云计算架构与实现

随着云计算技术的成熟和普及&#xff0c;医疗行业开始探索云端解决方案&#xff0c;以应对数据存储、计算能力和系统扩展性等方面的挑战。互联网医院系统作为医疗信息化的重要组成部分&#xff0c;通过云计算架构实现了高效、灵活和可扩展的医疗服务。本文将深入探讨互联网医院…

【vulnhub】DC-2靶机

信息收集 靶机扫描 nmap 192.168.93.1/24 端口扫描 网页访问 发现访问不到&#xff0c;根据显示考虑IP未遵循重定向到域名 在本机的C:\Windows\System32\drivers\etc 修改hosts⽂件&#xff0c;添加192.168.93.136 dc-2 再次进行访问&#xff0c;可以访问到 点击flag&#x…

牛客JS题(十七)总成绩排名

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 引用传值深拷贝合理封装 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charset"utf-8" /></head><body><script type"text/javascript&quo…

ResNet原理

一、ResNet基础架构 残差块 批量归一化层 二、ResNet网络参数详解 三、ResNet总结 残差网络的出现使人们摆脱了深度的束缚&#xff0c;大幅改善了深度神经网络中的模型退化问题&#xff0c;使网络层数从数十层跃升至几百上千层&#xff0c;大幅提高了模型精度&#xff0c;通用…

Linux 下 perf 的使用

目录 一、概述1、perf 作用2、常用的工具集 二、perf 工具的使用1、perf list2、perf stat3、perf top4、perf record/report4.1 perf record4.2 perf report 5、perf annotate 一、概述 1、perf 作用 perf 是一个性能分析工具&#xff08;基于 Linux 内核提供的性能事件 per…

jacodb导入带签名的jar包失败的问题

今天在执行某个项目分析时&#xff0c;100多个jar包只有BC库的三个jar包导入失败。错误如下&#xff1a; [DefaultDispatcher-worker-1 coroutine#1] INFO org.jacodb.impl.storage.PersistenceService - Starting app version [1.4] [DefaultDispatcher-worker-16 coroutine#…

世界上最小,装机量最多的数据库居然是...

最近在根客户沟通数据库选型的时候&#xff0c;聊到了手机应用内置的数据库SQLite。别小看它哈&#xff0c;它可是世界上最小&#xff0c;装机量最多的数据库&#xff0c;只是因为它着实太低调了&#xff0c;低调的连官网都是那么朴素。麻雀虽小&#xff0c;五脏俱全&#xff0…

还在人工代码review?reviewdog和PMD了解一下

前言 代码reivew作为团队协同开发时确保代码质量的手段之一&#xff0c;在软件开发团队中非常常见。特别是对于刚入门不久的团队成员&#xff0c;通过代码review也可一定程度上避免一些低级错误&#xff0c;提升整个部分的代码健壮性。 一般来讲&#xff0c;通过代码review可…

LearnOpenGL-入门章节学习笔记

LearnOpenGL-入门章节学习笔记 简介一、核心模式与立即渲染模式二、扩展三、状态机四、对象 创建窗口一、Main函数——实例化窗口二、Callback Function 回调函数三、processInput 函数 创建三角形一、顶点输入二、顶点着色器三、编译着色器四、片段着色器五、着色器程序六、链…

二叉树——1.翻转二叉树

力扣题目链接 翻转一棵二叉树。 输入&#xff1a; 输出&#xff1a; 可以发现&#xff0c;如何翻转一个二叉树&#xff1f;将每个父节点下的子节点互换就行了&#xff0c;4下面2分支树和7分支树互换&#xff0c;2下面1和3互换&#xff0c;7下面6和9互换。在前面可以自学一下关…

55 函数嵌套定义、可调用对象与修饰器

1 函数嵌套定义 Python 允许函数的嵌套定义&#xff0c;在函数内部可以再定义另外一个函数。 def func(iterable, operator, value): # 自定义函数if operator not in -*/:return operator errordef nested(item): # 嵌套定义函数return eval(repr(item) operator repr(v…

陷入复杂度深渊的ModularRAG..

最近又有一篇ModularRAG的论文&#xff0c;虽然没有太让人汗毛竖起的惊艳&#xff0c;但我想文中的几张配图冷不丁的也着实让部分密集恐惧症患者又一次炸毛了一下吧;)...ps&#xff0c;图画的还是十分规整和可读的&#xff0c;逻辑也很是清晰&#xff0c;为作者的用心点赞&…

如何快速上手Linux操作系统

&#x1f41f;作者简介&#xff1a;&#x1fab4; &#x1f421;&#x1f419;个人主页&#x1f947;&#xff1a;Aic山鱼 &#x1f420;WeChat&#xff1a;z7010cyy &#x1f988;系列专栏&#xff1a;&#x1f3de;️ 前端-JS基础专栏✨前端-Vue框架专栏✨✨前端-Vue3速学专…

OLAP知识地图思考(附地图)

OLAP知识地图思考&#xff08;附地图&#xff09; OLAP技术在当今数据驱动的时代具有举足轻重的地位。它的核心技术模块包括数据存储、计算引擎和查询语言等&#xff0c;这些模块的有效协同是实现高效数据分析的基础。然而&#xff0c;OLAP在运维方面面临着诸多挑战&#xff0c…