sprintf、vsprintf、sprintf_s、vsprintf_s、_snprintf、_vsnprintf、snprintf、vsnprintf 函数辨析

看了题目中的几个函数名是否是有点头晕?为了防止之后总在这样的细节里纠缠不清,今天咱们就来好好地辨析一下这几个函数的异同。安全

实验环境:函数

Windows下使用VS2017
Linux下使用gcc4.9.4测试

 

为了验证函数的安全性咱们设计了以下结构spa

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字段的内存会被修改。咱们以此来推断函数的安全性。设计

1、sprintf(Linux/Windows)code

Linux下的函数原型:int sprintf(char *str, const char *format, ...);
测试代码:orm

int main()
{
    Data data;
    data.Display();
    int ret = sprintf(data.buf, "%d", 12);
    std::cout << "ret = " << ret << std::endl;
    data.Display();
    std::cin.get();
    return 0;
}

在VS2017环境中,这个函数被标记为不安全的,若是使用了,编译器会报警告,若是非要使用,必须在编译的时候增长宏定义:_CRT_SECURE_NO_WARNINGS,告诉编译器忽略安全警告。在Linux下此函数能够正常使用。并且这个函数在Windows下和Linux下行为也是同样的。具体以下:blog

1.当源数据的长度【小于】len,sprintf把数据完整的写到目标内存,并保证尾部以0结尾,返回写入的字节数。此时该函数的行为是安全的。
例如:内存

 sprintf(data.buf, "%d", 12); ci

输出:

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同样。

 

2、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同样。

 

3、_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同样。

 

4、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::cin.get();
    return 0;
}

1、strcpy(Linux/Windows)
函数原型为:char *strcpy(char *dest, const char *src);
最古老的字符串拷贝函数,原理很简单,从源字符串依次拷贝字符到目标地址,直到遇到0为止,如遇到内存重叠的时候,须要特殊处理。老是返回实际写入的字符数,不会处理内存越界的状况,也是毫无安全性,在此不作赘述。


2、strcpy_s(Windows only)
是Windows独有的函数,原型能够描述为:
int strcpy_s(char *dest, size_t size, const char *src);
注意返回值再也不是目标字符串的首地址,而是一个int。
当源字符串长度【小于】或【等于】目标内存的时候,此函数能够安全执行,返回值为【0】,当源字符串长度【大于】目标内存的时候,此函数会触发assert断言,致使程序中断。这个函数不会致使内存破坏。

3、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中断。

4、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
---------------
相关文章
相关标签/搜索