Linux——日志的编写与线程池

news2024/11/26 4:53:44

目录

前言

一、日志的编写

二、线程池

1.线程池基本原理

2.线程池作用 

3.线程池的实现 


前言

学了很多线程相关的知识点,线程控制、线程互斥、线程同步,今天我们将他们做一个总结,运用所学知识写一个较为完整的线程池,同时把日志编写也学一下。

一、日志的编写

在企业开发过程中,经常会通过打印日志来查看当前项目的运行情况。写一个日志难度并不大,用到的都是之前学的知识,注释写得比较详细,代码如下。

Log.hpp

#pragma once

#include<iostream>
#include<cstdarg>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
enum{
    Debug = 0,
    Info,
    Warnig,
    Error,
    Fatal
};

enum{
    Screen = 10,
    OneFile,
    ClassFile
};

string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warnig:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    
    default:
        return "Unkonw"; 
    }
}
 
const int default_style = Screen;
const string default_filename = "Log.";
const string logdir = "log";

class Log  
{
public:
    Log(int style = default_style,string filename = default_filename)
        :_style(style),_filename(filename)
    {
        mkdir(logdir.c_str(),0775);
    }

    //更改打印方式
    void Enable(int style)
    {
        _style = style;
    }

    //时间戳转化为年月日时分秒
    string GetTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
        curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
        return time_buffer;
    }

    //写入到文件中
    void WriteLogToOneFile(const string& logname,const string& message)
    {
        FILE* fp = fopen(logname.c_str(),"a");
        if(fp==nullptr)
        {
            perror("fopen filed");
            exit(-1);
        }
        fprintf(fp, "%s\n", message.c_str());

        fclose(fp);
    }

    //打印日志
    void WriteLogToClassFile(const string& levelstr,const string& message)
    {
        string logname = logdir;
        logname+="/";
        logname+=_filename;
        logname+=levelstr;
        WriteLogToOneFile(logname,message);
    }

    void WriteLog(const string& levelstr,const string& message)
    {
        switch (_style) 
        {
        case Screen:
            cout<<message<<endl;//打印到屏幕中
            break;
        case OneFile:
            WriteLogToClassFile("all",message);//给定all,直接写到all里
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr,message);//写入levelstr里
            break;
        default:
            break;
        }
    }

    //打印日志
    void LogMessage(int level,const char* format,...)
    {
        char rightbuffer[1024];//处理消息
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        //现在args指向了可变参数部分
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        
        va_end(args);

        char leftbuffer[1024];//处理日志等级、pid、时间
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string loginfo = leftbuffer;
        loginfo+=rightbuffer;
        WriteLog(levelstr,loginfo);
    }

    //提供接口给运算符重载使用
    void _LogMessage(int level,char* rightbuffer)
    {
        char leftbuffer[1024];
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string messages = leftbuffer;
        messages+=rightbuffer;
        WriteLog(levelstr,messages);
    }

    //运算符重载
    void operator()(int level,const char* format,...)
    {
        char rightbuffer[1024];
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        va_end(args);
        _LogMessage(level,rightbuffer);
    }

    ~Log() 
    {}
private:
    int _style;
    string _filename;
};

 Main.cc

#include "Log.hpp"

int main()
{
    Log log;
    log.Enable(ClassFile);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
    log(Debug, "%d %s %f", 10, "test", 3.14);
    log(Info, "%d %s %f", 10, "test", 3.14);
    log(Warnig, "%d %s %f", 10, "test", 3.14);
    log(Error, "%d %s %f", 10, "test", 3.14);
    log(Fatal, "%d %s %f", 10, "test", 3.14);
}

数据就被我们分门别类的写入到了文件中,后面查询起来就很方便。 

二、线程池

之前我们还学习过生产者消费者模型,创建一批线程去生产,还有一批线程去消费。线程池也类似于如此。只不过生产是主线程自己做。有了任务他只需要将内容分配给已经被创建好进程,让这些进程去消费就行了,而这些被提前创建好的进程,就叫做进程池

1.线程池基本原理

  • 线程池由一组预先创建的线程组成,这些线程等待接收并执行任务。
  • 当需要执行任务时,而且线程池中有空闲线程时,任务被分配给其中一个空闲线程执行。
  • 如果没有空闲线程,任务将被放入任务队列中等待执行,直到有线程空闲为止。

