一、sprintf() 函数详解

在将各种类 型的数据构造成字符串时,sprintf 的强大功能很少会让你失望。

由于 sprintf 跟 printf 在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致 sprintf 比 printf 有用得多。所以本文着重介绍 sprintf,有时也穿插着用用 pritnf。

sprintf是个变参函数,定义如下:

int sprintf( char *buffer, const char *format [, argument] … );

除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:格式化字符串上。

printf 和 sprintf 都使用格式化字符串来指 定串的格式,在格式串内部使用一些以“%”开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要 的字符串。

1、格式化数字字符串

sprintf最常见的应用之一莫过于把整数打印到字符串中,所以,spritnf 在大多数场合可以替代itoa。如:

// 把整数123打印成一个字符串保存在s中。

sprintf(s, "%d", 123); // 产生"123"

可以指定宽度,不足的左边补空格:

sprintf(s, "%8d%8d", 123, 4567); // 产生:"    123    4567"

当然也可以左对齐:

sprintf(s, "%-8d%8d", 123, 4567); // 产生:"123         4567"

也可以按照16进制打印:

sprintf(s, "%8x", 4567); // 小写16进制,宽度占8个位置,右对齐
sprintf(s, "%-8X", 4568); // 大写16进制,宽度占8个位置,左对齐

这样,一个整数的16进制字符串就很容易得到,但我们在打印16进制内容时,通常想要一种左边补0的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0就可以了。

sprintf(s, "%08X", 4567); // 产生:"000011D7"

上面以 "%d" 进行的10进制打印同样也可以使用这种左边补0的方式。

这里要注意一个符号扩展的问题:比如,假如我们想打印短整数(short)-1的内存16进制表示形式,在Win32平台上,一个 short 型占2个字节,所以我们自然希望用4个16进制数字来打印它:

short si = -1;
sprintf(s, "%04X", si);

产生 "FFFFFFFF",怎么回事?因为 spritnf 是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个“%X”就能得知当初函数调用前参数压栈时 被压进来的到底是个4字节的整数还是个2字节的短整数,所以采取了统一4字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32位的整数-1,打印时 4个位置不够了,就把32位整数-1的8位16进制都打印出来了。如果你想看si的本来面目,那么就应该让编译器做0扩展而不是符号扩展(扩展时二进制左 边补0而不是补符号位):

sprintf(s, "%04X", (unsigned short)si);

就可以了。或者:

unsigned short si = -1;
sprintf(s, "%04X", si);

sprintf 和 printf 还可以按8进制打印整数字符串,使用“%o”。注意8进制和16进制都不会打印出负数,都是无符号的,实际上也就是变量的内部编码的直接的16进制或8进制表示。

2、控制浮点数打印格式

浮点数的打印和格式控制是 sprintf 的又一大常用功能,浮点数使用格式符“%f”控制,默认保留小数点后6位数字,比如:

sprintf(s, "%f", 3.1415926); // 产生 "3.141593"

但有时我们希望自己控制打印的宽度和小数位数,这时就应该使用:“%m.nf”格式,其中m表示打印的宽度,n表示小数点后的位数。比如:

sprintf(s, "%10.3f", 3.1415626);	// 产生:"     3.142″
sprintf(s, "%-10.3f", 3.1415626); // 产生:"3.142     "
sprintf(s, "%.3f", 3.1415626); // 不指定总宽度,产生:"3.142"

注意一个问题,你猜

int i = 100;
sprintf(s, "%.2f", i);

会打出什么东东来?“100.00”?对吗?自己试试就知道了,同时也试试下面这个:

sprintf(s, "%.2f", (double)i);

第一个打出来的肯定不是正确结果,原因跟前面提到的一样,参数压栈时调用者并不知道跟i相对应的格式控制符是个“%f”。而函数执行时函数本身则并不知道当年被压入栈里的是个整数,于是可怜的保存整数i的那4个字节就被不由分说地强行作为浮点数格式来解释了,整个乱套了。

不过,如果有人有兴趣使用手工编码一个浮点数,那么倒可以使用这种方法来检验一下你手工编排的结果是否正确。

字符/Ascii码对照

我们知道,在C/C++语言中,char也是一种普通 的scalable类型,除了字长之外,它与short,int,long这些类型没有本质区别,只不过被大家习惯用来表示字符和字符串而已。

(或许当年 该把这个类型叫做“byte”,然后现在就可以根据实际情况,使用 byte 或 short 来把 char 通过 typedef 定义出来,这样更合适些)

