计算机网络(10) --- 高级IO

计算机网络(9) --- 数据链路层与MAC帧_哈里沃克的博客-CSDN博客数据链路层与MAC帧https://blog.csdn.net/m0_63488627/article/details/132178583?spm=1001.2014.3001.5501

目录

1.IO介绍

1.IO本质

2.IO模型

2.非阻塞

3.IO多路转接

1.select

编写代码

优缺点

2.poll

编写代码

3.epoll

1.接口

2.实现原理

3.编程


1.IO介绍

1.IO本质

1.如果数据没有出现,那么读取文件其实会被阻塞住,以等待资源的就绪;或者数据还在网络上传输,并没有到来,需要等待数据到来

2.而操作系统给我们的读取接口,其实是对数据的拷贝

本质:IO=等数据到来+数据拷贝

其实拷贝数据是两个硬件之间的传输,对于软件层的我们而言无法进行进一步优化;又因为等待的时间其实比拷贝时间要来的多。所以拷贝在IO中的效率占比不是很大

高效IO本质:减少等待的时间带来的成本

2.IO模型

1.阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式

2.非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询.

3.信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作

4.IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态

5.异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

2.非阻塞

void setNoBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); //得到原先文件描述符的状态
    if (fl < 0)
    {
        std::cerr << "fcntl : " << strerror(errno) << std::endl;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); //追加文件描述符的状态信息
}

int main()
{
    char buffer[1024];
    while (1)
    {
        setNonBlock(0);
        std::cout << ">>>> ";
        fflush(stdout);
        ssize_t s = read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "echo# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "read end" << std::endl;
            break;
        }
        else
        {
            if (errno == EAGAIN)
            {
                std::cout << "没有错,只是没有数据" << std::endl;
            }
            else if (errno == EINTR)
            {
                std::cout << "系统调用被中断" << std::endl;
                continue;
            }
            else
            {
                std::cout << "出错" << std::endl;
                break;
            }
        }
    }
    return 0;
}

F_GETFL:得到文件描述符的状态

F_SETFL:追加文件描述符的状态信息

O_NONBLOCK:非阻塞模式

3.IO多路转接

1.select

1.select表现为等待数据

2.select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
3.程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

1.nfds:监视的多个文件描述符中,最大的文件描述符+1为输入值

2.timeout:等待多个文件描述符时,等待的方式。输入nullptr则表示阻塞式等待;设置传入的数据结构timeval = {0,0}表示非阻塞等待;timeval = {x,0}表示x秒内为阻塞式等待,超过5秒为非阻塞等待

3.返回值:多少文件描述符就绪,则返回多少个文件描述符的数;返回值为0,表示返回超时了;返回值小于0,表示select调用失败

4.select关心的时间只有三类:读、写、异常。fd_set是一个位图,用于表示文件描述符的集合。

5.fd_set:输入的位图参数为自己的需要进行管理的文件描述符置为1;返回则是内核告诉用户哪些文件描述符已经就绪了

编写代码

1.listen套接字也需要被select连接,将其归类为读事件

2.检测事件只有select有这个功能设计,所以需要将连接交给select进行处理

3.操作系统提供的位图大小为1024bite,所以我们需要拿出一个数组fdarray大小也为1024进行管理。

namespace select_ns
{
    static const int defaultport = 8081;
    static const int fdnum = sizeof(fd_set) * 8;
    static const int defaultfd = -1;

