printf()
是 C 语言中一个非常重要的函数,它的核心功能是打印格式化的字符串。而其中的关键则是第一个参数——格式字符串(format string)。虽然大多数人都会使用格式字符串,但一些细节可能未必了解。本文将详细说明格式字符串的使用方法。
格式字符串(format string)
格式字符串是包含转换规范(conversion specification)的字符串。每一个转换规范都对应于 printf()
调用时从第二个开始的参数,说明要如何呈现该参数的内容,比如浮点数数据要打印几位小数等。每一个转换规范的指定方法如下:
%[旗标][宽度][.精确度][长度修饰符]格式代码
其中只有开头的百分比符号和最后的格式代码(format code)是必要的,其余项目都可视需要再加上。
格式代码(format code)
格式代码有许多种,都以单一英文字母表示。我们先以最简单、用于转换字符串数据的 s
为例,并借此说明转换规范中其他项目的用法。
转换字符串的格式代码——s
#include <stdio.h>
int main(){
char s[] = "hello";
printf("%s:\n", s);
return 0;
}
转换规范 %s
会将对应的字符串类型参数带入,所以实际打印的内容为:
hello:
我们故意在转换规范后加一个 ':'
,以便能看出单一参数转换后的结束位置。
宽度(width)
如果你希望打印出像是表格的样式,让个别参数占据特定栏宽,可以在转换规范加上宽度(width),例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%10s:\n", s);
return 0;
}
在百分比符号和 s
间加上宽度 10 后,结果变成:
12345678901234567890
hello:
我们特意先打印出一列序号让大家方便看出打印位置,你可以看到 "hello" 的前面多了 5 个空格,这是因为宽度是 10,但 "hello" 只有 5 个字符,不足的部分默认会在左边补上空白字符。
你也可以用星号 *
指定宽度,这样就可以通过额外的整数参数来指定宽度,例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%*s:\n", 10, s);
return 0;
}
注意到用来提供宽度的参数必须在要打印的数据前,结果如下:
12345678901234567890
hello:
旗标(flags)
如果你希望字符串长度不足时把空白补在右边,可以加上 -
旗标:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%-10s:\n", s);
return 0;
}
结果如下:
12345678901234567890
hello :
-
旗标表示要将数据靠左对齐栏位开头。
精确度(precision)
如果数据的长度超过指定的宽度,转换后并不会截掉超过的部分,仍然会打印出来,例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%4s:\n", s);
return 0;
}
虽然指定的宽度是 4,但仍然会打印出完整 5 个字符的内容。如果你希望打印时不要超过栏宽,可以在转换规范加上精确度(precision),例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%4.4s:\n", s);
return 0;
}
有小数点开头的就是精确度,表示最多只要打印原字符串内的前几个字符,因此虽然字符串的长度是 5,但因为精确度是 4,所以只会打印前 4 个字符:
12345678901234567890
hell:
精确度也一样可以用星号 *
表示要由参数来决定实际的位数,例如:
#include <stdio.h>
int main(){
char s[] = "hello";
printf("12345678901234567890\n");
printf("%4.*s:\n", 4, s);
return 0;
}
会得到一样的结果。
以 10 进位呈现有号整整数的格式代码——d/i
如果要打印的是有号整数(signed integer),可以用格式代码 d
或 i
转换成 10 进位数字:
printf("%10d:\n", 300);
printf("%-10d:\n", -300);
结果如下:
300:
-300 :
旗标
如果希望正数的前方显示 +
号,可以加上 +
旗标:
printf("%+10d:\n", 300);
结果如下:
+300:
你也可以加上 0
旗标,改用 '0' 填补不足宽度的部分:
printf("%010d:\n", -300);
printf("%-010d:\n", 300);
结果如下:
-000000300:
300 :
请特别留意正负号现在会出现在最左端,另外如果有使用 -
旗标,右侧空位并不会补 0
,否则会造成误解。
如果你不想显示正号,但又希望若是正数,可以在符号的位置空一格,那么可以使用空白字符旗标:
printf("% 010d:\n", 300);
结果如下:
000000300:
精确度
你也可以使用精确度来指定最少要显示的位数,默认为 1,若实际的位数不足,会在前面补 0
:
printf("%10.6d:\n", 300);
printf("%010.6d:\n", 300);
结果如下:
000300:
000300:
要注意的是,与 0
旗标并用的话,不足宽度的部分会补空白,不是补 0
。
另外,如果转换后的值是 0
,而且精确度也是 0
,那就不会打印任何内容。
长度修饰符(length modifier)
如果格式代码 d/i
对应的参数是 long
类型,就必须要在格式代码前加上 l
长度修饰符,否则当数值超过 int
范围时就会出错:
#include <stdio.h>
int main(){
long l1 = 300l;
long l2 = 0x1FFFFFFFFl; //8589934591
printf("int is %d bytes.\n", sizeof(int));
printf("long is %d bytes.\n", sizeof(long));
printf("%d:\n", l1);
printf("%d:\n", l2);
printf("%ld:\n", l2);
return 0;
}
结果如下:
int is 4 bytes.
long is 8 bytes.
300:
-1:
8589934591:
由于 l2
超过 int
的范围,所以若是用格式代码 d
转换,只会取最低的 4 个字节 0xFFFFFFFF
,转换结果就变成 -1
了,只要加上 l
长度修饰符,就会取得完整的 long
数据转换成正确的值了。
在某些平台或编译器上,int
和 long
占的字节数一样,加或不加 l
长度修饰符结果虽然一样,但为了程序的相容性,请都还是养成加上长度修饰符的好习惯。
如果数据是 short
,就要使用 h
长度修饰符;若数据是 long long
,就要改用 ll
长度修饰符。
以 10 进位呈现无号整数的格式代码——u
格式代码 u
的用法和 d
一样,只是它会把对应的参数视为无号整数,例如:
#include <stdio.h>
int main(){
int i1 = 300;
int i2 = -1;
unsigned int i3 = 0xFFFFFFFF;
unsigned long l = 0xFFFFFFFFFFFFFFFFl; //8589934591
printf("%u:\n", i1);
printf("%u:\n", i2);
printf("%d:\n", i3);
printf("%u:\n", i3);
printf("%u:\n", l);
printf("%lu:\n", l);
return 0;
}
结果如下:
300:
4294967295:
-1:
4294967295:
4294967295:
18446744073709551615:
你可以看到 i2
虽然是 -1
,但用格式代码 u
打印却是正整数;反之,i3
虽然是无号整数,但是若是使用格式代码 d
打印,它会当成是有号整数处理,反而打印出 -1
了。
对于无号长整数,一样要加上 l
长度修饰符,否则只会截取到部分数据,打印出错误的值。
以 16 进位呈现无号整数的格式代码——x/X
x/X
的用法就如同 u
,但是会用 16 进位的格式,例如:
#include <stdio.h>
int main(){
int i1 = 300;
int i2 = -1;
unsigned long l = 0xFFFFFFFFFFFFFFFFl; //8589934591
printf("%08x:\n", i1);
printf("%X:\n", i2);
printf("%X:\n", l);
printf("%lx:\n", l);
return 0;
}
结果如下,X
与 x
的差别就是使用大写还是小写 a~z
英文字母来表示 10 进位的 10~15:
0000012c:
FFFFFFFF:
FFFFFFFF:
ffffffff:
替代格式(alternative implementation)旗标——#
如果加上 #
旗标,就会额外加上 '0x' 或是 '0X' 字首,让打印结果更清楚是 16 进位。
printf("%#x:\n", i2);
结果如下:
0xffffffff:
以 10 进位呈现双精度浮点数的格式代码——f/F
f
或 F
功能相同,可用于打印浮点数,这时精确度指的是要打印的小数位数,会以四舍五入的方式截掉多余的位数,没有指定精确度时,默认是 6 位。要特别留意的是小数点也会占掉一个字符,指定宽度时要算进去。范例如下:
#include <stdio.h>
#include <math.h>
int main(){
printf("%10f:\n", 300.0);
printf("%010.0f:\n", 300.3f);
printf("%#10.0f:\n", 300.3);
printf("%010.4f:\n", log2(3));
return 0;
}
结果如下:
300.000000:
0000000300:
300.:
00001.5850:
若精确度设为 0
,因为没有小数,默认就不会打印小数点,但只要加上替代格式旗标 #
,就还是会打印小数点,明确表示这是浮点数。
由于默认引数类型提升规则的关系,传给 printf()
的 float
数据会先被转成 double
才传入 printf()
,因此虽然 f
格式代码处理的对象是 double
,但传入 float
类型的数据也能正确处理。
如果数据是 long double
,就要加上 L
长度修饰符。
以科学记号显示双精度浮点数的格式代码——e/E
e/E
功能同 f/F
,但改成以科学记号形式,其中 e/E
的差异就在于用 'e' 还是 'E' 表示指数,指数部分至少会有 2 位数字:
#include <stdio.h>
#include <math.h>
int main(){
printf("%10e:\n", 30.0);
printf("%010.0e:\n", 300.3f);
printf("%#10.0E:\n", 300.3);
printf("%010.4E:\n", log2(3));
return 0;
}
结果如下,注意到精确度设定的是实数的小数位数,指数的位数无法变更:
3.000000e+01:
000003e+02:
3.E+02:
1.5850E+00:
在 Windows 平台上,不知道是什么原因,指数至少会有 3 位数。如果希望能和其他平台一致,可以使用以下函数设定成 2 位:
_set_output_format(_TWO_DIGIT_EXPONENT);
本文都假设采用 C 语言标准规格,指数至少 2 位。
以精简格式显示双精度浮点数格式代码——g/G
这种格式会根据转换的数值从 f/F
或 e/E
挑选格式使用,假设精确度是 P
,若未指定精确度时默认是 6,若精确度设为 0,会自动调升为 1;而使用 e/E
格式转换后的指数部分为 X
,规则如下:
- 若
P > X ≧ -4
,就以精确度P - 1 - X
采用f/F
格式。 - 否则就以精确度
P - 1
依照格式代码大小写套用e/E
格式。
请看以下例子:
#include <stdio.h>
int main(){
printf("e:%10.4e\n", 10.12345);
printf("f:%10.4f\n", 10.12345);
printf("g:%10.4g\n", 10.12345);
return 0;
}
结果如下:
e:1.0123e+01
f: 10.1235
g: 10.12
在这个例子中,P
是 4,X
是 1,符合 P > X ≧ -4
的条件,所以会套用 f/F
格式,并设定精确度为 4 - 1 - 1
,也就是 2,因此小数有 2 位。若是将例子改成以下:
#include <stdio.h>
int main(){
printf("e:%10.4e\n", 0.000012345);
printf("f:%10.4f\n", 0.000012345);
printf("g:%10.4g\n", 0.000012345);
return 0;
}
结果就会变成:
e:1.2345e-05
f: 0.0000
g: 1.235e-05
这是因为虽然 P
还是 4,但 X
是 -5,不再符合 P > X ≧ -4
的条件,所以会套用 e
格式,并设定精确度为 4 - 1
,也就是 3,因此以小数 3 位的科学记号表示法呈现。
有些书籍或是教学文章说 g/G
格式是挑选 e/E
和 f/F
两者转换后较短的结果,这并不正确,从上面的例子就可以看到不但不一定是采用较短的结果,连精确度都不一样。
g/G
格式还有一个很重要的特色就是会帮你把小数尾端的 0
自动去除,例如:
#include <stdio.h>
int main(){
printf("e:%10.6e\n", 0.55);
printf("f:%10.6f\n", 0.55);
printf("g:%10.6g\n", 0.55);
return 0;
}
结果如下:
e:5.500000e-01
f: 0.550000
g: 0.55
P
是 6,X
是 1,所以 g
格式的精确度应该是 6 - 1 - 1
为 4,但是实际看到的小数只有 2 位,因为尾端的 00
被删除了。如果要保留小数尾端的 0
,可以加上替代格式旗标 #
:
#include <stdio.h>
int main(){
printf("e:%10.6e\n", 0.55);
printf("f:%10.6f\n", 0.55);
printf("g:%#10.6g\n", 0.55);
return 0;
}
结尾的 0
就会出现了:
e:5.500000e-01
f: 0.550000
g: 0.550000
以字符呈现整数的格式代码——c
这个格式会将对应的参数先转型为 unsigned char
,再显示对应的字符:
#include <stdio.h>
int main(){
printf("%c\n", 65);
printf("%c\n", 'A');
return 0;
}
不论传入整数或是字符,都可以正常显示:
A
A
显示指针的格式代码——p
如果需要打印变量的地址,那 p
格式就非常好用:
#include <stdio.h>
int main(){
int a = 20;
printf("%p\n", &a);
return 0;
}
会以 16 进位格式打印地址:
0X000000000061FE1C
显示目前转换字符数的格式代码——n
如果想知道已经处理了多少字符,可以使用 n
格式。与之前说明过的格式代码不同,对应的参数必须是指针,它会将字符数放入所指向的地址:
#include <stdio.h>
int main(){
int numOfChars;
printf("1234567890\n");
printf("%5d :%n\n", 10, &numOfChars);
printf("%5d\n", numOfChars);
return 0;
}
结果如下,n
不会打印任何字符:
1234567890
10 :
7
由于到 ':'
共 7 个字符,因此会将 7
写入 numOfChars
变量内。
在 Arduino 中使用 printf()
如果你想在 Arduino 中使用 printf()
,会发现在串口监视器窗口中看不到任何输出,这是因为 printf()
是输出到 stdout
,而不是串口。
使用 sprintf()
要将 stdout
设置成串口比较费工,我们可以改用 sprintf()
先将格式化输出的结果放置在自定义的缓冲区中,再使用 Serial.println()
送至串口即可:
void setup() {
// put your setup code here, to run once:
char buf[40];
Serial.begin(9600);
sprintf(buf, "%10d, %06.3f", 20, 3.14159);
Serial.println(buf);
}
void loop() {
// put your main code here, to run repeatedly:
}
串口监视器窗口看到的结果如下:
20, ?
咦,浮点数的结果怎么变问号了?这是因为 Arduino UNO 的 AVR 芯片工具链默认链接的是精简版库,为了减少程序代码的大小,所以并不支持格式代码 f/F
。
让 sprintf() 支持完整的格式
只要加上必要的编译器选项,就可以链接支持浮点数格式的库:
compiler.c.elf.extra_flags=-Wl,-u,vfprintf -lprintf_flt -lm
你可以加在 Arduino 安装文件夹下 \hardware\arduino\avr
路径的 platform.txt
文件中,也可以在同路径下新增 platform.local.txt
文件,并在此文件中加入上述编译器选项,后者的好处是可以将自定义的选项独立出来,不会跟默认的选项混在一起。重新编译后就可以看到正确的结果了:
20, 03.142
不过这样的功能是要付出代价的,不支持浮点数格式时代码大小如下:
草稿代码使用了 3028 bytes (9%) 的程序存储空间。上限为 32256 bytes。
全局变量使用了 200 bytes (9%) 的动态内存,剩余 1848 bytes 给局部变量。上限为 2048 bytes 。
支持浮点数格式后的代码大小为:
草稿代码使用了 4500 bytes (13%) 的程序存储空间。上限为 32256 bytes。
全局变量使用了 200 bytes (9%) 的动态内存,剩余 1848 bytes 给局部变量。上限为 2048 bytes 。
程序代码足足多了近 1.5KB。
使用 dtostrf()/dtostre()
如果刚刚那 1.5K 一定要省,可以改用 dtostrf()/dtostre()
来实现 f/e
格式的功能,例如:
void setup() {
// put your setup code here, to run once:
char buf[40];
Serial.begin(9600);
dtostrf(3.14159, 6, 3, buf);
Serial.println(buf);
dtostre(3.14159, buf, 3, DTOSTR_PLUS_SIGN);
Serial.println(buf);
}
void loop() {
// put your main code here, to run repeatedly:
}
结果如下:
3.142
+3.142e+00
这两个函数的说明可以在 AVR 的参考网页找到,你也可以在 Arduino 安装文件夹下 hardware\tools\avr\avr\include
的 stdlib.h
文件中找到,不过要注意的是 Arduino 给 dtostre()
的旗标定义名称是 DTOSTR_XXXX
,不是 AVR 网页中的 DTOSTRE_XXXX
,不要弄错。使用这两个函数的代码大小为:
草稿代码使用了 3412 bytes (10%) 的程序存储空间。上限为 32256 bytes。
全局变量使用了 188 bytes (9%) 的动态内存,剩余 1860 bytes 给局部变量。上限为 2048 bytes 。
可以看到少了 1KB 了。
标签:main,return,int,printf,字符串,格式,include From: https://blog.csdn.net/mzgxinhua/article/details/139393560