• muduo库的高性能日志库(二)——LogStream文件


    概述

    该文件下有三个类如下图所示

    在这里插入图片描述

    FixBuffer类(模板缓冲区)

    该类实现为非类型参数的模板类,通过模板传入一个参数来设置实例化参数缓冲区的大小
    通过data_,cur_,end()完成对该缓冲区的全部操作

    该缓冲区思想极为简单,不过多赘述

    //一个模板buffer,Logstream中的类长远变量Buffer是该类的一个实例化
    //SIZE指缓冲区大小,实际该类维护了一个SIZE大小的char数组
    //通过 data_,cur_.end_完成对缓冲区的各项操作
    template<int SIZE>
    class FixedBuffer : noncopyable
    {
     public:
      FixedBuffer()
        : cur_(data_)
      {
        setCookie(cookieStart);
      }
    
      ~FixedBuffer()
      {
        setCookie(cookieEnd);
      }
    
    
     //将数据写入缓冲区当中
      void append(const char* /*restrict*/ buf, size_t len)
      {
        // FIXME: append partially
        //判断缓冲区剩余空间是否足够
        if (implicit_cast<size_t>(avail()) > len)
        {
          memcpy(cur_, buf, len);
          cur_ += len;
        }
      }
    
    
      //返回缓冲区首地址
      const char* data() const { return data_; }
      //返回缓冲区长度
      int length() const { return static_cast<int>(cur_ - data_); }
    
      // write to data_ directly
      //返回缓冲区可写入的位置(cur_指针指向已写入数据的末尾)
      char* current() { return cur_; }
      //返回空闲缓冲区长度
      int avail() const { return static_cast<int>(end() - cur_); }
      //增加已写入数据的长队,(cur指针往后移)
      void add(size_t len) { cur_ += len; }
    
      //重置缓冲区(将cur_重新指向开头)
      void reset() { cur_ = data_; }
      //将缓冲区全置空
      void bzero() { memZero(data_, sizeof data_); }
    
      // for used by GDB
      //供gdb使用
      const char* debugString();
      void setCookie(void (*cookie)()) { cookie_ = cookie; }
      // for used by unit test
      //供单元测试使用
      string toString() const { return string(data_, length()); }
      StringPiece toStringPiece() const { return StringPiece(data_, length()); }
    
     private:
      const char* end() const { return data_ + sizeof data_; }
      // Must be outline function for cookies.
      static void cookieStart();  //空
      static void cookieEnd();  //空
    
      void (*cookie_)();
      char data_[SIZE];
      char* cur_;
    };
    
    
    • 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

    LogStream类

    muduo库并没有使用标准库iostream,而是为了出于性能,自己实现了一个Logstream类

    设计这个LogStream类,让它如同C++的标准输出流对象cout,能用<<符号接收输入,cout是输出到终端,而LogStream类是把输出保存自己内部的缓冲区,可以让外部程序把缓冲区的内容重定向输出到不同的目标,如文件、终端、socket等。

    这个类主要完成了对运算符<<的重载,将输出保存入缓冲区(Buffer_中)

    LogStream.h

    /该类负责将要记录的数据写入缓冲区(Buffer_)当中对<<操作符进行了重载
    //LogStream这个类的重点难点在于重载运算符<<,以整型的<<运算符重载的优化实现,效率高于标准库std::iostream。
    
    class LogStream : noncopyable
    {
      typedef LogStream self;
     public:
      typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;
    
     //针对各种类型数据eg:short,unsigneg short等对<<运算符进行了重载
     
     //针对bool值
      self& operator<<(bool v)
      {
        buffer_.append(v ? "1" : "0", 1);
        return *this;
      }
      self& operator<<(short);
      self& operator<<(unsigned short);
      self& operator<<(int);
      self& operator<<(unsigned int);
      self& operator<<(long);
      self& operator<<(unsigned long);
      self& operator<<(long long);
      self& operator<<(unsigned long long);
    
      self& operator<<(const void*);
    
      self& operator<<(float v)
      {
        *this << static_cast<double>(v);
        return *this;
      }
      self& operator<<(double);
      // self& operator<<(long double);
    
      self& operator<<(char v)
      {
        buffer_.append(&v, 1);
        return *this;
      }
    
      // self& operator<<(signed char);
      // self& operator<<(unsigned char);
    
      self& operator<<(const char* str)
      {
        if (str)
        {
          buffer_.append(str, strlen(str));
        }
        else
        {
          buffer_.append("(null)", 6);
        }
        return *this;
      }
    
      self& operator<<(const unsigned char* str)
      {
        return operator<<(reinterpret_cast<const char*>(str));
      }
    
      self& operator<<(const string& v)
      {
        buffer_.append(v.c_str(), v.size());
        return *this;
      }
    
      self& operator<<(const StringPiece& v)
      {
        buffer_.append(v.data(), v.size());
        return *this;
      }
    
      self& operator<<(const Buffer& v)
      {
        *this << v.toStringPiece();
        return *this;
      }
    
     //将数据写入缓冲区当中
      void append(const char* data, int len) { buffer_.append(data, len); }
      //获取缓冲区对象
      const Buffer& buffer() const { return buffer_; }
    
      //重置缓冲区
      void resetBuffer() { buffer_.reset(); }
    
     private:
      void staticCheck();
    
    
     //将整形转换为字符串的模板函数
      template<typename T>
      void formatInteger(T);
    
      //缓冲区
      Buffer buffer_;
     
     //最大位数
      static const int kMaxNumericSize = 48;
    };
    
    • 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

    LogStream.cc

    十进制整数转化为字符串

    思路:先创建一个字符查询表,用于快速找到十进制数字对应的字符
    通过余10除10的方法,获取每一位的数字,然后通过查询表,将对应字符存入数组当中
    ,最后判断正负获得符号位字符,应为是从低位开始存入的,后面需要对该字符数组进行反转。

    //创建查询表,
    const char digits[] = "9876543210123456789";
    const char* zero = digits + 9;
    static_assert(sizeof(digits) == 20, "wrong number of digits");
    
    const char digitsHex[] = "0123456789ABCDEF";
    static_assert(sizeof digitsHex == 17, "wrong number of digitsHex");
    
    // Efficient Integer to String Conversions, by Matthew Wilson.
    
    //将整形转换为字符串
    template<typename T>
    size_t convert(char buf[], T value)
    {
      T i = value;
      char* p = buf;
    
    
     //循环余10除10,得到每一位的数字(从高位开始)
      do
      {
        int lsd = static_cast<int>(i % 10);
        i /= 10;
        //通过查询表获取对应字符
        *p++ = zero[lsd];
      } while (i != 0);
    
    
     //获取符号位
      if (value < 0)
      {
        *p++ = '-';
      }
      *p = '\0';
    
      //前面存储是将低位存在前面的
      //翻转
      std::reverse(buf, p);
    
      return p - buf;
    }
    
    • 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

    整形数据流式操作方法的实际处理函数

    template<typename T>
    void LogStream::formatInteger(T v)
    {
      if (buffer_.avail() >= kMaxNumericSize) {
        size_t len = convert(buffer_.current(), v);
        buffer_.add(len);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    地址(指针)数据转换为16进制字符串

    实现思路与上相似

    size_t convertHex(char buf[], uintptr_t value)
    {
      uintptr_t i = value;
      char* p = buf;
    
      do{
        int lsd = static_cast<int>(i % 16);
        i /= 16;
        *p++ = digitsHex[lsd];
      } while (i != 0);
    
      *p = '\0';
      std::reverse(buf, p);
    
      return p - buf;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    指针数据格式化为16进制字符串实际处理函数

    LogStream& LogStream::operator<<(double v)
    {
      if (buffer_.avail() >= kMaxNumericSize)
      {
        int len = snprintf(buffer_.current(), kMaxNumericSize, "%.12g", v);
        buffer_.add(len);
      }
      return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    浮点类型数据转化为字符串

    这个就仅仅只是调用了库函数snprintf,
    float类型的先强转为double类型,在调用下面的重载

    LogStream& LogStream::operator<<(double v)
    {
      if (buffer_.avail() >= kMaxNumericSize)
      {
        int len = snprintf(buffer_.current(), kMaxNumericSize, "%.12g", v);
        buffer_.add(len);
      }
      return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    Fmt类

    该类就就实现了将一个数值类型的数据转换为一个长度不超过32位的字符串

    //Logstream类并未提供数据类型转换接口
    //该类实现了数据格式化,将一个数值类型的数据转换为一个长度不超过32位的字符串
    class Fmt // : noncopyable
    {
     public:
      template<typename T>
      Fmt(const char* fmt, T val);
    
      const char* data() const { return buf_; }
      int length() const { return length_; }
    
     private:
      char buf_[32];
      int length_;
    };
    
    inline LogStream& operator<<(LogStream& s, const Fmt& fmt)
    {
      s.append(fmt.data(), fmt.length());
      return s;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    具体实现

    template<typename T>
    Fmt::Fmt(const char* fmt, T val)
    {
    
      //is_arithmetic是C++11的函数,头文件 
      //功能判断T是否为算数类型(eg:int,double,int_32....)则提供trie的bool值,反之false
      static_assert(std::is_arithmetic<T>::value == true, "Must be arithmetic type");
    
      length_ = snprintf(buf_, sizeof buf_, fmt, val);
      assert(static_cast<size_t>(length_) < sizeof buf_);
    }
    
    // Explicit instantiations
    //显示实例化
    //(为特定的函数模板生成定义)
    template Fmt::Fmt(const char* fmt, char);
    
    template Fmt::Fmt(const char* fmt, short);
    template Fmt::Fmt(const char* fmt, unsigned short);
    template Fmt::Fmt(const char* fmt, int);
    template Fmt::Fmt(const char* fmt, unsigned int);
    template Fmt::Fmt(const char* fmt, long);
    template Fmt::Fmt(const char* fmt, unsigned long);
    template Fmt::Fmt(const char* fmt, long long);
    template Fmt::Fmt(const char* fmt, unsigned long long);
    
    template Fmt::Fmt(const char* fmt, float);
    template Fmt::Fmt(const char* fmt, double);
    
    • 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

    C++单元测试框架(简略)

    什么是单元测试

    复杂的C/C++代码很可能会包含错误,并且在编写代码后尝试对其进行测试类似于在大海捞针。 一种更审慎的方法是通过添加专门针对特定区域的小型(单元)测试来测试编写的各个代码段,例如,一些计算密集型C函数或某些C++类声称对某个数据结构进行建模,例如队列。 然后,以此理念构建的回归套件将具有单元测试的集合以及运行测试并报告结果的测试驱动程序。

    常用测试工具

    1.测试的开始与结束

    BOOST_AUTO_TEST_SUITE (testname)
    BOOST_AUTO_TEST_SUITE_END( )
    
    • 1
    • 2

    这两个宏分别指示测试套件的开始和结束。
    从某种意义上来说有点类似于C++的命名空间

    2.验证表达式

    BOOST_WARN()
    BOOST_CHECK()
    BOOST_REQUIRE()
    
    • 1
    • 2
    • 3

    以上三个用与验证表达式
    BOOST_CHECK和BOOST_REQUIRE之间的区别在于,在前一种情况下,即使断言失败,测试仍将继续,而在后一种情况下,则认为是严重错误并且测试将停止。

    BOOST_REQUIRE()也可以用于检测函数函数和类方法

    3.浮点数比较

    BOOST_WARN_CLOSE_FRACTION(left-value, right-value, tolerance-limit)
    BOOST_CHECK_CLOSE_FRACTION(left-value, right-value, tolerance-limit)
    BOOST_REQUIRE_CLOSE_FRACTION(left-value, right-value, tolerance-limit)
    
    • 1
    • 2
    • 3

    使用2中的宏进行浮点数比较是不对的,总是会因为精度问题导致测试过不去
    Boost测试实用程序提供了BOOST_WARN_CLOSE_FRACTION,BOOST_CHECK_CLOSE_FRACTION,BOOST_REQUIRE_CLOSE_FRACTION宏。 要使用这三个宏中的任何一个,必须包含预定义的Boost头float_point_comparison.hpp。 这三个宏的语法都相同

    前两个参数是要比较的两个数,第三个参数为公差极限(即误差不能超过)

    宏中的左值和右值必须是同一类型float或double。

    测试

    #include "muduo/base/LogStream.h"
    
    #include 
    #include 
    
    //#define BOOST_TEST_MODULE LogStreamTest
    #define BOOST_TEST_MAIN
    #define BOOST_TEST_DYN_LINK
    #include 
    #define BOOST_TEST_MODULE First_TestSuite
    #include 
    using muduo::string;
    
    BOOST_AUTO_TEST_CASE(testLogStreamBooleans)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
      BOOST_CHECK_EQUAL(buf.toString(), string(""));
      os << true;
      BOOST_CHECK_EQUAL(buf.toString(), string("1"));
      os << '\n';
      BOOST_CHECK_EQUAL(buf.toString(), string("1\n"));
      os << false;
      BOOST_CHECK_EQUAL(buf.toString(), string("1\n0"));
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamIntegers)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
      BOOST_CHECK_EQUAL(buf.toString(), string(""));
      os << 1;
      BOOST_CHECK_EQUAL(buf.toString(), string("1"));
      os << 0;
      BOOST_CHECK_EQUAL(buf.toString(), string("10"));
      os << -1;
      BOOST_CHECK_EQUAL(buf.toString(), string("10-1"));
      os.resetBuffer();
    
      os << 0 << " " << 123 << 'x' << 0x64;
      BOOST_CHECK_EQUAL(buf.toString(), string("0 123x100"));
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamIntegerLimits)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
      os << -2147483647;
      BOOST_CHECK_EQUAL(buf.toString(), string("-2147483647"));
      os << static_cast<int>(-2147483647 - 1);
      BOOST_CHECK_EQUAL(buf.toString(), string("-2147483647-2147483648"));
      os << ' ';
      os << 2147483647;
      BOOST_CHECK_EQUAL(buf.toString(), string("-2147483647-2147483648 2147483647"));
      os.resetBuffer();
    
      os << std::numeric_limits<int16_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("-32768"));
      os.resetBuffer();
    
      os << std::numeric_limits<int16_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("32767"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint16_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("0"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint16_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("65535"));
      os.resetBuffer();
    
      os << std::numeric_limits<int32_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("-2147483648"));
      os.resetBuffer();
    
      os << std::numeric_limits<int32_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("2147483647"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint32_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("0"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint32_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("4294967295"));
      os.resetBuffer();
    
      os << std::numeric_limits<int64_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("-9223372036854775808"));
      os.resetBuffer();
    
      os << std::numeric_limits<int64_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("9223372036854775807"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint64_t>::min();
      BOOST_CHECK_EQUAL(buf.toString(), string("0"));
      os.resetBuffer();
    
      os << std::numeric_limits<uint64_t>::max();
      BOOST_CHECK_EQUAL(buf.toString(), string("18446744073709551615"));
      os.resetBuffer();
    
      int16_t a = 0;
      int32_t b = 0;
      int64_t c = 0;
      os << a;
      os << b;
      os << c;
      BOOST_CHECK_EQUAL(buf.toString(), string("000"));
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamFloats)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
    
      os << 0.0;
      BOOST_CHECK_EQUAL(buf.toString(), string("0"));
      os.resetBuffer();
    
      os << 1.0;
      BOOST_CHECK_EQUAL(buf.toString(), string("1"));
      os.resetBuffer();
    
      os << 0.1;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.1"));
      os.resetBuffer();
    
      os << 0.05;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.05"));
      os.resetBuffer();
    
      os << 0.15;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.15"));
      os.resetBuffer();
    
      double a = 0.1;
      os << a;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.1"));
      os.resetBuffer();
    
      double b = 0.05;
      os << b;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.05"));
      os.resetBuffer();
    
      double c = 0.15;
      os << c;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.15"));
      os.resetBuffer();
    
      os << a+b;
      BOOST_CHECK_EQUAL(buf.toString(), string("0.15"));
      os.resetBuffer();
    
      BOOST_CHECK(a+b != c);
    
      os << 1.23456789;
      BOOST_CHECK_EQUAL(buf.toString(), string("1.23456789"));
      os.resetBuffer();
    
      os << 1.234567;
      BOOST_CHECK_EQUAL(buf.toString(), string("1.234567"));
      os.resetBuffer();
    
      os << -123.456;
      BOOST_CHECK_EQUAL(buf.toString(), string("-123.456"));
      os.resetBuffer();
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamVoid)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
    
      os << static_cast<void*>(0);
      BOOST_CHECK_EQUAL(buf.toString(), string("0x0"));
      os.resetBuffer();
    
      os << reinterpret_cast<void*>(8888);
      BOOST_CHECK_EQUAL(buf.toString(), string("0x22B8"));
      os.resetBuffer();
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamStrings)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
    
      os << "Hello ";
      BOOST_CHECK_EQUAL(buf.toString(), string("Hello "));
    
      string chenshuo = "Shuo Chen";
      os << chenshuo;
      BOOST_CHECK_EQUAL(buf.toString(), string("Hello Shuo Chen"));
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamFmts)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
    
      os << muduo::Fmt("%4d", 1);
      BOOST_CHECK_EQUAL(buf.toString(), string("   1"));
      os.resetBuffer();
    
      os << muduo::Fmt("%4.2f", 1.2);
      BOOST_CHECK_EQUAL(buf.toString(), string("1.20"));
      os.resetBuffer();
    
      os << muduo::Fmt("%4.2f", 1.2) << muduo::Fmt("%4d", 43);
      BOOST_CHECK_EQUAL(buf.toString(), string("1.20  43"));
      os.resetBuffer();
    }
    
    BOOST_AUTO_TEST_CASE(testLogStreamLong)
    {
      muduo::LogStream os;
      const muduo::LogStream::Buffer& buf = os.buffer();
      for (int i = 0; i < 399; ++i)
      {
        os << "123456789 ";
        BOOST_CHECK_EQUAL(buf.length(), 10*(i+1));
        BOOST_CHECK_EQUAL(buf.avail(), 4000 - 10*(i+1));
      }
    
      os << "abcdefghi ";
      BOOST_CHECK_EQUAL(buf.length(), 3990);
      BOOST_CHECK_EQUAL(buf.avail(), 10);
    
      os << "abcdefghi";
      BOOST_CHECK_EQUAL(buf.length(), 3999);
      BOOST_CHECK_EQUAL(buf.avail(), 1);
    }
    
    
    • 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
    • 222
    • 223
    • 224
    • 225
    • 226
    • 227
    • 228
    • 229
    • 230
    • 231
    • 232
    • 233
    • 234
    • 235
    • 236
    • 237

    在这里插入图片描述

  • 相关阅读:
    软考高级-系统架构师-软件架构设计
    平衡二叉搜索树--AVL树
    产品人生(9):从“波士顿矩阵”看“个人职业规划”
    Hive数据仓库工具基本架构和入门部署详解
    yolov5注意力机制改进
    Linux下PCIE设备分析软件
    node.js的认识与安装
    C++ 虚函数和多态性
    echarts地图各种点位实现
    (4)STM32的SPI协议及LED点亮
  • 原文地址:https://blog.csdn.net/m0_61705102/article/details/127897057