关于C++日志库spdlog

news2025/4/12 13:56:50

关于C++日志库spdlog

spdlog是一个高性能、易于使用的C++日志库,广泛应用于现代C++项目中。它支持多线程、异步日志记录、多种日志格式、以及灵活的输出方式(如控制台、文件、甚至自定义输出)。下面将就常用功能方面介绍spdlog的安装、配置和使用方法

在Ubuntu下安装和配置spdlog库

安装spdlog库方式如下:

sudo apt install libspdlog-dev

安装完成后,还不可以直接使用,如果使用的是g++编译器,那么编译时需要写为如下例子:

g++ -std=c++11 -o test test.cpp -lspdlog -lfmt -lpthread

其中fmt是一个格式化库,spdlog使用fmt库来格式化日志消息,如果没有按照会出现链接时报错,Ubuntu下按照方式如下:

sudo apt-get install libfmt-dev

何为sink

为了后面的理解顺利,首先了解何为sink。在spdlog中,sink(接收器) 是一个核心概念,它决定了日志消息最终输出到哪里,每个日志对象可以关联一个或多个sink,其决定了日志的物理存储位置或显示方式

常见sink类型有如下几种:

  • 控制台输出:stdout_color_sink_mt(彩色)、stdout_sink_mt
  • 文件输出:basic_file_sink_mtrotating_file_sink_mt
  • 系统日志:syslog_sink_mt(Linux)、win_eventlog_sink_mt(Windows)
  • 其他:UDP网络、数据库等(需自定义)

在spdlog中,命名后缀含义如下:
- _mt:多线程安全版本(mutex protected)
- _st:单线程版本(无锁,性能更高)

例如下面的代码:

// 创建控制台彩色sink(多线程安全)
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

// 创建文件sink(自动创建文件)
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log");

spdlog中的sink工作原理如下:

  • 当调用logger->info()时,消息会传递给所有关联的sink
  • 每个sink独立处理消息(格式化、过滤、输出)
  • sink之间互不影响

在实际应用中,sink的组合使用可以实现多种复杂的日志输出需求,例如:

  • 同时输出到控制台和文件
  • 不同级别日志输出到不同文件
  • 关键错误同时发送到邮件/短信

基本用法

包含头文件

在代码中引入spdlog头文件:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h" // 文件日志
#include "spdlog/sinks/stdout_color_sinks.h" // 控制台彩色日志

创建日志记录器

spdlog提供了多种日志记录器(logger),可以根据需求选择不同的类型:

  1. 控制台日志
  2. 文件日志
  3. 混合日志(控制台 + 文件)
控制台日志
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() 
{
    // 创建一个控制台日志记录器
    auto console = spdlog::stdout_color_mt("console");

    // 记录日志
    console->info("Welcome to spdlog!");
    console->error("This is an error message.");
    console->warn("This is a warning message.");

    return 0;
}

输出结果:

在这里插入图片描述

在上面的代码中,stdout_color_mt函数的参数表示日志记录器的名称标识符,这是一个字符串标识符,用于唯一标识这个特定的日志记录器实例

如果想取出这个实例,可以通过下面的方式:

auto console = spdlog::get("console");

例如:

// 通过名称获取已注册的记录器
auto logger = spdlog::get("console");
if(logger) 
    logger->info("Got existing logger");
文件日志
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() 
{
    // 创建一个文件日志记录器
    auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt");

    // 记录日志
    file_logger->info("Log message to file.");
    file_logger->error("Error message to file.");

    return 0;
}

当前目录结构:

.
├── test.cc
├── logs
    └── basic-log.txt

文件内容如下:

[2025-04-09 15:28:07.245] [file_logger] [info] Log message to file.
[2025-04-09 15:28:07.245] [file_logger] [error] Error message to file.

basic_logger_mt中还可以设置第三个参数,表示是否启用截断模式,即不论是否文件中存在内容都会清除该文件内容再继续写入,即:

auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt", true); // true 表示截断文件
混合日志(控制台 + 文件)

