C++还在用printf/cout进行Debug?学习一下如何自己写日志库吧(上篇)

举报
花狗Fdog 发表于 2021/10/24 23:19:02 2021/10/24
【摘要】 文章目录 一. 前言二. 基本功能三. 代码实现1. fdoglogger.h2. fdoglogger.cpp 四. 测试用例1. fdoglogger_test.cpp ...


一. 前言

哈喽,自从实习以来很久没有更文了,一是没有时间,二是实习了之后突然发现自己能写的东西也没有多少了。赶上1024有征文活动,就写一篇吧,在实习的这段时间,我更加认识到日志的重要性,客户端值没传过来?看日志,服务崩溃了?看日志,没错,日志是出现异常第一个想到的东西,它记录了程序运行过程中所调用的函数,所接受到的值,所执行的行为等等。大家也都看到这篇的标题了,我这个人有一个缺点,就是不太喜欢用别人的东西,如果有能力,我希望自己造,所以今天我们自己来动手撸一个日志库,文章重点讲实现过程,如果需要源码,可以前往github获取FdogLog,一个轻量级C++日志库,用于日志服务。

跪求三连!


二. 基本功能

我们先来捋一捋这个日志库应该实现那些功能。

  1. 日志最最最基本的功能是什么,当然是打印或记录日志。
  2. 信息应该包括哪些信息,时间?运行用户?所在文件?想要显示的信息?(自定义显示信息下篇实现)
  3. 信息虽然显示丰富,但是要尽可能让代码自己获取其他信息,调用者只需要设置最主要的信息。
  4. 信息有重要等级之分,所以我们需要对信息做必要分类,提高效率。
  5. 如何实现全局尽可能简洁的调用。
  6. 如果日志库是运行在多线程环境,如何保证线程安全。(下篇实现)

这些就是一个日志库所具备的最基本的功能,接下来继续思考,还需要什么。

  1. 怎么控制日志的行为。
  2. 如果保存在文件,如何定义文件名。
  3. 随着日志增加,文件会越来越大,如何解决。(下篇实现)

简单规划完一个不那么完美的日志库所具备的能力,现在我们来对这几条做更详细的规划。

  1. 日志最最最基本的功能是什么,当然是打印或记录日志。
  2. 信息应该包括哪些信息,时间?运行用户?所在文件?想要显示的信息?

当我在调用一个名为function的函数时。

function();

  
 
  • 1

你希望它输出怎么样的信息。

我被调用
[2021-10-20 23:27:23] 我被调用
[2021-10-20 23:27:23] INFO 我被调用
[2021-10-20 23:27:23] INFO root 我被调用
[2021-10-20 23:27:23] INFO root 17938 我被调用
[2021-10-20 23:27:23] INFO root 17938 [/media/rcl/FdogIM/service.h function:8] 我被调用

我想大部分人都会选择最后一种输出信息吧(虽然在这之前,我们都大量使用cout输出第一种),所以我们的日志应该包括时间,日志等级,运行用户,进程ID,调用函数所在文件,以及调用时所在行数。当然总会有人不想全部输出,这将在后面给出方案。

  1. 信息虽然显示丰富,但是要尽可能让代码自己获取其他信息,调用者只需要设置最主要的信息。

  2. 信息有重要等级之分,所以我们需要对信息做必要分类,提高效率。

  3. 如何实现全局尽可能简洁的调用.

信息有重要等级之分,要可以对信息做区分,按照常见的等级之分,有:

ERROR: 此信息输出后,主体系统核心模块不能正常工作,需要修复才能正常工作。
WARN:   此信息输出后,系统一般模块存在问题,不影响系统运行。
INFO:     此信息输出后,主要是记录系统运行状态等关联信息。
DEBUG: 最细粒度的输出,除去上面各种情况后,你希望输出的相关信息,都可以在这里输出。
TRACE:  最细粒度的输出,除去上面各种情况后,你希望输出的相关信息,都可以在这里输出。

有了等级之分,如何实现全局尽可能简洁的调用,通俗的说就是去掉一切不必要的调用,只留下最主要的调用。

例如:

#include<iostream>
#include"fdoglogger.h"  //添加日志库头文件

using namespace fdog;   //日志库的命名空间

int main(){
    FdogError("错误");
    FdogWarn("警告");
    FdogInfo("信息");
    FdogDebug("调试");
    FdogTrace("追踪");
    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

你不必初始化什么信息,调用什么多余的初始化函数,只需要用这五个类似函数的东西来输出即可,同样,如果是另一个源文件,依旧是这样的调用方式(这里可以使用单一模式来实现,其意图是保证一个类仅有一个实列,并提供一个访问它的全局访问点,该实例被所有程序模块共享。就比如日志的输出。)。

  1. 如果日志库是运行在多线程环境,如何保证线程安全。

到目前,一个基本的日志库的调用基本成形,如果在单线程,它可以很好的工作,但是到了多线程环境下,就不能保证了,第一点就是单例模式的创建,当两个线程同时去初始化时,无法保证单一实例被成功创建,第二,日志既然是输出到文件,不同线程写入文件时,如何保证写入数据不会错乱。既然写的是C++的日志输出,必然用到了cout ,cout 不是原子性的操作,所以在多线程下是不安全的,这些都是我们需要考虑到的。

  1. 怎么控制日志的行为。

这里使用配置文件进行日志的行为规定,包括打印什么日志,输入到文件,还是终端,输出的等级,以及日志开关,等等,配置文件将在程序启动时被读取。(提醒各位千万不要写死代码,后患无穷!!!)

  1. 如果保存在文件,如何定义文件名。

  2. 随着日志增加,文件会越来越大,如何解决。

日志的文件名由配置文件指定,但是创建时会在后面加上创建日期后缀,并且可以在配置文件中配置每隔多少天创建一个新的日志文件,如果配置中心有设置日志文件大小,则会优先大小判断,超过便创建一个新文件。


三. 代码实现

1. fdoglogger.h
#ifndef FDOGLOGGER_H
#define FDOGLOGGER_H
#include<iostream>
#include<fstream>
#include<map>
#include<mutex>
#ifndef linux
#include<unistd.h>
#include<sys/syscall.h>
#include<sys/stat.h>
#include<sys/types.h>
#include <pwd.h>
#endif
#ifndef WIN32
//TODO
#endif
using namespace std;

namespace fdog {

#define RED   "\e[1;31m"
#define BLUE  "\e[1;34m"
#define GREEN "\e[1;32m"
#define WHITE "\e[1;37m"
#define DEFA  "\e[0m"

enum class coutType: int {Error, Warn, Info, Debug, Trace};
enum class fileType: int {Error, Warn, Info, Debug, Trace};
enum class terminalType: int {Error, Warn, Info, Debug, Trace};

struct Logger {
    string logSwitch;           //日志开关
    string logFileSwitch;       //是否写入文件
    string logTerminalSwitch;   //是否打印到终端
    string logName;             //日志文件名字
    string logFilePath;         //日志文件保存路径
    string logMixSize;          //日志文件最大大小
    string logBehavior;         //日志文件达到最大大小行为
    string logOverlay;          //日志文件覆盖时间
    string logOutputLevelFile;  //日志输出等级(file)
    string logOutputLevelTerminal;//日志输出等级
};

class FdogLogger {
public:
    void initLogConfig();

    void releaseConfig();

    static FdogLogger* getInstance();

    string getCoutType(coutType coutType);

    bool getFileType(fileType fileCoutBool);

    bool getTerminalType(terminalType terminalCoutTyle);

    string getLogCoutTime();

    string getLogNameTime();

    string getFilePash();

    string getLogCoutProcessId();

    string getLogCoutThreadId();

    string getLogCoutUserName();

    bool createFile(string filePash);

    bool logFileWrite(string messages);

    bool bindFileCoutMap(string value1, fileType value2);

    bool bindTerminalCoutMap(string value1, terminalType value2);

private:
    char szbuf[128];
    Logger logger;
    static FdogLogger * singleObject;
    static mutex * mutex_new;
    map<coutType, string> coutTypeMap;
    map<fileType, bool> fileCoutMap;
    map<terminalType, bool> terminalCoutMap;

private:
    FdogLogger();
    ~FdogLogger();
};

#define Error1 __FDOGNAME__(Error)
#define Warn1 __FDOGNAME__(Warn)
#define Info1 __FDOGNAME__(Info)
#define Debug1 __FDOGNAME__(Debug)
#define Trace1 __FDOGNAME__(Trace)


#define SQUARE_BRACKETS_LEFT " ["
#define SQUARE_BRACKETS_RIGHT "] "
#define SPACE " "
#define LINE_FEED "\n"
#define COLON ":"
#define SLASH "/"

#define __FDOGTIME__  FdogLogger::getInstance()->getLogCoutTime()          //时间宏
#define __FDOGPID__   FdogLogger::getInstance()->getLogCoutProcessId()     //进程宏
#define __FDOGTID__   FdogLogger::getInstance()->getLogCoutThreadId()      //线程宏
#define __FDOGFILE__  __FILE__        //文件名宏
#define __FDOGPASH__  FdogLogger::getInstance()->getFilePash() + __FDOGFILE__ //文件路径
#define __FDOGFUNC__   __func__        //函数名宏
#define __FDOGLINE__  __LINE__        //行数宏
#define __USERNAME__  FdogLogger::getInstance()->getLogCoutUserName()     //获取调用用户名字
#define __FDOGNAME__(name) #name        //名字宏


#define COMBINATION_INFO_FILE(coutTypeInfo, message) \
    do{\
        string messagesAll = __FDOGTIME__ + coutTypeInfo + __USERNAME__ + __FDOGTID__ + SQUARE_BRACKETS_LEFT + \
        __FDOGPASH__  + SPACE +__FDOGFUNC__ + COLON + to_string(__FDOGLINE__) + SQUARE_BRACKETS_RIGHT + message + LINE_FEED;\
        FdogLogger::getInstance()->logFileWrite(messagesAll); \
    }while(0);

#define COMBINATION_INFO_TERMINAL(coutTypeInfo, message) \
    do{\
        string messagesAll = __FDOGTIME__ + WHITE + coutTypeInfo + DEFA + __USERNAME__ + __FDOGTID__ + SQUARE_BRACKETS_LEFT + \
        __FDOGPASH__  + SPACE +__FDOGFUNC__ + COLON + to_string(__FDOGLINE__) + SQUARE_BRACKETS_RIGHT + message + LINE_FEED;\
        cout << messagesAll;\
    }while(0);

#define LoggerCout(coutTyle, coutTypeInfo, fileCoutBool, terminalCoutBool, message) \
    do {\
        string coutType = FdogLogger::getInstance()->getCoutType(coutTyle);\
        if (FdogLogger::getInstance()->getFileType(fileCoutBool)) {\
            COMBINATION_INFO_FILE(coutTypeInfo, message)\
        }\
        if (FdogLogger::getInstance()->getTerminalType(terminalCoutBool)) {\
            COMBINATION_INFO_TERMINAL(coutTypeInfo, message)\
        }\
    }while(0);

#define FdogError(message) \
    do{\
        LoggerCout(fdog::coutType::Error, Error1, fdog::fileType::Error, fdog::terminalType::Error, message)\
    }while(0);

#define FdogWarn(message)  \
    do{\
        LoggerCout(fdog::coutType::Warn, Warn1, fdog::fileType::Warn, fdog::terminalType::Warn, message)\
    }while(0);

#define FdogInfo(message)  \
    do{\
        LoggerCout(fdog::coutType::Info, Info1, fdog::fileType::Info, fdog::terminalType::Info, message)\
    }while(0);

#define FdogDebug(message) \
    do{\
        LoggerCout(fdog::coutType::Debug, Debug1, fdog::fileType::Debug, fdog::terminalType::Debug, message)\
    }while(0);

#define FdogTrace(message) \
    do{\
        LoggerCout(fdog::coutType::Trace, Trace1, fdog::fileType::Trace, fdog::terminalType::Trace, message)\
    }while(0);

}

#endif


  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
2. fdoglogger.cpp
#include"fdoglogger.h"

using namespace fdog;


FdogLogger * FdogLogger::singleObject = nullptr;
mutex * FdogLogger::mutex_new = new(mutex);

FdogLogger::FdogLogger(){
    initLogConfig();
}
FdogLogger::~FdogLogger(){

}

FdogLogger* FdogLogger::getInstance(){
    mutex_new->lock();
    if (singleObject == nullptr) {
        singleObject = new FdogLogger();
    }
    mutex_new->unlock();
    return singleObject;
}

void FdogLogger::initLogConfig(){

    map<string, string *> flogConfInfo;
    flogConfInfo["logSwitch"] = &this->logger.logSwitch;
    flogConfInfo["logFileSwitch"] = &this->logger.logFileSwitch;
    flogConfInfo["logTerminalSwitch"] = &this->logger.logTerminalSwitch;
    flogConfInfo["logName"] = &this->logger.logName;
    flogConfInfo["logFilePath"] = &this->logger.logFilePath;
    flogConfInfo["logMixSize"] = &this->logger.logMixSize;
    flogConfInfo["logBehavior"] = &this->logger.logBehavior;
    flogConfInfo["logOverlay"] = &this->logger.logOverlay;
    flogConfInfo["logOutputLevelFile"] = &this->logger.logOutputLevelFile;
    flogConfInfo["logOutputLevelTerminal"] = &this->logger.logOutputLevelTerminal;

    string str;
    ifstream file;
    char str_c[100]={0};
    file.open("fdoglogconf.conf");
    if(!file.is_open()){
        cout<<"文件打开失败\n";
    }
    while(getline(file, str)){
        if(!str.length()) {
            continue;
        }
        string str_copy = str;
        //cout<<"获取数据:"<<str_copy<<endl;
        int j = 0;
        for(int i = 0; i < str.length(); i++){
            if(str[i]==' ')continue;
            str_copy[j] = str[i];
            j++;
        }
        str_copy.erase(j);
        if(str_copy[0]!='#'){
            sscanf(str_copy.data(),"%[^=]",str_c);
            auto iter = flogConfInfo.find(str_c);
            if(iter!=flogConfInfo.end()){
                sscanf(str_copy.data(),"%*[^=]=%s",str_c);
                *iter->second = str_c;
            } else {
            }
        }
    }
    logger.logName = logger.logName + getLogNameTime() + ".log";

    bindFileCoutMap("5", fileType::Error);
    bindFileCoutMap("4", fileType::Warn);
    bindFileCoutMap("3", fileType::Info);
    bindFileCoutMap("2", fileType::Debug);
    bindFileCoutMap("1", fileType::Trace);

    bindTerminalCoutMap("5", terminalType::Error);
    bindTerminalCoutMap("4", terminalType::Warn);
    bindTerminalCoutMap("3", terminalType::Info);
    bindTerminalCoutMap("2", terminalType::Debug);
    bindTerminalCoutMap("1", terminalType::Trace);

    if(logger.logFileSwitch == "on"){
        if(!createFile(logger.logFilePath)){
            std::cout<<"Log work path creation failed\n";
        }
    }

    cout << "|========FdogLogger v2.0==========================|" <<endl << endl;
    cout << "  日志开关:" << logger.logSwitch << endl;
    cout << "  文件输出:" << logger.logFileSwitch << endl;
    cout << "  终端输出:" << logger.logTerminalSwitch << endl;
    cout << "  日志输出等级(文件):" << logger.logOutputLevelFile << endl;    
    cout << "  日志输出等级(终端):" << logger.logOutputLevelTerminal << endl;
    cout << "  日志文件名:" << logger.logName << endl;
    cout << "  日志保存路径:" << logger.logFilePath << endl;
    cout << "  单文件最大大小:"<< logger.logMixSize << "M" << endl;
    cout << "  日志保存时间 :" << logger.logOverlay << "天" << endl << endl;
    cout << "|=================================================|" <<endl;
    
    return;
}

string FdogLogger::getCoutType(coutType coutType){
    return singleObject->coutTypeMap[coutType];
}

bool FdogLogger::getFileType(fileType fileCoutBool){
    return singleObject->fileCoutMap[fileCoutBool];
}

bool FdogLogger::getTerminalType(terminalType terminalCoutTyle){
    return singleObject->terminalCoutMap[terminalCoutTyle];
}

string FdogLogger::getLogCoutTime(){
    time_t timep;
    time (&timep);
    char tmp[64];
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep));
    string tmp_str = tmp;

    return SQUARE_BRACKETS_LEFT + tmp_str + SQUARE_BRACKETS_RIGHT;
}

