如何将代码中的相关调试信息输出到对应的日志文件中

news2024/11/24 20:45:38

一、将调试信息输出到屏幕中

1.1 一般写法

我们平常在写代码时,肯定会有一些调试信息的输出:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    char szFileName[] = "test.txt";
    FILE *fp = fopen(szFileName, "r");
    if (fp == NULL)
    {
        // 文件打开失败,提示错误并退出
        printf("open file(%s) error.\n", szFileName);
        exit(0);
    }
    else
    {
        // 文件打开成功,进行相应的文件读/写操作
    }

    return 0;
}

假设当前目录下没有 test.txt 文件。当程序执行到第 7 行时,必然返回 NULL,这时候通过第 11 行的调试信息,我们可以帮助我们精确排查到程序退出的原因:原来是文件打开失败了。

 那如果当前目录下存在 test.txt 文件,只是不可读呢?

 同样输出了 open file(test.txt) error

在这种情况下如何快速定位文件打开失败的原因呢?我们可以考虑使用 errno

1.2 使用 errno

errno 是记录系统的最后一次错误代码。错误代码是一个 int 型的值,在 errno.h 中定义。

#include <errno.h>	// errno 头文件
#include <string.h>	// strerror 头文件

// 文件打开失败,提示错误并退出
printf("open file(%s) error, errno[%d](%s).\n", szFileName, errno, strerror(errno));

修改后再次运行 main.exe:

如果代码中包含很多的调试信息呢?我们并不能一下子知道这条信息到底是在哪里打印出来的,于是,我们又想,能不能把当前调试信息所在的文件名和源码行位置也打印出来呢,这样不就一目了然了吗。基于此,便有了 1.3 的内容。

1.3 编译器内置宏

ANSI C 标准中有几个标准预定义宏:

  • __LINE__:在源代码中插入当前源代码行号
  • __FILE__:在源文件中插入当前源文件名
  • __FUNCTION_:在源文件中插入当前函数名
  • __DATE__:在源文件中插入当前的编译日期
  • __TIME__:在源文件中插入当前编译时间
  • __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为 1
  • __cplusplus:当编写C++程序时该标识符被定义

于是我们这么修改输出语句:

// 文件打开失败,提示错误并退出
printf("[%s][%s:%d] open file(%s) error, errno[%d](%s).\n", 
                                                    __FILE__,
                                                    __FUNCTION__,
                                                    __LINE__,
                                                    szFileName, 
                                                    errno, strerror(errno));

 从日志信息中,我们可以精确的获取到:main.c 文件中的 main 函数的第 16 行报错了,错误原因是 Permission denied

相比于之前,确实是能帮助我们精准的定位问题,但是,总不能每次都要写这么长的 printf 吧,有没有偷懒的办法呢?

1.4 使用可变宏输出调试信息

1.4.1 可变宏介绍

用可变参数宏(variadic macros)传递可变参数表,你可能很熟悉在函数中使用可变参数表,如:

void printf(const char* format, ...);

在 1999 年版本的 ISO C 标准中,宏可以像函数一样,定义时可以带有可变参数。宏的语法和函数的语法类似,如下所示:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • 缺省号(...)指可变参数
  • __VA_ARGS__宏用来接收不定数量的参数

这类宏在被调用时,它(这里指缺省号...)被表示成零个或多个符号(包括里面的逗号),一直到右括弧结束为止。当被调用时,在宏体( macro body )中,这些符号序列集合将代替里面的 _VA_ARGS_ 标识符。当宏的调用展开时,实际的参数就传递给 printf 了。

相比于 ISO C 标准,GCC 始终支持复杂的宏,它使用一种不同的语法从而可以使你可以给可变参数一个名字,如同其它参数一样。例如下面的例子:

#define DEBUG(format, args...) printf(format, args)

int main()
{
    int x = 10;
    DEBUG("x = %d\n", x); // 等价于 printf("x = %d\n", x);
    
    return 0;
}
  • 这和上面举的「ISO C」定义的宏例子是完全一样的,但是这么写可读性更强并且更容易进行描述

