snprintf()函数使用方法、注意事项及内部实现方法探究

一、定义

函数:int snprintf(char *str, size_t size, const char *format, ...)

作用:将“给定的内容”按照“指定的格式”输出到“指定目标内”。

参数:char *str,指定目标

           size_t size ,输出到指定目标的最大字节数

           const char *format,指定格式

           ...,可变参数,参数数量不定,0~其他

返回值:写入的实际字符串长度,不包括空字符

二、应用:

1、无可变参数

若显式字符长度(不包括末尾'\0')小于size,则将此字符串全部复制到str中,末尾补一个'\0'; 若显式字符长度(不包末尾'\0')大于等于size,则只将其中的前(size-1)个显式字符复制到str中,并给其后添加一个字符串结束符('\0').

    例子如下所示:

   1)  

  #include<stdio.h>

  #include<stdlib.h>

  int main()

  { 

           char str[10]={0};

           int nLen=snprintf(str,sizeof(str),"1234567890");

           printf("str=%s\n",str);

           printf("nLen=%d\n",nLen);

           return 0;

  }

  输出:

           str=123456789

           nLen=9

因为显示字符串"1234567890"显式字符长度为10,等于str的长度10,故取前9个字符1~9复制到str中并给str[9]赋值'\0'(0x00),并不会打印成1234567890,函数返回值为实际字符串的大小9。

2)

#include<stdio.h>

#include<stdlib.h>

int main()

{ 

     char str[10]={0};

     int nLen=snprintf(str,sizeof(str),"12345");

     printf("str=%s\n",str);

     printf("nLen=%d\n",nLen);

     return 0;

}

输出:

     str=12345

     nLen=5

因为显示字符串"12345"显式字符长度为5,小于str的长度10,故取所有5个字符1~5复制到str[0]~str[4]中,并给str[5]赋值'\0'(0x00),str[6]~str[9]不变,为初始值。函数返回值为实际写入字符串的大小5。

2、有可变参数

当存在可变参数“...”时,首先要把可变参数值按照打印格式(格式化输出不好%)填入指定格式中,然后按照上述  “无可变参数”的方法根据size大小情况复制给str。

 C语言中的格式化输出符号有很多,以下是一些常见的:

%d:用于输出十进制整数。

%u:用于输出无符号十进制整数。

%f:用于输出固定点浮点数。

%s:用于输出字符串。

%c:用于输出字符。

%x 或 %X:用于输出十六进制数,%x表示输出小写字母,%X表示输出大写字母。

       例子如下所示:

#include<stdio.h>

#include<stdlib.h>

int main()

{ 

        char str[100]={0};

        int nLen_d=0;


        //%d

        memset(str, 0x00, sizeof(str));

        int para_d = -5;

        nLen_d=snprintf(str,sizeof(str),"%d", para_d);
 
        printf("str_d=%s\n",str);


        //%u

        memset(str, 0x00, sizeof(str));

        uint8_t para_u = 5;

        nLen_d=snprintf(str,sizeof(str),"%u", para_u);

        printf("str_u=%s\n",str);


        //%f

        memset(str, 0x00, sizeof(str));

        float  para_f_1 = 123.4567890;

        nLen_d=snprintf(str,sizeof(str),"%f", para_f_1);

        printf("str_f_1=%s\n",str);


        memset(str, 0x00, sizeof(str));

        double para_f_2 = 123.4567890;

        nLen_d=snprintf(str,sizeof(str),"%f", para_f_2);

        printf("str_f_2=%s\n",str);
    

        //%s

        memset(str, 0x00, sizeof(str));

        char  *para_s = "test";

        nLen_d=snprintf(str,sizeof(str),"%s", para_s);

        printf("str_s=%s\n",str);


        //%c

        memset(str, 0x00, sizeof(str));

        char  para_c_1 = '1';

        char  para_c_2 = 'x';

        nLen_d=snprintf(str,sizeof(str),"%c_%c", para_c_1, para_c_2);

        printf("str_c=%s\n",str);


        //%x,%X

        memset(str, 0x00, sizeof(str));

        int  para_x = 254;

        nLen_d=snprintf(str,sizeof(str),"0x%x", para_x);

        printf("str_x=%s\n",str);

   return 0;
}

输出:

        str_d=-5

        str_u=5

        str_f_1=123.456787           //float可表示的精度不够

        str_f_2=123.456789           

        str_s=test

        str_c=1_x

        str_x=0xfe

   对于格式化输出符号给定可变参数时,可变参数的类型要与格式化输出符号对应,否则可能产生问题。其中,

             1)%u 要赋值无符号数,若赋值负数,会出错;

             2)%f  要赋值浮点数,若赋值整数,会出错;

             3)%x 要赋值整数,若赋值浮点数,会出错;

      错误例子如下所示:

#include<stdio.h>

#include<stdlib.h>

int main()