    class SelectServer
    {
    public:
        SelectServer(int port = defaultport) : _port(port), _listensock(-1), fdarray(nullptr)
        {
        }
        void Print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < fdnum; i++)
            {
                if (fdarray[i] != defaultfd)
                    std::cout << fdarray[i] << " ";
            }
            std::cout << std::endl;
        }
        void HandlerEvent(fd_set &rfds)
        {
            //? 目前一定是listensock,只有这一个
            if (FD_ISSET(_listensock, &rfds))
            {
                // 走到这里,accept 函数,会不会阻塞???1 0
                // select 告诉我, listensock读事件就绪了
                std::string clientip;
                uint16_t clientport = 0;
                int sock = Sock::Accept(_listensock, &clientip, &clientport); // accept = 等 + 获取
                if (sock < 0)
                    return;
                logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
                // sock我们能直接recv/read 吗?不能,整个代码,只有select有资格检测事件是否就绪
                // 将新的sock 托管给select!
                // 将新的sock托管给select的本质,其实就是将sock,添加到fdarray数组中即可!
                int i = 0;
                for (; i < fdnum; i++)
                {
                    if (fdarray[i] != defaultfd)
                        continue;
                    else
                        break;
                }
                if (i == fdnum)
                {
                    logMessage(WARNING, "server if full, please wait");
                    close(sock);
                }
                else
                {
                    fdarray[i] = sock;
                }

                Print();
            }
        }

        void initServer()
        {
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);
            fdarray = new int[fdnum];
            for (int i = 0; i < fdnum; i++)
                fdarray[i] = defaultfd;
            fdarray[0] = _listensock; // 不变了
        }

        void start()
        {
            for (;;)
            {
                fd_set rfds;
                FD_ZERO(&rfds);
                int maxfd = fdarray[0];

                for (int i = 0; i < fdnum; i++)
                {
                    if (fdarray[i] == defaultfd)
                        continue;
                    FD_SET(fdarray[i], &rfds); // 合法 fd 全部添加到读文件描述符集中

                    if (maxfd < fdarray[i])
                        maxfd = fdarray[i]; // 更新所有fd中最大的fd
                }

                // struct timeval timeout = {1, 0};
                // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout); // ??
                // 一般而言,要是用select,需要程序员自己维护一个保存所有合法fd的数组!
                int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // ??
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "get a new link...");
                    HandlerEvent(rfds);
                    break;
                }
            }
        }
        ~SelectServer()
        {
            if (_listensock < 0)
                close(_listensock);
            if (fdarray)
                delete[] fdarray;
        }

    private:
        int _port;
        int _listensock;
        int *fdarray;
    };
}

优缺点

1.select等待的文件描述符是有上限的,除非重新改内核能提高上限,否则无法解决。

2.需要借助第三方数组对select的文件描述符进行管理

3.需要不断检查不同的位图,进行循环管理,时间成本高

4.select的第一个参数为最大fd+1的目的是:用于select遍历合法文件描述符的范围

2.poll

1.poll解决了select的fd有上限问题

2.解决select需要反复设置fd问题

1.fds:为一个动态数组

2.nfds:fds数组的长度

3.timeout:ms为单位,当数>0在timeout内阻塞,超过时间非阻塞方式进行等待;=0以非阻塞方式进行等待;<0以阻塞方式进行等待

4.pollfd:为一个结构体表示fd和对应的events事件。event表示内核告诉用户哪些事件准备就绪;revent则是输出

特点:输入输出分离,大小可设置

编写代码

namespace poll_ns
{
    static const int defaultport = 8081;
    static const int num = 2048;
    static const int defaultfd = -1;

    using func_t = std::function<std::string (const std::string&)>;