在标准 C 里,你不能省略可变参数,但是你却可以给它传递一个空的参数。例如,下面的宏调用在「ISO C」里是非法的,因为字符串后面没有逗号:

#define DEBUG(...) printf(__VA_ARGS__)

int main()
{
    DEBUG("hello world.\n"); // 非法调用
}

GCC 在这种情况下可以让你完全的忽略可变参数。在上面的例子中,编译是仍然会有问题,因为宏展开后,里面的字符串后面会有个多余的逗号。为了解决这个问题, GCC 使用了一个特殊的##操作。书写格式为:

#define DEBUG(format, args...) printf(format, ##args)
  • 这里,如果可变参数被忽略或为空,##操作将使预处理器去除掉它前面的那个逗号

  • 如果你在宏调用时,确实提供了一些可变参数,该宏定义也会工作正常,它会把这些可变参数放到逗号的后面

1.4.2 使用可变宏输出调试信息

有了 1.4.1 的基础知识,我们可以这么修改代码:

#define DEBUG(format, args...) \
            printf("[%s][%s:%d] "format"\n", \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

// 文件打开失败,提示错误并退出
DEBUG("open file(%s) error, errno[%d](%s).", szFileName, errno, strerror(errno));
  • 通过可变宏,完美解决了调试信息书写过长的问题

书写过长的问题解决后,又来新问题了,如果我想知道某一调试信息是何时被打印的呢?

下面让我们学习一下 Linux 中与时间相关的内容。

二、Linux 中与时间相关的函数

2.1 表示时间的结构体

通过查看头文件「/usr/include/time.h」和「/usr/include/bits/time.h」,我们可以找到下列四种表示「时间」的结构体:

/* Returned by `time'. */
typedef __time_t time_t;
/* A time value that is accurate to the nearest
   microsecond but also has a range of years. */
struct timeval
{
    __time_t tv_sec;       /* Seconds. */
    __suseconds_t tv_usec; /* Microseconds. */
};
struct timespec
{
    __time_t tv_sec;  /* Seconds. */
    long int tv_nsec; /* Nanoseconds. */
};
struct tm
{
    int tm_sec;   /* Seconds.		[0-59] (1 leap second) */
    int tm_min;   /* Minutes.		[0-59] */
    int tm_hour;  /* Hours.    		[0-23] */
    int tm_mday;  /* Day.			[1-31] */
    int tm_mon;   /* Month.			[0-11] */
    int tm_year;  /* Year.			自 1900 起的年数 */
    int tm_wday;  /* Day of week.	[0-6] */
    int tm_yday;  /* Days in year.	[0-365] */
    int tm_isdst; /* DST.			夏令时 */

#ifdef __USE_BSD
    long int tm_gmtoff;    /* Seconds east of UTC. */
    __const char *tm_zone; /* Timezone abbreviation. */
#else
    long int __tm_gmtoff;    /* Seconds east of UTC. */
    __const char *__tm_zone; /* Timezone abbreviation. */
#endif
};
  1. time_t 是一个长整型,用来表示「秒数」
  2. struct timeval 结构体用「秒和微秒」来表示时间
  3. struct timespec 结构体用「秒和纳秒」来表示时间
  4. struct tm 直接用「秒、分、小时、天、月、年」等来表示时间

2.2 获取当前时间

// 可以获取精确到秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的秒数
time_t time(time_t *t); 
// 可以获取精确到微秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的微秒数
int gettimeofday(struct timeval *tv, struct timezone *tz);
// 可以获取精确到纳秒的当前距离1970-01-01 00:00:00 +0000 (UTC)的纳秒数
int clock_gettime(clockid_t clk_id, struct timespec *tp)

使用方式如下所示:

#include <stdio.h>
#include <time.h>
#include <sys/time.h>

int main()
{
    time_t lTime;
    time(&lTime);
    printf("lTime       : %ld\n", lTime);

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    printf("stTimeVal   : %ld\n", stTimeVal.tv_sec);

    struct timespec stTimeSpec;
    clock_gettime(CLOCK_REALTIME, &stTimeSpec);
    printf("stTimeSpec  : %ld\n", stTimeSpec.tv_sec);

    return 0;
}
  • 我们可以通过上面三个函数获得三种不同精度的当前时间

Notes:

  1. POSIX.1-2008 marks gettimeofday() as obsolete, recommending the use of clock_gettime(2) instead.
  2. 并且,有人曾经做过测试,连续两次使用 gettimeofday 时,会以一种小概率出现「时光倒流」的现象,第二次函数调用得到的时间要小于或说早于第一次调用得到的时间。
  3. gettimeofday 函数并不是那么稳定,没有 times 或 clock 计时准确,但它们用法相似。
  4. clock有计时限制,据说是 596.5+小时,一般情况足以应付。
  5. ntpd 之类的进程可能会修改系统时间,导致计时出现误差。
  6. 据网上的讨论来看,TSC 和 HPET 中断之类的东西,可能导致系统的 wall time 回退。这个应该和具体的系统实现有关了,总之 gettimeofday 函数并没有保证提供怎样的精度,也不保证得到系统的准确时间,它返回的结果是「the system's best guess at wall time」。
  7. 有可能的话,尽量使用 clock_gettime(CLOCK_MONOTONIC),不过不是所有系统都实现了 posix realtime,例如 mac os x。
  8. 所以现在应该用:int clock_gettime(CLOCK_MONOTONIC, struct timespec *tp);
    CLOCK_MONOTONIC:Clock that cannot be set and represents monotonic time since some unspecified starting point.

2.3 秒、毫秒、微秒、纳秒之间的转换

  • 1 秒 = 1000 毫秒
  • 1 毫秒 = 1000 微秒
  • 1 微秒 = 1000 纳秒

so:

  • 1 秒 = 1000,000 微秒(一百万微秒)
  • 1 秒 = 1000,000,000 纳秒(十亿纳秒)

从秒到毫秒,毫秒到微秒,微秒到纳秒都是 1000 的倍关系,也就是多 3 个 0 的关系。

另:个人电脑的微处理器执行一道指令(如将两数相加)约需 2~4 纳秒,所以程序只要精确到纳秒就够了。

2.4 对时间进行格式化输出

  1. 首先将 struct timeval 或 struct timespec 转换成 time_t 表示的秒数:

    struct timeval stTimeVal;
    gettimeofday(&stTimeVal, NULL);
    
    time_t lTime = stTimeVal.tv_sec;
  2. 利用系统函数将 time_t 转换为 struct tm

    struct tm stTime;
    localtime_r(&lTime, &stTime); // 注意,localtime_r 的第二个参数是入参
  3. 格式化输出:

    char buf[128];
    // 自定义输出格式:YYYY-MM-DD hh:mm:ss
    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d", 
                            stTime.tm_year + 1900,
                            stTime.tm_mon + 1,
                            stTime.tm_mday,
                            stTime.tm_hour,
                            stTime.tm_min,
                            stTime.tm_sec);
    puts(buf);

