在C++代码中应该使用哪个C I/O库?

35

在新的C++代码中,我倾向于使用C++ iostream库而不是C stdio库。

我注意到一些程序员似乎坚持使用stdio,认为它更方便移植。

这是真的吗?哪种更好使用呢?

13个回答

40
为了回答原始问题:
使用iostream库可以完成使用stdio完成的任何操作。
Disadvantages of iostreams: verbose
Advantages    of iostreams: easy to extend for new non POD types.

C++相对于C语言的进步之一是类型安全。
iostreams旨在显式地保证类型安全。因此,对象的赋值会在编译时显式地检查被分配的对象的类型(如果需要,会生成编译时错误)。这样可以防止运行时内存溢出或将浮点值写入char对象等问题。
scanf()/printf()及其相关函数则依赖于程序员正确设置格式字符串,并且没有类型检查(我相信gcc有一个扩展来帮助)。结果导致了许多错误(因为程序员的分析不如编译器完美[不会说编译器完美只是比人类更好])。
仅澄清Colin Jensen的评论。
自最后一个标准发布以来,iostream库已经稳定(我忘记实际年份,但大约是10年前)。
澄清Mikael Jansson的评论。
他提到的其他使用格式样式的语言都具有明确的保护措施,以防止C标准I/O库的危险副作用,这可能会(在C中但不是在上述语言中)导致运行时崩溃。 注意:我同意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;
}

1
以下代码容易崩溃:'char s[2];scanf("%s",s);' iostream 在编译时进行类型检查,因此问题在编译时而不是运行时被发现。 - Martin York
7
iostreams 的另一个好处是,它们可以与任何派生自 std::iostream 的东西一起使用。因此,接受流的函数可以同样适用于控制台、文件或字符串输入。 - rlbond
1
@rlbond - fprintf 可以在任何 FILE 对象上工作,这个对象可以从任何文件描述符创建。因此,它也同样适用于各种输入/输出机制:任何可以实现为文件描述符的东西。 - Tom
iostreams 的另一个主要缺点是 i18n。 - Deduplicator
@Deduplicator:您是指不同语言中的单词顺序。 - Martin York
显示剩余2条评论

17

它太啰嗦了。

考虑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是令人误导的,充其量也只能算是一种错误的做法。


12
我真的想不到任何一种语言“窃取”了 iostreams,这可能意味着它确实是每个人都认为的那样糟糕的一个想法。 :) - Brad Wilson
4
使用g++编译时添加-Wformat参数,就能实现对printf等函数的参数类型检查。 - richq
3
我很确定Java中的Streams受到iostreams的启发,至少在它们可插拔的特性方面是这样。 - Skurmedel
19
你没有正确使用iostreams。你应该重载你的统计数据类型的operator<<,这样客户端就可以像这样使用它:std::cout << stats;。这比使用printf(...)或自定义打印函数要好得多,后者需要接受一个文件句柄作为参数。 - Sebastian Mach
4
即使你超载了 << 运算符,你仍需要在函数体中编写这个庞然大物。 - Calmarius
显示剩余4条评论

14

Boost Format Library提供了一种类型安全、面向对象的printf风格字符串格式化替代方案,它是iostreams的补充,由于巧妙地使用了operator%,不会遭受通常由于冗长而产生的问题。如果您不喜欢使用iostream的operator<<进行格式化,请考虑使用它而不是使用普通的C printf。


9

回到不好的时代,C++标准委员会一直在修改语言,iostreams也是一个不断变化的目标。如果你使用iostreams,你就需要每年或每隔一段时间重写你的代码的部分。因此,我总是使用自1989年以来没有显著变化的stdio。

如果我今天要做东西,我会使用iostreams。


7

如果和我的情况一样,在学习 C++ 之前是学习了 C 的,那么使用 stdio 库可能会更加自然。使用 iostream 和 stdio 都有其优缺点,但是我在使用 iostream 时确实会感到遗憾不能使用 printf()。


5

原则上我会使用iostreams,在实践中,我也用到了太多格式化小数等内容,使得iostreams难以阅读,所以我使用stdio。Boost::format是一种改进,但对我来说还不够令人满意。在实践中,由于大多数现代编译器都进行参数检查,因此stdio几乎是类型安全的。

这是一个领域,我仍然对任何解决方案都不完全满意。


