【C语言进阶】sprintf和snprintf的区别

描述

C语言上总有些非常相近的接口函数,比如sprintf和snprintf就是其中的一对。以笔者多年的工作经验,这对接口函数在平时的编程中,使用的频度是非常高,只是你真的了解它们俩的区别吗?

带着这个问题,请跟随笔者的思路梳理一遍sprintf和snprintf。通过阅读本文,你将了解到以下内容:

sprintf和snpintf分别是什么?

sprintf和snprintf的区别与联系

sprintf和snprintf的使用秘诀


sprintf和snpintf分别是什么?


【sprintf】的函数原型如下所示:

/**
功能: 把格式化的数据写入某个字符串缓冲区
入参:format,输出字符串的格式化列表,比如"%s %d %c"等
入参: [argument],format对应的不定参数列表,与printf的不定入参类似
出参:buffer,指向一段存储空间,用于存储格式化之后的字符串
返回值:返回写入buffer 的字符数,出错则返回-1. 如果 buffer 或 format 是空指针,
且不出错而继续,函数将返回-1,并且 errno 会被设置为 EINVAL。
备注:它是个变参函数
*/
int sprintf( char *buffer, const char *format, [ argument] … );

【snprintf】的函数原型如下所示:

/**
功能: 有长度限制地,把格式化的数据写入某个字符串缓冲区
入参:format,输出字符串的格式化列表,比如"%s %d %c"等
入参: [argument],format对应的不定参数列表,与printf的不定入参类似
入参:size,表示buffer指向存储空间的大小
出参:buffer,指向一段存储空间,用于存储格式化之后的字符串
返回值:返回写入buffer 的字符数,出错则返回-1. 如果 buffer 或 format 是空指针,
且不出错而继续,函数将返回-1,并且 errno 会被设置为 EINVAL。
备注:它是个变参函数
*/
int snprintf( char *buffer, size_t size, const char *format, [ argument] … );

sprintf和snprintf的区别与联系


通过对比sprintf和snprintf的函数原型,我们可以发现两者其实完成相同功能的接口,都是将一段数据经格式化操作之后,转换成一段字符串,通过接口传入的buffer指针将格式化的字符串内容输出。

我们细细比对两个函数原型,我们会发现snprintf比sprintf多了一个表示buffer指针指向存储空间的大小的入参size,那么它到底有什么作用呢?我们先来分析下snprintf接口的内部行为与size的关系:

如果格式化后的字符串长度 < size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符('\0')

如果格式化后的字符串长度 >= size,则只将其中的(size-1)个字符复制到str中,并给其后添加一个字符串结束符('\0'),返回值为欲写入的字符串长度。

看完这一段解释之后,大概你就明白了,原来snprintf就是sprintf的安全版本,因为单从sprintf的内部行为来看,它是没有办法保证对buffer指针的赋值操作是没有越界的,因为它压根就不知道buffer的存储空间多少有多大,所以它只能认为是【无穷大】。但是snprintf通过入参size,恰好可以很好的解决这个问题,它可以很明确的告知snprintf的内部操作,以size作为界线,当输出的字符串长度要超过size时,应做出裁剪输出。在很多的编程宝典中,都是推荐使用snprintf,而要求编程者尽可能地避免使用sprintf这种不安全接口。


sprintf和snprintf的使用秘诀


我们通过一段测试代码来展示下两者的使用方法,以及上一小结中提及的可能导致buffer溢出的严重问题:

//sprintf的用法
{
    char buffer[10]; //定义一个只有10个字节空间的buffer数组
    const int a = 12345; //定义一个int型的常量
    const char *msg = "012345678901234567890"; //定义一个长度为20字节的字符串常量

    sprintf(buffer, "%d", a); //将a变量按int类型打印成字符串,输出到buffer中
    /*
    输出分析:
    输出结果: buffer="12345"
    因为最后输出的buffer内容长度不超过10字节,所以此时sprintf操作是没有溢出风险的
    */

    sprintf(buffer, "%d+%s", a, msg); //将a变量和msg字符串通过“+”连接成一个字符串
    /*
    输出分析:
    由于buffer只有10个字节空间,而sprintf在执行字符串格式化输出的时,并不知道buffer的真实长度,
    所以它会将"12345+012345678901234567890"这整个字符串都拷贝到buffer空间上,这就导致了buffer存储空间溢出了。
    从存储位置上分析,我们知道buffer空间属于一个栈空间,在它自己的10字节之外的空间很明显是其他栈变量的存储空间,
    一旦sprintf将10字节外的其他空间也操作了,这就有可能破坏了其他栈变量的内容,这有可能是致命的。
    */
}

//snprintf的用法
{
    char buffer[10]; //定义一个只有10个字节空间的buffer数组
    const int a = 12345; //定义一个int型的常量
    const char *msg = "012345678901234567890"; //定义一个长度为20字节的字符串常量

    snprintf(buffer, sizeof(buffer), "%d", a); //将a变量按int类型打印成字符串,输出到buffer中
    /*
    输出分析:
    输出结果: buffer="12345"
    因为最后输出的buffer内容长度不超过10字节,所以snprintf操作是没有溢出风险的;
    此种情况下,使用sprintf和snpintf都可以得到同样的结果,且都不会产生数组溢出。
    */

    sprintf(buffer, sizeof(buffer), "%d+%s", a, msg); //将a变量和msg字符串通过“+”连接成一个字符串
    /*
    输出分析:
    输出结果是: buffer="12345+0123",加上一个'\0'的字符串结束符,
    刚好占用了buffer的10字节的存储空间,不存在任何的buffer溢出风险。而"0123"后面的字符串都被snprintf内部裁剪掉了,这就体现了snprintf操作安全的特性。
    */
}

通过以上分析,我们很好地认识到了sprintf的操作是不安全的。在C语言的语法上,指针的灵活性也带来可能导致的指针溢出风险,而snprintf恰好就是解决了这个困惑的sprintf升级版本。

类似的,还有strcat和strncat、strcpy和strncpy等等。通过本文的方法,读者也可以写一小段测试代码,好好捋一捋本文提及的这几组函数,一起领悟下其他的奥秘和使用风险吧。

 

以上总结,均来自笔者多年的实践经验,如有发现不正确的陈述或错误的观点,还望读者指正,感激不尽。
 

 审核编辑:汤梓红
 
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分