在新的 C++ 代码中,我倾向于使用 C++ iostream 库而不是 C stdio 库。
我注意到一些程序员似乎坚持使用 stdio,坚持认为它更便携。
真的是这样吗?用什么比较好?
回答最初的问题:
可以使用 stdio 完成的任何事情都可以使用 iostream 库完成。
Disadvantages of iostreams: verbose
Advantages of iostreams: easy to extend for new non POD types.
C++ 在 C 上的进步是类型安全。
iostreams 被设计为明确的类型安全。因此,对对象的赋值也显式检查了被赋值对象的类型(在编译器时)(如果需要,生成编译时错误)。从而防止运行时内存溢出或将浮点值写入 char 对象等。
另一方面,scanf()/printf() 和 family 依赖于程序员获取正确的格式字符串并且没有类型检查(我相信 gcc 有一个可以帮助的扩展)。结果,它是许多错误的根源(因为程序员的分析不如编译器完美[并不是说编译器比人类更完美])。
只是为了澄清 Colin Jensen 的评论。
澄清 Mikael Jansson 的评论。
注意我同意 iostream 库有点冗长。但我愿意忍受冗长以确保运行时安全。但是我们可以通过使用Boost Format Library来减轻冗长。
#include <iostream>
#include <iomanip>
#include <boost/format.hpp>
struct X
{ // this structure reverse engineered from
// example provided by 'Mikael Jansson' in order to make this a running example
char* name;
double mean;
int sample_count;
};
int main()
{
X stats[] = {{"Plop",5.6,2}};
// nonsense output, just to exemplify
// stdio version
fprintf(stderr, "at %p/%s: mean value %.3f of %4d samples\n",
stats, stats->name, stats->mean, stats->sample_count);
// iostream
std::cerr << "at " << (void*)stats << "/" << stats->name
<< ": mean value " << std::fixed << std::setprecision(3) << stats->mean
<< " of " << std::setw(4) << std::setfill(' ') << stats->sample_count
<< " samples\n";
// iostream with boost::format
std::cerr << boost::format("at %p/%s: mean value %.3f of %4d samples\n")
% stats % stats->name % stats->mean % stats->sample_count;
}
这太冗长了。
考虑执行以下操作的 iostream 构造(与 scanf 类似):
// nonsense output, just to examplify
fprintf(stderr, "at %p/%s: mean value %.3f of %4d samples\n",
stats, stats->name, stats->mean, stats->sample_count);
这将需要类似的东西:
std::cerr << "at " << static_cast<void*>(stats) << "/" << stats->name
<< ": mean value " << std::precision(3) << stats->mean
<< " of " << std::width(4) << std::fill(' ') << stats->sample_count
<< " samples " << std::endl;
字符串格式化是一种可以而且应该回避面向对象的情况,转而支持嵌入在字符串中的格式化 DSL。考虑 Lisp 的format
、Python 的 printf 样式格式,或 PHP、Bash、Perl、Ruby 和它们的字符串内插。
iostream
因为那个用例充其量是被误导的。
Boost 格式库为 printf 样式的字符串格式提供了一个类型安全、面向对象的替代方案,并且是对 iostreams 的补充,它不会因巧妙地使用 operator% 而遭受通常的冗长问题。如果您不喜欢使用 iostream 的 operator<< 进行格式化,我建议您考虑使用普通 C printf。
回到糟糕的过去,C++ 标准委员会一直在讨论这种语言,而 iostreams 是一个不断变化的目标。如果您使用 iostreams,那么您就有机会每年左右重写部分代码。正因为如此,我一直使用自 1989 年以来没有显着变化的 stdio。
如果我今天做事,我会使用 iostreams。
如果像我一样,您在学习 C++ 之前学习了 C,那么 stdio 库似乎更易于使用。iostream 与 stdio 各有利弊,但在使用 iostream 时我确实想念 printf()。
原则上我会使用 iostreams,实际上我做了太多格式化的小数等,这使得 iostreams 太不可读,所以我使用 stdio。Boost::format 是一种改进,但对我来说不够激励。实际上,stdio 几乎是类型安全的,因为大多数现代编译器都会进行参数检查。
这是一个我仍然对任何解决方案都不完全满意的领域。
对于二进制 IO,我倾向于使用 stdio 的 fread 和 fwrite。对于格式化的东西,我通常会使用 IO Stream 虽然正如 Mikael 所说,非平凡(非默认?)格式可以是 PITA。
我将比较 C++ 标准库中的两个主流库。
有几个原因可以限制它们的使用:
不仅 printf 本身不好。软件变旧并被重构和修改,并且可能从偏远的地方引入错误。假设你有
.
// foo.h
...
float foo;
...
在某处......
// bar/frob/42/icetea.cpp
...
scanf ("%f", &foo);
...
三年后,您发现 foo 应该是某种自定义类型...
// foo.h
...
FixedPoint foo;
...
但在某个地方......
// bar/frob/42/icetea.cpp
...
scanf ("%f", &foo);
...
...那么你的旧 printf/scanf 仍然可以编译,除了你现在得到随机的段错误并且你不记得为什么。
如果您认为 printf() 不那么冗长,那么您有一定的可能性不会使用他们的 iostream 的全部力量。例子:
printf ("My Matrix: %f %f %f %f\n"
" %f %f %f %f\n"
" %f %f %f %f\n"
" %f %f %f %f\n",
mat(0,0), mat(0,1), mat(0,2), mat(0,3),
mat(1,0), mat(1,1), mat(1,2), mat(1,3),
mat(2,0), mat(2,1), mat(2,2), mat(2,3),
mat(3,0), mat(3,1), mat(3,2), mat(3,3));
将其与正确使用 iostreams 进行比较:
cout << mat << '\n';
您必须为 operator<< 定义一个适当的重载,它大致具有 printf-thingy 的结构,但显着的区别是您现在有了可重用和类型安全的东西;当然,您也可以为类似 printf 的内容制作一些可重复使用的东西,但随后您又拥有 printf(如果您用新的替换矩阵成员FixedPoint
怎么办?),除了其他不重要的事情,例如您必须传递 FILE* 句柄.
请注意,格式字符串通常被认为是国际化的救援,但在这方面它们并不比 iostream 更好:
printf ("Guten Morgen, Sie sind %f Meter groß und haben %d Kinder",
someFloat, someInt);
printf ("Good morning, you have %d children and your height is %f meters",
someFloat, someInt); // Note: Position changed.
// ^^ not the best example, but different languages have generally different
// order of "variables"
即,旧式 C 格式字符串与 iostreams 一样缺少位置信息。
您可能需要考虑boost::format,它支持在格式字符串中明确说明位置。从他们的示例部分:
cout << format("%1% %2% %3% %2% %1% \n") % "11" % "22" % "333"; // 'simple' style.
一些 printf 实现提供了位置参数,但它们是非标准的。
除了性能(正如 Jan Hudec 指出的那样),我看不出有什么原因。但请记住:
“我们应该忘记小的效率,比如大约 97% 的时间:过早优化是万恶之源。然而,我们不应该放弃那关键的 3% 的机会。一个好的程序员不会因为这样的推理而自满,他会明智地仔细查看关键代码;但只有在识别出该代码之后” - Knuth
和
“瓶颈出现在令人惊讶的地方,所以在你证明瓶颈在哪里之前,不要试图猜测并进行速度破解。” - 派克
是的,printf 实现通常比 iostream 快,通常比 boost::format 快(来自我写的一个小而具体的基准测试,但它应该在很大程度上取决于具体情况:如果 printf=100%,那么 iostream=160% , 和 boost::format=220%)
但不要盲目地忽略思考:你真正花多少时间在文本处理上?您的程序在退出之前运行了多长时间?回退到 C 风格的格式字符串、松散的类型安全性、降低可重构性、增加可能隐藏多年并且可能只会在您最喜欢的客户面前显露出来的非常微妙的错误的可能性是否完全相关?
就个人而言,如果我不能获得超过 20% 的加速,我不会退缩。但是因为我的应用程序几乎把所有时间都花在了字符串处理之外的其他任务上,所以我从来不需要这样做。我编写的一些解析器几乎将所有时间都花在了字符串处理上,但它们的总运行时间太小了,不值得进行测试和验证工作。
最后,我想预设一些谜语:
查找所有错误,因为编译器不会(他只能建议他是否很好):
shared_ptr<float> f(new float);
fscanf (stdout, "%u %s %f", f)
如果没有别的,这个有什么问题?
const char *output = "in total, the thing is 50%"
"feature complete";
printf (output);
虽然 C++ iostreams API 有很多好处,但一个重要的问题是 i18n。问题是参数替换的顺序可能因文化而异。经典的例子是这样的:
// i18n UNSAFE
std::cout << "Dear " << name.given << ' ' << name.family << std::endl;
虽然这适用于英语,但在中文中,姓氏是第一位的。
在为国外市场翻译代码时,翻译代码片段充满了危险,因此新的 l10ns 可能需要更改代码而不仅仅是不同的字符串。
boost::format 似乎结合了 stdio(一个可以以不同顺序使用参数然后它们出现的顺序的单个格式字符串)和 iostreams(类型安全、可扩展性)的优点。
我使用 iostreams,主要是因为这使得以后更容易摆弄流(如果我需要的话)。例如,您可能会发现您想在某个跟踪窗口中显示输出——使用 cout 和 cerr 相对容易做到这一点。当然,您可以在 unix 上摆弄管道和其他东西,但这不是那么便携。
我确实喜欢类似 printf 的格式,所以我通常先格式化一个字符串,然后将它发送到缓冲区。对于 Qt,我经常使用QString::sprintf(尽管他们建议使用QString::arg代替)。我也看过boost.format,但不能真正习惯语法(太多的 %'s)。不过,我真的应该看看。
我对 iolibraries 怀念的是格式化的输入。
iostreams 没有复制 scanf() 的好方法,甚至 boost 也没有输入所需的扩展名。
stdio 更适合读取二进制文件(例如将块读取到 vector<unsigned char> 并使用 .resize() 等)。有关示例,请参见http://nuwen.net/libnuwen.html中 file.hh 中的 read_rest 函数。
读取二进制文件时,C++ 流可能会阻塞大量字节,从而导致错误的 eof。
由于 iostreams 已经成为一种标准,因此您应该知道您的代码肯定可以与较新版本的编译器一起使用。我想现在大多数编译器都非常了解 iostreams,使用它们应该没有任何问题。
但如果你想坚持使用 *printf 函数,我认为没有问题。