C++早期的格式化输出,是没用C语言的格式化输出:printf(),首先是不推荐的,历史车轮滚滚向前,我们不能一直停留在旧石器时期啊。其优缺点也比较明显,缺点是没有类型保护,也不支持自定义类型;优点是易于阅读,因为它将格式与变量分隔的比较清晰,因此也易于转化成其他编程语言,还是举一个例子吧,空口白牙大家印象不深:
int x = 10, y = 11;
printf("x has value %d and y has value %d.\n", x, y);
结果为:
x has value 10 and y has value 11.
这些都与C完全一致,随着C++语言的发展,它需要有自己的独特的字符串输出方式,这就诞生了cout方式,这种I/O流的方式解决了C语言的类型保护以及对扩展类型的支持,但其缺点也比较明显,那就是字符串与变量交织在一起,不易迁移转化成其他编程语言。
还是上面的例子,把输出方式修改一下:
int x = 10, y = 11;
cout << "x has value " << x << " and y has value " << y << '.' << endl;
输出结果与上面的输出结果完全一致,就不再贴了。
好,C++继续演进,到了C++20,出现了定义在
int x = 10, y = 11;
cout << format("x has value {} and y has value {}.", x, y) << endl;
输出结果与上面的输出结果也完全一致,就不再贴了。
这个嘛,好像与我们以前写的代码也不完全一样啊,对喽,我的笔记用的是C++23,到了C++23中,发展演进到了出现了std::print()与std::println(),我们以前输出用的都是println(),好了,再把上面代码中的cout与<<修改成println()格式,当然,也不用format了:
int x = 10, y = 11;
println("x has value {} and y has value {}.", x, y);
结果自然是一致的,就不多说了吧。
既然已经发展演进到现在了,是不是应该好好夸一夸这种格式化输出的好处了呢,那当然了,当仁不让啊,这种输出方式的优点可多了,且听我娓娓道来:首先是支持UTF-8的编码方式,这就解决了不同编码字符集的问题,省去了许多麻烦事儿;类型安全,支持扩展到用户自定义类型,易于阅读,支持Unicode输出(无国界),支持不同国别语言的本地化,等等等等吧,在这些好处的顶端,就是其效率要比直接使用cout的I/O流要高,当然了,其低层也使用了这些I/O流,毕竟是一步上步发展演化而来的,是站在巨人的肩膀上,前人种树,后人乘凉,怎么能不利用前人好的成果呢,只是要百尺竿头更进一步而已。
记得在前面的文章中,只有一处提到了格式化字符串输出的格式问题,就是对于字符串转数字时用了一下{:X},文章中也简单解释了一下:X的意义,进行了一点小扩展。现在到了看println()格式化字符串强大力量的时刻了,让我们走起。
对于format(),print(),println(),其第一个参数就是格式化字符串,字符串中可以包含多个{}对,代表的是要被替换的部分,可以有任意多个,反正我没试过,也没有去查相关资料,到底有多少个,我认为探究这个没有意义;后面的参数就是填充或者说是去替换{}的内容,如果在输出中需要输出{或者},那就用转义字符的方式,用{{或}}达到相关目的。
好了,我们现在就来看{}的大魔法,其定义为[index] [:specifier],我们知道,这些都是可省略项,index为参数索引,后面系统化的论述时再解释,specifier通过前面的文章,大家也应该知道它是格式化输出相关参数的灵魂了,具体也在后面再详述,我们现在要解决的,是周边的几个小问题,也可能是大家比较关心,或者说是比较容易犯的几个小错误,直接用代码来说明吧,在代码中讲解更直观一些:
int n1{ 42 };
println(n1); //错误,格式化字符串是必输项,不能省略
println("{}", n1); //正确
println();//错误,格式化字符串是必输项,不能省略,即使目的只是为了打印一个空行,也不能省略
println(""); //正确
这一小节我们系统化的论述一下格式化字符串中的index参数索引,这个参数可以省略这一点大家都知道了,先解释一下这个参数的意思吧,这个参数是个数字,从0开始,正常的话,也就是省略时,其意义就相当于在一系列的{}中省略了从0开始的系列数字,第一个{}省略了0,第二个省略了1,第三个省略了2,以此类推,等等等等。那如果在{}中加上这个数字的话,就可以灵活输出字符串格式参数后面的输出输出的实际参数的值了,这个灵活是指,可以打乱顺序,可以多次输出同一个参数或者不输出某一个参数等等。还是通过程序示例来说明吧。
import std;
using namespace std;
int main()
{
int n{ 42 };
println("Read {} bytes from {}", n, "file1.txt");
return 0;
}
结果如下:
Read 42 bytes from file1.txt
好,这是一个省略了index的示例,与以前的代码一致,结果也是我们前面说的,第一个{}输出n的值42,第二个{}输出常量或者说文本file1.txt。
下面我们把index加上,但是要注意,要加都加,不能有的加有的没加,这是错误的,为了节省大家的时间,下面的例子会把刚才所说的几种情况都进行展示,也会在程序中加以说明,请看大屏幕:
import std;
using namespace std;
int main()
{
int n{ 42 };
println("Read {} bytes from {}", n, "file1.txt"); //正确,省略index
println("Read {0} bytes from {1}", n, "file1.txt"); //正确,与省略index时一样
//println("Read {0} bytes from {}", n, "file1.txt"); //执行时会报错,index要写都写,要不写都不写
println("从{1}中读取{0}个字节", n, "file1.txt"); //正确,可以任意调整index的位置
println("Read {0} bytes from {1},Again read {0} bytes.", n, "file1.txt");//正确,可以多次使用同一个index。
return 0;
}
看结果:
Read 42 bytes from file1.txt
Read 42 bytes from file1.txt
从file1.txt中读取42个字节
Read 42 bytes from file1.txt,Again read 42 bytes.
不难的,是吧。
我们的输出函数中,上来就是格式化字符串,其实前面还有一个参数,就是要输出的地方,省略的话,是指输出到默认的地方,一般是标准控制台,其实也可以输出到其他地方,比如标准错误输出等,我们简单修改一下上面的程序,如下:
import std;
using namespace std;
int main()
{
int n{ 42 };
println(cerr,"Read {} bytes from {}", n, "file1.txt"); //正确,省略index
println(cerr,"Read {0} bytes from {1}", n, "file1.txt"); //正确,与省略index时一样
//println("Read {0} bytes from {}", n, "file1.txt"); //执行时会报错,index要写都写,要不写都不写
println("从{1}中读取{0}个字节", n, "file1.txt"); //正确,可以任意调整index的位置
println("Read {0} bytes from {1},Again read {0} bytes.", n, "file1.txt");//正确,可以多次使用同一个index。
return 0;
}
结果与上面一样,这是因为我的开发环境的标准输出与标准错误输出是一样的,本示例只是说明,输出目的地也是可以根据实际需要进行指定的。
对于format()、print()、println()函数,其参数格式化字符串如果有错误的话,编译时报错,因为这个参数要求是常量字符串,如果定义一个正常的字符串并进行输出的话,肯定会出错的,示例:
import std;
using namespace std;
int main()
{
string s{ "Hello World!" };
println(s);//
return 0;
}
会报错:error C7595: “std::basic_format_string
其实,这个已经不是格式化字符串的错误了,它就是一个正常的字符串的输出,正确的代码应该是这样:
import std;
using namespace std;
int main()
{
string s{ "Hello World!" };
//println(s);
println("{}", s);
return 0;
}
那么好,回过头来,根据报错信息的提示,它说不是常量,那就改成常量,再看一下结果:
import std;
using namespace std;
int main()
{
constexpr auto s{ "Hello World!" };
println(s);
constexpr auto formatString{ "Value {}" };
println(formatString, 11);
return 0;
}
结果:
Hello World!
Value 11
从结果可以看出,第一行其实是打印了一个没有参数的格式化字符串,第二行打印了一个拥有一个参数的格式化字符串,与上一个例子要输出常量字符串的含义是不一样的。当然了,这里面有点绕,大家下点功夫,好好理解一下,我觉得该说的我都说清楚了,祝大家学习愉快。