可以同时向多个目标输出日志:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() 
{
    // 创建控制台和文件日志记录器
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/mixed-log.txt", true);

    // 组合多个 sink
    spdlog::sinks_init_list sink_list = {console_sink, file_sink};
    auto combined_logger = std::make_shared<spdlog::logger>("multi_sink", sink_list.begin(), sink_list.end());

    // 注册日志记录器
    spdlog::register_logger(combined_logger);

    // 记录日志
    combined_logger->info("This message will appear in both console and file.");
    combined_logger->error("Error message to both outputs.");

    return 0;
}

在上面代码中,sinks_init_list是spdlog定义的一个类型别名,本质上是std::initializer_list的封装,专门用于传递多个sink的初始化列表

实际上在spdlog内部定义为:

using sinks_init_list = std::initializer_list<sink_ptr>;

主要用于需要将日志同时输出到多个目标的情况,比如:

  • 同时输出到控制台和文件
  • 同时输出到本地文件和网络
  • 不同日志级别输出到不同目标

日志级别

spdlog支持以下几种日志级别,等级程度从低到高(从详细到不启动日志,处于上面的日志会包含其下的所有日志等级):

  • trace: 最详细的调试信息
  • debug: 调试信息
  • info: 一般信息
  • warn: 警告信息
  • error: 错误信息
  • critical: 致命错误信息
  • off: 关闭日志

可以通过以下方式设置全局日志级别:

spdlog::set_level(spdlog::level::debug); // 设置为 debug 级别

此时文件和控制台就会输出同样等级的日志信息,例如下面的示例:

#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"
#include "spdlog/sinks/basic_file_sink.h"

int main()
{
    // 创建控制台和文件日志记录器
    auto console = spdlog::stdout_color_mt("console");
    auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/level-demo.txt", true);

    // 设置不同日志级别
    console->set_level(spdlog::level::debug);    // 控制台显示debug及以上级别
    file_logger->set_level(spdlog::level::warn); // 文件只记录warn及以上级别

    // 测试不同级别的日志
    console->trace("控制台trace消息 - 不会显示");
    console->debug("控制台debug消息");
    console->info("控制台info消息");
    console->warn("控制台warn消息");
    console->error("控制台error消息");

    file_logger->trace("文件trace消息 - 不会记录");
    file_logger->debug("文件debug消息 - 不会记录");
    file_logger->info("文件info消息 - 不会记录");
    file_logger->warn("文件warn消息");
    file_logger->error("文件error消息");

    // 动态修改日志级别
    console->info("准备修改日志级别...");
    console->set_level(spdlog::level::info); // 提高控制台日志级别

    console->debug("修改后的debug消息 - 现在不会显示了");
    console->info("修改后的info消息 - 仍然显示");

    return 0;
}

=== “控制台输出”

[2025-04-09 17:13:32.158] [console] [debug] 控制台debug消息
[2025-04-09 17:13:32.158] [console] [info] 控制台info消息
[2025-04-09 17:13:32.158] [console] [warning] 控制台warn消息
[2025-04-09 17:13:32.158] [console] [error] 控制台error消息
[2025-04-09 17:13:32.158] [console] [info] 准备修改日志级别...
[2025-04-09 17:13:32.158] [console] [info] 修改后的info消息 - 仍然显示

=== “文件内容”

[2025-04-09 17:13:32.158] [file_logger] [warning] 文件warn消息
[2025-04-09 17:13:32.158] [file_logger] [error] 文件error消息

也可以为每个日志记录器单独设置日志级别,例如下面的代码只会记录warn及之后所有的信息:

#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"

int main()
{
    // 首先删除旧的日志文件,确保结果准确
    // 创建一个文件日志记录器
    auto file_logger = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt", true); // true 表示截断文件

    file_logger->set_level(spdlog::level::warn);

    // 尝试记录不同级别的消息
    file_logger->trace("这是 trace 消息");
    file_logger->debug("这是 debug 消息");
    file_logger->info("这是 info 消息");
    file_logger->warn("这是 warn 消息");
    file_logger->error("这是 error 消息");
    file_logger->critical("这是 critical 消息");

    return 0;
}

文件中的内容如下:

[2025-04-09 17:11:03.019] [file_logger] [warning] 这是 warn 消息
[2025-04-09 17:11:03.019] [file_logger] [error] 这是 error 消息
[2025-04-09 17:11:03.019] [file_logger] [critical] 这是 critical 消息

格式化日志

默认情况下,spdlog有自己的格式风格,spdlog支持丰富的日志格式化功能,也可以通过 set_pattern 方法自定义日志格式

自定义日志格式

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() 
{
    auto console = spdlog::stdout_color_mt("console");

    // 自定义日志格式
    console->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v");

    // 记录日志
    console->info("Custom format log message.");

    return 0;
}

对于上面的代码,格式说明如下:

  • %Y-%m-%d %H:%M:%S.%e: 时间戳(年-月-日 时:分:秒.毫秒)
  • %^%l%$: 日志级别(带颜色)
  • %v: 日志消息内容

详细的日志格式说明

spdlog提供了丰富的格式占位符,可以自定义日志输出的格式。以下是常用的格式占位符及其说明:

基础格式占位符

  1. 时间相关

    • %Y:4位年份(如2025)
    • %m:2位月份(01-12)
    • %d:2位日期(01-31)
    • %H:24小时制小时(00-23)
    • %M:分钟(00-59)
    • %S:秒(00-59)
    • %e:毫秒(000-999)
    • %f:微秒(000000-999999)
    • %z:时区偏移(如+0800)
  2. 日志内容

    • %v:实际日志消息内容
    • %n:日志记录器名称
    • %l:日志级别(小写:trace/debug/info等)
    • %L:日志级别(大写:TRACE/DEBUG/INFO等)
    • %t:线程ID
  3. 颜色控制

    • %^:开始颜色范围
    • %$:结束颜色范围
    • 示例:%^%l%$会给日志级别着色

高级格式占位符

显示进程信息:

  • %P:进程ID
  • %u:线程名称(如果设置了)

例如:

#include "spdlog/spdlog.h"

int main() {
    auto logger = spdlog::stdout_color_mt("logger");
    
    // 自定义格式:时间+级别(彩色)+线程ID+消息
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [thread:%t] %v");
    logger->info("这是一条测试消息");
    
    return 0;
}

输出结果如下:

[2025-04-09 15:30:45.678] [info] [thread:1234] 这是一条测试消息

注意事项

  1. 要使用显示文件名和行号的日志,需要使用相关的宏函数,例如SPDLOG_INFO("信息"),具体使用见下面的日志宏函数部分

  2. 颜色控制占位符%^%$必须成对使用,通常用于日志级别显示

  3. 性能考虑:越详细的格式(特别是源码位置)对性能影响越大,建议生产环境使用简单格式

异步日志

本部分后续会进一步补充,此处给出一个简单的示例,后续会详细介绍异步日志的使用

spdlog支持异步日志记录,可以显著提高性能。需要在初始化时启用异步模式

#include "spdlog/async.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() 
{
    // 初始化异步日志队列(建议大小为 8192)
    spdlog::init_thread_pool(8192, 1);

    // 创建异步文件日志记录器
    auto async_file = spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async-log.txt");

    // 记录日志
    async_file->info("Asynchronous log message.");
    async_file->error("Asynchronous error message.");

    // 刷新日志队列
    spdlog::flush_on(spdlog::level::info);

    return 0;
}

高级特性

日志轮转

日志轮转(Log Rotation)是一种日志管理技术,主要用于解决日志文件不断增长导致的问题,当日志文件达到特定条件(如大小限制或时间间隔)时,自动创建新的日志文件,同时归档或删除旧的日志文件。需要日志轮转的原因:

  1. 防止单个日志文件过大
  2. 自动归档历史日志
  3. 节省磁盘空间
  4. 便于日志管理和分析

常见轮转策略有下面两种:

  1. 基于大小的轮转:当日志文件达到指定大小时创建新文件
  2. 基于时间的轮转:按固定时间间隔(如每天)创建新文件