2.线程池作用 

  • 降低了线程创建和销毁的开销:线程的创建和销毁是比较昂贵的操作,线程池可以避免频繁地创建和销毁线程。
  • 提高了性能:通过重用线程,可以减少线程的上下文切换和内存占用,提高了系统的整体性能。
  • 控制并发度:可以限制并发执行的任务数量,防止系统资源被耗尽。

3.线程池的实现 

我们需要如下变量:

  1. 一个任务队列,用于存储待执行的任务。
  2. 一个容器,来存放线程。
  3. 整形变量:存放线程最多的数量
  4. 保护临界资源:互斥锁
  5. 通知线程去处理任务:条件变量

具体代码如下

LockGuard.hpp   不生产锁,做线程的守护者,作用域到了自动释放锁

#pragma once
#include <pthread.h>

// 不定义锁,外部会传递锁
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock)
        : _lock(lock)
    {
    }
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    void UnLock()
    {
        pthread_mutex_unlock(_lock);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock)
        : _mutex(lock)
    {
        _mutex.Lock();
    }
    ~LockGuard()
    {
        _mutex.UnLock();
    }
private:
    Mutex _mutex;
};

 Thread.hpp   模拟C++实现的线程,封装了一下

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>

// 设计方的视角
//typedef std::function<void()> func_t;
template<class T>
using func_t = std::function<void(T&)>;

template<class T>
class Thread
{
public:
    Thread(const std::string &threadname, func_t<T> func, T &data)
    :_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
    {}

    // 不加static会有this指针,无法调用pthread_creadte
    static void *ThreadRoutine(void *args) // 类内方法,
    {
        // (void)args; // 仅仅是为了防止编译器有告警
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    //运行线程
    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this/*?*/);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }

    //等待线程
    bool Join()
    {
        if(!_isrunning) return true;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }
    std::string ThreadName()
    {
        return _threadname;
    }
    bool IsRunning()
    {
        return _isrunning; 
    }
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _threadname;
    bool _isrunning;
    func_t<T> _func;
    T _data;
};

 Log.hpp   前面写的日志

#pragma once

#include<iostream>
#include<cstdarg>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
using namespace std;
enum{
    Debug = 0,
    Info,
    Warnig,
    Error,
    Fatal
};

enum{
    Screen = 10,
    OneFile,
    ClassFile
};

string LevelToString(int level)
{
    switch (level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warnig:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    
    default:
        return "Unkonw"; 
    }
}
 
const int default_style = Screen;
const string default_filename = "Log.";
const string logdir = "log";

class Log  
{
public:
    Log(int style = default_style,string filename = default_filename)
        :_style(style),_filename(filename)
    {
        mkdir(logdir.c_str(),0775);
    }

    //更改打印方式
    void Enable(int style)
    {
        _style = style;
    }

    //时间戳转化为年月日时分秒
    string GetTime()
    {
        time_t currtime = time(nullptr);
        struct tm* curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer,sizeof(time_buffer),"%d-%d-%d %d:%d:%d",
        curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec);
        return time_buffer;
    }

    //写入到文件中
    void WriteLogToOneFile(const string& logname,const string& message)
    {
        FILE* fp = fopen(logname.c_str(),"a");
        if(fp==nullptr)
        {
            perror("fopen filed");
            exit(-1);
        }
        fprintf(fp, "%s\n", message.c_str());

        fclose(fp);
    }

    //打印日志
    void WriteLogToClassFile(const string& levelstr,const string& message)
    {
        string logname = logdir;
        logname+="/";
        logname+=_filename;
        logname+=levelstr;
        WriteLogToOneFile(logname,message);
    }

    void WriteLog(const string& levelstr,const string& message)
    {
        switch (_style) 
        {
        case Screen:
            cout<<message<<endl;//打印到屏幕中
            break;
        case OneFile:
            WriteLogToClassFile("all",message);//给定all,直接写到all里
            break;
        case ClassFile:
            WriteLogToClassFile(levelstr,message);//写入levelstr里
            break;
        default:
            break;
        }
    }

    //打印日志
    void LogMessage(int level,const char* format,...)
    {
        char rightbuffer[1024];//处理消息
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        //现在args指向了可变参数部分
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        
        va_end(args);

        char leftbuffer[1024];//处理日志等级、pid、时间
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string loginfo = leftbuffer;
        loginfo+=rightbuffer;
        WriteLog(levelstr,loginfo);
    }

    //提供接口给运算符重载使用
    void _LogMessage(int level,char* rightbuffer)
    {
        char leftbuffer[1024];
        string levelstr = LevelToString(level);
        string currtime = GetTime();
        string idstr = to_string(getpid());

        snprintf(leftbuffer,sizeof(leftbuffer),"[%s][%s][%s]",levelstr.c_str()
        ,currtime.c_str(),idstr.c_str());

        string messages = leftbuffer;
        messages+=rightbuffer;
        WriteLog(levelstr,messages);
    }

    //运算符重载
    void operator()(int level,const char* format,...)
    {
        char rightbuffer[1024];
        va_list args;   //va_list 是指针
        va_start(args,format);//初始化va_list对象,format是最后一个确定的参数
        vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//写入到leftbuffer中
        va_end(args);
        _LogMessage(level,rightbuffer);
    }

    ~Log() 
    {}
