C++中的'printf'和'cout'有什么区别?

516
16个回答

519

我很惊讶这个问题中每个人都声称std::coutprintf更好,即使问题只是问有什么区别。现在,确实存在一个区别-std::cout是C ++,printf是C(但是您可以像使用C中的几乎任何其他东西一样在C ++中使用它)。现在,我要诚实地说; both printf and std::cout have their advantages。

真正的区别

可扩展性

std::cout是可扩展的。我知道人们会说printf也是可扩展的,但在C标准中没有提到这样的扩展(因此您必须使用非标准功能-但甚至没有常见的非标准功能存在),而且这样的扩展只有一个字母(因此很容易与已经存在的格式发生冲突)。

printf不同,std::cout完全依赖于运算符重载,因此没有自定义格式的问题-所有您需要做的就是定义一个子程序,将std::ostream作为第一个参数,将您的类型作为第二个参数。因此,没有命名空间问题-只要您有一个类(不局限于一个字符),就可以为其具有工作的std::ostream重载。

然而,我怀疑许多人会想要扩展ostream(说实话,即使很容易制作这样的扩展,我也很少看到这样的扩展)。但是,如果需要,它在这里。

语法

可以轻松注意到,printfstd::cout都使用不同的语法。printf使用模式字符串和可变长度参数列表的标准函数语法。实际上,printf是C具有可变长度参数列表的原因-printf格式太复杂,无法在没有它们的情况下使用。但是,std::cout使用不同的API-返回自己的operator << API。

通常,这意味着C版本将更短,但在大多数情况下并不重要。当打印许多参数时,差异是明显的。如果您必须编写类似于Error 2: File not found.的内容,假设错误编号及其说明是占位符,则代码如下所示。两个例子工作方式相同(嗯,有点不同,std::endl实际上会刷新缓冲区)。

printf("Error %d: %s.\n", id, errors[id]);
std::cout << "Error " << id << ": " << errors[id] << "." << std::endl;

虽然这看起来并不太疯狂(只是两倍长),但当您实际格式化参数而不是仅仅打印它们时,情况变得更加疯狂。例如,打印像0x0424这样的东西就很疯狂。这是因为std::cout混合了状态和实际值。我从未见过像std::setfill这样的类型(除了当然是C++)。printf明确地将参数和实际类型分开。相比之下,我真的更喜欢维护printf版本(即使它看起来有点神秘),而不是iostream版本(因为它包含太多噪音)。

printf("0x%04x\n", 0x424);
std::cout << "0x" << std::hex << std::setfill('0') << std::setw(4) << 0x424 << std::endl;

翻译

这就是printf的真正优势所在。 printf的格式化字符串是一个字符串,这使得它相对于iostreamoperator <<滥用来说,翻译起来非常容易。假设gettext()函数已经翻译好了,并且你想显示Error 2: File not found.,获取先前显示的格式化字符串的翻译代码如下:

printf(gettext("Error %d: %s.\n"), id, errors[id]);

现在,假设我们翻译成 Fictionish,其中错误数字在描述之后。翻译的字符串将如下所示:%2$s oru %1$d.\n。现在,如何在 C++ 中实现呢?嗯,我不知道。我想你可以制作一个假的 iostream,它构造了一个你可以传递给 gettextprintf,或者其他用于翻译的内容。当然,$ 不是 C 标准,但它很常见,在我看来使用它是安全的。

无需记住/查找特定整数类型的语法

C 有很多整数类型,C++ 也是如此。 std::cout 会处理所有类型,而 printf 则需要根据整数类型使用特定的语法(虽然有非整数类型,但实际上您将仅使用 const char *(C 字符串),可以使用 std::stringto_c 方法获得)。例如,要打印 size_t,您需要使用 %zu,而 int64_t 将需要使用 %"PRId64"。表格可在http://en.cppreference.com/w/cpp/io/c/fprintfhttp://en.cppreference.com/w/cpp/types/integer上找到。

您无法打印 NUL 字节,\0

