muduo源码学习(1):异步日志——日志消息的存储及输出

2023-05-16

目录

前言

日志存储的实现

日志输出的实现

总结


前言

        muduo中的日志,是诊断日志。用于将代码运行时的重要信息进行保存,方便故障诊断和追踪。

        日志一般有两种,一种是同步日志,一种是异步日志,同步日志就是当需要写出一条日志信息的时候,只有等到这条日志消息完全写出之后才能执行后续的程序,可见,这种方式的日志的问题就在于程序可能会阻塞在磁盘写入操作上;

        而另一种异步日志则不会,异步日志的思路是需要写日志消息的时候只是将日志消息进行存储,当积累到一定量或者到达一定时间间隔时,由后台线程自动将存储的所有日志进行输出,可见,对于异步日志来说,每次有日志消息产生的时候,只需要一个存储的行为即可,存储结束就可以执行后面的业务代码了,而真正写入到磁盘的操作,是由后台线程进行的,这样做的好处就是:前台线程不会阻塞在写日志上,后台线程真正写出日志时,日志消息往往已经积累了很多,此时只需要调用一次IO函数,如fwrite,而不需要每条消息都调用一个IO函数,如此减少了IO函数调用次数,提高了效率。

        而muduo中实现的,就是异步日志。

        muduo中的日志消息格式为:时间 线程id 日志级别 日志正文 源文件名及行号。

        日志设置了6种级别:TRACE、DEBUG、INFO、WARN、ERROR和FATAL。

        日志的输出形式为流形式:日志级别<<message; 如 LOG_ERROR<<"file cannot open!";

        如上所述,异步日志的实现关键,是先将日志消息进行存储,然后由后台线程进行写出。muduo将“日志存储”和“日志输出”分为了两大块,现在先来看看日志的存储部分。

日志存储的实现

        前面说过,日志的输出形式为流形式,但muduo并没有使用C++自带的iostream等流库,而是使用自己实现的LogStream,用简短的代码量实现需要的功能,抛去其它不需要的冗杂的功能,使得代码效率更高。

        实现”日志流“的基础,还需要一个内置的缓冲区,muduo通过FixedBuffer类来实现,如下所示:

/*LogStream.h*/
template<int SIZE>//构造时需要指定缓冲区大小
class FixedBuffer : noncopyable
{
 public:
  FixedBuffer()
    : cur_(data_)
  {
    setCookie(cookieStart);
  }

  ~FixedBuffer()
  {
    setCookie(cookieEnd);
  }

  void append(const char* /*restrict*/ buf, size_t len)//在缓冲区空闲处memcpy
  {
    // FIXME: append partially
    if (implicit_cast<size_t>(avail()) > len)//如果剩余的缓冲区空间足以放下len长度的buf
    {
      memcpy(cur_, buf, len);
      cur_ += len;
    }
  }

  const char* data() const { return data_; }
  int length() const { return static_cast<int>(cur_ - data_); }

  char* current() { return cur_; }//返回当前缓冲区空闲位置
  int avail() const { return static_cast<int>(end() - cur_); } //返回剩余有用的空间大小
  void add(size_t len) { cur_ += len; }//当前位置指针往后偏移

  void reset() { cur_ = data_; }//相当于缓冲区重置为空闲
  void bzero() { memZero(data_, sizeof data_); }//清零

  ...
  // for used by unit test
  string toString() const { return string(data_, length()); }//把当前缓冲区的内容转换为string
  StringPiece toStringPiece() const { return StringPiece(data_, length()); }//

 private:
  const char* end() const { return data_ + sizeof data_; }  //返回最后一个字符的地址
  ...
  char data_[SIZE];  //缓冲区
  char* cur_;   //当前空闲缓冲区头部地址
};

     FixedBuffer是一个模板类,模板参数用来指定缓冲区data_的大小,并提供一系列缓冲区操作函数。        

     此时再来看LogStream类,LogStream重载了多个版本的"<<"运算符,如下所示:

/*LogStream.h*/
class LogStream : noncopyable   
{
  typedef LogStream self;
 public:
  typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;  //4K大小的缓冲区类型
 
/*多个<<重载版本,内部实际上都调用了append函数*/
  self& operator<<(bool v);
  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);
  self& operator<<(char v);
  self& operator<<(const char* str);
  self& operator<<(const unsigned char* str);
  self& operator<<(const string& v);
  self& operator<<(const StringPiece& v);
  self& operator<<(const Buffer& v);

  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);  //用来将T类型的数字转换为字符串,放到缓冲区中

  Buffer buffer_;   //缓冲区

  static const int kMaxNumericSize = 32;
};

       可以看到,LogStream类通过重载”<<“来实现”日志流“的形式,不管是哪一种重载版本,最终都是会调用append函数,而append函数中又会去调用LogStream类中buffer_的append函数,可以看到,这里的buffer_实际上就是一个FixedBuffer的对象,也就相当于是一个缓冲区,其大小由kSmallBuffer指定,为4K字节。

 

       通过上面的分析,可以知道”日志流“是通过LogStream来实现的,而”日志流“中的缓冲区则是由FixedBuffer来实现。

       至此也就明白了每条日志是如何通过"<<"来进行存储的,但是光是存储是不行的,存储只是一个收集日志消息的作用,不可能把大量的日志都放在内存中,最终还是得将收集到的所有日志输出到某一个指定的地方去,因此接着来分析日志的输出。

 

日志输出的实现

       显然,作为一个日志输出的类,其中应包含LogStream的一个实例用来存储日志消息,这一点体现在Impl类中,如下所示:

class Impl
{
 public:
  typedef Logger::LogLevel LogLevel;
  Impl(LogLevel level, int old_errno, const SourceFile& file, int line);//需要用日志级别、错误信息、文件名和行号构造
  void formatTime();  //获取当前的时间到time_中
  void finish(); 

  Timestamp time_;     //时间
  LogStream stream_;   //自带一个4K大小缓冲区
  LogLevel level_;     //日志级别
  int line_;           //行数
  SourceFile basename_;    //文件名
};

        Impl的5个成员,分别对应了日志消息生成的时间、存储的日志消息、日志消息级别、日志消息所在的文件名(注意,这个文件名指的是产生日志的线程所在的文件名)及其行数,这几乎构成了在本文最初所说的muduo日志消息格式中的大部分要素。而对于Impl的构造函数,则需要指定这些信息,来看下Impl类唯一的构造函数以及finish函数:

Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
  : time_(Timestamp::now()),
    stream_(),
    level_(level),
    line_(line),
    basename_(file)
{
  formatTime();//根据time_获取当前时间,写入缓冲区
  CurrentThread::tid();
  stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());//向缓冲区中写入线程id
  stream_ << T(LogLevelName[level], 6);//向缓冲区中写入日志级别
  if (savedErrno != 0)
  {
    stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
  }
}
void Impl::finish()//向缓冲区中写入文件名和行号
{
  stream_ << " - " << basename_ << ':' << line_ << '\n';
}

        可以看到,在构造函数中,就会将当前时间、当前线程id以及日志级别写入到缓冲区stream_中,如果构造函数的第二个参数不为0,那么还会把系统错误errno对应的错误描述也添加到缓冲区stream_中。而在finish函数中,则会将文件名和行号写入缓冲区。至此可以看到,通过Impl类可以将日志格式中除日志正文外的所有要素都写入缓冲区stream_中,那么,日志正文又是何时写入呢?

        完整的日志输出,是通过Logger类来实现的,其定义如下所示:

class Logger
{
 public:
  enum LogLevel   //几种日志级别
  {
    TRACE,  
    DEBUG,  
    INFO,   
    WARN,   
    ERROR,  
    FATAL,  
    NUM_LOG_LEVELS,
  };

  // compile time calculation of basename of source file
  ...
  //用构造函数的参数去初始化impl类型成员,即初始化缓冲区
  Logger(SourceFile file, int line);
  Logger(SourceFile file, int line, LogLevel level);
  Logger(SourceFile file, int line, LogLevel level, const char* func);
  Logger(SourceFile file, int line, bool toAbort);
  ~Logger();

  LogStream& stream() { return impl_.stream_; }

  static LogLevel logLevel();
  static void setLogLevel(LogLevel level);