    class PollServer
    {
    public:
        PollServer(func_t f, int port = defaultport) : _func(f), _port(port), _listensock(-1), _rfds(nullptr)
        {
        }
        void initServer()
        {
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);
            _rfds = new struct pollfd[num];
            for (int i = 0; i < num; i++) ResetItem(i);
            _rfds[0].fd = _listensock; // 不变了
            _rfds[0].events = POLLIN;
        }
        void Print()
        {
            std::cout << "fd list: ";
            for (int i = 0; i < num; i++)
            {
                if (_rfds[i].fd != defaultfd)
                    std::cout << _rfds[i].fd << " ";
            }
            std::cout << std::endl;
        }
        void ResetItem(int i)
        {
            _rfds[i].fd = defaultfd;
            _rfds[i].events = 0;
            _rfds[i].revents = 0;
        }
        void Accepter(int listensock)
        {
            logMessage(DEBUG, "Accepter in");
            // 走到这里,accept 函数,会不会阻塞???1 0
            // select 告诉我, listensock读事件就绪了
            std::string clientip;
            uint16_t clientport = 0;
            int sock = Sock::Accept(listensock, &clientip, &clientport); // accept = 等 + 获取
            if (sock < 0)
                return;
            logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);
            // sock我们能直接recv/read 吗?不能,整个代码,只有select有资格检测事件是否就绪
            // 将新的sock 托管给select!
            // 将新的sock托管给select的本质,其实就是将sock,添加到fdarray数组中即可!
            int i = 0;
            for (; i < num; i++)
            {
                if (_rfds[i].fd != defaultfd)
                    continue;
                else
                    break;
            }
            if (i == num)
            {
                logMessage(WARNING, "server if full, please wait");
                close(sock);
            }
            else
            {
                _rfds[i].fd = sock;
                _rfds[i].events = POLLIN;
                _rfds[i].revents = 0;
            }
            Print();
            logMessage(DEBUG, "Accepter out");
        }
        void Recver(int pos)
        {
            logMessage(DEBUG, "in Recver");

            // 1. 读取request
            // 这样读取是有问题的!
            char buffer[1024];
            ssize_t s = recv(_rfds[pos].fd, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?1, 0
            if (s > 0)
            {
                buffer[s] = 0;
                logMessage(NORMAL, "client# %s", buffer);
            }
            else if (s == 0)
            {
                close(_rfds[pos].fd);
                ResetItem(pos);
                logMessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(_rfds[pos].fd);
                ResetItem(pos);
                logMessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            // 2. 处理request
            std::string response = _func(buffer);

            // 3. 返回response
            // write bug
            write(_rfds[pos].fd, response.c_str(), response.size());

            logMessage(DEBUG, "out Recver");
        }
        // 1. handler event rfds 中,不仅仅是有一个fd是就绪的,可能存在多个
        // 2. 我们的select目前只处理了read事件
        void HandlerReadEvent()
        {
            for (int i = 0; i < num; i++)
            {
                // 过滤掉非法的fd
                if (_rfds[i].fd == defaultfd)
                    continue;
                if (!(_rfds[i].events & POLLIN)) continue;
                // 正常的fd
                // 正常的fd不一定就绪了
                // 目前一定是listensock,只有这一个
                if (_rfds[i].fd== _listensock && (_rfds[i].revents & POLLIN))
                    Accepter(_listensock);
                else if(_rfds[i].revents & POLLIN)
                    Recver(i);
                else{}
            }
        }

        void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = poll(_rfds, num, timeout);
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "poll error, code: %d, err string: %s", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "have event ready!");
                    HandlerReadEvent();
                    break;
                }
            }
        }
        ~PollServer()
        {
            if (_listensock < 0)
                close(_listensock);
            if (_rfds)
                delete[] _rfds;
        }

    private:
        int _port;
        int _listensock;
        struct pollfd *_rfds;
        func_t _func;
    };
}

3.epoll

1.接口

epoll_create:创建一个epoll

epoll_ctl:加入准备好的文件描述符

epoll_event:为一个结构体,其中的events表示文件描述符的事件;epoll_data_t为一个联合体。

epfd:表示添加的epoll文件描述符

op:表示添加epoll结构的文件描述符需要进行什么操作

fd:为文件描述符

epoll_wait:捞取准备好的文件描述符进行执行,返回值为可以处理的文件描述符数量

2.实现原理

1.数据一定会从驱动层发送到此操作系统中。

2.先通过epoll_create创建epoll的文件描述符,该文件描述符指向所谓的epoll模型

3.epoll模型中,一旦需要关注某个文件描述符的从套接字处接收,那么通过epoll_ctl能对文件描述符和需要处理的事件一起放入epoll结构体中。由于需要管理,epoll_ctl的过程一并将epoll结构收录到操作系统的epoll模型的红黑树中进行管理。

4.红黑树中的文件描述符如果准备就绪,那么就会通过epoll_wait将epoll的结构插入到准备队列中,那么当启动epoll_wait,就会一连串的进行所加载的文件

3.编程

namespace epoll_ns
{
    static const int defaultport = 8888;
    static const int size = 128;
    static const int defaultvalue = -1;
    static const int defalultnum = 64;

    class EpollServer
    {
    public:
        EpollServer(uint16_t port = defaultport, int num = defalultnum)
            : _num(num), _revs(nullptr), _port(port), _listensock(defaultvalue), _epfd(defaultvalue)
        {
        }

        void initServer()
        {
            // 1. 创建socket
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            // 2. 创建epoll模型
            _epfd = epoll_create(size);
            if (_epfd < 0)
            {
                logMessage(FATAL, "epoll create error: %s", strerror(errno));
                exit(EPOLL_CREATE_ERR);
            }

            // 3. 添加listensock到epoll中!
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = _listensock;
            epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);

            // 4.申请就绪事件的空间
            _revs = new struct epoll_event[_num];
            logMessage(NORMAL, "init server success");
        }