将 time_t 转换成 struct tm 的函数一共有 4 个,分别为:

  1. struct tm *gmtime(const time_t *timep);
  2. struct tm *gmtime_r(const time_t *timep, struct tm *result);
  3. struct tm *localtime(const time_t *timep);
  4. struct tm *localtime_r(const time_t *timep, struct tm *result);

形如 localtime 和形如 localtime_r 函数的区别是:localtime 获得的返回值存在于一个 static 的 struct tm 型的变量中,可能被后面的 localtime 调用覆盖掉。如果要防止覆盖,我们可以自己提供一个 struct tm 型的变量,利用 localtime_r 函数,将我们自己定义的变量的地址传进去,将结果保存在其中,这样就可以避免覆盖。

因此可知,函数 gmtime 和 localtime 是线程不安全的,多线程编程中要慎用!

2.5 获取毫秒时间

#include <stdio.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
#include <string.h>

char *GetMsecTime()
{
    static char buf[128];
    time_t lTime = 0;
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};

    gettimeofday(&stTimeVal, NULL);
    lTime = stTimeVal.tv_sec;
    localtime_r(&lTime, &stTime);

    snprintf(buf, 128, "%.4d-%.2d-%.2d %.2d:%.2d:%.2d.%.3d",
             stTime.tm_year + 1900,
             stTime.tm_mon + 1,
             stTime.tm_mday,
             stTime.tm_hour,
             stTime.tm_min,
             stTime.tm_sec,
             stTimeVal.tv_usec / 1000); // 微秒 -> 毫秒
    return buf;
}
int main()
{
    puts(GetMsecTime());

    return 0;
}
  • 注意,该函数所返回的 buf 是通过 static 修饰的,是线程不安全的