日志轮转过程模拟,假设配置为保留3个文件,文件名为mylog.txt

  • 初始:mylog.txt(当前日志)
  • 第一次轮转:
    • mylog.txt.1(最新备份,包含的是最初的mylog.txt中的内容)
    • 新建空mylog.txt
  • 第二次轮转:
    • mylog.txt.2(最新备份,最开始的mylog.txt中的内容)
    • mylog.txt.1(上一次mylog.txt中的内容)
    • mylog.txt
  • 第三次轮转:
    • 删除mylog.txt.3(最新备份,包含的是最初的mylog.txt中的内容 )
    • mylog.txt.2(包含的是第二次mylog.txt.1中的内容)
    • mylog.txt.1 (包含的是第二次的mylog.txt中的内容)
    • mylog.txt

日志轮转的实际应用场景一般有:

  • 长期运行的服务程序
  • 高频率日志记录的应用
  • 磁盘空间有限的系统
  • 需要长期保存历史日志的场景

在spdlog中,轮转是自动完成的,开发者只需配置轮转策略即可。spdlog支持基于文件大小或时间的日志轮转功能

需要注意,spdlog本身并不支持日志回滚(Log Rollback),但是支持日志轮转(Log Rotation)。如果想通过spdlog实现日志回滚可以结合C++ 17的filesystem库实现,例如:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h"
#include <filesystem>

int main() {
    // 定义日志文件路径
    std::string log_file = "logs/mylog.txt";
    std::string backup_file = "logs/mylog_backup.txt";

    // 如果需要回滚,从备份文件恢复
    if (std::filesystem::exists(backup_file)) {
        std::filesystem::copy(backup_file, log_file, std::filesystem::copy_options::overwrite_existing);
    }

    // 创建日志器
    auto logger = spdlog::basic_logger_mt("logger_name", log_file);

    // 记录日志
    logger->info("This is a test log message.");

    // 备份当前日志文件
    std::filesystem::copy(log_file, backup_file, std::filesystem::copy_options::overwrite_existing);

    return 0;
}
基于文件大小的轮转
#include "spdlog/sinks/rotating_file_sink.h"

int main() 
{
    // 创建一个轮转文件日志记录器(最大5MB,保留3个文件)
    auto rotating_logger = spdlog::rotating_logger_mt("rotating_logger", "logs/rotate-log.txt", 1024 * 1024 * 5, 3);

    // 记录日志
    for (int i = 0; i < 10; ++i) {
        rotating_logger->info("Log message {}", i);
    }

    return 0;
}
基于时间的轮转
#include "spdlog/sinks/daily_file_sink.h"

int main() 
{
    // 创建一个每日轮转日志记录器(每天凌晨 0 点轮转)
    auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily-log.txt", 0, 0);

    // 记录日志
    daily_logger->info("Daily log message.");

    return 0;
}

自定义Sink

如果需要将日志输出到其他目标(如网络、数据库等),可以实现自定义Sink,例如下面的示例:

#include "spdlog/sinks/base_sink.h"

// 自定义 Sink 类
template<typename Mutex>
class MySink : public spdlog::sinks::base_sink<Mutex> 
{
protected:
    void sink_it_(const spdlog::details::log_msg& msg) override {
        // 将日志消息转换为字符串
        spdlog::memory_buf_t formatted;
        this->formatter_->format(msg, formatted);

        // 输出到自定义目标(例如网络)
        std::cout << fmt::to_string(formatted);
    }

    void flush_() override {
        // 刷新操作
    }
};

using MySinkMT = MySink<std::mutex>;

int main() 
{
    auto my_sink = std::make_shared<MySinkMT>();
    auto logger = std::make_shared<spdlog::logger>("custom_logger", my_sink);

    // 记录日志
    logger->info("Custom sink log message.");

    return 0;
}

日志宏函数

spdlog提供了一组宏函数,用于简化日志记录操作,同时支持自动捕获源码位置(文件名、行号和函数名)。这些宏函数是spdlog的核心特性之一,尤其在调试和开发过程中非常有用

以下是spdlog中常见的宏函数及其用途的详细说明:

基础日志宏函数

这些宏函数对应于不同的日志级别,自动捕获源码位置信息并记录日志,常见的有如下:

宏函数对应的日志级别用途
SPDLOG_TRACEtrace记录最详细的调试信息,通常仅在开发或调试阶段使用。
SPDLOG_DEBUGdebug记录调试信息,适合开发阶段使用。
SPDLOG_INFOinfo记录一般信息,适合生产环境中的常规日志记录。
SPDLOG_WARNwarn记录警告信息,提示潜在问题但不影响程序运行。
SPDLOG_ERRORerror记录错误信息,表示程序运行中发生了错误但未导致崩溃。
SPDLOG_CRITICALcritical记录严重错误信息,通常表示程序即将崩溃或无法继续运行。

例如下面的代码:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    auto logger = spdlog::stdout_color_mt("logger");

    // 设置日志格式:显示文件名、行号和消息
    SPDLOG_TRACE("Trace message");
    SPDLOG_DEBUG("Debug message");
    SPDLOG_INFO("Info message");
    SPDLOG_WARN("Warning message");
    SPDLOG_ERROR("Error message");
    SPDLOG_CRITICAL("Critical message");

    return 0;
}

输出结果如下:

[2025-04-09 17:55:01.327] [info] [test.cc:151] Info message
[2025-04-09 17:55:01.327] [warning] [test.cc:152] Warning message
[2025-04-09 17:55:01.327] [error] [test.cc:153] Error message
[2025-04-09 17:55:01.327] [critical] [test.cc:154] Critical message
自定义宏函数

除了上述预定义的宏函数,spdlog还允许用户定义自己的宏函数。这在需要扩展功能或适配特定需求时非常有用,例如下面的代码:

#define MY_LOG_INFO(logger, ...) SPDLOG_INFO(__VA_ARGS__)
#define MY_LOG_ERROR(logger, ...) SPDLOG_ERROR(__VA_ARGS__)

int main() {
    auto logger = spdlog::stdout_color_mt("logger");

    // 使用自定义宏函数
    MY_LOG_INFO(logger, "This is a custom info message.");
    MY_LOG_ERROR(logger, "This is a custom error message.");

    return 0;
}

在上面的代码中,在C/C++宏定义中,...__VA_ARGS__是可变参数宏(Variadic Macros)的语法,用于处理不定数量的参数。具体解释如下:

  1. ...(省略号):在宏定义中表示该宏接受可变数量的参数,必须出现在宏参数列表的最后。
  2. __VA_ARGS__:是一个预定义的宏,表示"variable arguments"(可变参数),用于展开宏定义中...部分传入的所有参数

例如,上面的宏还可以像下面这样使用:

MY_LOG_INFO(my_logger, "用户{}登录成功,IP地址:{}", user_id, ip_address);

会被展开为:

SPDLOG_INFO("用户{}登录成功,IP地址:{}", user_id, ip_address);
异步日志的宏函数

如果启用了异步日志记录功能(通过 spdlog::init_thread_pool 初始化线程池),所有宏函数仍然可以正常工作。异步日志记录不会影响宏函数的行为

#include "spdlog/async.h"
#include "spdlog/sinks/basic_file_sink.h"

int main() {
    // 初始化异步日志队列
    spdlog::init_thread_pool(8192, 1);

    // 创建异步文件日志记录器
    auto async_logger = spdlog::basic_logger_mt<spdlog::async_factory>("async_logger", "logs/async-log.txt");

    // 设置日志格式
    async_logger->set_pattern("[%s:%#] [%^%l%$] %v");

    // 使用宏函数记录日志
    SPDLOG_INFO("Asynchronous info message");
    SPDLOG_ERROR("Asynchronous error message");

    return 0;
}
其他相关宏

除了日志级别的宏函数外,spdlog还提供了一些辅助宏,用于增强日志功能:

宏名称含义
SPDLOG_LOGGER_CALL通用日志记录宏,允许指定日志级别、日志记录器和消息
SPDLOG_LOGGER_INFO类似于SPDLOG_INFO,但允许指定特定的日志记录器
SPDLOG_LOGGER_ERROR类似于SPDLOG_ERROR,但允许指定特定的日志记录器
SPDLOG_LOGGER_TRACE类似于SPDLOG_TRACE,但允许指定特定的日志记录器