  typedef void (*OutputFunc)(const char* msg, int len);
  typedef void (*FlushFunc)();
  static void setOutput(OutputFunc);
  static void setFlush(FlushFunc);
  static void setTimeZone(const TimeZone& tz);
 private:
  Impl impl_;//构造Logger类时就会构造impl_,此时缓冲区中就会自动写入当前线程id和日志级别
};
Logger::~Logger()
{
  impl_.finish();//析构时向缓冲区写入文件名和行号
  const LogStream::Buffer& buf(stream().buffer());//获取当前缓冲区的所有数据
  g_output(buf.data(), buf.length());//调用输出函数
  ...
}

        可以看到,在Logger类中,存在一个Impl类的实例,Logger类的多种构造函数实际上也是用构造参数去构造了impl,因此,日志消息本身实际上是存在于impl_中的,如前所述,它在构造时会向impl_的缓冲区stream_中写入时间、线程id和日志级别,而在Logger析构时,又会调用finish函数,向impl_的缓冲区stream_中写入文件名和行号。因此,如果想要写入日志正文,实际上也是对impl_的缓冲区stream_进行写入,如通过Logger实例执行stream()<<"log message";那么在该Logger实例析构时,就会将时间+线程id+日志级别+"log message"+文件名+行号写到Logger中impl_成员的缓冲区中。

        当然,Logger调用析构函数时,日志才算是按照格式完整写好,此时就需要立刻将这条日志”写出到某个指定的地方“来完成日志的输出功能。实际的日志输出,是通过一个全局函数指针g_output实现的,当日志写完后,Logger的析构函数就会调用g_output所指向的函数进行日志输出,g_output可由setOutput函数进行设置,也提供默认的输出函数,如下所示:

void defaultOutput(const char* msg, int len)
{
  size_t n = fwrite(msg, 1, len, stdout);
  //FIXME check n
  (void)n;
}

       可见,默认的输出函数就是将日志消息写到到stdout即标准输出中,在未重定向stdout的情况下,会将这条日志消息打印到终端屏幕上。 

       至此,也就完成了日志的输出。

 

       根据Logger类也可以知道,每一条日志消息应当都对应一个Logger类的实例,实例构造和析构时会自动写入一些必备的消息元素,并在析构时自动将该日志消息通过g_output进行输出。

 

       muduo中每一种日志级别都对应了一条宏定义,如下所示:

#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
  muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
  muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()

        这些宏定义,最终都是对应impl_成员的缓冲区,只不过用来构造impl_的日志级别参数有所不同。这也就相当于构造了不同的临时缓冲区对象,如调用LOG_Info<<"log info";就会构造一个临时的Logger对象,在该条语句执行结束后就会析构,这样就可以通过一条语句实现日志消息的写入和输出,最终将 时间+线程id+日志级别+"log message"+文件名+行号 打印到终端屏幕上。

        每一条日志消息都会对应一个Logger的临时对象,这样的好处是可以及时的将日志消息进行输出,占用的内存也可以立即释放。

 