        void HandlerEvent(int readyNum)
        {
            logMessage(DEBUG, "HandlerEvent in");
            for (int i = 0; i < readyNum; i++)
            {
                uint32_t events = _revs[i].events;
                int sock = _revs[i].data.fd;
                if (sock == _listensock && (events & EPOLLIN))
                {
                    //_listensock读事件就绪, 获取新连接
                    std::string clientip;
                    uint16_t clientport;
                    int fd = Sock::Accept(sock, &clientip, &clientport);
                    if (fd < 0)
                    {
                        logMessage(WARNING, "accept error");
                        continue;
                    }
                    // 获取fd成功,可以直接读取吗??不可以,放入epoll
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
                }
                else if (events & EPOLLIN)
                {
                    // 普通的读事件就绪
                }
                else
                {
                    // 其他事件不进行操作
                }
            }
            logMessage(DEBUG, "HandlerEvent out");
        }

        void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = epoll_wait(_epfd, _revs, _num, timeout);
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout ...");
                    break;
                case -1:
                    logMessage(WARNING, "epoll_wait failed, code: %d, errstring: %s", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "have event ready");
                    HandlerEvent(n);
                    break;
                }
            }
        }

        ~EpollServer()
        {
            if (_listensock != defaultvalue)
                close(_listensock);
            if (_epfd != defaultvalue)
                close(_epfd);
            if (_revs)
                delete[] _revs;
        }

    private:
        uint16_t _port;
        int _listensock;
        int _epfd;
        struct epoll_event *_revs;
        int _num;
    };
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

电阻器件的分类

电阻器的种类碳膜电阻膜式电阻器中的一种。气态碳氢化合物在高温和真空中分解&#xff0c;碳沉积在瓷棒或者瓷管上&#xff0c;形成一层结晶碳膜。改变碳膜厚度和用刻槽的方式变更碳膜的长度可以得到不同的阻值。碳膜电阻成本较低&#xff0c;电性能和稳定性较差&#xff0c;一…

Python自动化小技巧21——实现PDF转word功能(程序制作)

案例背景 为什么这个年代PDF转word&#xff0c;某wps居然还要收费.....很多软件都可以实现这个功能&#xff0c;但是效果都有好有坏&#xff0c;而且有的还付费&#xff0c;很麻烦。 那就用python实现这个功能吧&#xff0c;然后把代码打包为.exe的程序&#xff0c;这样随便在…

企业网络日志安全与 EventLog Analyzer

企业的网络日志安全是一项至关重要的任务。随着信息技术的迅猛发展&#xff0c;网络攻击和数据泄露的威胁也与日俱增。为了应对这些威胁&#xff0c;企业需要强大的工具来监控、分析和保护其网络日志。而ManageEngine的EventLog Analyzer正是这样一款卓越的解决方案。 网络日志…

Sentinel 控制台(集群流控管理)

规则配置 要通过 Sentinel 控制台配置集群流控规则&#xff0c;需要对控制台进行改造。我们提供了相应的接口进行适配。 从 Sentinel 1.4.0 开始&#xff0c;我们抽取出了接口用于向远程配置中心推送规则以及拉取规则&#xff1a; DynamicRuleProvider<T>: 拉取规则Dy…

网站和API支持HTTPS,最好在Nginx上配置

随着我们网站用户的增多&#xff0c;我们会逐渐意识到HTTPS加密的重要性。在不修改现有代码的情况下&#xff0c;要从HTTP升级到HTTPS&#xff0c;让Nginx支持HTTPS是个很好的选择。今天我们来讲下如何从Nginx入手&#xff0c;从HTTP升级到HTTPS&#xff0c;同时支持静态网站和…

Spark Standalone环境搭建及测试

&#x1f947;&#x1f947;【大数据学习记录篇】-持续更新中~&#x1f947;&#x1f947; 篇一&#xff1a;Linux系统下配置java环境 篇二&#xff1a;hadoop伪分布式搭建&#xff08;超详细&#xff09; 篇三&#xff1a;hadoop完全分布式集群搭建&#xff08;超详细&#xf…

神经网络学习小记录75——Keras设置随机种子Seed来保证训练结果唯一

神经网络学习小记录75——Keras设置随机种子Seed来保证训练结果唯一 学习前言为什么每次训练结果不同什么是随机种子训练中设置随机种子 学习前言 好多同学每次训练结果不同&#xff0c;最大的指标可能会差到3-4%这样&#xff0c;这是因为随机种子没有设定导致的&#xff0c;我…

Unity3D软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Unity3D是一款全球知名的游戏开发引擎&#xff0c;由Unity Technologies公司开发。它提供了一个跨平台、多功能的开发环境&#xff0c;支持创建2D和3D游戏、交互式应用、虚拟现实、增强现实等多种类型的应用程序。以下是Unity3D…

CFC编程入门_【10分钟学会】