string FdogLogger::getLogNameTime(){
    time_t timep;
    time (&timep);
    char tmp[64];
    strftime(tmp, sizeof(tmp), "%Y-%m-%d-%H:%M:%S",localtime(&timep));
    return tmp;
}

string FdogLogger::getFilePash(){
    getcwd(szbuf, sizeof(szbuf)-1);
    string szbuf_str = szbuf;
    return szbuf_str + SLASH;
}

string FdogLogger::getLogCoutProcessId(){
#ifndef linux
    return to_string(getpid());
#endif
#ifndef WIN32
//  unsigned long GetPid(){
//     return GetCurrentProcessId();
// }
#endif
}

string FdogLogger::getLogCoutThreadId(){
#ifndef linux
    return to_string(syscall(__NR_gettid));
#endif
#ifndef WIN32
//  unsigned long GetTid(){
//     return GetCurrentThreadId();
// }
#endif
}

string FdogLogger::getLogCoutUserName(){
    struct passwd *my_info;
    my_info = getpwuid(getuid());
    string name = my_info->pw_name;
    return SPACE + name + SPACE;
}

bool FdogLogger::createFile(string filePash){
    int len = filePash.length();
    if(!len){
        filePash = "log";
        if (0 != access(filePash.c_str(), 0)){
            if(-1 == mkdir(filePash.c_str(),0)){
                std::cout<<"没路径";
                return 0;
            }
        }
    }
    std::string filePash_cy(len,'\0');
    for(int i =0;i<len;i++){
        filePash_cy[i]=filePash[i];
        if(filePash_cy[i]=='/' || filePash_cy[i]=='\\'){
            if (-1 == access(filePash_cy.c_str(), 0)){
                if(0!=mkdir(filePash_cy.c_str(),0)){
                    std::cout<<"有路径";
                    return 0;
                }
            }
        }
    }
    return 1;
}

