共享内存详解

news2024/11/25 16:48:31

        共享内存是操作系统在内存中开辟一块空间,通过页表与共享区建立映射关系,使两个进程看到同一份资源,实现进程间通信。

1、创建共享内存 

       

        参数:第一个参数为key,一般使用ftok()函数生成,key值不能冲突,标识共享内存的唯一性

                  第二个参数 size 表示要创建的共享内存的大小。(会向上取整至4KB的整数倍) 

                  第三个参数为标志位,用IPC_CREAT表示如果共享内存不存在就创建,存在就获取。另一个为IPC_EXCL,单独使用没有意义,要和IPC_CREAT配合使用.(IPC_CREAT | IPC_EXCL)表示如果共享内存不存在就创建,存在就出错返回。还可以在后面加上 | mode 表示设置mode权限

         可以看到库里面定义的后三位为0,所以我们用(IPC_CREAT | IPC_EXCL | 0666)可以设置权限为666.

        返回值:成功则1返回共享内存的 id ;失败返回 -1,并设置错误码。

生成创建共享内存需要的 key:

         参数:第一个参数为路径名,第二个参数为一个整数。(两个都可以随便传)

        它是根据传入的这两个参数,生产一个key值,只要我们拿到了一样的 pathname 和 proj_id 就能拿到同一个key,就可以用 shmget 拿到同一块共享内存的id,就可以保证两个进程看到同一份资源。

        返回值:成功返回一个 key 值,失败返回 -1,并设置错误码。

示例:测试ftok

#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

using namespace std;

const char *pathname = "/home/lw/storehouse/process_communication";
const int proj_id = 0x66;

int main()
{
    // 创建5次key,验证只有pathname和proj_id相同,key就相同
    int i = 5;
    while (i--)
    {
        int key = ftok(pathname, proj_id);

        printf("%d\n", key);
    }
    return 0;
}

 示例:测试shmget

#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>

using namespace std;

const char *pathname = "/home/lw/storehouse/process_communication";
const int proj_id = 0x66;

int main()
{

    int key = ftok(pathname, proj_id);

    printf("%d\n", key);

    shmget(key, 4096, IPC_CREAT | 0666);

    return 0;
}

可以看到创建成功。

有关共享内存的命令:

1、ipcs -m :可以看到创建出的共享内存的信息。

        key:key值;shmid:共享内存的id;owner:创建者;perms:权限;

        bytes:大小;nattch:附接数(附接可以看下文);status:状态。

2、ipcrm -m 【shmid】:删除 id 为 shmid 的共享内存。

2、挂接上地址空间

        我们在内存中开辟了内存,之后就需要挂接上我们的地址空间,让它与页表建立映射关系,连到我们的共享区。

        参数:第一个参数:shmid 为 shmget() 创建共享内存时返回的 id。

                   第二个参数:shmaddr 为一个地址,代表你想把共享内存映射到哪个地址上。我们一般设为nullptr,让操作系统帮我们选择。

                  第三个参数:标志位,一般设为0,表示正常读写。 

        返回值:成功就返回已经构建好映射的虚拟地址;失败返回(void*) -1。

示例:使用shmat

#include <iostream>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

using namespace std;

const char *pathname = "/home/lw/storehouse/process_communication";
const int proj_id = 0x66;

int main()
{
    // 创建key
    key_t key = ftok(pathname, proj_id);
    printf("%d\n", key);

    // 创建共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmget success, shmid = %d\n", shmid);

    // 挂接上共享区
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shmat fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmat success\n");


    // 让我们有时间输命令,看到挂接数+1
    sleep(10);
    return 0;
}

 3、使用

        挂接上地址空间后,就可以使用共享内存了。我们使用共享内存不用向管道一样读写,可以把 shmat() 返回的地址看做我们自己 malloc 出的空间来使用。

示例:

客户端:写数据

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建key
    key_t key = ftok("/home/lw/storehouse/process_communication", 0x66);
    printf("%d\n", key);

    // 创建共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmget success, shmid = %d\n", shmid);

    // 挂接上共享区
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shmat fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmat success\n");


    // client 写数据, 从A写到Z
    memset(shmaddr, 0, 4096);
    for(char ch = 'A'; ch <= 'Z'; ++ch)
    {
        shmaddr[ch-'A'] = ch;
    }

    return 0;
}