什么是CFC&#xff1a; 【差不多10分钟全学会】 CFC是图形化编程&#xff0c; 跟单片机的连线一样&#xff0c; 唯一的区别&#xff1a;功能块右侧是【只能输出】引脚。 只有左侧引脚可以输入输出。 有哪些控件&#xff1a; 指针&#xff1a;用于拖动功能块。 控制点&#xf…

VR全景:助力乡村振兴,实现可持续发展

引言&#xff1a; 随着科技的飞速发展&#xff0c;虚拟现实&#xff08;VR&#xff09;全景技术正在以惊人的速度改变着我们的生活方式和产业格局。全景技术不仅在娱乐、教育等领域取得了巨大成功&#xff0c;也为乡村振兴提供了全新的机遇。通过以乡村为背景的VR全景体验&…

【Linux】以太网协议以及MTU

以太网协议 数据链路层的功能以太网的数据格式MTUMTU对IP协议的影响MTU对UDP协议的影响MTU对TCP协议的影响 数据链路层的功能 数据链路层的主要功能是&#xff1a;控制链路。包括数据链路的建立、链路的维护和释放。MAC寻址也是它的功能&#xff0c;寻址是指计算机网卡的MAC地…

数据结构(Java实现)-包装类和泛型

包装类 在Java中&#xff0c;由于基本类型不是继承自Object&#xff0c;为了在泛型代码中可以支持基本类型&#xff0c;Java给每个基本类型都对应了 一个包装类型。 基本数据类型和对应的包装类 装箱和拆箱 装箱操作&#xff0c;新建一个 Integer 类型对象&#xff0c;将 i 的…

【趣味随笔】怎么维护自己的电脑?

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

Go并发可视化解释 - Select语句

昨天&#xff0c;我发布了一篇文章&#xff0c;用可视化的方式解释了Golang中通道&#xff08;Channel&#xff09;的工作原理。如果你对通道的理解仍然存在困难&#xff0c;最好呢请在阅读本文之前先查看那篇文章。作为一个快速的复习&#xff1a;Partier、Candier 和 Stringe…

Git 原理与使用

1.版本控制器 所谓的版本控制器&#xff0c;就是能让你了解到⼀个⽂件的历史&#xff0c;以及它的发展过程的系统。通俗的讲就是⼀个可以记录⼯程的每⼀次改动和版本迭代的⼀个管理系统&#xff0c;同时也⽅便多⼈协同作业。 ⽬前最主流的版本控制器就是 Git 。Git 可以控制电脑…

几个nlp的小任务(抽取式问答)

几个nlp的小任务(抽取式问答) 安装库抽取式问答介绍、SQuAD数据集初始化参数加载、导入数据集查看数据集示例加载tokenizer对长文本处理的演示对答案的位置进行验证整合刚才的步骤对数据集中的数据进行预处理加载微调模型设置args 参数使用数据清洗设置训练函数,开始训练安装…

【Git Bash】简明从零教学

目录 Git 的作用官网介绍简明概要 Git 下载链接Git 的初始配置配置用户初始化本地库 Git 状态查询Git 工作机制本地工作机制远端工作机制 Git 的本地管理操作add 将修改添加至暂存区commit 将暂存区提交至本地仓库日志查询版本穿梭 Git 分支查看分支创建与切换分支跨分支修改与…

设计模式之原型模式

文章目录 概述克隆羊问题传统方式解决克隆羊问题传统的方式的优缺点原型模式原理结构图-uml 类图原理结构图说明 原型模式解决克隆羊问题的应用实例原型模式在Spring框架中的应用深入讨论-浅拷贝和深拷贝浅拷贝的介绍深拷贝基本介绍深拷贝应用实例注意事项 概述 原型模式&…

【C语言】操作符大全(保姆级介绍)

&#x1f6a9;纸上得来终觉浅&#xff0c; 绝知此事要躬行。 &#x1f31f;主页&#xff1a;June-Frost &#x1f680;专栏&#xff1a;C语言 &#x1f525;该篇将详细介绍各种操作符的功能。 目录&#xff1a; &#x1f4d8; 前言① 算术操作符②移位操作符③位操作符④赋值操…

Unity——拖尾特效

拖尾是一种很酷的特效。拖尾的原理来自人类的视觉残留&#xff1a;观察快速移动的明亮物体&#xff0c;会看到物体移动的轨迹。摄像机通过调整快门时间&#xff0c;也可以拍出具有拖尾效果的照片&#xff0c;如在城市的夜景中&#xff0c;汽车的尾灯拖曳出红色的线条。 在较老…