bool FdogLogger::logFileWrite(string messages){
    ofstream file;
    file.open(logger.logFilePath + logger.logName, ::ios::app | ios::out);
    if(!file){
        cout<<"写失败"<<endl;
        return 0;
    }
    file << messages;
    file.close();
    return 1;
}

bool FdogLogger::bindFileCoutMap(string value1, fileType value2){
    if(logger.logOutputLevelFile.find(value1)!=std::string::npos) {
        fileCoutMap[value2] = true;
    } else {
        fileCoutMap[value2] = false;
    }
}

bool FdogLogger::bindTerminalCoutMap(string value1, terminalType value2){
    if(logger.logOutputLevelTerminal.find(value1)!=std::string::npos) {
        terminalCoutMap[value2] = true;
    } else {
        terminalCoutMap[value2] = false;
    }
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221

四. 测试用例

在这里插入图片描述

1. fdoglogger_test.cpp
#include<iostream>
#include"fdoglogger.h"  //添加日志库头文件

using namespace fdog;   //日志库的命名空间

int main(){
    FdogError("错误");
    FdogWarn("警告");
    FdogInfo("信息");
    FdogDebug("调试");
    FdogTrace("追踪");
    return 0;
}

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这里插入图片描述

在这里插入图片描述

暂时考虑到的就是这些,如有缺陷,欢迎评论区补充。(比如文件写入打开就关闭,很浪费资源,如何优化,下篇见)。

源码已上传github,还原star! FdogLog,一个轻量级C++日志库,用于日志服务。


文章来源: zxfdog.blog.csdn.net,作者:花狗Fdog,版权归原作者所有,如需转载,请联系作者。

原文链接:zxfdog.blog.csdn.net/article/details/120932776

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。