例如下面的代码:

#include "spdlog/spdlog.h"
#include "spdlog/sinks/stdout_color_sinks.h"

int main() {
    auto logger1 = spdlog::stdout_color_mt("logger1");
    auto logger2 = spdlog::stdout_color_mt("logger2");

    // 使用SPDLOG_LOGGER_INFO指定日志记录器
    SPDLOG_LOGGER_INFO(logger1, "This is a message from logger1.");
    SPDLOG_LOGGER_INFO(logger2, "This is a message from logger2.");

    return 0;
}

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

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

相关文章

回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测

回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测 目录 回归预测 | Matlab实现RIME-CNN-GRU-Attention霜冰优化卷积门控循环单元注意力机制多变量回归预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现RIME…

液氮恒温器是做什么的

‌液氮恒温器‌是一种利用液氮作为冷源的恒温装置&#xff0c;主要用于提供低温、恒温或变温环境&#xff0c;广泛应用于科研、工业和医疗等领域。液氮恒温器通过液氮的低温特性来实现降温效果&#xff0c;具有效率高、降温速度快、振动小、成本低等优点。 液氮恒温器应用场景和…

`mpi4py` 是什么; ModuleNotFoundError: No module named ‘mpi4py

mpi4py 是什么 目录 `mpi4py` 是什么ModuleNotFoundError: No module named mpi4pyModuleNotFoundError: No module named mpi4py mpi4py 是一个 Python 模块,它提供了对 MPI(Message Passing Interface)标准的接口,使得 Python 程序能够利用 MPI 进行并行计算。其作用主要…

大数据 - 1. 概述

早期的计算机&#xff08;上世纪70年代前&#xff09; 是相互独立的&#xff0c;各自处理各自的数据上世纪70年代后&#xff0c;出现了基于TCP/IP协议的小规模的计算机互联互通。上世纪90年代后&#xff0c;全球互联的互联网出现。当全球互联网逐步建成&#xff08;2000年左右&…

Java基础下

一、Map Map常用的API //map常用的api//1.添加 put: 如果map里边没有key&#xff0c;则会添加&#xff1b;如果有key&#xff0c;则会覆盖&#xff0c;并且返回被覆盖的值Map<String,String> mnew HashMap<>();m.put("品牌","dj");m.put("…

数据结构和算法(十二)--最小生成树

一、有向图 定义: 有向图是一副具有方向性的图&#xff0c;是由一组顶点和一组有方向的边组成的&#xff0c;每条方向的边都连着一对有序的顶点。 出度: 由某个顶点指出的边的个数称为该顶点的出度。 入度: 指向某个顶点的边的个数称为该顶点的入度。 有向路径: 由一系列顶点组…

TK广告素材优化:提升投放效果的核心策略

在广告投放领域&#xff0c;决定投放效果的三大关键要素是&#xff1a;产品、素材和人群。由于产品相对固定且人群多采用通投策略&#xff0c;因此素材质量成为影响投放效果的决定性因素。 为什么素材如此重要&#xff1f; 素材质量直接影响广告的点击率&#xff0c;进而影响…

8.3.1 MenuStrip(菜单)控件

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 MenuStrip控件提供了程序窗体的主菜单&#xff0c;即显示于窗体顶端部分的菜单。 MenuStrip常用属性&#xff1a; ImageScalingSize…

STM32单片机入门学习——第29节: [9-5] 串口收发HEX数据包串口收发文本数据包

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.09 STM32开发板学习——第29节: [9-5] 串口收发HEX数据包&串口收发文本数据包 前…

Skyline配置指南-微信小程序

Skyline 是微信小程序推出的新一代渲染引擎&#xff0c;提供了更强大的渲染能力和更流畅的性能体验。以下是配置 Skyline 的详细步骤&#xff1a; 一、app.json文件配置 "componentFramework": "glass-easel", "lazyCodeLoading": "requi…

Spring MVC 重定向(Redirect)详解