2.6 调试信息中新增时间信息

#define DEBUG(format, args...) \
            printf("%s [%s][%s:%d] "format"\n", \
                        		GetMsecTime(), \
                        		__FILE__, \
                        		__FUNCTION__, \
                        		__LINE__, \
                        		##args)

至此,我们已经将调试信息的输出格式完善了,接下来就要考虑怎么将调试信息输出到日志文件中了。

三、将调试信息输出到日志文件中

3.1 日志等级

Log4J 定义了 8 个级别的 Log(除去 OFF 和 ALL,可以说分为 6 个级别),优先级从高到低依次为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE、 ALL。

  • OFF:最高等级的,用于关闭所有日志记录

  • FATAL:指出每个严重的错误事件将会导致应用程序的退出。这个级别比较高了,重大错误,这种级别你可以直接停止程序了

  • ERROR:指出虽然发生错误事件,但仍然不影响系统的继续运行。打印错误和异常信息,如果不想输出太多的日志,可以使用这个级别

  • WARN:表明会出现潜在错误的情形,有些信息不是错误信息,但是也要给程序员的一些提示

  • INFO:打印一些你感兴趣的或者重要的信息,这个可以用于生产环境中输出程序运行的一些重要信息,但是不能滥用,避免打印过多的日志

  • DEBUG:主要用于开发过程中打印一些运行信息

  • TRACE: 很低的日志级别,一般不会使用

  • ALL: 最低等级的,用于打开所有日志记录

Log4J 建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG。我们下面的程序也将围绕这四个日志等级来进行编码。

先贴上源码,后续有时间在详细解释~

3.2 源码

3.2.1 log.h

#ifndef __LOG_H__
#define __LOG_H__

#ifdef __cplusplus
extern "C"
{
#endif

// 日志路径
#define LOG_PATH       "./Log/"
#define LOG_ERROR             "log.error"
#define LOG_WARN              "log.warn"
#define LOG_INFO              "log.info"
#define LOG_DEBUG             "log.debug"
#define LOG_OVERFLOW_SUFFIX             "00"    // 日志溢出后的文件后缀,如 log.error00

#define LOG_FILE_SIZE  (5*1024*1024)            // 单个日志文件的大小,5M

// 日志级别
typedef enum tagLogLevel
{
    LOG_LEVEL_ERROR    = 1,                             /* error级别 */
    LOG_LEVEL_WARN     = 2,                             /* warn级别  */
    LOG_LEVEL_INFO     = 3,                             /* info级别  */
    LOG_LEVEL_DEBUG    = 4,                             /* debug级别 */
} LOG_LEVEL_E;

typedef struct tagLogFile
{
    char szCurLog[64];
    char szPreLog[64];
} LOG_FILE_S;

#define PARSE_LOG_ERROR(format, args...)  \
    WriteLog(LOG_LEVEL_ERROR, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_WARN(format, args...)  \
    WriteLog(LOG_LEVEL_WARN, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_INFO(format, args...)  \
    WriteLog(LOG_LEVEL_INFO, __FILE__, __FUNCTION__, __LINE__, format, ##args)

#define PARSE_LOG_DEBUG(format, args...)  \
    WriteLog(LOG_LEVEL_DEBUG, __FILE__, __FUNCTION__, __LINE__, format, ##args)

extern void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
);

#ifdef __cplusplus
}
#endif

#endif

3.2.2 log.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>     // va_stat 头文件
#include <errno.h>      // errno 头文件
#include <time.h>       // 时间结构体头文件
#include <sys/time.h>   // 时间函数头文件
#include <sys/stat.h>   // stat 头文件
#include "log.h"

static LOG_FILE_S gstLogFile[5] = 
{
    {"", ""},
    {
        /* error级别 */
        LOG_PATH LOG_ERROR,                     // ./Log/log.error
        LOG_PATH LOG_ERROR LOG_OVERFLOW_SUFFIX  // ./Log/log.error00
    },
    {
        /* warn级别 */
        LOG_PATH LOG_WARN,                      // ./Log/log.warn
        LOG_PATH LOG_WARN LOG_OVERFLOW_SUFFIX   // ./Log/log.warn00
    }, 
    {
        /* info级别 */
        LOG_PATH LOG_INFO,                      // ./Log/log.info
        LOG_PATH LOG_INFO LOG_OVERFLOW_SUFFIX   // ./Log/log/info00
    }, 
    {
        /* debug级别 */
        LOG_PATH LOG_DEBUG,                     // ./Log/log.debug
        LOG_PATH LOG_DEBUG LOG_OVERFLOW_SUFFIX  // ./Log/log.debug00
    }, 
};

static void __Run_Log
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format,
    va_list vargs
)
{
    FILE *logfile = NULL;
    logfile = fopen(gstLogFile[enLogLevel].szCurLog, "a");
    if (logfile == NULL)
    {
        printf("open %s error[%d](%s).\n", gstLogFile[enLogLevel].szCurLog, errno, strerror(errno));
        return;
    }

    /* 获取时间信息 */
    struct timeval stTimeVal = {0};
    struct tm stTime = {0};
    gettimeofday(&stTimeVal, NULL);
    localtime_r(&stTimeVal.tv_sec, &stTime);

    char buf[768];
    snprintf(buf, 768, "%.2d-%.2d %.2d:%.2d:%.2d.%.3lu [%s][%s:%d] ",
                                            stTime.tm_mon + 1,
                                            stTime.tm_mday,
                                            stTime.tm_hour,
                                            stTime.tm_min,
                                            stTime.tm_sec,
                                            (unsigned long)(stTimeVal.tv_usec / 1000),
                                            pcFileName,
                                            pcFuncName,
                                            iFileLine);

    fprintf(logfile, "%s", buf);
    vfprintf(logfile, format, vargs);
    fprintf(logfile, "%s", "\r\n");
    fflush(logfile);

    fclose(logfile);

    return;
}
static void __LogCoverStrategy(char *pcPreLog) // 日志满后的覆盖策略
{
    int iLen = strlen(pcPreLog);
    int iNum = (pcPreLog[iLen - 2] - '0') * 10 + (pcPreLog[iLen - 1] - '0');
    iNum = (iNum + 1) % 10;

    pcPreLog[iLen - 2] = iNum / 10 + '0';
    pcPreLog[iLen - 1] = iNum % 10 + '0';
}

void WriteLog
(
    LOG_LEVEL_E enLogLevel,
    const char *pcFileName,
    const char *pcFuncName,
    int iFileLine,
    const char *format, 
    ...
)
{
    char szCommand[64]; // system函数中的指令
    struct stat statbuff;
    if (stat(gstLogFile[enLogLevel].szCurLog, &statbuff) >= 0) // 如果存在
    {
        if (statbuff.st_size > LOG_FILE_SIZE) // 如果日志文件超出限制
        {
            printf("LOGFILE(%s) > 5M, del it.\n", gstLogFile[enLogLevel].szCurLog);
            snprintf(szCommand, 64, "cp -f %s %s", gstLogFile[enLogLevel].szCurLog, gstLogFile[enLogLevel].szPreLog); 
            puts(szCommand);
            system(szCommand);      // 将当前超出限制的日志保存到 log.error00 中

            snprintf(szCommand, 64, "rm -f %s", gstLogFile[enLogLevel].szCurLog);
            system(szCommand);      // 删掉 log.error
            printf("%s\n\n", szCommand);
            
            // 如果 log.error 超出 5M 后,将依次保存在 log.error00、log.error01、... 中
            __LogCoverStrategy(gstLogFile[enLogLevel].szPreLog); 
        }
    }
    else // 如果不存在,则创建
    {
        printf("LOGFILE(%s) is not found, create it.\n\n", gstLogFile[enLogLevel].szCurLog);
        snprintf(szCommand, 64, "touch %s", gstLogFile[enLogLevel].szCurLog);
        system(szCommand);
    }

    va_list argument_list;
    va_start(argument_list, format);

    if (format)
    {
        __Run_Log(enLogLevel, pcFileName, pcFuncName, iFileLine, format, argument_list);
    }

    va_end(argument_list);

    return;
}

3.3.3 main.c

#include <stdio.h>
#include <unistd.h> // sleep 头文件
#include "log.h"

int main()
{
    for (int i = 0; i < 5; i++)
    {
        PARSE_LOG_ERROR("我是第 %d 条日志", i+1);
    }

    return 0;
}

3.3.4 Tutorial

将 log.h、log.c、main.c 置于同一个目录中

并新建一个 Log 目录

编译、运行

 

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

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

相关文章

R语言 tidyverse系列学习笔记(系列5)dplyr 数据分析之across

成绩单 score install.packages("dplyr") library(dplyr)install.packages("tibble") library(tibble)install.packages("stringr") library(stringr)score tibble(IDc("1222-1","2001-0","3321-1","4898-…

MySQL(八):排序与分页

排序与分页 前言一、排序数据1、排序规则2、单列排序3、多列排序 二、分页1、背景2、实现规则3、拓展 前言 本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识&#xff0c;有兴趣的小伙伴可以关注博主&#xff01;也许一个人独行&#xff0c;可以走的很快&…

从零开始Vue项目中使用MapboxGL开发三维地图教程(三)添加全屏,缩放旋转和比例控制面板以及自定义图标、标记点击弹窗、地图平移等功能

文章目录 1、添加各种控制面板1.1、添加全屏1.2、缩放旋转控制1.3、比例尺 2、获取并显示鼠标移动位置的经纬度坐标3、添加图标3.1、添加图片图层的图标3.2、添加带有标记的自定义图标3.3、悬停时显示弹出窗口 1、添加各种控制面板 1.1、添加全屏 //添加全屏控制this.map.addC…

管理类联考——逻辑——知识篇——第一章 性质命题

第一章 性质命题&#xff08;最基础&#xff0c;最难*****&#xff09; 一、性质命题定义&#xff08;必考&#xff09; 判断事物具有或不具有某种性质的命题。 二、性质命题的四种基本形式 全称肯定&#xff1a;①所有的A都是B 全称否定&#xff1a;②所有的A不是B 特称肯…

Nature子刊:光遗传在绒猴执行检测任务中的行为效应

狨猴体型小&#xff0c;具有巨大的基因修饰潜力&#xff0c;并可表现复杂的行为&#xff0c;已经成为神经科学领域的一个关键模型。 德国恩斯特斯特朗格曼神经科学研究所与马克斯普朗克学会的研究人员设计了一种轻质的、3D打印的植入物&#xff0c;利用高密度硅基微电极阵列&am…

Monorepo vs. Microrepo: 选择适合你的代码仓库策略

简介 在软件开发领域&#xff0c;选择合适的代码仓库策略对于优化协作、可扩展性和代码质量至关重要。Monorepo和Microrepo是两种流行的方法&#xff0c;它们提供了各自的优势和考虑因素。本文将探讨这两种策略的特点&#xff0c;解释为何不同的公司选择不同的选项&#xff0c;…

基于Hexo和Butterfly创建个人技术博客,(5) 使用Hexo的Tags Plugin插件增强博客文章内容和视觉表现力

Hexo官司网查看 这里 注意&#xff1a; Tags语法是Hexo插件提供的&#xff0c;是非标准语言&#xff0c;写文章时要注意以下几点&#xff1a; 用于在文章中快速插入特定的内容&#xff0c;作用等同于其它语言&#xff0c;可理解为一种增强版本的markdown&#xff1b;可混合Mark…

Linux系统编程学习 NO.7 ——sudo配置、编译器的使用

引言&#xff1a; 现在是北京时间2023年6月14日8点16分&#xff0c;期末考试在即&#xff0c;重心可能得转移到考试上了。不过想到马上就可以回家陪家人过端午节&#xff0c;还是非常开心的。放暑假了就可以好好陪家人了。尝试一下换个环境复习吧&#xff0c;洗漱一下就去图书…

智见|比亚迪廉玉波:2023年销量目标300万辆

营收4240亿元&#xff0c;同比增长96%&#xff1b;归属上市公司股东净利润166亿元&#xff0c;同比增长445.86%&#xff1b;累计销售186.85万辆&#xff0c;同比增长152.5%……这是2022年比亚迪的成绩单。 据比亚迪披露&#xff0c;仅2022年一年新能源汽车的销量&#xff0c;就…

【重要】MThings V0.5.0更新要点

再见了BUG&#xff0c;为了改善质量&#xff0c;我们已修正了一些问题&#xff0c;以便您能继续富有成效且令人敬畏。 下载地址&#xff1a; http://gulink.cn/download 01. [新增]支持数据网关功能。 数据网关功能提供协议转换和数据汇聚功能&#xff0c;可实现不同的通道类型…

1--Gradle入门 - 简介、安装、目录结构、创建项目

1--Gradle入门 - 简介、安装、目录结构、创建项目 Gradle 官网 Gradle官网地址&#xff1a;Gradle Build Tool Gradle官方下载安装教程页面&#xff1a;https://gradle.org/install/ Gradle官方用户手册&#xff1a;https://docs.gradle.org/current/userguide/userguide.html …

4--Gradle入门 - 创建普通的web工程

4--Gradle入门 - 创建普通的web工程 前言 上一篇&#xff0c;我们已经创建了java工程&#xff0c;本篇章再来创建web工程。 “ 其实没啥用&#xff0c;还是研究如何部署 Springboot 环境就好了 ” 创建普通的web工程 1. 新建 gradle 管理的工程项目 1685862847224 2. 设置项目使…

前端构建工具的未来

本文作者系360奇舞团前端开发工程师 本文为翻译 原文标题&#xff1a;The Future Of Frontend Build Tools 原文作者&#xff1a;Alexander Joseph 原文链接&#xff1a;https://www.smashingmagazine.com/2022/06/future-frontend-build-tools/ 前端构建工具对现代前端开发者的…

基于SpringCloud+Vue房产销售平台的设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

数据库从业者在行业的变革下------越来越难

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

有趣的数学 依靠想象力的微积分

1、无限分割计算圆的面积 考虑将圆切成若干等份&#xff0c;下图为4等份。 下图为8等份。 下图为16等份。 下图为最终想象出来的极限矩形&#xff0c;据此分割为无穷等份的圆拼接为一个矩形。 矩形的面积 r * C / 2。 r为半径&#xff0c;C为周长。 2、夹逼法计算圆周率 借助…

使用SwinTransformer进行图片分类

SwinTransformer 是微软亚洲研究院在2021年提出的适用于CV领域的一种基于Tranformer的backbone结构。 它是 Shift Window Transformer 的缩写&#xff0c;主要创新点如下。 1&#xff0c;分Window进行Transformer计算&#xff0c;将自注意力计算量从输入尺寸的平方量级降低为线…

力扣刷题记录(一)剑指Offer(第二版)

1、本栏用来记录社招找工作过程中的内容,包括基础知识学习以及面试问题的记录等,以便于后续个人回顾学习; 暂时只有2023年3月份,第一次社招找工作的过程; 2、个人经历: 研究生期间课题是SLAM在无人机上的应用,有接触SLAM、Linux、ROS、C/C++、DJI OSDK等; 3、参加工作后…

【JVM系列】jvm内存结构详解

文章目录 前言HotSpot VM介绍内存结构程序计数器JVM栈本地方法栈Java堆方法区运行时常量池 永久代和元空间永久代&#xff08;PermGen&#xff09;元空间&#xff08;Metaspace&#xff09; 直接内存总结 前言 我们为什么要学习JVM&#xff1f; 面试的需要中高级程序员的必备技…

java的字符流

字符流的底层也是字节流。字符流字节流字符集。 特点是输入流一次读一个字节&#xff0c;遇到中文时&#xff0c;一次读多个字节&#xff08;读多少个与字符集有关&#xff09;&#xff1b;输出流底层会把数据按照指定的编码方式进行编码&#xff0c;变成字节再写到文件中。 字…