C语言Printf函数深入解析

2023-05-16

Printf这个函数让大家又爱又恨,第一次接触c语言编程,基本都是调用printf打印“Hellow World!”,但当真正深入使用编程后,才发现printf并不是一个简单的函数。尤其是从事嵌入式软件工作的开发人员,会经常接触printf挂接驱动、printf重入的问题。

本文详细解释printf函数的工作原理,希望对大家有所帮助。

一、函数栈

分析printf之前首先了解函数的工作机制,程序运行前需要分配好内存空间,如图1所示(本文给出一个简图,实际编译器分配的会更加细致):

图1

代码、全局变量、常量内存位置固定,堆可以用于分配动态内存,而栈区则用于程序的运行。函数调用时将形参从右向左压入栈,等函数运行完成,通过出栈,将形参的存储空间释放。不同的编译器对函数入栈、出栈的内容会有所区别,但是对于c语言,形参的格式遵循_cdedl调用规则,有以下特点:

  1. 函数形参入栈顺序是从右向左

  1. 函数形参存储空间为连续存储,且参数按照固定字节对齐;编译器根据程序运行平台的字长进行对齐,32位字长平台按照4字节对齐,64位的会按照8字节对齐。

二、printf函数栈

printf 函数原型为int printf(const char *fmt, ...),使用了可变参数的模式,我们通过图2例子来分析函数栈。

图2

fmt:“%d,%c,%c,%f\n”为常量字符串,存储在内存的常量字段,fmt为该字符串首地址

可变形参1:与变量a类型和数值一致,为int类型;

可变形参2:与变量b类型和数值一致, 为char类型;

可变形参3:与变量c类型和数值一致,为char类型;

可变形参4:与变量d类型和数值一致,float的可变形参会被编译器强制转换为double类型

假设该代码运行在32位字长的平台,且栈底->栈顶为“高地址->低地址”,函数栈中所有参数的存储地址按照4字节对齐存储,设fmt存储地址为0x30000000;则其函数栈如下图:

图3

三、printf代码解析

  1. printf代码框架

printf代码及注释如下所示:

注:本例为32位平台,所以参数出入栈地址均为4字节对齐。

#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif                /* _VALIST */
typedef int acpi_native_int;
#define  _AUPBND                (sizeof (acpi_native_int) - 1)                               // 入栈4字节对齐
#define  _ADNBND                (sizeof (acpi_native_int) - 1)                               // 出栈4字节对齐
#define _bnd(X, bnd)            (((sizeof (X)) + (bnd)) & (~(bnd)))                          // 4字节对齐
#define va_arg(ap, T)           (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) // 按照4字节对齐取下一个可变参数,并且更新参数指针
#define va_end(ap)              (void) 0                                                     // 与va_start成对,避免有些编译器告警
#define va_start(ap, A)         (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))       // 第一个可变形参指针
#endif                /* va_arg */

static char sprint_buf[2408];

int printf(const char *fmt, ...)
{
    va_list args;
    int n;
    // 第一个可变形参指针
    va_start(args, fmt);
    // 根据字符串fmt,将对应形参转换为字符串,并组合成新的字符串存储在sprint_buf[]缓存中,返回字符个数。
    n = vsprintf(sprint_buf, fmt, args);
    //c标准要求在同一个函数中va_start 和va_end 要配对的出现。
    va_end(args);
    // 调用相关驱动接口,将将sprintf_buf中的内容输出n个字节到设备,
    // 此处可以是串口、控制台、Telnet等,在嵌入式开发中可以灵活挂接
    if (console_ops.write)
        console_ops.write(sprint_buf, n);
    return n;
}
  1. vsprintf解析模式详解

vsprintf采用%[flags][width][.prec][length][type]模式对各个参数进行解析各标志解析如下表:

  1. 标志(flags)

标志(flags)用于规定输出样式,含义如下:

flags(标志)

字符名称

描述

-

减号

在给定的字段宽度内左对齐,右边填充空格(默认右对齐)

+

加号

强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号;默认情况下,只有负数前面会显示一个 - 号

(空格)

空格

输出值为正时加上空格,为负时加上负号

#

井号

specifier 是 o、x、X 时,增加前缀 0、0x、0X;

specifier 是 e、E、f、g、G 时,一定使用小数点;

specifier 是 g、G 时,尾部的 0 保留

0

数字零

对于所有的数字格式,使用前导零填充字段宽度(如果出现了减号标志或者指定了精度,则忽略该标志)

  1. 最小宽度(width)

最小宽度(width)用于控制显示字段的宽度,即打印输出的总宽度,取值和含义如下:

width(最小宽度)

