一、定义
函数: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库下载连接