我应该在我的C++代码中使用printf吗?

77

我通常使用coutcerr向控制台输出文本。然而,有时我发现使用老式的printf语句更容易。当我需要格式化输出时,我就会使用它。

以下是我使用这种方法的一个例子:

// Lets assume that I'm printing coordinates... 
printf("(%d,%d)\n", x, y);

// To do the same thing as above using cout....
cout << "(" << x << "," << y << ")" << endl;

我知道可以使用cout格式化输出,但我已经知道如何使用printf。有什么理由不能使用printf语句吗?


3
对于控制台I/O,“es macht nichts”(没有关系)。就大局而言,“printf”与C++流不兼容。C++流允许您轻松地将控制台输出转换为文件。(虽然您可以使用“fprintf”实现类似的功能)。 - Thomas Matthews
1
使用 sprintf + cout 怎么样? - Larry Watanabe
14
请注意,您的两行代码并不严格等价。endl也会刷新流,就像您编写了printf("(%d,%d)\n", x, y); fflush(stdout);一样。如果在循环中重复执行此操作,可能会导致大量的性能损耗。要在C ++中获得类似于printf语句的真正等效语句,您应该编写cout << "(" << x << "," << y << ")\n"; - Didier Trosset
2
@bobobobo,“Note”的打字错误。 - strager
可能是['printf' vs. 'cout' in C++]的重复问题 (https://dev59.com/kHE85IYBdhLWcg3wPBB8) - Konrad Borowski
20个回答

77

我的学生们先学习cincout,然后再学习printf,但是他们大多数都更喜欢使用printf(通常是fprintf)。我本人觉得printf模型足够易读,所以我把它移植到其他编程语言中。Olivier Danvy也这么做了,他甚至让它变成了类型安全的。

只要你有一个能够检查printf调用类型的编译器,我认为在C++中使用fprintf等函数是没有问题的。

免责声明:我是个糟糕的C++程序员。


17
我经常使用Java的String.format。在C++中,我经常使用Boost.Format,它既友好于iostreams,又有点类似于printf - C. K. Young
5
*printf的类型不安全性虽然可以通过编译器的检查来缓解,但无法完全消除,因为将变量用作格式字符串是一种完全有效的用例,例如国际化(i18n)。这个函数可能会出现很多问题,甚至不好笑。我已经不再使用它了。我们有很好的格式化工具,比如boost::format或Qt::arg。 - rpg
2
@Norman Ramsey:我看到多次提到printf()的“类型安全问题”。那么,printf()的类型安全问题具体是什么? - Lazer
@Norman Ramsey:我不确定如果类型未经检查会出现什么问题。你能引用一个例子吗?我经常在C中使用printf(),从未遇到过问题。 - Lazer
@Lazer:缓冲区溢出、未定义行为、核心转储、偏移一错误、安全漏洞。你可能想在维基百科上查找格式字符串攻击。 - Sebastian Mach
显示剩余2条评论

49

如果你希望国际化你的程序,请远离iostreams。问题在于,如果句子由多个片段组成,就像iostream所做的那样,正确本地化字符串可能是不可能的。

除了消息片段的问题,你还面临着排序问题。考虑一个打印学生姓名和平均绩点的报告:

std::cout << name << " has a GPA of " << gpa << std::endl;

如果你需要将文本翻译成其他语言,那么该语言的语法可能要求你在名字之前显示GPA。 据我所知,iostreams无法重新排列插入值。

如果你想兼顾类型安全性和国际化能力,请使用Boost.Format


5
在boost::format()中通过位置指定格式参数的能力非常适合本地化。 - Ferruccio
11
但是boost::format可以提供类型安全等好处。printf则会出现问题。 - jalf
2
不错的回答。只想补充一下,除了 Boost Format 之外,还有一些替代方案,速度快几倍,同时提供相同级别的安全性:https://github.com/vitaut/format 和 https://github.com/c42f/tinyformat。 - vitaut

21

适应性

任何尝试对非平凡数据类型进行 printf 的操作都会导致未定义的行为:

struct Foo { 
    virtual ~Foo() {}
    operator float() const { return 0.f; }
};

printf ("%f", Foo());

std::string foo;
printf ("%s", foo);

上述printf调用会导致未定义的行为。虽然您的编译器可能会发出警告,但这些警告不是标准所要求的,而且对于仅在运行时已知的格式字符串是不可能发出警告的。
IO-Streams:
std::cout << Foo();
std::string foo;
std::cout << foo;

请自行判断。

可扩展性

struct Person {
    string first_name;
    string second_name;
};
std::ostream& operator<< (std::ostream &os, Person const& p) {
    return os << p.first_name << ", " << p.second_name;
}

cout << p;
cout << p;
some_file << p;

C:

// inline everywhere
printf ("%s, %s", p.first_name, p.second_name);
printf ("%s, %s", p.first_name, p.second_name);
fprintf (some_file, "%s, %s", p.first_name, p.second_name);

或者:

// re-usable (not common in my experience)
int person_fprint(FILE *f, const Person *p) {
    return fprintf(f, "%s, %s", p->first_name, p->second_name);
}
int person_print(const Person *p) {
    return person_fprint(stdout, p);
}

Person p;
....
person_print(&p);

请注意在C语言中使用正确的调用参数/签名(例如:person_fprint(stderr, ...person_fprint(myfile, ...),而在C ++中,“FILE”参数会自动从表达式“派生”出来。更精确等效于此推导的示例实际上更像是这样的:
FILE *fout = stdout;
...
fprintf(fout, "Hello World!\n");
person_fprint(fout, ...);
fprintf(fout, "\n");

国际化

我们重用了我们的Person定义:

cout << boost::format("Hello %1%") % p;
cout << boost::format("Na %1%, sei gegrüßt!") % p;

printf ("Hello %1$s, %2$s", p.first_name.c_str(), p.second_name.c_str()); 
printf ("Na %1$s, %2$s, sei gegrüßt!", 
        p.first_name.c_str(), p.second_name.c_str()); 

请自行判断。

我认为这个问题在今天(2017年)已经不那么重要了。也许只是一种直觉,但是I18N并不是普通的C或C++程序员每天都会做的事情。此外,它仍然是一件麻烦的事情。

性能

  1. 你有测量过printf性能的实际意义吗?你的瓶颈应用程序真的很懒惰,计算结果的输出是一个瓶颈吗?你确定你需要使用C++吗?
  2. 可怕的性能惩罚是为了满足那些想要使用printf和cout混合的人而设计的。这是一个特性,而不是缺陷!

如果您始终使用iostreams,则可以

std::ios::sync_with_stdio(false);

使用好的编译器,可以获得相等的运行时间:

#include <cstdio>
#include <iostream>
#include <ctime>
#include <fstream>

void ios_test (int n) {
    for (int i=0; i<n; ++i) {
        std::cout << "foobarfrob" << i;
    }
}

void c_test (int n) {
    for (int i=0; i<n; ++i) {
        printf ("foobarfrob%d", i);
    }
}


int main () {
    const clock_t a_start = clock();
    ios_test (10024*1024);
    const double a = (clock() - a_start) / double(CLOCKS_PER_SEC);

    const clock_t p_start = clock();
    c_test (10024*1024);
    const double p = (clock() - p_start) / double(CLOCKS_PER_SEC);

    std::ios::sync_with_stdio(false);
    const clock_t b_start = clock();
    ios_test (10024*1024);
    const double b = (clock() - b_start) / double(CLOCKS_PER_SEC);


    std::ofstream res ("RESULTS");
    res << "C ..............: " << p << " sec\n"
        << "C++, sync with C: " << a << " sec\n"
        << "C++, non-sync ..: " << b << " sec\n";
}

结果 (g++ -O3 synced-unsynced-printf.cc, ./a.out > /dev/null, cat RESULTS):

C ..............: 1.1 sec
C++, sync with C: 1.76 sec
C++, non-sync ..: 1.01 sec

请自行判断...

不,你不能禁止我使用printf。

在C++11中,由于可变参数模板,您可以拥有一个类型安全、I18N友好的printf。并且通过使用用户定义的字面量,您将能够使它们非常高效,即可以编写完全静态的实现。

我有一个概念证明。当时对C++11的支持还不如现在成熟,但您可以得到一个想法。

时间适应性

// foo.h
...
struct Frob {
    unsigned int x;
};
...

// alpha.cpp
... printf ("%u", frob.x); ...

// bravo.cpp
... printf ("%u", frob.x); ...

// charlie.cpp
... printf ("%u", frob.x); ...

// delta.cpp
... printf ("%u", frob.x); ...

后来,你的数据变得非常庞大,你必须采取措施。
// foo.h
...
    unsigned long long x;
...

这是一个有趣的练习,要保持它没有漏洞。特别是当其他非耦合项目使用foo.h时。

其他。

  • 漏洞潜在性:使用printf时有很多出错空间,特别是当您将用户输入的字符串混合在一起时(考虑I18N团队)。您必须小心地正确转义每个格式字符串,确保传递正确的参数等。

  • IO-Streams会使我的二进制文件变大:如果这比可维护性、代码质量、可重用性更重要,那么(在验证问题后!)请使用printf。


2
现代编译器可以诊断格式规范和参数之间的不匹配。 - vitaut
1
谢谢 - 这是一个非常好的解释!@vitaut 一般来说是不行的。例如,如果格式规范本身不是一个常量字符串文字,而是来自配置文件。这不是一个假设性的例子,在大多数商业软件中,为了18n和维护通信的一致性,大多数字符串都不是原地字面值而是分隔开来的,并不总是作为字面值。 - necromancer
1
@agksmehx:非常好的例子,反映了格式字符串检查的可靠性。 - Sebastian Mach

21

我使用printf,因为我讨厌丑陋的<<cout<<语法。


3
有更好的理由吗?:P - Matt Joiner

19

使用boost::format。你将获得类型安全性,std::string支持,类似printf的接口,能够使用cout以及许多其他好东西。你不会再回去了。


9
使用printf,不要使用C++流。printf能够提供更好的控制(例如浮点精度等)。代码也通常更短更易读。 Google C++风格指南 也认同这一点。
引用块中写道:
除了日志接口需要之外,不要使用流。请使用类似于printf的例程。
使用流有各种利弊,但在这种情况下,与许多其他情况一样,一致性胜过争论。不要在代码中使用流。

1
+1 给这个链接。虽然它不是《十诫》,但他们确实很明智。 - ojrac
2
在我看来,虽然 Google C++ 风格指南在很多方面都非常好,但他们所提到的一致性主要是他们自己代码的一致性。请记住,Google 已经存在了 10 年,他们重视代码一致性(这是一件非常好的事情)。他们不使用 printf 的原因是因为人们在之前版本的代码中使用了它,而他们希望保持一致性。如果不是这种情况,我相信他们会使用流。 - Geoff
1
什么?!你也可以使用iostreams进行精确控制。 - Sebastian Mach

7

没有任何理由。我认为只是一些奇怪的意识形态驱使人们仅使用C++库,尽管好用的老式C库仍然有效。我是一个C ++ 程序员,并且经常使用C函数,从来没有遇到过任何问题。


4
C语言的库非常高质量且易于使用。我同意人们不应该感到被迫使用C++库。 - Matt Joiner
1
我也从未遇到过我的锤子有问题。它简单而坚固,我知道在用它建房子时会得到什么样的效果。 - Sebastian Mach
@mingos:我更喜欢用水泥、螺丝和螺栓固定的房子(但好的锤子也可以)。用锤子把砖头搬到三楼,为屋顶锯木梁有点难,但它确实有效。我有时也会使用C函数,特别是在与其他软件交互时,但在很大程度上,我更喜欢编译器通过强大的静态类型安全性来捕捉错误。这样,如果我在某个地方更改了一些代码,并进行完整的重新编译,我就会得到到处都是的错误提示,我可以依赖它们(但这只是我的理由冰山一角)。 - Sebastian Mach
@mingos:没错。但更严肃地说:你写了“没有任何理由”,但实际上确实有很多理由反对它(我唯一发现printf更好的是节省字节)。而且虽然你可能知道如何正确使用它,但许多人不知道(我记得像光栅化器中出现闪烁的三角形之类的不感激的调试会话,偶然发现一个误用的printf是问题所在,在完全远离的地方,甚至不在同一个应用程序代码中)。 - Sebastian Mach
1
滥用函数也不是完全不使用它的理由 :). 我并不主张使用 printf 而不是 cout(虽然其他人可能会 - 在这个问题的另一个答案中有链接),我同意有些情况下 cout 明显更适合。然而,我坚信仅仅因为“这是 C++ 的方式”而使用 cout 而不是 printf 不过是一种宗教迷信罢了。 - mingos
显示剩余2条评论

6

总的来说,我同意(尤其是如果你需要复杂格式,讨厌 << 语法)

但我应该指出安全方面的问题。

printf("%x",2.0f)
printf("%x %x",2)
printf("%x",2,2)

这可能不会被编译器注意到,但可能会导致你的应用崩溃。


2
同意这个观点。虽然可能有些烦人,但尽可能地使用编译器的限制来编译代码总是一个好主意。启用所有警告,将所有警告视为错误,并且如果可能的话,尽可能经常地使用电子围栏+ gdb。但这只是一个一般的编码提示 ;) - mingos
有趣的是,xCode IDE在其智能感知中检测到了您在这里提到的这些内容。 - bobobobo
代码并不总是在IDE或本地系统上构建。它并不总是由同一方设计。格式化字符串是平台相关的(因为不同平台上变量的大小不同)。在VS 2010中,long是64位,在GCC中是32位。这可能会导致缓冲区溢出。但是我很惊讶很少有人知道有一个标准化的头文件,其中包含适当格式化字符串的字符串字面值。 - Swift - Friday Pie

5
您可以通过 {fmt} 库获得最佳的双重优势,它将iostreams的安全性和可扩展性与(s)printf的易用性和性能相结合。示例:
fmt::print("The answer is {}.", 42);

这个库支持类Python和printf格式字符串语法。

声明:我是这个库的作者。


5

流是官方推荐的方法。尝试使用 printf 使这段代码工作:

template <typename T>
void output(const T& pX)
{
    std::cout << pX << std::endl;
}

祝你好运。

我的意思是,你可以创建操作符使你的类型能够输出到 ostream 中,而且使用起来就像任何其他类型一样轻松。 printf 不适用于 C++ 的通用性,或者更具体地说是模板。

还有更多的可用性。还有一致性。在我所有的项目中,我都将 cout(和 cerrclog)连接到一个文件中进行输出。如果你使用 printf,你就会跳过所有这些。另外,一致性本身就是一件好事;混合使用 coutprintf,虽然完全有效,但很丑陋。

如果你有一个对象,并且想让它能够输出,最干净的方法是为该类重载 operator<<。那么你要如何使用 printf 呢?你最终会得到一堆混合了 coutprintf 的代码。

如果你真的想要格式化,可以使用 Boost.Format 来保持流接口。一致性格式化。


10
你没有因为提出正确的观点而被踩,而是因为你的观点与问题无关。我们知道printf不能做到这一点,因为它不是设计成这样的。这就像说:“哦,我打赌你不能在C语言中创建一个类”。这个观点也是正确的,因为该语言并没有设计用于此。 - Bob Dylan
10
这并不是无关紧要的。你在问是否应该使用 printf。为了保持一致性,不要使用。流永远都能用,但 printf 并非总是能用。不一致的代码很丑陋。如果我将 cout 与一个日志文件同时输出会怎样呢?(是的,我所有的项目都这样做!)现在你仅仅是绕过了这些。有比节省打字更需要考虑的事情。 - GManNickG
1
所以你需要为T编写一个重载的<<运算符,而不是一个print(T)函数。 - Martin Beckett
1
@bobobobo:所以你认为printf(stdout, "Hello "); person.print(stdout); printf("! I see you've subscribed to the "); tags.print(stdout); printf("tags, nice!");cout << "Hello " << person << "! I see you've subscribed to the " << tags << ", nice!";更加优美,更不疯狂? - Sebastian Mach
1
你的代码假设 pX 有一个 << 操作符方法。如果我假设 pX 有一个 (char *) 类型方法,我可以使用带有 (char *) 强制转换的 printf() 方法。这样就没有使用 cout 的优势了。个人而言,我非常不喜欢 << 操作符的重载含义,所以我倾向于使用 printf() 或其他解决方案。 - Bill Weinman
显示剩余9条评论

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