sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函数辨析
看了题目中的几个函数名是不是有点头晕?为了防止以后总在这样的细节里纠缠不清,今天我们就来好好地辨析一下这几个函数的异同。
实验环境:
Windows下使用VS2017
Linux下使用gcc4.9.4
为了验证函数的安全性我们设计了如下结构
const int len = ;
#pragma pack(push)
#pragma pack(1)
struct Data
{
char buf[len];
char guard;
Data()
{
for (int i = ; i < len; ++i)
{
buf[i] = '*';
}
guard = 0xF;
}
void Display()
{
std::cout << "sizeof(Data) = " << sizeof(Data) << std::endl;
std::cout << "buf = " << buf << std::endl;
std::cout << "guard = " << (unsigned int)guard << std::endl;
if (guard != 0xF)
{
std::cout << "memory has been broken." << std::endl;
}
std::cout << "---------------" << std::endl;
}
};
#pragma pack(pop)
当我们把数据写到Data.buf字段中去的时候,如果发生了内存越界的情况,Data.gurad字段的内存会被修改。我们以此来推断函数的安全性。
一、sprintf(Linux/Windows)
Linux下的函数原型:int sprintf(char *str, const char *format, ...);
测试代码:
int main()
{
Data data;
data.Display();
int ret = sprintf(data.buf, "%d", );
std::cout << "ret = " << ret << std::endl;
data.Display();
std::cin.get();
return ;
}
在VS2017环境中,这个函数被标记为不安全的,如果使用了,编译器会报警告,如果非要使用,必须在编译的时候增加宏定义:_CRT_SECURE_NO_WARNINGS,告诉编译器忽略安全警告。在Linux下此函数可以正常使用。而且这个函数在Windows下和Linux下行为也是一样的。具体如下:
1.当源数据的长度【小于】len,sprintf把数据完整的写到目标内存,并保证尾部以0结尾,返回写入的字节数。此时该函数的行为是安全的。
例如:
sprintf(data.buf, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
2.当源数据的长度【等于】len,sprintf把数据完整的写到目标内存,并在目标内存的尾部多写入一个0,返回写入的字节数。此时该函数已经发生拷贝越界的情况了。所以,当用户以为分配的内存刚刚好满足拷贝需求的时候,其实已经发生了潜在的风险。
例如:
sprintf(data.buf, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
memory has been broken.
---------------
3.当源数据的长度【大于】len,sprintf把数据完整的写到目标内存,返回写入的字节数,压根不管内存越界的情况,甚至连个错误码都不返回。
例如:
sprintf(data.buf, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
memory has been broken.
---------------
总结:以上三组实验结果,在Windows和Linux下均可以得到验证,可见sprintf函数的安全系数几乎为0,不推荐大家使用。
vsprintf的行为与sprintf一样。
二、sprintf_s(Windows only)
为了弥补sprintf函数的不足,高版本的MSVC环境中引入了sprintf_s函数,在调用的时候支持用户传入目标内存的长度,函数原型可以简略的表示为:
int sprintf_s(char *buf, size_t buf_size, const char *format, ...);
1.当源数据的长度【小于】len,sprintf把数据完整的写到目标内存,并保证尾部以0结尾,返回写入的字节数。此时该函数的行为是安全的。
例如:
sprintf_s(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
2.当源数据的长度【等于】或者【大于】len的时候,调用此函数将会触发断言。Debug模式下会弹出运行时错误提示框,告诉用户"Buffer too small";Release模式下程序会直接崩溃。
例如:
sprintf_s(data.buf, len, "%d", );
Debug模式下执行,会触发assert,如下图:

总结:sprintf_s函数只能在Windows下使用,虽然不会出现写坏内存的情况,但是会触发assert,导致程序中断,使用起来也要慎重。
vsprintf_s的行为与sprintf_s一样。
三、_snprintf(Windows only)
也许是觉得sprintf_s也不够安全,MSVC环境中还引入了一个名为_snprintf的函数,其函数原型和sprintf_s类似,可以表示为:
int _snprintf(char *buf, size_t buf_size, const char *format, ...);
其表现行为如下:
例1,当源数据的长度【小于】len,能保证完整写入,并以0结尾,返回实际写入的字节数:
_snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
例2,当源数据的长度【等于】len,能保证完整写入,结尾不做任何处理,返回实际写入的字节数:
_snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf = 烫烫烫
guard =
---------------
例3,当源数据的长度【大于】len,最多写入【len】个字符,结尾不错任何处理,返回【-1】:
_snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret = -
sizeof(Data) =
buf = 烫烫烫
guard =
---------------
总结:_snprintf函数只能在Windows下使用,最多写入【size】个字符,永远不破坏内存,也不会触发中断,但不能保证目标内存以0结尾。通过返回值可以知道函数调用是否成功,返回值>=0的时候,表示调用成功,返回了实际写入的字符数;返回值为-1的时候,表示目标内存太小,导致调用失败,但是已经尽力做了填充。
_vsnprintf的行为与_snprintf一样。
四、snprintf(Linux/Windows)
Linux下的函数原型为:
int snprintf(char *str, size_t size, const char *format, ...);
这个函数在Windows和Linux下均可以使用,并且行为一致。即:最多写入【size-1】个字符到目标内存,并保证以0结尾。返回值是【应该写入的字节数】,而不是【实际写入的字节数】
例1,当源数据的长度【小于】len,能保证完整写入,并以0结尾,返回实际写入的字节数:
snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
例2:当源数据的长度【等于】len,实际上只写入了【len-1】个字符,最后一个字符用0填充,但返回值却是【len】:
snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
例3,当源数据的长度【大于】len,最多也只写入【len-1】个字符,最后一个字符用0填充,但返回值却是【应该要写入的字节数】:
snprintf(data.buf, len, "%d", );
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret =
sizeof(Data) =
buf =
guard =
---------------
总结:snprintf函数,可以在Linux/Windows双平台下使用,最多写入【size-1】个字符,永远不会破坏内存,也不会触发中断,并总能保证目标内存能以0结尾。唯一的问题是返回值不可靠,无法推断调用是否失败。
vsnprintf的行为与snprintf一样。
写到这里,sprintf系列的相关函数都讲完了,貌似没有一个完美的函数。不过既然知道了它们的具体行为,就可以根据应用场景挑选适合的函数。
补充:既然已经写到这儿了,就顺便利用这个机会顺便把strcpy函数簇也研究一下吧。
测试代码:
int main()
{
Data data;
data.Display();
const char * ret = strncpy(data.buf, "", len);
std::cout << "ret = " << ret << std::endl;
data.Display();
std::cin.get();
return ;
}
一、strcpy(Linux/Windows)
函数原型为:char *strcpy(char *dest, const char *src);
最古老的字符串拷贝函数,原理很简单,从源字符串依次拷贝字符到目标地址,直到遇到0为止,如遇到内存重叠的时候,需要特殊处理。总是返回实际写入的字符数,不会处理内存越界的情况,也是毫无安全性,在此不做赘述。
二、strcpy_s(Windows only)
是Windows独有的函数,原型可以描述为:
int strcpy_s(char *dest, size_t size, const char *src);
注意返回值不再是目标字符串的首地址,而是一个int。
当源字符串长度【小于】或【等于】目标内存的时候,此函数可以安全执行,返回值为【0】,当源字符串长度【大于】目标内存的时候,此函数会触发assert断言,导致程序中断。这个函数不会导致内存破坏。
三、strncpy_s(Windows only)
是Windows独有的函数,原型可以描述为:
int strncpy_s(char *dest, size_t dest_size, const char *src, size_t count);
返回值也是一个int。
这个函数除了能指定目标内存的大小,还能指定拷贝的字符数量,相当于做了双重保护。
但是注意必须满足【count <= dest_size - 1】,这个函数才能正确调用,否则也会触发assert中断。
四、strncpy(Linux/Windows)
函数原型:char *strncpy(char *dest, const char *src, size_t size);
行为与strcpy类似,从源字符串依次拷贝字符到目标地址,直到遇到0或者目标内存已写满为止,最多拷贝【size】个字符。这个函数不会破坏内存,也不会导致程序中断,但是无法保证目标字符串以0结尾。
例如:
strncpy(data.buf, "", len);
输出:
sizeof(Data) =
buf = ****烫烫烫
guard =
---------------
ret = 烫烫烫
sizeof(Data) =
buf = 烫烫烫
guard =
---------------
sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函数辨析的更多相关文章
- printf,sprintf,vsprintf
printf,sprintf比较常用,vsprintf不常用. 1. 三个函数的声明: int printf (const char * szFormat, ...); int sprintf (ch ...
- printf,sprintf,vsprintf 区别【转】
转自:http://blog.csdn.net/anye3000/article/details/6593551 有C语言写作历史的程序员往往特别喜欢printf 函数.即使可以使用更简单的命令(例如 ...
- PHP中函数sprintf .vsprintf (占位符)
sprintf()格式化字符串写入一个变量中. vsprintf()格式化字符串些写入变量中. <?php $num1 = 123; $num2 = 456; $txt = vsprintf(& ...
- PHP字符串函数之 sscanf echo print sprintf vsprintf printf vprintf fprintf vfprintf
sscanf – 根据指定格式解析输入的字符 echo – 输出一个或多个字符串 print – 输出字符串 sprintf – 返回格式化字符串 vsprintf – 返回格式化字符串 (参数为数组 ...
- c++中sprintf和sprintf_s的区别
参考:https://blog.csdn.net/qq_37221466/article/details/81140901 sprintf_s是sprintf的安全版本,指定缓冲区长度来避免sprin ...
- 【C++】sprintf 与sprintf_s
(转自: http://blog.sina.com.cn/s/blog_4ded4a890100j2nz.html) 将过去的工程用VS2005打开的时候.你有可能会遇到一大堆的警告:warning ...
- sprintf、fprintf和printf这三个函数
都是把格式好的字符串输出,只是输出的目标不一样:1 printf,是把格式字符串输出到标准输出(一般是屏幕,可以重定向).2 sprintf,是把格式字符串输出到指定字符串中,所以参数比printf多 ...
- sprintf、fprintf和printf这三个函数有什么区别?
都是把格式好的字符串输出,只是输出的目标不一样:1 printf,是把格式字符串输出到标准输出(一般是屏幕,可以重定向).2 sprintf,是把格式字符串输出到指定字符串中,所以参数比printf多 ...
- printf, fprintf, sprintf, snprintf, vprintf, vfprintf, vsprintf, vsnprintf - 输出格式转换函数
参考:http://blog.sina.com.cn/s/blog_674b5aae0100prv3.html 总览 (SYNOPSIS) #include <stdio.h> int p ...
随机推荐
- 【javascript】基于javascript的小时钟
计时事件:通过JavaScript,我们可以设置在一段时间间隔后执行一段代码,而不仅仅是在函数调用后立即执行. 在JavaScript中,使用计时事件是很容易的,主要有两个事件供我们使用 setTim ...
- ajax向php传参数对数据库操作
刚入门php,要求要对多用户进行批量删除(当然实际中是不可能的),在这就以此为例. 大意就是通过对数据库中用户查询,将用户信息显示在页面表格中,在进行多项选择后将所选行参数通过ajax传入后台php文 ...
- phpstorm中Xdebug的使用
目 录 1.Xdebug简介 2.Xdebug的安装.操作 2.1环境搭建 2.2配置php.ini 2.3配置PhpStorm 2.4配置PHP Debug 2.5进行调试 1.Xdebug简介 ...
- VB查询数据库之报表——机房收费系统总结(六)
我们要用一个软件做报表的模板.然后在VB里面添加部件.代码调用模板,详细步骤如下. 一.下载安装 首先做报表要下载安装Grid++Report 4.5 报表设计器 点击下载(内含破解补丁) 二.制作模 ...
- 【BZOJ 1815】【SHOI 2006】color 有色图
http://www.lydsy.com/JudgeOnline/problem.php?id=1815 这道题好难啊,组合数学什么根本不会啊qwq 题解详见08年的Pólya计数论文. 主要思想是只 ...
- 【UOJ #279】【UTR #2】题目交流通道
http://uoj.ac/problem/279 先判断答案为0的情况,\(d(i,i)\neq 0\),\(d(i,j)\neq d(j,i)\),\(d(i,j)>d(i,k)+d(k,j ...
- bzoj 1483: [HNOI2009]梦幻布丁
1483: [HNOI2009]梦幻布丁 Description N个布丁摆成一行,进行M次操作.每次将某个颜色的布丁全部变成另一种颜色的,然后再询问当前一共有多少段颜色.例如颜色分别为1,2,2,1 ...
- [转]SpringMVC拦截器详解[附带源码分析]
目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:ht ...
- Ubuntu 16.04重启Nautilus
关闭: nautilus -q 启动: 不要在命令行启动,直接在Dash中找到“文件”,然后启动,这样就可以在后台直接运行.
- HDU 4630 No Pain No Game(2013多校3 1010题 离线处理+树状数组求最值)
No Pain No Game Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)T ...