总结

       至此,分析了muduo中日志消息的存储以及输出,不过还未涉及到异步的实现,将在下文中分析。

       日志的存储主要涉及到两个类:

        FixedBuffer用于实现一个指定大小的缓冲区,是每条日志消息的实际存储位置;

        LogSteam中含有FixedBuffer的一个实例,其大小默认为4K,因此LogStream也通过FixedBuffer实现了日志消息的存储,除此之外,LogStream类还重载了一系列的"<<",由此来实现”日志流“,从而可以流的形式来写入日志消息;

       日志的输出也主要涉及到两个类:

        Impl类包含了一个LogStream实例,用于存储日志消息,Impl类在构造和调用finish函数时会向LogStream中写入时间等信息;

       Logger类中包含了一个Impl类实例,可以通过stream()函数来获取该实例下的LogStream,从而向其中写入日志正文。在Logger析构时调用g_output所指向的函数,将缓冲区中的日志消息全部输出。

       每条日志消息对应了一个Logger的临时对象,每条日志消息对应的缓冲区大小默认为4K。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo源码学习(1):异步日志——日志消息的存储及输出 的相关文章

  • 从旋转矩阵计算欧拉角

    旋转矩阵和欧拉角之间的正向转换关系比较好推理 xff0c 而逆向变换就显得不是那么容易了 这篇博客介绍由旋转矩阵计算欧拉角的方法 xff0c 参考了一篇Paper xff1a Computing Euler angles from a ro
  • aiohttp 异步http请求-1.快速入门 get 请求示例

    前言 在 python 的众多 http 请求库中 xff0c 大家最熟悉的就是 requests 库了 xff0c requests 库上手非常容易 xff0c 适合入门学习 如果平常工作中对发请求不追求效率和并发的情况下 xff0c r
  • Flask 学习-67.钩子函数before_request 和 before_first_request 的使用

    前言 学过pytest框架的肯定知道什么叫钩子 xff08 hook xff09 函数 钩子函数的作用是在程序运行的过程中插入一段代码做一些事情 四个钩子 请求钩子是通过装饰器的形式实现 xff0c Flask支持如下四种请求钩子 xff1
  • 30岁自学嵌入式找工作,可行吗?前景怎么样?

    大家好 xff0c 我是张巧龙 xff0c 在知乎上看到一个问题 xff1a 30岁自学嵌入式找工作 xff0c 可行吗 xff1f 看看一个高赞回答 xff1a 注 xff1a 以下内容不代表本公众号观点 xff0c 仅供参考 不可行 嵌
  • 0基础在ROS系统中实现RRT算法(四)URDF集成gazebo并搭建gazebo仿真环境

    小白一枚 xff0c 毕设突发奇想加入了ROS的内容 xff0c 不知道自己还能不能毕业 以下均为通过看视频 xff0c 翻博客等整理而成的笔记 xff0c 并非我的原创 可能会出现一些报错的修改或者简单的代码是我自己做的 哈哈 Gazeb
  • 如何在vscode中优雅的编写C语言

    如何在vscode中优雅的编写C语言 各位好 xff0c 我认为vscode编辑器在windows环境下除了Pycharm外是最方便的IDE了 xff0c 但在初学C语言时很少有人的第一个C语言软件使用的是vscode来编译与运行 xff0
  • Unity 使用RVO2(orca)算法

    RVO算法官方下载 https github com snape RVO2 CS git 官方版本的RVO只支持增加移动代理和障碍物 xff0c 不支持删除移动代理和障碍物 不太符合实际应用 我拓展了删除移动代理与障碍物的方法 示例项目 x
  • 51单片机串口通讯UART

    1 串行通信的的基本知识 在实际的工业生产 xff0c 或者生活中 xff0c 计算机的CPU要与外部的设备之间进行信息的交流 xff0c 数据的交换 xff0c 所有的这些信息交换均可称为通信 通信的方式有两种 xff0c 分别为串行通信
  • 库函数开发与寄存器开发

    在以前 8 位机时代的程序开发中 xff0c 一般直接配置芯片的寄存器 xff0c 控制芯片的工作方式 xff0c 如中断 xff0c 定时器等 配置的时候 xff0c 常常要查阅寄存器表 xff0c 看用到哪些配置位 xff0c 为了配置
  • Arduino修改Serial接收缓冲区大小

    看到网上有资料说 xff0c 直接添加以下宏定义就可以了 xff1a span class token macro property span class token directive keyword define span SERIAL
  • RT-Thread nmealib库WH-GN100模块设置仅支持北斗

    RT Thread nmealib库主页 在nmea thread init函数的末尾 xff0c 添加以下代码块 xff0c 发送配置指令 xff0c 仅使用北斗卫星 xff0c 即可配置成仅GPS卫星工作模式 span class to
  • C#中字符串判断为空或者空格

    最近遇到这个问题 xff0c 来大概说一下C 中字符串判断为空或者空格这个问题 xff08 1 xff09 字符串为空null xff0c 怎么讲就是内存中没有放东西 xff0c 比如新创建的字符串就为空null xff0c string
  • 【冷知识】火车票座位分布知识点

    最近到了每年过年 xff0c 春运火车高峰期的时候了 xff0c 有的人想知道自己具体的位置在哪里 xff08 比如硬座是不是靠窗的 xff0c 座位的大小号排序等 xff09 xff0c 现在来讲讲这方面的知识点 xff0c 个人整理 列
  • QT中的自定义信号以及自定义函数

    信号与槽函数是QT的一大创新 xff0c 通过自定义信号与槽函数可以实现自己想实现的功能 标准的信号与槽写法如下 xff1a connect amp button amp QPushButton clicked this amp QWidg
  • 如何摆放PCB元器件?(建议收藏)

    PCB设计 xff0c 既是科学也是艺术 其中有非常多关于布线线宽 布线叠层 原理图等等相关的技术规范 xff0c 但当你涉及到PCB设计中具有艺术特质元器件布局问题时 xff0c 问题就变得有趣起来了 事实上 xff0c 关于元器件摆放限
  • 【MFC开发(6)】复选框按钮控件Check Box

    1 新建复选框 直接拖拽即可 xff0c 设置名字可修改caption内容 2 设置默认选中 复选框可多选 xff0c 所以可以给很多复选框按钮进行选中 xff0c 代码如下所示 xff0c 放在dlg初始化函数中实现 获取多选框香蕉的指针
  • 【MFC开发(15)】进度条控件Progress Control

    1 进度条控件的常用方法 首先给控件添加一个变量 在dlg初始化函数钟进行方法的实现 进度条显示区域 设置进度条的范围 m progress SetRange 0 100 设置进度条当前的位置 m progress SetPos 75 获取
  • 【MFC开发(16)】树形控件Tree Control

    1 树形控件的属性配置 xff08 1 xff09 Check Boxes xff1a 默认为false xff0c 如果选择为true的话每个节点前面会带有一个方框 xff08 2 xff09 Edit Labels xff1a 默认为f
  • 【MFC开发(17)】高级列表控件List Control

    1 介绍 ListCtrl 高级列表控件也是我们平时编程过程中很常用的一个控件 xff0c 一般涉及到报表展示 记录展示之类的 xff0c 都需要ListCtrl 高级列表控件 例如 xff1a 任务管理器啊 xff0c 文件列表啊 xff
  • STM32L4单片机连接语音模块NVC的源码

    这周写了一下STM32L4的语音模块 xff0c 使用的语音芯片是NVC系列芯片 xff0c 提供一下代码给以后需要的朋友们 xff0c 不喜勿喷 头文件NVC h ifndef NVC H define NVC H 音源 define S