private:
    int _style;
    string _filename;
};

Log lg;

class Conf
{
public:
    Conf()
    {
        lg.Enable(ClassFile); 
    }
    ~Conf()
    {}
};

Conf conf;

 ThreadPool.hpp   线程池的实现

#pragma once

#include <pthread.h>
#include <vector>
#include <functional>
#include <queue>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"
using namespace std;

static const int default_num = 5;

class ThreadData
{
public:
    ThreadData(string name)
        :thread_name(name)
    {}
    string thread_name;
};

template <class T>
class ThreadPool
{
public:
    ThreadPool(int thread_num = default_num)
        : _thread_num(thread_num)
    {
        pthread_mutex_init(&_mutex, nullptr); // 初始化
        pthread_cond_init(&_cond, nullptr);

        // 创建指定个数的线程
        for (int i = 0; i < _thread_num; i++)
        {
            string thread_name = "thread_";
            thread_name += to_string(i + 1);

            ThreadData td(thread_name);//ThreadData为线程数据类型
            
            Thread<ThreadData> t(thread_name, bind(&ThreadPool<T>::ThreadRun, this, 
            placeholders::_1), td);
            _threads.emplace_back(t);

            lg(Info,"%s 被创建...",thread_name.c_str());//写入
        }
    }

    //线程运行
    bool Start()
    {
        for (auto &thread : _threads)
        {
            thread.Start();
            lg.LogMessage(Info,"%s 正在运行!",thread.ThreadName().c_str());
        }
    }

    //线程条件变量等待
    void ThreadWait(ThreadData &td)
    {
        lg.LogMessage(Debug,"没有任务,%s休眠了",td.thread_name.c_str());
        pthread_cond_wait(&_cond,&_mutex);
    }

    //线程条件变量唤醒
    void ThreadWakeUp()
    {
        pthread_cond_signal(&_cond);
    }

    //执行任务
    void ThreadRun(ThreadData &td)
    {
        while (1)
        {
            T t;
            // 取出任务
            {
                LockGuard lockguard(&_mutex);//代码块中自动加锁与解锁
                while (_q.empty())
                {
                    ThreadWait(td);
                    lg.LogMessage(Debug,"有任务了,%s去执行任务了",td.thread_name.c_str());
                }
                t = _q.front();
                _q.pop();
            }
            //处理任务 我们通过打印消息来模拟任务
            // cout<<t<<endl;
            lg.LogMessage(Debug,"%s 计算结果为:%d",td.thread_name.c_str(),t);
        }
    }

    //将任务放到队列中
    void Push(const T &in)
    {
        {
            LockGuard lockguard(&_mutex);
            _q.push(in);
        }
        lg.LogMessage(Debug,"任务push成功,任务是: %d",in);
        ThreadWakeUp();
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex); // 销毁
        pthread_cond_destroy(&_cond);
    }

    //进程等待
    void Wait()
    {
        for (auto &thread : _threads)
        {
            thread.Join();
        }
    }

