对于程序猿来说,printf函数可以说是最熟悉的一个工具了。利用它可以将各类调试信息输出到指定的设备(比如串口)中,实现对程序运行状态的掌控和分析。不过,在实际的应用中,相信大家除了printf函数之外,应该还见过几个与其类似的函数,包括fprintf、sprintf、snprintf、vprintf、vfprintf、vsprintf、vsnprintf等等。那么,这些看上去很类似的函数之间,到底有什么区别,各自的作用到底是什么?今天就来总结一下。
首先列出全部的函数申明,以供参考。
#include
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
怎么样,是不是看的有点晕?没关系,我们可以先用一个表格来大致区分一下上述这些函数的异同点,就不会那么晕了。
参数类型 | 输出到设备 | 输出到文件 | 输出到字符串 |
---|---|---|---|
可变参数 | printf | fprintf | sprintf、snprintf |
固定参数 | vprintf | vfprintf | vsprintf、vsnprintf |
OK,下面我们就来逐个进行详细的介绍和比对。
一、printf和vprintf
多数情况下使用printf() 。只有当你需要自己写一个printf()那样的专有函数的时候才需要vprintf()。比如你写一个自己专门的错误输出函数:
int error(char *fmt, ...)
{
int result;
va_list args;
va_start(args, fmt);
// 一些内容
va_end(args);
return result;
}
应当注意到,你不能转发参数给printf,因为printf是变长参数的,而不是vprintf的单独一个va_list。然而vprintf() 函数, 只取一个合并的va_list 参数, 所以完整的版本是:
int error(char *fmt, ...)
{
int result;
va_list args;
va_start(args, fmt);
fputs("Error: ", stderr);
result = vfprintf(stderr, fmt, args);
va_end(args);
return result;
}
二、fprintf和vfprintf
两个函数从声明看,第三个参数有区别,这样就形成了两个函数不同的作用。比如,你要写一个日志函数:
void log(FILE *file, const char* format, ... )
{
va_list args;
va_start (args, format);
fprintf(file, "%s: ", getTimestamp());
vfprintf (file, format, args); //在这个地方用vfprintf函数就很合适,因为第三个参数可以直接得到
va_end (args);
}
vfprintf适合参数可变列表传递。
三、sprintf和vsprintf
先看一个例子:
#include
#include
int _tmain(int argc, _TCHAR* argv[])
{
char *p1="China";
char a[20];
sprintf(a,"%s",p1);
printf("%s\n",a);
memset(a,0,sizeof(a));
snprintf(a,3,"%s",p1);
printf("%s\n",a);
printf("%d\n",strlen(a));
return 0;
}
结果输出:
China
Chi
3
过程分析:
sprintf(a,”%s”,p1) 把p1字符串拷贝到数组a中(‘\0’也拷贝过去了)。
snprintf(a,3,”%s”,p1) 拷贝P1中前3个字符到数组a中,并在末尾自动添加’\0’。
sprintf属于I/O库函数,snprintf函数并不是标准c/c++中规定的函数,但是在许多编译器中,厂商提供了其实现的版本。在gcc中,该函数名称就snprintf,而在VC中称为_snprintf。 如果你在VC中使用snprintf(),会提示此函数未声明,改成_snprintf()即可。
注意点:
1 sprintf是一个不安全函数,src串的长度应该小于dest缓冲区的大小,(如果src串的长度大于或等于dest缓冲区的大小,将会出现内存溢出。)
2 snprintf中源串长度应该小于目标dest缓冲区的大小,且size等于目标dest缓冲区的大小。(如果源串长度大于或等于目标dest缓冲区的大小,且size等于目标dest缓冲区的大小,则只会拷贝目标dest缓冲区的大小减1个字符,后加’\0’;该情况下,如果size大于目标dest缓冲区的大小则溢出。)
3 snprintf ()函数返回值问题, 如果输出因为size的限制而被截断,返回值将是“如果有足够空间存储,所应能输出的字符数(不包括字符串结尾的’\0’)”,这个值和size相等或者比size大!也就是说,如果可以写入的字符串是”0123456789ABCDEF”共16位,但是size限制了是10,这样 snprintf() 的返回值将会是16 而不是10!
四、snprintf和vsnprintf
同样来看一个例子:
#include
#include
#define snprintf _snprintf
using namespace std;
int main()
{
char str[10] = {0};
char *data = "abcdefg";
sprintf(str, "debug : %s", data);
cout << str << endl;
return 0;
}
该程序可以编译过,但是在运行期间会崩溃,原因相信大家都能看的出来。那么,应该如何处理呢?
#include
#include
#define snprintf _snprintf
using namespace std;
int main()
{
char str[10] = {0};
char *data = "abcdefg";
snprintf(str, sizeof(str) - 1, "debug : %s", data);
cout << str << endl;
return 0;
}
这样就安全了,和strncpy非常类似。
另外,需要特别注意的是: Windows和Linux中的snprintf函数有区别, 在linux代码中,经常见到snprintf(str, sizeof(str), “…”)这样的用法, 为什么这里不是sizof(str) - 1呢?
我们看看Windows下这么用会怎样:
#include
#include
#define snprintf _snprintf
using namespace std;
int main()
{
char str[10] = {0};
char *data = "abcdefgddddddddddddddddddddd";
snprintf(str, sizeof(str), "debug : %s", data);
cout << str << endl;
return 0;
}
我运行的时候,程序没有崩溃,算是万幸。 但结果乱码。看来,没有自动在str最后加’\0’, 在linux中, 就安全了, 会自动补哈, 所以永远不会越界。
总结一下:
1. Linux中, 对于snprintf, 用sizeof(str), 最后会自动加’\0’, 比strncpy更安全省事。
2. Windows中, 就把snprintf和strncpy理解为类似的, 要用sizeof(str) - 1, 需要注意最后的’\0’, 当然啦,你可以在每次用strncpy之前,利用memset将串清零, 这样比较好。VC++6.0中的_snprintf(snprintf)并没有按要求实现, 晕。