字符名称

描述

digit(n)

数字

字段宽度的最小值,如果输出的字段长度小于该数,结果会用前导空格填充;如果输出的字段长度大于该数,结果使用更宽的字段,不会截断输出

*

星号

宽度在 format 字符串中规定位置未指定,使用星号标识附加参数,指示下一个参数是width

  1. 精度(.prec)

精度(.precision)用于指定输出精度,即输出数据占用的宽度,取值和含义如下:

.pre(精度)

字符名称

描述

.digit(n)

点+数字

对于整数说明符(d、i、o、u、x、X):precision 指定了要打印的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符;

对于 e、E 和 f 说明符:要在小数点后输出的小数位数;

对于 g 和 G 说明符:要输出的最大有效位数;

对于 s 说明符:要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符;

对于 c 说明符:没有任何影响;

当未指定任何精度时,默认为 1。如果指定时只使用点而不带有一个显式值,则标识其后跟随一个 0。

.*

点+星号

精度在 format 字符串中规定位置未指定,使用点+星号标识附加参数,指示下一个参数是精度

  1. 类型长度(length)

类型长度(length)用于控制待输出数据的数据类型长度,取值和含义如下:

length(类型长度)

描述

h

参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)

l

参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)

ll

参数被解释为超长整型或无符号超长长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)

  1. 说明符(type)

说明符(type)用于规定输出数据的类型,含义如下:

说明符(specifier)

对应数据类型

描述

d / i

int

输出类型为有符号的十进制整数,i 是老式写法

o

unsigned int

输出类型为无符号八进制整数(没有前导 0)

u

unsigned int

输出类型为无符号十进制整数

x / X

unsigned int

输出类型为无符号十六进制整数,x 对应的是 abcdef,X 对应的是 ABCDEF(没有前导 0x 或者 0X)

f / lf

double

输出类型为十进制表示的浮点数,默认精度为6(lf 在 C99 开始加入标准,意思和 f 相同)

e / E

double

输出类型为科学计数法表示的数,此处 "e" 的大小写代表在输出时用的 “e” 的大小写,默认浮点数精度为6

g

double

根据数值不同自动选择 %f 或 %e,%e 格式在指数小于-4或指数大于等于精度时用使用 [1]

G

double

根据数值不同自动选择 %f 或 %E,%E 格式在指数小于-4或指数大于等于精度时用使用

c

char

输出类型为字符型。可以把输入的数字按照ASCII码相应转换为对应的字符

s

char *