private:
    queue<T> _q;
    vector<Thread<ThreadData>> _threads;
    int _thread_num;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

 Main.cc

#include <memory>
#include "Threadpool.hpp"


int main()
{
    unique_ptr<ThreadPool<int>> tp(new ThreadPool<int>());
    tp->Start();
    int i = 1;
    while(true)
    {
        //放入数据模拟任务
        tp->Push(i++);
        sleep(1);
    }
    tp->Wait();
    return 0;
}

执行后,就将内容写入到了日志里面。 

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

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

相关文章

算法打卡day38

今日任务&#xff1a; 1&#xff09;完全背包理论基础(卡码网52. 携带研究材料) 2&#xff09;518.零钱兑换II 3&#xff09;377. 组合总和 Ⅳ 4&#xff09;复习day13 完全背包理论基础(卡码网52. 携带研究材料) 题目链接&#xff1a;52. 携带研究材料&#xff08;第七期模拟…

go语言是如何实现协程的

写在文章开头 go语言的精华就在于协程的设计&#xff0c;只有理解协程的设计思想和工作机制&#xff0c;才能确保我们能够完全的利用协程编写强大的并发程序。 Hi&#xff0c;我是 sharkChili &#xff0c;是个不断在硬核技术上作死的 java coder &#xff0c;是 CSDN的博客专…

51-M.2 B Key-5G模块 (U)SIM卡电路设计

视频链接 M.2 B Key-5G模块 &&#xff08;U&#xff09;SIM卡电路设计01_哔哩哔哩_bilibili M.2 B Key-5G模块 &&#xff08;U&#xff09;SIM卡电路设计 1、5G模块 &&#xff08;U&#xff09;SIM卡相关概念 1.1、5G模块&#xff08;RM500Q-GL&#xff09; R…

✯✯✯绍兴ISO9001认证:打造卓越质量管理的核心引擎✯✯✯

&#x1f308;绍兴ISO9001认证&#xff1a;&#x1f33a;打造卓越质量管理的&#x1f497;核心引擎&#x1f955; &#x1f688;在绍兴这座历史悠久、&#x1f345;文化底蕴深厚的城市中&#xff0c;&#x1f3e3;企业间的竞争日趋激烈。&#x1f481;‍♂️为了在这场激烈的&a…

LeetCode第797题: 所有可能的路径

目录 1.问题描述 2.问题分析 1.问题描述 给你一个有 n 个节点的有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不要求按特定顺序&#xff09;。 graph[i] 是一个从节点 i 可以访问的所有节点的列表&#xff08…

openai api_key分享

sk-proj-aHU3aSlMAReiF8d6li9BT3BlbkFJsxmlRhLKlR55xIjpeJ10 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

二次元AI绘画生成器免费:教你生成精美图片

二次元AI绘画生成器&#xff0c;无疑是现代技术与艺术完美结合的典范。这些工具不仅将复杂的绘画过程简化&#xff0c;更让每一个艺术爱好者的创意得以充分展现。这些生成器能够精准捕捉大家的创意精髓&#xff0c;将其转化为细腻、独特的二次元画作。无论是角色设计、场景描绘…

波奇学Linux:ip协议

ip报文解析 4位版本&#xff1a;一般是4表示通信的ip版本号是ipv4还是ipv6 4位首部长度&#xff1a;数值*4ip报头长度 取值范围为[0101,1111], 报头长度就是[5*420,15*460] 8位服务类型(TOS)&#xff1a;4位TOS位段和3位优先权字段和一位保留字段 4位TOS相当于给路由器转发…

Redis快速入门操作

启动Redis 进入命令行客户端 字符串命令常用操作&#xff08;redis默认使用字符串来存储数据&#xff09; 列表&#xff08;Lists&#xff09;常用操作 集合&#xff08;Sets&#xff09;常用操作 &#xff08;无序集合且元素不可重复&#xff09; 有序集合&#xff08;So…

windows和虚拟机互传文件

在虚拟机中设置共享文件夹 操作方法&#xff1a;打开VMware–>虚拟机–>设置–>选项–>共享文件夹&#xff08;见下图&#xff09;&#xff0c;大家在共享文件夹当中就可以把Windows当中的D盘或者其它盘共享到虚拟机中。比如我就是将D盘和E盘共享到了虚拟机中。 共…