于是,使用“%d”或者“%x”打印一个字符,便能得 出它的10进制或16进制的ASCII码;反过来,使用“%c”打印一个整数,便可以看到它所对应的ASCII字符。以下程序段把所有可见字符的 ASCII码对照表打印到屏幕上(这里采用printf,注意“#”与“%X”合用时自动为16进制数增加“0X”前缀):

for (int i = 32; i < 127; i++)
{
printf("[ %c ]: %3d 0x%#04X/n", i, i, i);
}

3、连接字符串

sprintf的格式控制串中既然可以插入各种东西,并最终把它们“连成一串”,自然也就能够连接字符串,从而在许多场合可以替代 strcat,但 sprintf 能够一次连接多个字符串(自然也可以同时在它们中间插入别的内容,总之非常灵活)。比如:

char *who = "I";
char *whom = "52PHP"; sprintf(s, "%s love %s.", who, whom); // 产生:"I love 52PHP. "

strcat 只能连接字符串(一段以 '\0' 结尾的字 符数组或叫做字符缓冲,null-terminated-string),但有时我们有两段字符缓冲区,他们并不是以 '\0' 结尾。比如许多从第三方库函 数中返回的字符数组,从硬件或者网络传输中读进来的字符流,它们未必每一段字符序列后面都有个相应的 '\0' 来结尾。如果直接连接,不管是 sprintf 还是 strcat 肯定会导致非法内存操作,而 strncat 也至少要求第一个参数是个 null-terminated-string,那该怎么办呢?我们 自然会想起前面介绍打印整数和浮点数时可以指定宽度,字符串也一样的。比如:

char a1[] = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
char a2[] = {'H', 'I', 'J', 'K', 'L', 'M', 'N'};

如果:

sprintf(s, "%s%s", a1, a2); // Don’t do that!

十有八九要出问题了。是否可以改成:

sprintf(s, "%7s%7s", a1, a2);

也没好到哪儿去,正确的应该是:

sprintf(s, "%.7s%.7s", a1, a2); // 产生:"ABCDEFGHIJKLMN"

这可以类比打印浮点数的“%m.nf”,在“%m.ns”中,m表示占用宽度(字符串长度不足时补空格,超出了则按照实际宽度打印),n才表示从相应的字符串中最多取用的字符数。通常在打印字符串时m没什么大用,还是点号后面的n用的多。自然,也可以前后都只取部分字符:

sprintf(s, "%.6s%.5s", a1, a2); // 产生:"ABCDEFHIJKL"

在许多时候,我们或许还希望这些格式控制符中用以指定 长度信息的数字是动态的,而不是静态指定的,因为许多时候,程序要到运行时才会清楚到底需要取字符数组中的几个字符,这种动态的宽度/精度设置功能在 sprintf 的实现中也被考虑到了,sprintf 采用“*”来占用一个本来需要一个指定宽度或精度的常数数字的位置,同样,而实际的宽度或精度就可以 和其它被打印的变量一样被提供出来,于是,上面的例子可以变成:

sprintf(s, "%.*s%.*s", 7, a1, 7, a2);

或者:

sprintf(s, "%.*s%.*s", sizeof(a1), a1, sizeof(a2), a2);

实际上,前面介绍的打印字符、整数、浮点数等都可以动态指定那些常量值,比如:

sprintf(s, "%-*d", 4, 'A');				// 产生 "65 "
sprintf(s, "%#0*X", 8, 128); // 产生 "0X000080″,"#"产生0X
sprintf(s, "%*.*f", 10, 2, 3.1415926); // 产生"      3.14″

4、打印地址信息

有时调试程序时,我们可能想查看某些变量或者成员的地址,由于地址或者指针也不过是个32位的数,你完全可以使用打印无符号整数的“%u”把他们打印出来:

sprintf(s, "%u", &i);

不过通常人们还是喜欢使用16进制而不是10进制来显示一个地址:

sprintf(s, "%08X", &i);

然而,这些都是间接的方法,对于地址打印,sprintf 提供了专门的“%p”:

sprintf(s, "%p", &i);

我觉得它实际上就相当于:

sprintf(s, "%0*x", 2 * sizeof(void *), &i);

5、利用sprintf的返回值

较少有人注意 printf/sprintf 函数的返回值,但有时它却是有用的,spritnf 返回了本次函数调用最终打印到字符缓冲区中的字符数目。也就是说每当一次 sprinf 调用结束以后,你无须再调用一次strlen 便已经知道了结果字符串的长度。如:

int len = sprintf(s, "%d", i);

对于正整数来说,len便等于整数i的10进制位数。

下面的是个完整的例子,产生10个[0, 100)之间的随机数,并将他们打印到一个字符数组s中,以逗号分隔开。

#include <stdio.h>
#include <stdlib.h>
#include <time.h> int main(int argc, char **argv)
{
srand(time(0));
char s[64];
int offset = 0; for (int i = 0; i < 10; i++)
{
offset += sprintf(s + offset, "%d, ", rand() % 100);
} s[offset - 1] = '\n'; // 将最后一个逗号换成换行符。
printf(s); return 0;
}

设想当你从数据库中取出一条记录,然后希望把他们的各 个字段按照某种规则连接成一个字符串时,就可以使用这种方法,从理论上讲,他应该比不断的 strcat 效率高,因为 strcat 每次调用都需要先找到最后 的那个 '\0' 的位置,而在上面给出的例子中,我们每次都利用 sprintf 返回值把这个位置直接记下来了。

6、使用 sprintf 的常见问题

sprintf是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访问错误,但好在由sprintf误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通常用眼睛再把出错的代码多看几眼就看出来了。

?? 缓冲区溢出

第一个参数的长度太短了,没的说,给个大点的地方吧。当然也可能是后面的参数的问题,建议变参对应一定要细心,而打印字符串时,尽量使用“%.ns”的形式指定最大字符数。

?? 忘记了第一个参数

低级得不能再低级问题,用 printf 用得太惯了。// 偶就常犯。

?? 变参对应出问题

通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位,检查检查吧。尤其是对应“*”的那些参数,都提供了吗?不要把一个整数对应一个“%s”,编译器会觉得你欺她太甚了。


二、snprintf() 详解

函数原型:

int snprintf(char *restrict buf, size_t n, const char * restrict  format, ...);

函数说明:最多从源串中拷贝 n-1 个字符到目标串中,然后再在后面加一个 '\0'。所以如果目标串的大小为 n 的话,将不会溢出。

函数返回值:若成功则返回欲写入的字符串长度,若出错则返回负值。

1、推荐的用法

#include <stdio.h>

int main(int argc, char **argv)
{
char str[10] = {0,};
snprintf(str, sizeof(str), "0123456789012345678");
printf("str=%s\n", str); return 0;
}

输出:

str=012345678

2、不推荐的用法

#include <stdio.h>

int main(int argc, char **argv)
{
char str[10] = {0,};
snprintf(str, 18, "0123456789012345678");
printf("str=%s/n", str); return 0;
}

输出:

str=01234567890123456

3、snprintf() 函数返回值的测试:

#include <stdio.h>
#include <string.h> int main(int argc, char **argv)
{
char str1[10] = {0,};
char str2[10] = {0,};
int ret1 = 0, ret2 = 0; ret1 = snprintf(str1, sizeof(str1), "%s", "abc");
ret2 = snprintf(str2, 4, "%s", "aaabbbccc"); printf("aaabbbccc length=%d\n", strlen("aaabbbccc"));
printf("str1=%s,ret1=%d\n", str1, ret1);
printf("str2=%s,ret2=%d\n", str2, ret2); return 0;
}

输出:

aaabbbccc length=9
str1=abc,ret1=3
str2=aaa,ret2=9

特别注意:

snprintf() 的返回值是欲写入的字符串长度,而不是实际写入的字符串度。如:

#include <stdio.h>

int main(int argc, char **argv)
{
char test[8];
int ret = snprintf(test, 5, "1234567890");
printf("%d|%s\n", ret, test); return 0;
}

输出:

10|1234

Linux C 字符串函数 sprintf()、snprintf() 详解的更多相关文章

  1. [转载]Linux C 字符串函数 sprintf()、snprintf() 详解

    一.sprintf() 函数详解 在将各种类 型的数据构造成字符串时,sprintf 的强大功能很少会让你失望. 由于 sprintf 跟 printf 在用法上几乎一样,只是打印的目的地不同而已,前 ...

  2. linux select函数:Linux下select函数的使用详解【转】

    本文转载自;http://www.bkjia.com/article/28216.html Linux下select函数的使用 Linux下select函数的使用 一.Select 函数详细介绍 Se ...

  3. PHP截取字符串函数substr()函数实例用法详解

    在PHP中有一项非常重要的技术,就是截取指定字符串中指定长度的字符.PHP对于字符串截取可以使用PHP预定义函数substr()函数来实现.下面就来介绍一下substr()函数的语法及其应用. sub ...

  4. Linux Bash命令关于程序调试详解

    转载:http://os.51cto.com/art/201006/207230.htm 参考:<Linux shell 脚本攻略>Page22-23 Linux bash程序在程序员的使 ...

  5. 节点地址的函数list_entry()原理详解

    本节中,我们继续讲解,在linux2.4内核下,如果通过一些列函数从路径名找到目标节点. 3.3.1)接下来查看chached_lookup()的代码(namei.c) [path_walk()> ...

  6. Linux C 语言之 Hello World 详解

    目录 Linux C 语言之 Hello World 详解 第一个 C 语言程序 程序运行原理 编译,链接 运行时 链接库 编译器优化 Hello World 打印原理 stdout, stdin 和 ...

  7. Linux Shell编程与编辑器使用详解

    <Linux Shell编程与编辑器使用详解> 基本信息 作者: 刘丽霞 杨宇 出版社:电子工业出版社 ISBN:9787121207174 上架时间:2013-7-22 出版日期:201 ...

  8. 云计算:Linux运维核心管理命令详解

    云计算:Linux运维核心管理命令详解 想做好运维工作,人先要学会勤快: 居安而思危,勤记而补拙,方可不断提高: 别人资料不论你用着再如何爽那也是别人的: 自己总结东西是你自身特有的一种思想与理念的展 ...

  9. linux sort,uniq,cut,wc命令详解

    linux sort,uniq,cut,wc命令详解 sort sort 命令对 File 参数指定的文件中的行排序,并将结果写到标准输出.如果 File 参数指定多个文件,那么 sort 命令将这些 ...

随机推荐

  1. Let it go.Let it be.Keep it up!

    第三份工作仅仅持续了三个月,今天是last day. 虽然时间很短,但也是经历一场,认识了一些人,知道了一些事. 来去匆匆,难免有一点遗憾,还有一点愧疚. 只能放下,顺其自然,继续努力!

  2. python基础之编码问题

    python基础之编码问题 本节内容 字符串编码问题由来 字符串编码解决方案 1.字符串编码问题由来 由于字符串编码是从ascii--->unicode--->utf-8(utf-16和u ...

  3. HTML 学习笔记 JavaScript(数组)

    1.数组的创建 var arrayObj = new Array(); //创建一个数组var arrayObj = new Array([size]); //创建一个数组并指定长度,注意不是上限,是 ...

  4. echarts在.Net中使用实例(一) 简单的Demo

    前言 这个必须要有前言,即便很短,对于有强迫症的人来说不容易啊.言归正传,之前做图一直使用rdlc自带的格式,虽然任务完成,但是一直觉得不太美观, 空余时间开始找其他的插件,终于找到了Highchar ...

  5. 原创jquery插件treeTable(转)

    由于工作需要,要直观的看到某个业务是由那些子业务引起的异常,所以我需要用树表的方式来展现各个层次的数据. 需求: 1.数据层次分明: 2.数据读取慢.需要动态加载孩子节点: 3.支持默认展开多少层. ...

  6. JS处理四舍五入函数 toFixed(n)(可取小数点后n位)

    在JS中四舍五入的函数 toFixed(n) , n为要保留的小数位数. n为0~20,当n超过20的时候,JS会出错. 如果小数点前和要截取的前一位都是0时,不会按常理截取.       var h ...

  7. Dell xps 13 9350待机时总是关机的处理方法

    现象: 年初买的dell xps13 9350,最近可能是由于win10做了一些更新,每次睡眠就自动关机了,重启很多次,修改电源选项都没用, 原因分析: 在网上搜了一下,有人发现xps15 9350也 ...

  8. [转]Python yield 使用浅析

    您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ? 我们先抛开 generator,以一个常见的编程题目来展示 yield ...

  9. 恢复 Windows 7 的“回到父目录”按钮

    Windows 7 使用以来很多方面一直不习惯,特别是让我无比纠结的”回到父目录“ 按钮从资源管理器中消失了. 不能不说这是一个失败! 很多时候,Win 7 地址栏中自以为是的显示的很多层目录层次的面 ...

  10. CSS编写技巧

    1.尽量少的使用全局的重置代码 全局重置代码:*{margin:0; padding:0;}对于熟悉CSS的人来说并不陌生,并且有很多人的第一句CSS代码就是这句.它可以避免不同浏览器的默认间距不同而 ...