服务端:读数据 

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建key
    key_t key = ftok("/home/lw/storehouse/process_communication", 0x66);
    printf("%d\n", key);

    // 创建共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmget success, shmid = %d\n", shmid);

    // 挂接上共享区
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shmat fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmat success\n");

    // 服务端读取
    sleep(1);
    printf("server 读取数据: %s\n", shmaddr);


    return 0;
}

4、去挂接 

        我们使用完以后需要将共享内存与我们的共享区去挂接。

         参数:shmaddr 表示挂接时拿到的虚拟地址。

        返回值:成功为0;失败返回 -1,错误码被设置。

服务端示例: 

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建key
    key_t key = ftok("/home/lw/storehouse/process_communication", 0x66);
    printf("%d\n", key);

    // 创建共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmget success, shmid = %d\n", shmid);

    // 挂接上共享区
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shmat fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmat success\n");

    // 服务端读取
    sleep(1);
    printf("server 读取数据: %s\n", shmaddr);

    // 去挂接
    shmdt(shmaddr);
    printf("去挂接成功\n");
    

    return 0;
}

         运行前,我们在命令行输入 while :;do ipcs -m;sleep 1; done  在shell中每隔一秒打印一次共享内存的信息。就可以看到上图,原本客户端和服务端都挂接上了,nattch为2,之后都去挂接就变为0。

5、删除共享内存

        当我们要删除共享内存时,使用函数shmctl()

        shmctl() 可以用来获取共享内存的属性,也可以用来删除。

        参数:shmid 就是共享内存的 id ,cmd 就是选项,填IPC_RMID 表示用来删除共享内存。buf 则是在获取属性时才用到,我们删除共享内存设为 nullptr 即可。

        返回值:成功为0;失败为 -1,并设置错误码。

服务端示例: 

#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <string.h>
#include <string>
#include <unistd.h>

using namespace std;

int main()
{
    // 创建key
    key_t key = ftok("/home/lw/storehouse/process_communication", 0x66);
    printf("%d\n", key);

    // 创建共享内存
    int shmid = shmget(key, 4096, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmget success, shmid = %d\n", shmid);

    // 挂接上共享区
    char* shmaddr = (char*)shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shmat fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return errno;
    }
    printf("shmat success\n");

    // 服务端读取
    sleep(2);
    printf("server 读取数据: %s\n", shmaddr);

    // 去挂接
    shmdt(shmaddr);
    printf("去挂接成功\n");
    
    // 删除共享内存
    sleep(2);
    shmctl(shmid, IPC_RMID, nullptr);
    printf("删除共享内存成功\n");


    return 0;
}

        我们在命令行输入 while :;do ipcs -m;sleep 1; done  在shell中每隔一秒打印一次共享内存的信息。可以看到共享内存被我们删除了 。

6、通过管道实现进程间协同

        我们在使用共享内存时可以感受到,不管有没有数据,或者数据有没有更新,读端都不会等待,而是直接读,而不像管道一样,读端没数据了会等待写端写。

        因此我们可以封装一个管道,实现让写端写好了,读端再读。

        做法:我们可以先创建一个命名管道文件,然后把read()封装成Wait(),写端不写,read()就会等待;然后把write()封装成WakeUp(),写端随便写一个字符,读端就会唤醒。用Wait和WakeUp就能控制共享内存的读写。

示例:

        我们可以先把管道的创建进行封装,封装到Fifo类里面,构造时创建,析构时删除供服务端使用;

同时把对管道的操作也封装进一个类Sync里面,供服务端和客户端使用。

// Fifo.hpp
#pragma once

#include <iostream>
#include <string>
#include <error.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

using namespace std;

#define Mode 0666
#define Path "./fifo"

class Fifo
{
public:
    // 构造时创建命名管道
    Fifo(const string path = Path)
        : _path(path)
    {

        int n = mkfifo(_path.c_str(), Mode);
        if (n < 0)
        {
            printf("create fifo fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
        }
        else
        {
            printf("create fifo successfully\n");
        }
    }

    // 删除管道文件
    ~Fifo()
    {
        unlink(_path.c_str());
        printf("destroy fifo successfully\n");
    }

private:
    string _path;
};


class Sync
{
public:
    Sync(const char* Pathname = Path)
        :pathname(Pathname)
    {}