密码学 | 椭圆曲线密码学 ECC 入门(三)

目录 7 这一切意味着什么&#xff1f; 8 椭圆曲线密码学的应用 9 椭圆曲线密码学的缺点 10 展望未来 ⚠️ 原文地址&#xff1a;A (Relatively Easy To Understand) Primer on Elliptic Curve Cryptography ⚠️ 写在前面&#xff1a;本文属搬运博客&#xff0c;自己留…

pip下载包opencv出错(报错failed building wheel for opencv-python解决方法)

文章目录 1 报错2 原因3 解决方法参考 1 报错 ERROR: Could not build wheels for opencv-python, which is required to install pypr2 原因 版本不兼容的问题,当使用pip install opencv-python命令安装的是最新版本&#xff0c;当前python版本不支持。需要安装当前版本pyth…

「GO基础」在Windows上安装Go编译器并配置Golang开发环境

文章目录 1、安装Go语言编译程序1.1、下载GoLang编译器1.2、安装GoLang编译器 2、配置Golang IDE运行环境2.1、配置GO编译器2.1.1、GOROOT 概述2.1.2、GOROOT 作用2.1.2、配置 GOROOT 2.2、配置GO依赖管理2.2.1、Module管理依赖2.2.2、GOPATH 管理依赖 2.3、运行GO程序2.3.1、创…

Embedding例子:简单NN网络、迁移学习例子

一、简单例子&#xff1a;构造简单NN网络生成Embedding 1、pytorch例子 2、tensorflow例子 # 1导入模块 import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Embedding import numpy as np# 2构建语料库 corpus[[…

配置静态IP【windows+ubuntu】

Windows配置静态IP 如下图所示&#xff0c;通过“网络和Internet进入设置界面”&#xff0c;依次操作“更改适配器选项”->选择要配置静态ip的网络“属性”->选择IPV4的属性->配置静态ip的地址、子网掩码、默认网关。默认网关应和路由器上的设置保持一致。 Ubuntu配…

2024红明谷杯——Misc 加密的流量

2024红明谷杯——Misc 加密的流量 写在前面&#xff1a; 这里是贝塔贝塔&#xff0c;照例来一段闲聊 打比赛但赛前一波三折&#xff0c;又是成功签到的一个比赛 说起来比赛全名叫红明谷卫星应用数据安全场景赛&#xff0c;但好像真的跟卫星的关系不大&#xff0c;没有bin方…

Redis中的订阅发布(三)

订阅发布 发送消息 当一个Redis客户端执行PUBLISH 命令将消息message发送给频道channel的时候&#xff0c;服务器需要执行以下 两个动作: 1.将消息message发送给channel频道的所有订阅者2.如果一个或多个模式pattern与频道channel相匹配&#xff0c;那么将消息message发送给…

基于SpringBoot+Vue的便利店管理系统 免费获取源码

项目源码获取方式放在文章末尾处 项目技术 数据库&#xff1a;Mysql5.7/8.0 数据表&#xff1a;11张 开发语言&#xff1a;Java(jdk1.8) 开发工具&#xff1a;idea 前端技术&#xff1a;vue 后端技术&#xff1a;SpringBoot 功能简介 (有文档) 项目获取关键字&#…

GUI02-在窗口上跟踪并输出鼠标位置(Win32版)

(1) 响应 WM_MOUSEMOVE 消息获得鼠标位置&#xff1b; (2) 响应 WM_PAINT 将鼠标位置输出到窗口中&#xff1b; (3) 学习二者之间的关键步骤&#xff1a;调用 InvalidateRect() 以通知窗口重绘。 零. 课堂视频 在窗口上跟踪输出鼠标位置-Win32版 一、关键知识点 1. BeginPaint…

Syncovery for Mac:高效文件备份和同步工具

Syncovery for Mac是一款专为Mac用户设计的文件备份和同步工具&#xff0c;凭借其高效、安全和易用的特点&#xff0c;深受用户好评。 Syncovery for Mac v10.14.2激活版下载 该软件具备强大的备份功能&#xff0c;支持多种备份方案和数据格式&#xff0c;用户可以根据需求轻松…