由于 printf 使用 C 字符串而不是 C++ 字符串,因此它不能打印 NUL 字节而不进行特定的技巧。在某些情况下,可以使用 %c'\0' 作为参数,尽管这显然是一个 hack。

没有人关心的区别

性能

更新:事实证明,iostream 如此缓慢,以至于它通常比您的硬盘更慢(如果将程序重定向到文件中)。关闭与 stdio 的同步可能有所帮助,如果需要输出大量数据。如果性能是真正的问题(而不是向 STDOUT 写入几行),只需使用 printf

每个人都认为他们关心性能,但没有人费心去衡量它。我的答案是,无论是使用 printf 还是 iostream,I/O 都是瓶颈。我认为,从快速查看汇编(使用 -O3 编译器选项使用 clang 编译)的角度来看,printf 可能会更快。假设错误示例,printf 示例比 cout 示例需要更少的调用。这是带有 printfint main

main:                                   @ @main
@ BB#0:
        push    {lr}
        ldr     r0, .LCPI0_0
        ldr     r2, .LCPI0_1
        mov     r1, #2
        bl      printf
        mov     r0, #0
        pop     {lr}
        mov     pc, lr
        .align  2
@ BB#1:

你可以轻松地注意到,两个字符串和数字2被推入printf参数。就是这样;没有别的。相比之下,这是将iostream编译成汇编语言。不,没有内联;每个单独的operator <<调用都意味着另一个具有另一组参数的调用。