{ 

       char str[100]={0};

       int nLen_d=0;


       //%u

       memset(str, 0x00, sizeof(str));

       int8_t para_u = -5;

       nLen_d=snprintf(str,sizeof(str),"%u", para_u);

       printf("str_u_wrong=%s\n",str);


       //%f

       memset(str, 0x00, sizeof(str));

       int  para_f = 123;

       nLen_d=snprintf(str,sizeof(str),"%f", para_f);

       printf("str_f__wron=%s\n",str);


       //%x,%X

       memset(str, 0x00, sizeof(str));

       float  para_x = 25.4;

       nLen_d=snprintf(str,sizeof(str),"0x%04x", para_x);

       printf("str_x_wrong=%s\n",str);

   return 0;

}

 输出:

        str_u_wrong=4294967291

        str_f__wron=0.000000

        str_x_wrong=0x60000000

三、函数内部实现

            具体内部实现与各个编译器有关,且内部源码无法看到,以下为查到的简化版本实现:           

#include <stdarg.h>

#include <stdio.h>



int my_snprintf(char *str, size_t size, const char *format, ...) 

{

      //定义一个可变参数列表变量,用来保存"..."  

      va_list args;


      //可变参数列表初始化,其中args是可变参数列表变量,format为指定的格式,其作用是让args  

        指向可变参数列表中的第一个元素

      va_start(args, format); 


      //格式化字符串并复制到str

      int result = vsnprintf(str, size, format, args);


      //释放内存,配合va_start使用

      va_end(args);


      //返回实际写入字符串长度    

      return result;

}

         其中,vsnprintf(str, size, format, args)函数作用与snprintf作用类似,区别是vsnprintf的参数接收可变参数列表变量args而不是像snprintf那样直接传递参数。总的来说,snprintf用于格式化一个可变数量的参数,而vsnprintf用于格式化一个已经存在的va_list中的参数。

        my_snprintf 函数参数压栈时,参数的入栈顺序是从右向左,出栈时是从左向右。函数调用时,先把若干个参数都压入栈中,再压fmt,最后压pc,这样一来,栈顶指针偏移便找到了fmt,通过fmt中的%占位符,取得后面参数的个数,从而正确取得所有参数。

        对于vsnprintf内部实现原理,类似如下流程:

        1)查询格式化字符串format中的%并提取后面的格式化输出符号;

        2)根据符号类型在可变参数列表中args中提取相应变量赋值。

       在步骤2)中,用va_arg(args, 类型)函数提取可变参数列表中的数据,它是C语言标准库中的一个宏,它的原型定义在<stdarg.h>头文件中它会根据提供的参数类型(type)从可变参数列表中读取对应类型的值,并将其返回给调用者a_arg宏的调用会导致va_list类型的变量args自动更新,以便指向可变参数列表中的下一个参数,从而实现对可变参数的逐个访问。其简单应用如下

#include <stdio.h>

#include <stdarg.h>



void print_numbers(int count, ...) 

{

       va_list args;

       printf("count_adr=%08x\n", &count);//打印count地址

       printf("sizeof_int=%d\n", sizeof(int));//打印int占位大小

       printf("args_adr_0=%08x\n", args);//打印args未赋值地址


       va_start(args, count); 

       printf("args_adr_1=%08x\n", args);//打印args初始化后的地址,指向可变参数列表首地址

       for (int i = 0; i < count; i++) 

       {

            int num = va_arg(args, int); 

            printf("args_adr_2_%d=%08x\n", i, args);//打印va_arg()函数执行后的地址

            printf("%d\n", num);

       }

      va_end(args);

}


int main() 

{

      int a=10;

      int b=10;

      int c=10;

      print_numbers(3, a, b, c);

      return 0;
}

输出:

     count_adr_0061fe00

     sizeof_int=4

     args_adr_0=00000000

     args_adr_1=0061fe08

     args_adr_3_0=0061fe10

     10

     args_adr_3_1=0061fe18

     20

     args_adr_3_2=0061fe20

     30

     1)在这个示例中,print_numbers函数接受一个整数count和一个可变数量的整数参数。在函数内部,首先使用va_start宏初始化args,然后使用va_arg宏从可变参数列表中获取整数值,并在循环中打印出来。最后,我们使用va_end宏结束对可变参数列表的访问。

     2)从输出结果来看,args执行完va_start()后,地址从00000000变成0061fe08,在之后每次执行完va_arg()后,args地址增加8。这边有个问题,就是从size_int的的打印看编译器对int的大小处理是4个字节,但是这边却变动8,查询相关资料,这可能是因为在某些体系结构或编译器中,可变参数列表中的参数可能需要按照一定的对齐方式进行存储,即参数列表中的参数可能需要按照8字节对齐。因此,即使sizeof(int)虽然为4,但在可变参数列表中,int类型的参数可能会按照8字节对齐方式进行存储,这样就会导致args的大小变动了8。

     以下为查到的某个编译器对va_start函数的定义,其中_INTSIZEOF()为对齐操作。

#define va_start(ap,fmt) ( ap = (va_list)&fmt + _INTSIZEOF(fmt) )

#define _INTSIZEOF(n)   ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )

 3)需要注意的是,va_start和va_end宏必须成对出现,且在同一个函数内部。

本文章参考了以下文章:

嵌入式操作系统---打印函数(printf/sprintf)的实现_printf volatile unsigned int *-CSDN博客

->关于编译器内部代码,可参考GNU发布的libc库,即C运行库,glibc库下载连接

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值