随机推荐

  • oled显示模块ssd1306

    管脚定义 GND 电源地 VCC xff1a 供电电源3 3v 5v都可以 D0 xff1a 串行输入时钟CLK D1 xff1a 串行输入数据 RES xff1a 复位 DC xff1a 控制输入数据 命令 xff08 高电平1为数据 低
  • 上位机串口数据检验方式(一)——校验和

    最近还是在写上位机软件 xff0c 还是有一堆问题 xff0c 因为是第一次做这个东西 xff0c 有些东西只能到论坛上来查 xff0c 最近做到了数据通信 xff0c 刚开始没有想到数据协议这些东西 xff0c 现在涉及到了 xff0c
  • c#上位机开发(三)——串口通信上位机开发1

    今天主要做一个跟市面上差不多的稍微简单点的上位机软件 xff0c 效果如下图所示 1 功能概述 xff08 1 xff09 端口扫描 xff0c 主要是扫描出可用的端口用来连接 xff08 2 xff09 波特率的选择 xff0c 使用一个
  • 使用python执行外部命令subprocess

    1 使用python执行外部命令subprocess subprocess模块是Python自带的模块 xff0c 无须再另行安装 xff0c 它主要用来取代一些旧的模块或方法 xff0c 如os system os spawn os po
  • #Qt on android#使用Qt 获取GPS信号

    注意事项 xff1a 1 Qt版本一定要大于等于5 3 xff0c 因为低于5 3的版本对于android系统来说并不能成功获取gps信号 2 环境正确搭建 xff0c 一定要注意 xff01 构建 xff08 build xff09 的系
  • 2023年TI杯全国大学生电子设计竞赛通知正式发布

    关于组织2023年 全国大学生电子设计竞赛的通知 xff08 电组字 2023 01号 xff09 各赛区组织委员会 各有关高等学校 xff1a 全国大学生电子设计竞赛 xff08 以下简称全国竞赛 xff09 组委会在认真总结往届电子设计
  • HTTPClient调用https请求,通过基本认证用户名密码(Basic Auth)

    本文来源是Apache官网例子 xff1a httpcomponents client ClientAuthentication java at 4 5 x apache httpcomponents client GitHub 之前找过很
  • c中结构体数据对齐问题

    1 为什么需要数据对齐 提升CPU读取数据的效率 CPU每次都是从以4字节 xff08 32位CPU xff09 或是8字节 xff08 64位CPU xff09 的整数倍的内存地址中读进数据的 xff08 例如32位的只能0x000000
  • js打开新窗口的方法总结

    Window open 方法 完整代码 window span class token punctuation span span class token function open span span class token punctu
  • 一文详解堆栈(二)——内存堆与内存栈

    前言 xff1a 我们经常听见一个概念 xff0c 堆 xff08 heap xff09 和栈 xff08 stack xff09 xff0c 其实在数据结构中也有同样的这两个概念 xff0c 但是这和内存的堆栈是不一样的东西哦 xff0c
  • 《动手学ROS2》3.2ROS2工作空间介绍

    本系列教程作者 xff1a 小鱼 公众号 xff1a 鱼香ROS QQ交流群 xff1a 139707339 教学视频地址 xff1a 小鱼的B站 完整文档地址 xff1a 鱼香ROS官网 版权声明 xff1a 如非允许禁止转载与商业用途
  • Ubuntu18.04 realsenseD435i深度摄像头外参标定的问题

    Ubuntu18 04 realsenseD435i深度摄像头外参标定的问题 鱼香ROS介绍 xff1a 鱼香ROS是由机器人爱好者共同组成的社区 xff0c 欢迎一起参与机器人技术交流 进群加V xff1a fishros2048 文章信
  • STM-32:USART串口协议、串口外设—数据发送/数据发送+接收

    目录 一 串口通信1 1通信接口1 2串口通信1 2 1简介1 2 2硬件电路1 2 3串口参数及时序 二 STM32的USART外设2 1USART简介2 2USART框图 三 数据传输3 1数据帧3 2输入数据策略3 2 1起始位侦测3
  • 大疆M3508、M2006必备CAN总线知识与配置方法

    大疆M3508 M2006必备CAN总线知识与配置方法 文章目录 大疆M3508 M2006必备CAN总线知识与配置方法前言 xff1a 0x00 需要 额外的 CAN收发器 xff01 xff01 xff01 0x01 硬件层面分析为什么
  • 换个角度聊聊PID吧,很干。

    01 前言 大家好 xff0c 前面发了几篇关于PID的文章 xff1a 点击图片即可阅读 教你10分钟完成智能小车的PID调速 快速调试PID参数的3种方法 02 自动控制系统 在直流有刷电机的基础驱动中 xff0c 如果电机负载不变 x
  • linux发起http请求,GET、POST

    GET请求 curl 推荐 curl v 34 https test com login username 61 tyw amp password 61 123 34 curl 34 https test com 34 URL指向的是一个文
  • VSCode对C++的DEBUG调试配置

    C 43 43 vscode上的调试配置 1 调试配置2 修改编译模式 按照本 的流程可在vscode平台上实现像在windows系统下VS调试C 43 43 程序的效果 1 调试配置 当写好代码和 CMakeLists txt 之后 xf
  • VSCode的C/C++扩展功能

    VSCode的C C 43 43 扩展功能 1 在 Linux 上 使用 C 43 43 1 1 创建 Hello World1 2 探索 IntelliSense1 3 构造 helloworld cpp1 3 1 运行 build1 3
  • 从源码理解智能指针(二)—— shared_ptr、weak_ptr

    目录 计数器 Ref count Ref count del Ref count del alloc Ptr base Ptr base的成员变量 构造函数 赋值重载 获取引用计数 减少引用计数 Reset函数 Resetw函数 share
  • muduo源码学习(1):异步日志——日志消息的存储及输出

    目录 前言 日志存储的实现 日志输出的实现 总结 前言 muduo中的日志 xff0c 是诊断日志 用于将代码运行时的重要信息进行保存 xff0c 方便故障诊断和追踪 日志一般有两种 xff0c 一种是同步日志 xff0c 一种是异步日志