main:                                   @ @main
@ BB#0:
        push    {r4, r5, lr}
        ldr     r4, .LCPI0_0
        ldr     r1, .LCPI0_1
        mov     r2, #6
        mov     r3, #0
        mov     r0, r4
        bl      _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
        mov     r0, r4
        mov     r1, #2
        bl      _ZNSolsEi
        ldr     r1, .LCPI0_2
        mov     r2, #2
        mov     r3, #0
        mov     r4, r0
        bl      _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
        ldr     r1, .LCPI0_3
        mov     r0, r4
        mov     r2, #14
        mov     r3, #0
        bl      _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
        ldr     r1, .LCPI0_4
        mov     r0, r4
        mov     r2, #1
        mov     r3, #0
        bl      _ZSt16__ostream_insertIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_PKS3_l
        ldr     r0, [r4]
        sub     r0, r0, #24
        ldr     r0, [r0]
        add     r0, r0, r4
        ldr     r5, [r0, #240]
        cmp     r5, #0
        beq     .LBB0_5
@ BB#1:                                 @ %_ZSt13__check_facetISt5ctypeIcEERKT_PS3_.exit
        ldrb    r0, [r5, #28]
        cmp     r0, #0
        beq     .LBB0_3
@ BB#2:
        ldrb    r0, [r5, #39]
        b       .LBB0_4
.LBB0_3:
        mov     r0, r5
        bl      _ZNKSt5ctypeIcE13_M_widen_initEv
        ldr     r0, [r5]
        mov     r1, #10
        ldr     r2, [r0, #24]
        mov     r0, r5
        mov     lr, pc
        mov     pc, r2
.LBB0_4:                                @ %_ZNKSt5ctypeIcE5widenEc.exit
        lsl     r0, r0, #24
        asr     r1, r0, #24
        mov     r0, r4
        bl      _ZNSo3putEc
        bl      _ZNSo5flushEv
        mov     r0, #0
        pop     {r4, r5, lr}
        mov     pc, lr
.LBB0_5:
        bl      _ZSt16__throw_bad_castv
        .align  2
@ BB#6:

然而,说实话,这没什么意义,因为I/O本来就是瓶颈。我只是想表明并不更快,因为它是“类型安全”的。大多数C实现使用计算跳转来实现格式,所以尽可能地快,即使编译器不知道(他们并不是不知道-某些编译器可以在某些情况下优化-常量字符串以<\n>结尾通常会优化为)。

继承

我不知道你为什么想继承,但我不在乎。用也是可能的。

class MyFile : public FILE {}

类型安全

没错,可变长度参数列表没有类型安全性,但这并不重要。因为流行的C编译器可以在启用警告的情况下检测到printf格式字符串的问题。事实上,Clang可以在不启用警告的情况下完成此操作。

$ cat safety.c

#include <stdio.h>

int main(void) {
    printf("String: %s\n", 42);
    return 0;
}

$ clang safety.c

safety.c:4:28: warning: format specifies type 'char *' but the argument has type 'int' [-Wformat]
    printf("String: %s\n", 42);
                    ~~     ^~
                    %d
1 warning generated.
$ gcc -Wall safety.c
safety.c: In function ‘main’:
safety.c:4:5: warning: format ‘%s’ expects argument of type ‘char *’, but argument 2 has type ‘int’ [-Wformat=]
     printf("String: %s\n", 42);
     ^

30
你说I/O是瓶颈,但显然你从未测试过这个假设。我引用自己的话:"另一方面,iostreams版本的速度为75.3MB/s,无法缓冲数据以跟上硬盘的速度。这很糟糕,而且它甚至还没有开始做任何真正的工作。我认为我的I/O库应该能够使磁盘控制器达到饱和,这不算要求过高。" - Ben Voigt
7
我承认,如果有可能的话,我会尽量避免使用C++。我曾经试着大量使用它,但是相比于我用过的其他编程语言,它更加繁琐、不易维护。这也是我避免C++的又一个原因 - 它甚至不够快(甚至不是iostream - 在大多数实现中,整个C++库都很慢,或许除了std::sort之外,后者比qsort快两倍,代价是可执行文件大小)。 - Konrad Borowski
3
没有人提到在使用 cout 时在并行环境下可能存在的问题。 - Nicholas Hamilton
11
你的表现论点完全没有意义。在程序中增加汇编并不意味着程序会变慢,因为你没有计算打印函数中所有代码的贡献,而打印函数包含了大量的代码。我认为,使用“<<”操作符可以更好地优化cout,胜过printf,因为编译器可以更好地理解变量和格式。 - Ignas2526
42
我喜欢这个答案的许多方面,但也许我最喜欢的部分是“每个人都认为他们关心性能,但没有人费心去衡量它。” - Kyle Strand
显示剩余12条评论

226

来自C++ FAQ

[15.1] 为什么我应该使用 <iostream> 而不是传统的 <cstdio> 增加类型安全性,减少错误,允许可扩展性,并提供继承性。 printf() 可能并没有问题,而且尽管易错,scanf() 也许还可以接受,但两者在 C++ I/O 所能做的方面都有限制。与 C(使用 printf()scanf())相比,C++ I/O(使用 <<>>)具有以下优点:
  • 更加类型安全:使用 <iostream>,编译器静态知道正在进行 I/O 的对象的类型。相反,<cstdio> 使用“%”字段动态地确定类型。
  • 更少出错:使用 <iostream>,不存在必须与实际进行 I/O 的对象一致的冗余“%”标记。消除冗余会消除一类错误。
  • 可扩展:C++ <iostream> 机制允许对新的用户定义类型进行 I/O,而不会破坏现有代码。想象一下,如果每个人都同时向 printf()scanf() 添加新的不兼容“%”字段,会发生什么混乱?!
  • 可继承:C++ <iostream> 机制是由真正的类(如 std::ostreamstd::istream)构建的。与 <cstdio>FILE* 不同,这些都是真正的类,因此可继承。这意味着您可以拥有其他用户定义的东西,看起来和行为像流,但可以执行任何奇怪和美妙的操作。您自动获得了由您甚至不认识的用户编写的数亿行 I/O 代码,而他们不需要知道您的“扩展流”类。
另一方面,printf 明显更快,这可能会证明在非常特定和有限的情况下使用它优于 cout。始终先进行剖析。 (例如,请参见http://programming-designs.com/2009/02/c-speed-test-part-2-printf-vs-cout /)

2
另一方面,有FastFormat库(http://www.fastformat.org),它同时提供类型安全、表现力和性能。(虽然我还没有尝试过...) - xtofl
3
@Marcelo 可能是因为这篇文章总结得很好,所有引用的内容都有注明。但格式确实很糟糕。我本该自己修复它,但似乎其他人(包括你)已经处理了,这当然比抱怨更具建设性。 - Mikeage
2
最近printf()也应该是可扩展的。请参阅http://udrepper.livejournal.com/20948.html上的“printf hooks”。 - Maxim Egorushkin
4
标准的printf函数没有这种能力。非可移植的库机制很难与iostreams的完全标准化扩展相提并论。 - Ben Voigt
7
另一方面,printf明显更快。printf也更加简洁易用,这就是为什么我尽可能避免使用cout的原因。 - FluorescentGreen5
显示剩余5条评论

54

人们经常声称printf更快。这在很大程度上是一个谬论。我刚刚进行了测试,以下是测试结果:

cout with only endl                     1461.310252 ms
cout with only '\n'                      343.080217 ms
printf with only '\n'                     90.295948 ms
cout with string constant and endl      1892.975381 ms
cout with string constant and '\n'       416.123446 ms
printf with string constant and '\n'     472.073070 ms
cout with some stuff and endl           3496.489748 ms
cout with some stuff and '\n'           2638.272046 ms
printf with some stuff and '\n'         2520.318314 ms

结论:如果你只需要换行符,请使用printf; 否则,cout几乎和printf一样快,甚至更快。更多细节可在我的博客中找到。
明确一点,我并不是在说iostream总是比printf好; 我只是想说,你应该根据真实数据做出知情决策,而不是基于一些常见、误导性的假设的猜测。
更新:这是我用于测试的完整代码。编译时没有使用任何额外的选项(除了-lrt用于计时)。
#include <stdio.h>
#include <iostream>
#include <ctime>

class TimedSection {
    char const *d_name;
    timespec d_start;
    public:
        TimedSection(char const *name) :
            d_name(name)
        {
            clock_gettime(CLOCK_REALTIME, &d_start);
        }
        ~TimedSection() {
            timespec end;
            clock_gettime(CLOCK_REALTIME, &end);
            double duration = 1e3 * (end.tv_sec - d_start.tv_sec) +
                              1e-6 * (end.tv_nsec - d_start.tv_nsec);
            std::cerr << d_name << '\t' << std::fixed << duration << " ms\n"; 
        }
};

int main() {
    const int iters = 10000000;
    char const *text = "01234567890123456789";
    {
        TimedSection s("cout with only endl");
        for (int i = 0; i < iters; ++i)
            std::cout << std::endl;
    }
    {
        TimedSection s("cout with only '\\n'");
        for (int i = 0; i < iters; ++i)
            std::cout << '\n';
    }
    {
        TimedSection s("printf with only '\\n'");
        for (int i = 0; i < iters; ++i)
            printf("\n");
    }
    {
        TimedSection s("cout with string constant and endl");
        for (int i = 0; i < iters; ++i)
            std::cout << "01234567890123456789" << std::endl;
    }
    {
        TimedSection s("cout with string constant and '\\n'");
        for (int i = 0; i < iters; ++i)
            std::cout << "01234567890123456789\n";
    }
    {
        TimedSection s("printf with string constant and '\\n'");
        for (int i = 0; i < iters; ++i)
            printf("01234567890123456789\n");
    }
    {
        TimedSection s("cout with some stuff and endl");
        for (int i = 0; i < iters; ++i)
            std::cout << text << "01234567890123456789" << i << std::endl;
    }
    {
        TimedSection s("cout with some stuff and '\\n'");
        for (int i = 0; i < iters; ++i)
            std::cout << text << "01234567890123456789" << i << '\n';
    }
    {
        TimedSection s("printf with some stuff and '\\n'");
        for (int i = 0; i < iters; ++i)
            printf("%s01234567890123456789%i\n", text, i);
    }
}

5
在得分方面,printf比cout更容易胜出(大多数情况)。我不知道为什么您在性能方面推荐使用cout。尽管我同意在实际情况下它们的性能差异并不太大。 - mishal153
3
我理解你的意思是表演效果并没有太大不同,因此通常听到的“绝不要使用cout,因为它速度超慢”的建议是纯粹愚蠢的。注意,cout具有明显的类型安全优势,并且通常也更易读。(使用iostreams进行浮点数格式化可真是糟糕得不得了……) - Thomas
47
printf()std::ostream的重要区别在于,前者可以在一次函数调用中输出所有参数,而std::ostream需要为每个<<操作单独调用一次函数。由于测试只输出一个参数和一个换行符,这就是为什么您看不到这种区别的原因。 - Maxim Egorushkin
12
编译器应该能够内联这些调用。此外,printf 在底层可能会对各种格式说明符调用多个辅助函数... 或者它是一个庞大的单块函数。由于内联,这在速度上并不会有任何差异。 - Thomas
4
你计时了你的终端。请使用sprintffprintf以及stringstreamfstream - Ben Voigt
显示剩余12条评论

43

我引用以下内容:

从高层次来看,主要区别在于类型安全性(cstdio没有它)、性能(大多数iostreams实现比cstdio慢)和可扩展性(iostreams允许自定义输出目标并无缝输出用户定义的类型)。


特别是在Unix上,使用POSIX时您永远不知道其中一个typedef的大小,因此您需要进行许多强制转换,或者像99%的程序一样冒着%d的风险。即使在C99中,%z也花费了很长时间才出现。但是对于time_t/off_t,正确的格式指令的寻找仍在继续。 - Lothar

31

一个是打印到标准输出的函数,另一个是提供多个成员函数和重载operator<<用于打印到标准输出的对象。我还可以列举更多差异,但我不确定你需要什么。


14

对我而言,让我选择“cout”而不是“printf”的真正区别是:

1)<<运算符可以被重载用于我的类。

2)cout的输出流可以轻松更改为文件: (: 复制粘贴 :)

#include <iostream>
#include <fstream>
using namespace std;

int main ()
{
    cout << "This is sent to prompt" << endl;
    ofstream file;
    file.open ("test.txt");
    streambuf* sbuf = cout.rdbuf();
    cout.rdbuf(file.rdbuf());
    cout << "This is sent to file" << endl;
    cout.rdbuf(sbuf);
    cout << "This is also sent to prompt" << endl;
    return 0;
}

3) 我发现cout更易读,尤其是当我们有许多参数时。

cout的一个问题是格式选项。在printf中格式化数据(精度、对齐等)更容易。


1
很好。我怎么知道在某个外部库线程中没有以这种方式修改全局cout? - vp_arth
9
你可以通过将 printf 替换为 fprintf 来将其轻松地转换成写入文件的形式,不改变原意。 - CoffeeTableEspresso

10

这里有两点我认为很重要,但未被提及:

1)如果您还没有使用STL,cout会增加大量代码到您的目标文件中,比printf多出一倍以上。对于string也是如此,这是我倾向于使用自己的字符串库的主要原因。

2)cout使用了重载的<<运算符,我认为这是不幸的。如果您还要使用<<运算符进行其预期用途(左移),这可能会增加混淆。我个人不喜欢为与预期用途有关的目的重载运算符。

总结:如果我已经使用STL,则会使用cout(和string)。否则,我倾向于避免使用它。


6
我不是程序员,但我是一名人因工程师。我认为编程语言应该易于学习、理解和使用,这就需要它具有简单和一致的语言结构。虽然所有语言在本质上都是符号化的,因此是任意的,但是存在惯例,遵循惯例可以使语言更易于学习和使用。
许多C++和其他语言中的函数都是以函数(参数)的形式编写的,这个语法最初用于计算机之前的数学中的函数关系。printf()也遵循这种语法,如果C++的作者想要创建任何逻辑上不同的读写文件方法,他们可以使用类似的语法创建不同的函数。
在Python中,我们当然可以使用非常标准的object.method语法来打印,即变量名.print,因为变量是对象,但在C++中不是这样。
我不喜欢cout的语法,因为<<运算符没有遵循任何规则。它是一种方法或函数,即它接受一个参数并对其执行某些操作。然而,它的书写方式就好像它是一个数学比较运算符。从人因工程的角度来看,这是一种不好的方法。

2
<< 不是比较运算符,而是位左移运算符。它需要两个整数参数(原始的)。10 << 1; 这将十的位向左移动1次,返回数字20。但Cpp允许运算符重载,并决定将此运算符用于stdout。cpp的比较运算符包括小于(<)、大于(>)、小于或等于(<=)、大于或等于(>=)、等于(==)、不等于(!=)。这些都只是cpp特定的语法,与数学无直接关系。请注意,赋值运算符是(=)。 - user13947194

5

对于基本数据类型,使用哪种可能并不完全重要。我认为它的有用之处在于当您想要输出复杂对象时。

例如,如果您有一个类,

#include <iostream>
#include <cstdlib>

using namespace std;

class Something
{
public:
        Something(int x, int y, int z) : a(x), b(y), c(z) { }
        int a;
        int b;
        int c;

        friend ostream& operator<<(ostream&, const Something&);
};

ostream& operator<<(ostream& o, const Something& s)
{
        o << s.a << ", " << s.b << ", " << s.c;
        return o;
}

int main(void)
{
        Something s(3, 2, 1);

        // output with printf
        printf("%i, %i, %i\n", s.a, s.b, s.c);

        // output with cout
        cout << s << endl;

        return 0;
}

看起来可能并不是很好,但是假设你必须在代码中的多个地方输出这个结果。使用cout,你只需在一个地方修改。然而,如果使用printf,则需要在可能很多的地方进行修改,并且你还必须自己想起要输出哪些东西。

因此,使用cout可以减少代码维护所需的时间,而且如果在新应用程序中重复使用“Something”对象,则不必担心输出问题。


另外,关于性能方面的问题,我想说的是,如果你的应用程序是为了性能而设计的,最好根本不要输出任何内容。任何一种对 std 的输出都相当昂贵和慢。我建议你尽量避免它,只有在绝对必要时才进行输出。 - Daniel
请记住,您的类可能具有私有成员,您无法轻松从外部访问。使用输出运算符,您只需要将一个位置设置为类的友元,现在您可以在任何地方输出它,甚至是您不知道的代码中。 - hochl

4
我想指出的是,如果您想在C++中使用线程,如果使用cout,则可能会得到一些有趣的结果。
考虑以下代码:
#include <string>
#include <iostream>
#include <thread>

using namespace std;

void task(int taskNum, string msg) {
    for (int i = 0; i < 5; ++i) {
        cout << "#" << taskNum << ": " << msg << endl;
    }
}

int main() {
    thread t1(task, 1, "AAA");
    thread t2(task, 2, "BBB");
    t1.join();
    t2.join();
    return 0;
}

// g++ ./thread.cpp -o thread.out -ansi -pedantic -pthread -std=c++0x

现在,输出结果是杂乱无序的。它也可能产生不同的结果,请尝试执行几次:
##12::  ABABAB

##12::  ABABAB

##12::  ABABAB

##12::  ABABAB

##12::  ABABAB

您可以使用printf来正确获取它,或者您可以使用mutex

#1: AAA
#2: BBB
#1: AAA
#2: BBB
#1: AAA
#2: BBB
#1: AAA
#2: BBB
#1: AAA
#2: BBB

玩得开心!


2
wtf thread 不会让输出变得疯狂。我刚刚复制并发现输出中有 xyzABC。在 ABC 之间没有混淆成 ABABAB - Abhinav Gauniyal
4
我不知道cout如何与线程一起使用,但我确定你展示的代码不是用于产生那些输出的代码。你的代码将字符串“ABC”传递给线程1,将“xyz”传递给线程2,但你的输出显示为“AAA”和“BBB”。请修正它,因为现在它很令人困惑。 - Fabio says Reinstate Monica
1
cout 会使事情交错,因为正如其他答案所指出的那样,cout 不会作为单个调用运行,而是会在一系列调用中运行,因此它们最终会交错。但与 @Apollo 所提到的不同的是,它永远不会交错 AB,因为 msg 将在单个调用中打印。但它可以打印类似于 #2: AAABBB 的东西。 - caiohamamura
1
不再相关了。秘密在于编译命令:-std=c++0x。使用一个不是十年前的C++标准,这个问题就会消失。 - Welgriv

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