        // 已只读方式打开命名管道
    int OpenRD()
    {
        int fd = open(pathname, O_RDONLY);
        if (fd < 0)
        {
            printf("open fifo fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
            return -1;
        }
        return fd;
    }

    // 已只写方式打开命名管道
    int OpenWR()
    {
        int fd = open(pathname, O_WRONLY);
        if (fd < 0)
        {
            printf("open fifo fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
            return -1;
        }
        return fd;
    }

    // 当服务器还没读完时,让客户端写等待
    bool Wait(int fd)
    {
        bool ret = true;
        uint32_t c = 0;
        ssize_t n = read(fd, &c, sizeof(uint32_t));
        if (n == 0)
        {
            ret = false;
        }
        else if (n < 0)
        {
            printf("read fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
            return false;
        }
        return ret;
    }

    // 服务器读完时,唤醒客户端写
    void WakeUp(int fd)
    {
        uint32_t c = 0;
        ssize_t n = write(fd, &c, sizeof(uint32_t));
        if (n < 0)
        {
            printf("write fail, errno is %d, errinfo is %s\n", errno, strerror(errno));
            return;
        }
    }

    // 关闭命名管道
    void Close(int fd)
    {
        close(fd);
    }

private:
    const char* pathname;
};

        同时将接口封装成函数方便我们使用

// Comm.hpp

// 将key转化为16进制,方便与ipcs -m 查看的信息对比
string ItoH(int x)
{
    char buffer[1024];

    snprintf(buffer, sizeof(buffer), "0x%x", x);
    return buffer;
}

// 封装ftok,获取key
key_t GetKey()
{
    key_t k = ftok(pathname, proj_id);
    if(k < 0)
    {
        printf("get key fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        exit(errno);
    }

    return k;
}

// 封装shmget,获取shmid
int _CreateShm(key_t key, int size, int flag)
{
    int shmid = shmget(key, size, flag);
    if(shmid < 0)
    {
        printf("shmget fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        exit(errno);
    }
    return shmid;
}

// 给服务器端提供,创建共享内存,已存在出错返回
int CreateShm(key_t key, int size)
{
    return _CreateShm(key, size, IPC_CREAT | IPC_EXCL | 0666);
}

// 给客户端提供,获取服务端创建的共享内存的shmid
int GetShm(key_t key, int size)
{
    return _CreateShm(key, size, IPC_CREAT);
}

// 封装shmctl,删除共享内存
void DeleteShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if(n < 0)
    {
        printf("DeleteShm fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        exit(errno);
    }
}

// 封装shmat,挂接共享内存
void* ShmAttach(int shmid)
{
    void* shmaddr = shmat(shmid, nullptr, 0);
    if((long)shmaddr == -1)
    {
        printf("shm attach fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        exit(errno);
    }
    return shmaddr;
}

// 封装shmdt,去挂接共享内存
void ShmDetach(const void* shmaddr)
{
    int n = shmdt(shmaddr);
    if(n == -1)
    {
        printf("shm detach fail, errno is %d, errstr is %s\n", errno, strerror(errno));
        return;
    }
}

服务器:1、负责打开、关闭管道

               2、负责打开、关闭共享内存

               3、负责读取数据

#include "Comm.hpp"
#include "Fifo.hpp"

int main()
{
    Fifo fifo;
    Sync syn;

    int rfd = syn.OpenRD();
    printf("成功打开fifo\n");
    key_t key = GetKey();
    cout << "成功创建key, key = " << ItoH(key) << endl;
    const int size = 4096;
    sleep(2);
    int shmid = CreateShm(key, size);
    cout << "成功创建共享内存" << endl;
    sleep(2);

    char* shmaddr = (char*)ShmAttach(shmid);
    cout << "成功挂接共享内存" << endl;
    sleep(2);

    // 读
    while(1)
    {
        bool ret = syn.Wait(rfd);
        if(!ret)
            break;
        printf("%s\n", shmaddr);
    }

    ShmDetach((void*)shmaddr);
    cout << "成功去挂接共享内存" << endl;
    sleep(2);

    DeleteShm(shmid);
    cout << "成功删除共享内存" << endl;

    syn.Close(rfd);

    return 0;
}

客户端:负责写

#include "Comm.hpp"
#include "Fifo.hpp"

int main()
{
    Sync syn;

    int wfd = syn.OpenWR();

    key_t key = GetKey();
    cout << "成功创建key, key = " << ItoH(key) << endl;
    const int size = 4096;
    sleep(2);

    int shmid = GetShm(key, size);
    cout << "成功获得共享内存" << endl;
    sleep(2);

    char* shmaddr = (char*)ShmAttach(shmid);
    cout << "成功挂接共享内存" << endl;
    sleep(2);

    // 写
    string str;
    for(char ch = 'A'; ch <= 'Z'; ++ch)
    {
        str += ch;
        memcpy(shmaddr, str.c_str(), str.size());
        shmaddr[str.size()] = '\0';
        //printf("%s\n", shmaddr);
        syn.WakeUp(wfd);
        usleep(100000);
    }

    ShmDetach((void*)shmaddr);
    cout << "成功去挂接共享内存" << endl;
    sleep(2);

    syn.Close(wfd);
    return 0;
}

运行结果:

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

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

相关文章

副业天花板流量卡推广,小白也可轻松操作

在如今的互联网时代&#xff0c;手机已经不仅仅是一款工具&#xff0c;更像是我们生活中的一部分&#xff0c;那么手机卡也是必需品&#xff0c;但存在的问题就是:很多手机卡的月租很贵&#xff0c;流量也不够用。所以大家都在寻找一个月租低&#xff0c;流量多的卡&#xff0c…

Java-Doc

Java-Doc javdoc命令是用来生成自己的API文档的 参数信息&#xff1a;author作者名version版本号since知名需要最早使用的jdk版本param参数名return返回值情况throws异常抛出情况 1.参数信息的使用&#xff1a; 未完待续... ...

【好消息】思维100活动历年真题模拟题700多道上线了,供反复吃透

今天是星期五&#xff0c;距离4月20日举办的上海小学生 2024年春季思维100活动线上比赛还有8天的时间&#xff0c;明天、后天的周末是可以用来备考的大块时间&#xff0c;报名了的同学要充分利用了。 为了帮助各位小朋友了解思维100活动的历年考试真题、官方发布的参考样题&…

【数组】5螺旋矩阵

这里写自定义目录标题 一、题目二、解题精髓-循环不变量三、代码 一、题目 给定⼀个正整数 n&#xff0c;⽣成⼀个包含 1 到 n^2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的正⽅形矩阵。 示例: 输⼊: 3 输出: [ [ 1, 2, 3 ], [ 8, 9, 4 ], [ 7, 6, 5 ] ] 二、解题精髓…

docker最简单教程(使用dockerfile构建环境)

一 手里有的东西 安装好的docker+dockerfile 二 操作 只需要在你的dockerfile文件下执行命令 docker build -t="xianhu/centos:gitdir" . 将用户名、操作系统和tag进行修改就可以了,这就相当于在你本地安装了一个docker环境,然后执行 docker run -it xianhu/ce…

阿里云4核16G服务器可以用来做什么?

阿里云4核16G服务器可以用来做什么&#xff1f;可用来搭建游戏服务器&#xff0c;阿里云4核16G服务器10M带宽30元1个月、90元3个月&#xff0c;优惠活动 aliyunfuwuqi.com/go/youhui 阿里云4核16G服务器可以用来做什么&#xff1f;除了搭建游戏服务器&#xff0c;还可以用来哪…

DVWA靶场的下载与搭建

目录 什么是靶场 DVWA靶场下载 下载地址 安装 什么是靶场 靶场就是人为提供的带有安全漏洞的服务&#xff0c;每一个学习者都可以在本地快速搭建来实操&#xff0c;回溯漏洞的发生原理以及操作方式。DVWA靶场呢就是一个可以通过浏览器访问的拥有可视化页面的web靶场。 DVW…

PostgreSQL入门到实战-第二十一弹

PostgreSQL入门到实战 PostgreSQL中表连接操作(五)官网地址PostgreSQL概述PostgreSQL中RIGHT JOIN命令理论PostgreSQL中RIGHT JOIN命令实战更新计划 PostgreSQL中表连接操作(五) 使用PostgreSQL RIGHT JOIN连接两个表&#xff0c;并从右表返回行 官网地址 声明: 由于操作系统…

vue3实现时钟效果

鼬鼬鼬鼬鼬被提需求了&#xff01;&#xff01;&#xff01; 产品&#xff1a;你学什么的&#xff1f; 我&#xff1a;跟CV有点关系 产品&#xff1a;control C加control V是吧 我&#xff1a;对对对 效果 时间实时变化&#xff1a; 页面部分 <template><div clas…

性能分析-数据库(安装、索引、sql、执行过程)与磁盘知识(读、写、同时读写、内存速度测试)

数据库 数据库&#xff0c;其实是数据库管理系统dbms。 数据库管理系统&#xff0c; 常见&#xff1a; 关系型数据库&#xff1a; mysql、pg、 库的表&#xff0c;表与表之间有关联关系&#xff1b; 表二维表统一标准的SQL&#xff08;不局限于CRUD&#xff09;非关系型数据…

【40分钟速成智能风控10】风控大数据体系2

目录 ​编辑 特征工程方法 统计量 离散化 时间周期趋势 交叉项 隐性特征 用户画像 特征工程方法 在模型圈内有这么一句俗话&#xff0c;“特征决定了模型的上限&#xff0c;而算法只是逼近这个上限”&#xff0c;由此可见特征工程在风控建模中的重要程度。特征工程的本…

调用R语言并提供Rest接口

文章目录 一、安装R语言环境二、qdiabetes三、安装Python环境四、提供Rest接口 一、安装R语言环境 安装 sudo apt-get update sudo apt-get install r-base/home/rscript/script.R # script.R cat("Hello, World!\n")测试 Rscript /home/rscript/script.R二、qdi…

如何开辟动态二维数组(C语言)

1. 开辟动态二维数组 C语言标准库中并没有可以直接开辟动态二维数组的函数&#xff0c;但我们可以通过动态一维数组来模拟动态二维数组。 二维数组其实可以看作是一个存着"DataType []"类型数据的一维数组&#xff0c;也就是存放着一维数组地址的一维数组。 所以&…

基于centos7安装docker+k8s+KubeSphere

实验环境&#xff1a;&#xff08;每个服务器推荐内存为8G&#xff09; 服务器 ip地址 主机名 centos7 192.168.80.1…

面试: 单例模式

目录 一、饿汉单例&#xff08;实现Serializable&#xff09; 1、破坏单例的三种情况 &#xff08;1&#xff09;反射破坏单例 &#xff08;2&#xff09;反序列化破坏单例 &#xff08;3&#xff09;Unsafe破坏单例 2、饿汉单例&#xff08;利用枚举实现&#xff09; 二…

44.基于SpringBoot + Vue实现的前后端分离-汽车租赁管理系统(项目 + 论文PPT)

项目介绍 本站是一个B/S模式系统&#xff0c;采用SpringBoot Vue框架&#xff0c;MYSQL数据库设计开发&#xff0c;充分保证系统的稳定性。系统具有界面清晰、操作简单&#xff0c;功能齐全的特点&#xff0c;使得基于SpringBoot Vue技术的汽车租赁管理系统设计与实现管理工作…

吴恩达机器学习:均值聚类法(K-means Clustering)

在本练习中&#xff0c;您将实现K-means算法并将其用于图像压缩。 您将从一个样本数据集开始&#xff0c;该数据集将帮助您直观地了解K-means算法的工作原理。之后&#xff0c;您将使用K-means算法进行图像压缩&#xff0c;将图像中出现的颜色数量减少到该图像中最常见的颜色。…

基于Springboot的网上商品订单转手系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的网上商品订单转手系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

Excel---一个工作簿中的多个sheet合并成一个PDF

0 Preface/Foreword 1 操作方法 1.1 方法一 文件》 导出 》创建PDF/XPS 》 选项 》发布内容 》“整个工作簿” 1.2 方法二 文件》 打印》 打印机选项中&#xff0c;选择一种PDF阅读器 》设置选项中&#xff0c;选择打印整个工作簿。

二维数组中的查找

&#x1f600;前言 在解决问题时&#xff0c;我们经常会遇到需要在二维数组中查找特定元素的情况。然而&#xff0c;如果直接使用暴力搜索&#xff0c;即遍历整个数组寻找目标元素&#xff0c;可能会导致时间复杂度较高&#xff0c;效率不高。然而&#xff0c;对于给定的二维数…