sprintf、vsprintf、sprintf_s、vsprintf_s、_snprin。。。
看了题⽬中的⼏个函数名是不是有点头晕?为了防⽌以后总在这样的细节⾥纠缠不清,今天我们就来好好地辨析⼀下这⼏个函数的异同。
实验环境:
Windows下使⽤VS2017
Linux下使⽤gcc4.9.4
为了验证函数的安全性我们设计了如下结构
const int len = 4;
#pragma pack(push)
#pragma pack(1)
struct Data
{
char buf[len];
char guard;
Data()
{
for (int i = 0; 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", 12);
std::cout << "ret = " << ret << std::endl;
data.Display();
std::();
return0;
}
在VS2017环境中,这个函数被标记为不安全的,如果使⽤了,编译器会报警告,如果⾮要使⽤,必须在编译的时候增加宏定义:
_CRT_SECURE_NO_WARNINGS,告诉编译器忽略安全警告。在Linux下此函数可以正常使⽤。⽽且这个函数在Windows下和Linux下⾏为也是⼀样的。具体如下:
1.当源数据的长度【⼩于】len,sprintf把数据完整的写到⽬标内存,并保证尾部以0结尾,返回写⼊的字节数。此时该函数的⾏为是安全的。
例如:
sprintf(data.buf, "%d", 12);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------
2.当源数据的长度【等于】len,sprintf把数据完整的写到⽬标内存,并在⽬标内存的尾部多写⼊⼀个0,
返回写⼊的字节数。此时该函数已经发⽣拷贝越界的情况了。所以,当⽤户以为分配的内存刚刚好满⾜拷贝需求的时候,其实已经发⽣了潜在的风险。
例如:
sprintf(data.buf, "%d", 1234);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 1234
guard = 0
memory has been broken.
---------------
3.当源数据的长度【⼤于】len,sprintf把数据完整的写到⽬标内存,返回写⼊的字节数,压根不管内存越界的情况,甚⾄连个错误码都不返回。
例如:
sprintf(data.buf, "%d", 123456);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 6
sizeof(Data) = 5
buf = 123456
guard = 53
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", 12);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------
2.当源数据的长度【等于】或者【⼤于】len的时候,调⽤此函数将会触发断⾔。Debug模式下会弹出运⾏时错误提⽰框,告诉⽤户"Buffer too small";Release模式下程序会直接崩溃。
例如:
sprintf_s(data.buf, len, "%d", 1234);
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", 12);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------
例2,当源数据的长度【等于】len,能保证完整写⼊,结尾不做任何处理,返回实际写⼊的字节数:
_snprintf(data.buf, len, "%d", 1234);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 1234烫烫烫
guard = 15
---------------
例3,当源数据的长度【⼤于】len,最多写⼊【len】个字符,结尾不错任何处理,返回【-1】:
_snprintf(data.buf, len, "%d", 123456);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
-
--------------
ret = -1
sizeof(Data) = 5
buf = 1234烫烫烫
guard = 15
---------------
总结:_snprintf函数只能在Windows下使⽤,最多写⼊【size】个字符,永远不破坏内存,也不会触发中断,但不能保证⽬标内存以0结尾。通过返回值可以知道函数调⽤是否成功,返回值>=0的时候,表⽰调⽤成功,返回了实际写⼊的字符数;返回值为-1的时候,表⽰⽬标内存太⼩,导致调⽤失败,但是已经尽⼒做了填充。
_vsnprintf的⾏为与_snprintf⼀样。
printf怎么加endl四、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", 12);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 2
sizeof(Data) = 5
buf = 12
guard = 15
---------------
例2:当源数据的长度【等于】len,实际上只写⼊了【len-1】个字符,最后⼀个字符⽤0填充,但返回值却是【len】:
snprintf(data.buf, len, "%d", 1234);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 4
sizeof(Data) = 5
buf = 123
guard = 15
---------------
例3,当源数据的长度【⼤于】len,最多也只写⼊【len-1】个字符,最后⼀个字符⽤0填充,但返回值却是【应该要写⼊的字节数】:
snprintf(data.buf, len, "%d", 123456);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 6
sizeof(Data) = 5
buf = 123
guard = 15
---------------
总结:snprintf函数,可以在Linux/Windows双平台下使⽤,最多写⼊【size-1】个字符,永远不会破坏内存,也不会触发中断,并总能保证⽬标内存能以0结尾。唯⼀的问题是返回值不可靠,⽆法推断调⽤是否失败。
vsnprintf的⾏为与snprintf⼀样。
写到这⾥,sprintf系列的相关函数都讲完了,貌似没有⼀个完美的函数。不过既然知道了它们的具体⾏为,就可以根据应⽤场景挑选适合的函数。
补充:既然已经写到这⼉了,就顺便利⽤这个机会顺便把strcpy函数簇也研究⼀下吧。
测试代码:
int main()
{
Data data;
data.Display();
const char * ret = strncpy(data.buf, "12345678", len);
std::cout << "ret = " << ret << std::endl;
data.Display();
std::();
return0;
}
⼀、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, "12345", len);
输出:
sizeof(Data) = 5
buf = ****烫烫烫
guard = 15
---------------
ret = 1234烫烫烫
sizeof(Data) = 5
buf = 1234烫烫烫
guard = 15
---------------
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论