输出类型为字符串。输出字符串中的字符直至遇到字符串中的空字符(字符串以 '\0‘ 结尾,这个 '\0' 即空字符)或者已打印了由精度指定的字符数

p

void *

以16进制形式输出指针

q

long long

输出类型为长整型有符号的十进制整数

%

不转换参数

不进行转换,输出字符‘%’(百分号)本身

n

int *

到此字符之前为止,一共输出的字符个数,不输出文本 [4]

  1. 转义字符

转义序列在字符串中会被自动转换为相应的特殊字符。printf() 使用的常见转义字符如下:

转义序列

描述

ASCII 编码

\'

单引号

0x27

\"

双引号

0x22

\?

问号

0x3f

\\

反斜杠

0x5c

\a

铃声(提醒)

0x07

\b

退格

0x08

\f

换页

0x0c

\n

换行

0x0a

\r

回车

0x0d

\t

水平制表符

0x09

\v

垂直制表符

0x0b

常见组合及输出结果

int main(void)
{
    int a = 10, b = 3;
    printf("%*.*d\n",a,b, -100);         // 输出数字,右对齐,宽度从变量获取
    printf("%10.3d\n", -100);            // 输出数字,右对齐
    printf("%-10.3d\n", -100);           // 输出数字,左对齐
    printf("%+10.3d\n", 100);            // 输出数字,正数带正号
    printf("%0.13d\n", 100);             // 输出文本格式,如员工号
    printf("%#.8x\n", 0x30ff);           // 输出8位16进制地址,小写字母
    printf("%#.8X\n", 0x30ff);           // 输出8位16进制地址,大写字母
    printf("%.3f\n", 3.14159267892);     // 保留浮点小数点后有效位数
    printf("%llu\n", 0xffffffffffffffff);// 输出64位长整型
}

输出结果:

  1. vsprintf流程图

  1. vsprintf源码及注释


#define ZEROPAD    1       /* 无数据位用0填充 */
#define SIGN       2       /* 符号位 */
#define PLUS       4       /* 符号位正数显示正号 */
#define SPACE      8       /* 符号位非负数显示空格 */
#define LEFT      16       /* 左对齐 */
#define SPECIAL   32       /* 显示其他进制的前缀,比如16进制添加前缀0x */
#define LARGE     64       /* 使用大写母 */

#define is_digit(c) ((c) >= '0' && (c) <= '9')
//浮点字符串缓存
#define SZ_NUM_BUF    32
static char sprint_fe[SZ_NUM_BUF+1];

static const char *digits = "0123456789abcdefghijklmnopqrstuvwxyz";
static const char *upper_digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
// 字符串长度
static size_t strnlen(const char *s, size_t count)
{
  const char *sc;
  for (sc = s; *sc != '\0' && count--; ++sc);
  return sc - s;
}
// 字符转10进制数
static int skip_atoi(const char **s)
{
  int i = 0;
  while (is_digit(**s)) i = i*10 + *((*s)++) - '0';
  return i;
}
// 
static char *number(char *str, long num, int base, int size, int precision, int type)
{
    char c, sign, tmp[66];
    const char *dig = digits;
    int i;
    // 
    if (type & LARGE)  dig = upper_digits;
    // 左对齐,去掉补0
    if (type & LEFT) type &= ~ZEROPAD;
    // 
    if (base < 2 || base > 36) return 0;
    // 补0或补空格
    c = (type & ZEROPAD) ? '0' : ' ';
    /*符号位处理,符号位占用1位-STR*/
    sign = 0;
    if (type & SIGN)
    {
        if (num < 0)
        {
            sign = '-';
            num = -num;
            size--;
        }
        // 正数打印正号
        else if (type & PLUS)
        {
            sign = '+';
            size--;
        }
        // 符号位打印空格
        else if (type & SPACE)
        {
            sign = ' ';
            size--;
        }
    }
/*符号位处理-END*/

/*进制转换-STR*/
    if (type & SPECIAL)
    {
        if (base == 16)
            size -= 2;
        else if (base == 8)
            size--;
    }
    i = 0;
    if (num == 0)
      tmp[i++] = '0';
    else
    {
        while (num != 0)
        {
            tmp[i++] = dig[((unsigned long) num) % (unsigned) base];
            num = ((unsigned long) num) / (unsigned) base;
        }
    }
/*进制转换-END*/

/*数据有效位置处理*/
    if (i > precision) precision = i;
        size -= precision;
/*非左对齐且非空余位置不补0,左侧补充空格*/
    if (!(type & (ZEROPAD | LEFT))) while (size-- > 0) *str++ = ' ';
    // 输出符号位
    if (sign) *str++ = sign;
    // 输出8进制或16进制前缀
    if (type & SPECIAL)
    {
      if (base == 8)
        *str++ = '0';//0
      else if (base == 16)
      {
        *str++ = '0';
        *str++ = digits[33];//0x
      }
    }
    /*左侧补充剩余位,如补0或者补充空格*/ 
    if (!(type & LEFT)) 
    while (size-- > 0) *str++ = c;
    /*实际数据小于有效长度,左侧补0处理*/
    while (i < precision--) *str++ = '0';
    /*复制有效字符*/ 
    while (i-- > 0) *str++ = tmp[i];
    /*左对齐,右侧补充空格*/ 
    while (size-- > 0) *str++ = ' ';
    return str;
}
// 计算浮点精度
static int ilog10(double n)    /* 在整数输出中计算log10(n) */
{
    int rv = 0;

    while (n >= 10)
    {    /* 右移小数位 */
        if (n >= 100000)
        {
            n /= 100000; rv += 5;
        }
        else
        {
            n /= 10; rv++;
        }
    }
    while (n < 1)
    {        /* 左移小数位 */
        if (n < 0.00001)
        {
            n *= 100000; rv -= 5;
        }
        else
        {
            n *= 10; rv--;
        }
    }
    return rv;
}
// 整数位数
static double i10x(int n)    /* 计算10^n的整数输入 */
{
    double rv = 1;

    while (n > 0)
    {
        if (n >= 5)
        {
            rv *= 100000; n -= 5;
        }
        else
        {
            rv *= 10; n--;
        }
    }
    while (n < 0)
    {        /* Right shift */
        if (n <= -5)
        {
            rv /= 100000; n += 5;
        }
        else
        {
            rv /= 10; n++;
        }
    }
    return rv;
}
// 浮点数据转字符串%f,%e,%E
static void ftoa(
    char* buf,    /* 字符串缓存 */
    double val,    /* 浮点数 */
    int prec,    /* 小数位数 */
    char fmt    /* 类型标识 */
)
{
    int d;
    int e = 0, m = 0;
    char sign = 0;
    double w;
    const char *er = 0;
    const char ds = '.';//FF_PRINT_FLOAT == 2 ? ',' : '.';
    unsigned int *pu=(unsigned int *)&val;
    // 阶码全1,尾数不为0,不存在的数"NaN"
    //if (isnan(val))
    if(((pu[1]&0x7ff00000)==0x7ff00000)&&(((pu[1]&0xfffff)!=0)||(pu[0]!=0)))
    {
        er = "NaN";
    }
    else
    {
        // 默认6 位小数
        if (prec < 0) prec = 6;
        // 符号处理
        if (val < 0)
        {
            val = 0 - val;
            sign = '-';
        }
        else
        {
            sign = '+';
        }
        // 阶码全1,尾数部分全0,符号位为0表示正无穷。
        // 阶码全1,尾数部分全0,符号位为1表示负无穷。
        //if (isinf(val))
        if(((pu[1]&0x7fffffff) ==0x7ff00000)&&(pu[0] == 0))
        {
            er = "INF";
        }
        //
        else
        {
            // ‘f’类型
            if (fmt == 'f')
            {
                val += i10x(0 - prec) / 2;    /*用于四舍五入,比如1.67保留1位小数为1.7 */
                m = ilog10(val);            /*整数位数*/
                if (m < 0) m = 0;
                if (m + prec + 3 >= SZ_NUM_BUF) er = "OV";    /* 最大缓存 */
            }
            else
            {    /* E类型 x.xxxxxxe+xx*/
                if (val != 0)
                {
                    val += i10x(ilog10(val) - prec) / 2;    /* 用于四舍五入,prec表示底数部分的小数位数*/
                    e = ilog10(val);                        /*整数位数*/

                    if (e > 99 || prec + 7 >= SZ_NUM_BUF)  //指数范围,及最大缓存,
                    {
                        er = "OV";
                    }
                    else
                    {
                        if (e < -99) e = -99;
                        val /= i10x(e);    /* 计算底数 */
                    }
                }
            }
        }
        // 有效浮点
        if (!er)
        {
            // 符号位
            if (sign == '-')
                *buf++ = sign;

            do
            {
                /* 进入小数部分时插入小数分隔符 */
                if (m == -1)
                    *buf++ = ds;
                //剪掉最高的数字d
                w = i10x(m);
                //
                d = (int)(val / w); val -= d * w;

                *buf++ = (char)('0' + d);    /* 记录10进制 */

            } while (--m >= -prec);

            if (fmt != 'f')
            {
                *buf++ = (char)fmt;
                if (e < 0) {
                    e = 0 - e; *buf++ = '-';
                }
                else {
                    *buf++ = '+';
                }
                *buf++ = (char)('0' + e / 10);
                *buf++ = (char)('0' + e % 10);
            }
        }
    }
    // 特殊值
    if (er)
    {    // 符号
        if (sign)  *buf++ = sign;
        // 数据字符串
        do
        {
            *buf++ = *er++;
        } while (*er);
    }
    *buf = 0;    /* 结束符 */
}
int vsprintf(char *buf, const char *fmt, va_list args)
{
    int len;
    unsigned long long num;
    int i, base;
    char * str;
    const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/
    int flags;        /* flags to number() */
    int field_width;  /* width of output field */
    int precision;    /* min. # of digits for integers; max number of chars for from string */
    int qualifier;    /* 'h', 'l', or 'L' for integer fields */
                      /* 'z' support added 23/7/1999 S.H.    */
                      /* 'z' changed to 'Z' --davidm 1/25/99 */
    for (str=buf ; *fmt ; ++fmt)
    {
        if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/
        {
            *str++ = *fmt;
            continue;
        }
        /* flags */
        flags = 0;
        repeat:
            ++fmt;    
            switch (*fmt)
            {
                case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/
                case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/
                case ' ': flags |= SPACE; goto repeat;/*p with space*/
                case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/
                case '0': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/
            }

       
        field_width = -1;
        if ('0' <= *fmt && *fmt <= '9')
            field_width = skip_atoi(&fmt);
        else if (*fmt == '*')
        {
            ++fmt;/*skip '*' */
            /* it's the next argument */
            field_width = va_arg(args, int);// 下个参数变量表述位宽
            if (field_width < 0) {
                field_width = -field_width;
                flags |= LEFT;
            }
        }
        /* get the precision-----即是处理.pre 有效位 */
        precision = -1;
        if (*fmt == '.')
        {
            ++fmt;
            if ('0' <= *fmt && *fmt <= '9')
                precision = skip_atoi(&fmt);
            else if (*fmt == '*') /*如果精度域中是字符'*',表示下一个参数指定精度。因此调用va_arg 取精度值。若此时宽度值小于0,则将字段精度值取为0。*/
            {
                ++fmt;
                /* it's the next argument */
                precision = va_arg(args, int);
            }
            if (precision < 0)
                precision = 0;
        }
        /* get the conversion qualifier 分析长度修饰符,并将其存入qualifer 变量*/
        qualifier = -1;
        if (*fmt == 'l' && *(fmt + 1) == 'l')
        {
            qualifier = 'q';
            fmt += 2;
        }
        else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z')
        {
            qualifier = *fmt;
            ++fmt;
        }
        /* default base */
        base = 10;
        /*处理type部分*/
        switch (*fmt)
        {
            case 'c':
                /*非左对齐,左侧填充空格-Satrt*/
                if (!(flags & LEFT))
                   while (--field_width > 0)
                   *str++ = ' ';
                /*非左对齐,左侧填充空格-End*/
                *str++ = (unsigned char) va_arg(args, int);
                /*左对齐,右侧填充空格-Satrt*/
                while (--field_width > 0)
                *str++ = ' ';
                /*左对齐,右侧填充空格-End*/    
                //continue在此处是跳出本次for循环
                continue;
            case 's':
                s = va_arg(args, char *);
                if (!s)
                    s = "";
                len = strnlen(s, precision);/*取字符串的长度,最大为precision*/
                /*非左对齐,左侧填充空格-Satrt*/
                if (!(flags & LEFT))
                while (len < field_width--)
                    *str++ = ' ';
                    /*非左对齐,左侧填充空格-End*/
                for (i = 0; i < len; ++i)
                    *str++ = *s++;
                /*左对齐,右侧填充空格-Satrt*/
                while (len < field_width--)
                    *str++ = ' ';
                /*左对齐,右侧填充空格-End*/    
                continue;
            case 'p':
                /*没有设置宽度域,则默认宽度为指针变量长度,32位系统默认为8,且需要添0处理*/
                if (field_width == -1)
                {
                    field_width = 2*sizeof(void *);
                    flags |= ZEROPAD;
                }
                str = number(str,(unsigned long) va_arg(args, void *), 16,field_width, precision, flags);
                continue;
            // 形参作为指针变量,向指针变量所指向的地址写入当前转换的字符长度
            case 'n':
                // ln长整型地址
                if (qualifier == 'l')
                {
                    long * ip = va_arg(args, long *);
                    *ip = (str - buf);
                }
                // zn 字节地址
                else if (qualifier == 'Z')
                {
                    size_t * ip = va_arg(args, size_t *);
                    *ip = (str - buf);
                }
                // n 整形地址
                else
                {
                    int * ip = va_arg(args, int *);
                    *ip = (str - buf);
                }
                continue;
            // %f %e %E %lf均使用double类型
            case 'f':                    /* Floating point (decimal) */
            case 'e':                    /* Floating point (e) */
            case 'E':                    /* Floating point (E) */
                // double数据转字符串
                ftoa(sprint_fe, va_arg(args, double), precision, *fmt);    /* 浮点转字符串*/
                // 右对齐 左侧补充空格
                if (!(flags&LEFT))
                {
                    for (j = strnlen(sprint_fe, SZ_NUM_BUF); j<field_width; j++)
                    *str++= '/0';
                }
                // 数据主体
                i = 0;
                while(sprint_fe[i]) *str++ = sprint_fe[i++];    /* 主体 */
                // 左对齐 右侧补充空格
                while (j++ < field_width) *str++ = '/0';
   
                continue;
            // %%表示%
            case '%':
                *str++ = '%';
                continue;
            /* 设置进制*/
            case 'o':
                base = 8;
                break;
                /*大写*/
            case 'X':
                flags |= LARGE;
            case 'x':
                base = 16;
                break;
            /*有符号类型*/
            case 'd':
            case 'i':
                flags |= SIGN;
            /*无符号类型*/
            case 'u':
                break;
            default:
                /*非参数打印*/
                *str++ = '%';
                if (*fmt)
                *str++ = *fmt;
                else
                --fmt;
                continue;
        }
        /*同时如果flags有符号位的话,将参数转变成有符号的数*/
        if (qualifier == 'l')
        {
            num = va_arg(args, unsigned long);
            if (flags & SIGN)
                num = (signed long) num;
        }
        else if (qualifier == 'q')
        {
            num = va_arg(args, unsigned long long);
            if (flags & SIGN)
                num = (signed long long) num;
        }
        else if (qualifier == 'Z')
        {
            num = va_arg(args, size_t);
        }
        else if (qualifier == 'h')
        {
            num = (unsigned short) va_arg(args, int);
            if (flags & SIGN)
                num = (signed short) num;
        }
        else
        {
            num = va_arg(args, unsigned int);
            if (flags & SIGN)
                num = (signed int) num;
        }
        str = number(str, num, base, field_width, precision, flags);
    }
    *str = '/0';/*最后在转换好的字符串上加上NULL*/
    return str-buf;/*返回转换好的字符串的长度值*/
}

四、printf不可重入

Printf函数是不可重入的,因为vsprintf函数调用了全局变量sprint_buf[],但是并且没有做任何边界保护,如果在多线程、或者中断程序中运行该函数,就难以避免资源重复抢占的问题。比如,线程A和线程B均调用了printf打印,线程A正在运行vsprintf函数,对sprint_buf正在操作中,此时被高优先级线程B抢占,B获取了sprint_buf的操作权,线程A生的数据就会被覆盖掉。

五、解决不可重入的方法

在嵌入式软件开发中经常会使用LogMsg打印,LogMsg可以自己编写,也可以借用操作系统的自带的,其根本的思想就是不同线程使用不同的sprint_buf缓存空间。主要方法有独立线程使用静态消息队列、申请动态内存、使用多组缓存空间等。

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

C语言Printf函数深入解析 的相关文章

  • C#中AForge库调节视频亮度、饱和度等属性

    参考链接 xff1a https www dianyuan com eestar article 1479 html 之前工作中需要调节摄像头的亮度 饱和度调节 xff0c 之前一直通过调用AForge的调节控件才能调节 xff0c 虽然可
  • Ubuntu下安装和配置Qtcreator5.15版本

    Qt选择 在官方的声明中 xff0c Qt5 15是Qt5 x的最后一个LTS版本 xff0c 增加了即将在2020年底推出的Qt6的部分新特性 xff0c 为了之后的新版本有更好的兼容性 xff0c 选择了Qt5 15 下载Qt 对于普通
  • python 获取文件夹下的文件名操作(两种)

    一 相对路径 文件存储的想对路径路径 path 61 39 data 0 39 os walk 是一个生成器 xff0c 返回三个值 xff1a 根目录 xff0c 根目录下的目录和文件列表 folder 61 os walk path 3
  • 1、串口(UART/COM/TTL/RS232/RS485)

    目录 串口简介 串行通讯制式 UART 2 1 简介 2 2 电平标准 TTL RS232 RS485 2 3 电平转换 xff08 重点讲解RS232 TTL xff09 USB转TTL USB转RS232 USB转RS485 RS232
  • 集成Python和QML

    Qt包括QML作为一种声明性地描述用户界面并使用JavaScript作为其中的脚本语言的手段 可以编写完整的独立QML应用程 序 xff0c 或将它们与C 43 43 结合使用 PyQt5允许QML以完全相同的方式与Python集成 特别是
  • 个人小型管理服务器 SVN的安装和使用方法

    1 下载TortoiseSVN客户端 官网下载地址 Downloads TortoiseSVN 注意下载跟你电脑位数匹配 64位 32位 的安装包 在页面的下面你还可以找到语言包 如图 下载完成后 应该有这些安装包 如图 接下来我们安装To
  • Ubuntu20.04下安装QtCreator 5.14.2(安装/卸载/创建快捷键/添加收藏)

    原文链接 xff1a https www dianyuan com eestar article 2864 html 都是自己原创 xff0c 发这里提高下知名度 xff0c 也帮助需要的人 一 Qt和Qt Creator的区别 Qt是C
  • WPF中自定义双滑块Slider

    项目中遇到需要双滑块的情况 xff0c 可以网上的基本都是单滑块的样式和例子 xff0c 但是双滑块的很少 xff0c 后来终于在网上找到一个大神的帖子 xff0c 通过修改和完善终于符合自己的需求 xff0c 可后来再查看代码的时候好像又
  • C#通过API对硬件进行禁用和启用

    慢慢的随着年龄的增长 xff0c 脑子越来越不够用 xff0c 而自己的收藏栏也越来越杂乱 xff0c 虽然现在的网上要啥都有 xff0c 但是没有经过自己亲手实验和整理总结 xff0c 下次需要的时候一切又要从头开始 xff0c 而电子星
  • Ubuntu下如何获取usb相机的PID/VID并打开指定的相机

    项目需求控制和打开两个USB摄像头 xff0c 并且根据相机的PID和VID来打开指定的相机 xff0c 来区分主副相机 xff0c 在Windows下可以通过AForge Video DirectShow库来实现 xff0c 但是Ubun
  • ubuntu在arm平台下编译安装opencv(亲测可用)

    X86平台安装很多软件一句话就可以搞定 xff0c 非常方便 xff0c 但是在arm平台一个简单的软件安装起来都非常费尽 xff0c 一个软件安装就要折腾好几天 下面就把安装opencv的过程记录下来 一 通过pip安装opencv xf
  • ubuntu在arm平台下编译安装Qt5.15.2和PySide2(亲测可用)

    一 安装Qt5 15 2 1 下载源码首先在官网下载Qt5 15 2的源码 xff1a https download qt io archive qt 5 15 5 15 2 single 2 先安装编译qt的环境 sudo apt get
  • ubuntu20.04安装cmake详细教程

    一 命令行安装 xff08 这种直接安装cmake xff0c 其实安装的版本都太老了 xff0c 这种方式不推荐 xff09 sudo apt install cmake 二 Cmake源码编译安装 1 更新一下g 43 43 已经安装请
  • ubuntu20.04编译安装qt5.14.2和qtcreator4.12.0

    编译qt5 14 2 本来编译安装的是qt5 15 2 xff0c 编译安装ok xff0c 一切就绪 xff0c 安装qtcreator安装后一直报错 xff0c 以下插件有错误 xff0c 无法载入 xff1a QmlDesigner
  • PX4飞控的PPM接收机

    xff08 一 xff09 原理图 xff1a PX4飞控的PPM输入捕获由协处理器完成 xff0c 接在A8引脚 xff0c 对应Timer1的通道1 xff08 二 xff09 PPM协议 xff1a PPM的每一帧数据间隔为20ms
  • WPF DocumentViewer控件如何隐藏搜索栏/工具栏/部分按钮

    WPF DocumentViewer中默认是显示工具栏和搜索栏的 xff0c 如果想隐藏工具栏和搜索栏 xff0c 可以通过以下代码实现 lt DocumentViewer gt lt DocumentViewer Resources gt
  • 什么是stl? 如何使用stl?

    1 什么是STL xff1f STL Standard Template Library 是C 43 43 的标准模板类库 STL是一个功能强大的基于模板的容器库 xff0c 通过直接使用这些现成的标准化组件可以大大提高算法设计的效率和可靠
  • STM32F4+LAN8720A+STM32CubeMX+Lwip 网络通讯(以太网通讯)小实例

    一 目标实现 通过网络通信的方式 xff0c 当上位机发出对应指令给STM32 xff0c STM32根据收到的指令来执行对应的操作 xff08 例如 xff1a 亮灯 灭灯 闪灯等 xff09 还有可以将STM32连上路由器 xff0c
  • STM32: ADC采样频率及相应时间的确定

    转载自 xff1a http m elecfans com article 594153 html 一 STM32 ADC 介绍 STM32 ADC 是一个12 位精度 一种逐次逼近型模拟数字转换器 它有多达18个通道 xff0c 可测量1
  • Windows中使用GCC编译STM32CubeMx生成的Makefile文件

    1 下载ARM官方对应Cortex M内核的GCC编译器 xff1a https developer arm com tools and software open source software developer tools gnu t

随机推荐

  • STM32-USB学习系列(四):USB-HID模拟鼠标功能

    一 整体步骤 使用STM32CubeMX 生成 HID 模版自己定义mouseHID 结构体 xff0c 然后通过发送鼠标报文控制鼠标的移动 二 STM32CubeMX 配置 芯片 xff1a STM32F407VG 使用USB的时候 xf
  • STM32实用应用系列:串口接收不定长数据(HAL库 UART + DMA)

    目录 一 串口接收不定长数据的实现思路 二 具体实现代码 1 USART 的配置 2 DMA的配置 xff08 放在 HAL UART MspInit 中 xff09 3 USART 中断处理函数 一 串口接收不定长数据的实现思路 在串口通
  • 使用Ventoy制作启动盘

    目录 一 Ventoy介绍 二 Ventoy的使用 三 Ventoy中MBR分区形式与GPT分区形式对比 四 使用自定义的EFI的步骤 Ventoy下载地址 xff1a https www ventoy net cn index html
  • DRAM知识整理系列(三):部分时序参数整理

    目录 一 时序参数整理 第一时序 xff1a 1 tCL CAS Lantency Control 2 tRCD RAS to CAS Delay 3 tRP Row Precharge Timing 4 tRAS RAS Active T
  • ROS移动机器人开发——硬件引脚

    我们使用的32开发板为冰达机器人官方的开发板 xff0c 类型为STM32RCT6 32章节的目的为 xff0c 将官方所给源码转化为 官方标准库函数来进行使用 需求提出 xff1a 电源 1 3 3V 200ma供电 2 输入 9 12
  • UDS知识整理(一):UDS简介与UDS要求规范简介

    本文参考自 xff1a ISO DIS 14229 1 一 汽车诊断与OSI模型对比 注 xff1a OSI全称 xff1a Open System Interconnection Reference Model xff0c 即开放式系统互
  • Renesas:配置中断的常规流程——使用C语言方式配置中断向量表(二)

    Renesas xff1a 配置中断的常规流程 不吃鱼的猫丿的博客 CSDN博客 在前面的文章讲到了Renesas的常规中断配置流程 xff0c 当需要添加新的中断时 xff0c 每次都需要到boot asm中添加中断服务的函数名与中断服务
  • 工作经验总结:MCU寄存器库的开发

    目录 一 寄存器的结构体定义 二 寄存器的地址的宏定义 在进行MCU开发的时候 xff0c 可能会遇到需要自己手写寄存器库的情况 xff0c 以下整理了一些相关的示例以及注意事项 一 寄存器的结构体定义 注意 xff1a 在定义寄存器结构体
  • 汽车标定知识整理(三):CCP报文可选命令介绍

    目录 一 可选命令 CRO命令报文的可选命令表 xff1a 二 可选命令帧格式介绍 1 GET SEED 获取被请求资源的种子 xff08 0x12 xff09 2 UNLOCK 解锁保护 xff08 0x13 xff09 3 SET S
  • LIN资料整理(一):LIN入门简介

    目录 一 LIN是什么 1 LIN简介 2 LIN的适用场景 3 LIN协议版本 4 LIN的主从机节点 主从机任务与LIN总线特点 一 LIN是什么 1 LIN简介 LIN 是Local Interconnect Network 的缩写
  • LIN资料整理(二): LIN协议层

    目录 一 LIN帧的结构 1 帧头的帧结构 xff08 1 xff09 同步间隔段 xff08 2 xff09 同步段 xff08 3 xff09 受保护ID段 2 应答的帧结构 xff08 1 xff09 数据段 xff08 2 xff0
  • Altium designer学习(一)AD画板流程总结

    AD画板 xff0c 初学者总是容易犯上一个手忙脚乱的错误 一会儿去找封装 xff0c 一会儿又去忙原理图 这样都是很影响效率的事情 xff0c 要解决这个问题 xff0c 那就是画板得确定下来一个合理的流程 因此 xff0c 在第一篇就强
  • 网络编程(UDP/TCP)

    1 网络模型 数据从网络中一个终端上的应用程序传送到另外一个终端的应用程序 xff0c 中间需要经历很多过程 xff0c 有多方参与对数据进行层层封装 转发 我们把这个流程在逻辑上进行分层 xff0c 每一层根据本层的规章制度 协议 各司其
  • postman使用教程

    Postman是一款功能强大的网页调试与发送网页HTTP请求的Chrome插件 市场上有很多优秀的 xff0c 完善的接口测试工具 xff0c 比如SoapUI xff0c Postman xff0c JMeter yapi等 xff0c
  • 请求头和请求体

    1 请求行 请求方式 请求url 请求协议 版本 GET login html HTTP 1 1 请求方式 xff1a HTTP协议有7中请求方式 xff0c 常用的有2种 GET xff1a 1 请求参数在请求行中 xff0c 在url后
  • spring security 自定义多种方式登录授权(普通用户、管理员、第三方登录)

    1 自定义token xff0c 继承 AbstractAuthenticationToken 目的 xff1a 主要用于包装区别过滤条件 第2步用到 span class token keyword public span span cl
  • STM32F30X USART串口初始化顺序

    void InitUart void GPIO InitTypeDef GPIO InitStructure if 1 USART USED 61 61 USART1 RCC AHBPeriphClockCmd RCC AHBPeriph
  • java学习记录(一)

    前言 因为在大创项目中要用到一点点java 所以想利用这段时间 快速的入门一下简单的Java 试图在项目中不拖后腿 ps 小白自学 如有错误还请指正 偶然间看到编程的学习要点 在此记录一下 明确学习目标 锁定一门语言版本 努力学习 认真研读
  • C++ 之HTTP post请求

    一 建立会话 xff08 Session xff09 对象 xff1a CInternetSession mysession 二 连接到Http服务器 xff1a CHttpConnection myconn 61 mysession Ge
  • C语言Printf函数深入解析

    Printf这个函数让大家又爱又恨 xff0c 第一次接触c语言编程 xff0c 基本都是调用printf打印 Hellow World xff01 但当真正深入使用编程后 xff0c 才发现printf并不是一个简单的函数 尤其是从事嵌入