Spring MVC 重定向&#xff08;Redirect&#xff09;详解 1. 核心概念与作用 重定向&#xff08;Redirect&#xff09; 是 Spring MVC 中一种客户端重定向机制&#xff0c;通过 HTTP 302 状态码&#xff08;默认&#xff09;将用户浏览器重定向到指定 URL。 主要用途&#xf…

window上 docker使用ros2开发并usbip共享usb设备

曾经参考 https://blog.csdn.net/laoxue123456/article/details/138339029 来共享windows上的usb 发现没有办法成功总是出现 tcp 错误。telnet测试能够正常连接 很是奇怪&#xff0c;window上换成低版本的usbipd仍然是同样的错误&#xff0c;没有办法的情况下参考了docker官方文…

基于MATLAB/simulink的信号调制仿真--AM调制

实验内容&#xff1a; 假设y(t)(20.5*2cos&#xff08;2*pi*1000*t&#xff09;)*5cos&#xff08;2*pi*2*1e4*t&#xff09;调幅系统&#xff0c;请将一个频率为1000HZ的余弦波信号&#xff0c;通过进行AM调制&#xff0c;载波信号频率为20kHZ的余弦波&#xff0c;调制度ma0.…

Vue3+Ts封装ToolTip组件(2.0版本)

本组件支持hover和click两种触发方式&#xff0c;需要更多的触发方式&#xff0c;可自行去扩展&#xff01;&#xff01;&#xff01; 1.传递三个参数&#xff1a; content&#xff1a;要展示的文本 position&#xff1a;文本出现的位置&#xff08;"top" | "t…

Latex语法入门之数学公式

Latex是一种高质量的排版系统&#xff0c;尤其擅长于数学公式的排版。本文我将带大家深入了解Latex在数学公式排版中的应用。从基础的数学符号到复杂的公式布局&#xff0c;我们都会一一讲解&#xff0c;通过本文的学习&#xff0c;你将能够轻松编写出清晰、美观的数学公式&…

shell脚本 - Linux定时温度监控-软硬件检测 - 服务器温度监控 - 写入日志

效果图 脚本 vi auto.sh (chmod x ./auto.sh) #!/bin/bash # 按照日期创建一个文件或目录 https://blog.csdn.net/shoajun_5243/article/details/83539069 datetimedate %Y%m%d-%H%M%S |cut -b1-20 dirpath/systemMonitor/$datetime file1$dirpath/sensors.log file2$dirpa…

Linux驱动开发进阶(六)- 多线程与并发

文章目录 1、前言2、进程与线程3、内核线程4、底半步机制4.1、软中断4.2、tasklet4.3、工作队列4.3.1、普通工作项4.3.2、延时工作项4.3.3、工作队列 5、中断线程化6、进程6.1、内核进程6.2、用户空间进程 7、锁机制7.1、原子操作7.2、自旋锁7.3、信号量7.4、互斥锁7.5、comple…

买不起了,iPhone 或涨价 40% ?

周知的原因&#xff0c;新关税对 iPhone 的打击&#xff0c;可以说非常严重。 根据 Rosenblatt Securities分析师的预测&#xff0c;若苹果完全把成本转移给消费者。 iPhone 16 标配版的价格&#xff0c;可能上涨43%。 iPhone 16 标配的价格是799美元&#xff0c;上涨43%&am…

Axure 列表滚动:表头非常多(横向滚动方向)、分页(纵向滚动) | 基于动态面板的滚动方向和取消调整大小以适合内容两个属性进行实现

文章目录 引言I 列表滚动的操作说明see also共享原型引言 Axure RP9教程 【数据传输】(页面值传递)| 作用域 :全局变量、局部变量 https://blog.csdn.net/z929118967/article/details/147019839?spm=1001.2014.3001.5501 基于动态面板的滚动方向和取消调整大小以适合内容两…

RBAC 权限控制:深入到按钮级别的实现

RBAC 权限控制&#xff1a;深入到按钮级别的实现 一、前端核心思路 1. 大致实现思路 后端都过SELECT连表查询把当前登录的用户对应所有的权限返回过来&#xff0c;前端把用户对应所有的权限 存起来to(vuex/pinia) 中 &#xff0c;接着前端工程师需要知道每个按钮对应的权限代…