关于编译器和类型安全的问题,我同意+1。如果没有良好的编译器支持(可辨识的模板错误),谁也不会使用C++,因此将C编译器提高到同样的标准似乎是合理的。 - Tom
您可能考虑创建一个IO操纵器,以您想要的方式进行格式化,而不影响流。 - EvilTeach

5
我将比较C++标准库中的两个主流库。在C++中,不应使用基于C风格格式化字符串的字符串处理例程,因为有以下几个原因需要限制它们的使用:
- 不是类型安全的。 - 无法将非POD类型传递给可变参数列表(即既不能传递给scanf等函数,也不能传递给printf等函数),否则您将进入未定义行为的黑暗堡垒。 - 易错:
- 必须始终保持格式字符串和“值参数列表”同步。 - 必须正确同步。
此外,使用printf本身并不好。软件会变老并进行重构和修改,可能会从远程地方引入错误。假设您有...
// foo.h
...
float foo;
...

而在某些地方...

// bar/frob/42/icetea.cpp
...
scanf ("%f", &foo);
...

三年后,您会发现 foo 应该是某个自定义类型的实例。
// foo.h
...
FixedPoint foo;
...

but somewhere ...

// bar/frob/42/icetea.cpp
...
scanf ("%f", &foo);
...

如果你仍然使用旧的printf/scanf,那么它们还是可以编译通过的,但你可能会遇到随机的段错误,而你不记得为什么会出现这种情况。

iostream的冗长性

如果你认为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';

您需要定义适当的重载运算符<<,其大致结构类似于printf函数,但显著的区别是您现在拥有可重用和类型安全的东西;当然,您也可以为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实现提供了位置参数,但它们是非标准的。

我应该从不使用C风格的格式化字符串吗?

除了性能(正如Jan Hudec所指出的那样),我没有看到任何理由。但请记住:

“我们应该忘记小效率,大约97%的时间:过早优化是万恶之源。然而,在关键的3%中,我们不应该放弃机会。一个好的程序员不会被这种推理所冷落,他将明智地仔细查看关键代码;但只有在确定了该代码之后。”- Knuth

“瓶颈出现在令人惊讶的地方,所以不要试图预测并放入速度优化,直到你已经证明那是瓶颈所在的地方。”- Pike

是的,printf实现通常比iostreams快,通常比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);

4

对于二进制IO,我倾向于使用stdio的fread和fwrite。对于格式化的内容,我通常会使用IO流,尽管正如Mikael所说,非平凡(非默认?)格式化可能会很麻烦。


你也可以使用 istream::read() 和 ostream::write(),它们执行未格式化 I/O。如果您需要同时执行格式化和未格式化的 I/O,则非常有用。 - Tom

3

虽然C++ iostreams API有很多好处,但一个显著的问题是它在i18n方面存在问题。问题在于参数替换的顺序可能会因文化差异而有所不同。经典例子如下:

// i18n UNSAFE 
std::cout << "Dear " << name.given << ' ' << name.family << std::endl;

虽然在英语中这种方法可行,但在汉语中姓氏排在名字前面。

当涉及到将您的代码翻译成外国市场时,翻译片段充满了风险,因此新的本地化可能需要对代码进行更改,而不仅仅是不同的字符串。

boost::format似乎结合了stdio(一个可以使用不同顺序的参数的单个格式字符串)和iostreams(类型安全性,可扩展性)的优点。


3
这实际上并不是 C++ 流的缺点,而是使用它们的代码的问题。任何 I/O 库都会有同样的问题。更好的解决方法是使用一个 name.formal_name() 方法,在基于 name.culture(例如)返回正确的结果。 - paxdiablo
@paxdiablo - 不,许多I/O库可以通过无序插值其参数来解决这个问题(包括boost::format和现代风格的stdio)。虽然我的原始示例可能做得更好,但请考虑这样一个问题:cout << student_name << " has a GPA of " << gpa; - R Samuel Klatchko
1
这也是C风格格式字符串的一个缺点,它们没有位置参数。 - Sebastian Mach
将本地化混合到C++流中是一个很大的错误。 - Lothar

2

我想念iolibraries的是格式化输入。

iostreams没有很好的方法来复制scanf(),即使使用boost也没有所需的输入扩展。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接