在C ++中,我是否要为自己不吃的东西付费?


170

让我们考虑以下C和C ++的世界示例:

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

int main()
{
    std::cout<<"Hello world"<<std::endl;
    return 0;
}

当我将它们编译为汇编时,C代码的大小只有9行(gcc -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

但是C ++代码的大小为22行(g++ -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

...更大。

众所周知,在C ++中,您需要为所吃的东西付费。那么,在这种情况下,我要支付什么?


3
评论不作进一步讨论;此对话已转移至聊天
塞缪尔·柳


26
从未听说过eat与C ++相关的术语。我相信您的意思是:“您只为使用的商品付费”?
Giacomo Alzetta

7
@GiacomoAlzetta,...这是一种口语化,采用了无限量自助餐的概念。在全球范围内使用更精确的术语当然是更可取的,但是作为美国英语母语的人,这个标题对我来说很有意义。
查尔斯·达菲

5
@ trolley813内存泄漏与引号和OP问题无关。“只为使用的内容付费” /“不为不使用的内容付费”的意思是,如果您不使用特定的功能/抽象,则不会对性能造成任何影响。内存泄漏根本与此无关,这仅表明该术语eat更加含糊,应避免使用。
Giacomo Alzetta

Answers:


60

您所需要支付的是调用一个繁重的库(不如打印到控制台中那么繁重)。您初始化一个ostream对象。有一些隐藏的存储。然后,您呼叫std::endl,这不是的同义词\n。该iostream库可帮助您调整许多设置,并减轻处理器而不是程序员的负担。这就是您要支付的。

让我们回顾一下代码:

.LC0:
        .string "Hello world"
main:

初始化一个ostream对象+ cout

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

cout再次调用以打印新行并刷新

    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
    xor     eax, eax
    add     rsp, 8
    ret

静态存储初始化:

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

同样,区分语言和库也是必不可少的。

顺便说一句,这只是故事的一部分。您不知道正在调用的函数中写了什么。


5
另外,全面的测试将显示在C ++程序之前加上“ ios_base :: sync_with_stdio(false);”。和“ cin.tie(NULL);” 将使cout比printf快(Printf具有格式字符串开销)。第一个消除了确保cout; printf; cout顺序写入的开销(因为它们具有自己的缓冲区)。第二个将使cout和不同步cin,从而cout; cin有可能首先要求用户提供信息。刷新将迫使它仅在实际需要时才进行同步。
Nicholas Pipitone

嗨,尼古拉斯,非常感谢您添加这些有用的注释。
阿拉斯(Arash)

“区分语言和库是必不可少的”:是的,但是语言附带的标准库是随处可见的唯一库,因此它是随处使用的库(是的,C标准库是其中的一部分属于C ++规范,因此可以在需要时使用)。关于“您不知道正在调用的函数中写了什么”:如果您确实想知道,则可以静态链接,实际上,您检查的调用代码可能是不相关的。
彼得-恢复莫妮卡

211

那么,在这种情况下,我要支付什么?

std::coutprintf。更加强大和复杂。它支持语言环境,有状态格式标志等。

如果您不需要这些,请使用std::printfstd::puts-它们在中可用<cstdio>


众所周知,在C ++中,您需要为所吃的东西付费。

我还想明确指出C ++ != C ++标准库。标准库应该是通用的并且“足够快”,但是它通常会比所需的专业实现慢。

另一方面,C ++语言努力使编写代码成为可能,而无需支付不必要的额外隐藏成本(例如,选择加入virtual,不进行垃圾回收)。


4
+1表示标准库应该是通用的并且“足够快”,但是它通常会比所需的专门实现慢。许多人似乎过分地使用STL组件,而没有考虑性能影响或滚动使用自己的组件。
Craig Estey '18

7
@Craig OTOH标准库的许多部分通常比通常可以生产的内容更快,更正确。
彼得-恢复莫妮卡

2
@ PeterA.Schneider OTOH,当STL版本慢20到30倍时,自己滚动是一件好事。在这里查看我的答案:codereview.stackexchange.com/questions/191747/…在此,其他人也建议[至少部分]自己动手。
Craig Estey '18

1
@CraigEstey一个向量(除了可能是重要的初始动态分配以外,取决于给定实例最终将完成多少工作)不比C数组有效。它被设计为不是。必须注意不要在周围复制,最初保留足够的空间等,但是所有这些操作也必须使用数组完成,而且操作不太安全。关于您的链接示例:是的,与2D数组相比,矢量的向量(除非被优化掉)将导致额外的间接访问,但是我认为20倍效率不是植根于此,而是植根于算法中。
彼得-恢复莫妮卡

174

您没有在比较C和C ++。您正在比较printfstd::cout,它们具有不同的功能(语言环境,有状态格式等)。

尝试使用以下代码进行比较。Godbolt会为两个文件生成相同的程序集(已通过gcc 8.2,-O3测试)。

main.c:

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp:

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}


为显示等效代码并解释原因而欢呼。
HackSlash

134

您的清单确实比较了苹果和橘子,但并非出于大多数其他答案中暗示的原因。

让我们检查一下代码的实际作用:

C:

  • 打印一个字符串, "Hello world\n"

C ++:

  • 串流"Hello world"std::cout
  • std::endl机械手串入std::cout

显然,您的C ++代码正在做两倍的工作。为了公平起见,我们应该结合以下内容:

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

…突然之间,您的汇编代码main看起来与C十分相似:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

实际上,我们可以逐行比较C和C ++代码,几乎没有什么区别

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

唯一真正的区别是,在C ++中,我们operator <<使用两个参数(std::cout和字符串)进行调用。通过使用更接近的C eqivalent:,我们甚至可以消除细微的差别fprintf,它也具有指定流的第一个参数。

这样就留下了的汇编代码_GLOBAL__sub_I_main,它是为C ++而不是C生成的。这是在该汇编清单中唯一可见的真实开销(当然,两种语言都有更多的看不见的开销)。此代码在C ++程序开始时执行一些C ++标准库功能的一次性设置。

但是,正如在其他答案中所解释的那样,main由于所有繁重的工作都在后台进行,因此在程序的输出中不会找到这两个程序之间的相关差异。


21
顺便说一句,需要设置C运行时,这发生在一个称为的函数中,_start但其代码是C运行时库的一部分。无论如何,C和C ++都会发生这种情况。
康拉德·鲁道夫

2
@Deduplicator:实际上,默认情况下,iostream库不进行任何缓冲,std::cout而是将I / O传递给stdio实现(使用其自身的缓冲机制)。特别是,当连接到(称为)交互式终端时,默认情况下,在写入时您将永远看不到完全缓冲的输出std::cout。如果要让iostream库将自己的缓冲机制用于,则必须显式禁用与stdio的同步std::cout

6
@KonradRudolph:实际上,printf不需要在此处冲洗流。实际上,在一个常见的用例(输出重定向到文件)中,您通常会发现该printf语句不会刷新。仅当输出是行缓冲或非缓冲输出时,才会printf触发刷新。

2
@PeterCordes:是的,您不能使用未刷新的输出缓冲区进行阻塞,但是当程序接受您的输入并继续前进而不显示预期的输出时,您可能会感到惊讶。我知道这一点是因为我曾经调试过一个“帮助,我的程序在输入期间挂起,但我不知道为什么!” 使另一名开发人员适应了几天。

2
@PeterCordes:我的论点是“写您的意思”-当换句话说输出最终可用时,换行是合适的;当换句话说输出立即可用时,endl是合适的。

53

众所周知,在C ++中,您需要为所吃的东西付费。那么,在这种情况下,我要支付什么?

很简单 您付钱std::cout。“您只为所吃的东西付钱”并不意味着“您总能获得最优惠的价格”。当然,printf便宜。有人可以说这std::cout是更安全,更通用的,因此它的更高成本是合理的(它成本更高,但提供了更多价值),但这没有意义。您不使用printf,您使用std::cout,因此您为使用付费std::cout。您无需为使用付费printf

虚函数就是一个很好的例子。虚拟函数有一些运行时成本和空间要求-但仅当您实际使用它们时。如果您不使用虚拟功能,则无需支付任何费用。

几句话

  1. 即使C ++代码评估了更多的汇编指令,它仍然是少数指令,并且实际的I / O操作可能会使任何性能开销相形见.。

  2. 实际上,有时它甚至比“用C ++为您所吃的东西付钱”更好。例如,编译器可以推断出在某些情况下不需要虚拟函数调用,并将其转换为非虚拟调用。这意味着您可以免费获得虚拟功能。那不是很好吗?


6
您不会免费获得虚拟功能。您仍然必须先付出代价,首先编写它们,然后在编译器对代码的转换与您应做的想法不符时对其进行调试。
alephzero

2
@alephzero我不确定将开发成本与性能成本进行比较是否特别有意义。

双关语浪费了如此巨大的机会……您可以使用“卡路里”一词来代替“价格”。由此,您可以说C ++比C更胖。或者至少是……所讨论的特定代码(我偏向于C ++而偏向于C,所以我不能公平地超越它)。唉。@Bilkokuya可能并非在所有情况下都相关,但肯定是不应该忽略的事情。因此,它总体上是相关的。
Pryftan

46

“ printf的程序集列表”不是用于printf的,而是用于puts(某种编译器优化的?);printf比pret复杂得多...不要忘记!


13
到目前为止,这是最好的答案,因为其他所有对象都对std::couts的内部挂了一条红色的鲱鱼,而这些在装配清单中是看不到的。
康拉德·鲁道夫

12
程序集列表用于的调用 puts,与printf您仅传递单个格式的字符串和零个额外的args时的调用相同。(除了还会有一个,xor %eax,%eax因为我们要将零个FP args传递给可变参数函数的寄存器。)这些都不是实现,只是将指向字符串的指针传递给库函数。但是,是的,优化printfputs的东西gcc在对格式,只有"%s",或在没有转换,并以新行字符串结束。
彼得·科德斯

45

我在这里看到了一些有效的答案,但是我将进一步详细介绍。

如果您不想遍历整个文本墙,请跳至以下摘要,以获取主要问题的答案。


抽象化

那么,在这种情况下,我要支付什么?

您正在为抽象付费。能够编写更简单,更人性化的代码是有代价的。在C ++(一种面向对象的语言)中,几乎所有东西都是对象。当您使用任何对象时,总会在幕后发生三件事:

  1. 对象创建,基本上是对象本身及其数据的内存分配。
  2. 对象初始化(通常通过某种init()方法)。通常,内存分配是在此步骤中首先进行的。
  3. 对象破坏(并非总是如此)。

您不会在代码中看到它,但是每次使用一个对象时,以上三件事都需要以某种方式发生。如果您要手动执行所有操作,则代码显然会更长一些。

现在,可以在不增加开销的情况下高效地进行抽象:编译器和程序员都可以使用方法内联和其他技术来消除抽象的开销,但这不是您的情况。

C ++真正发生了什么?

分解如下:

  1. std::ios_base类初始化,这是一切的I / O相关的基类。
  2. std::cout对象被初始化。
  3. 您的字符串将被加载并传递到std::__ostream_insert,该字符串(如您所顾名思义的)是一种std::cout(基本上是<<运算符)将字符串添加到流中的方法。
  4. cout::endl也传递给std::__ostream_insert
  5. __std_dso_handle传递给__cxa_atexit,这是一个全局函数,负责在退出程序之前进行“清理”。__std_dso_handle此函数本身调用它来取消分配并销毁剩余的全局对象。

因此,使用C ==不支付任何费用吗?

在C代码中,发生的步骤很少:

  1. 您的字符串被加载并传递到puts通过edi寄存器。
  2. puts 被叫。

任何地方都没有对象,因此无需初始化/销毁任何东西。

但是,这并不意味着您不必为C中的任何事情“付出”。您仍然需要为抽象付出代价,还要为C标准库进行初始化并为printf函数动态解析(或者实际上是puts,由于不需要任何格式字符串而由编译器进行了优化)仍在幕后进行。

如果要以纯汇编形式编写此程序,则它将类似于以下内容:

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

基本上,这只会导致调用write syscall,然后再exit调用syscall。现在,将是完成同一件事的最低要求。


总结一下

C是更准系统,仅满足所需的最低要求,完全由用户控制,从而可以完全优化和自定义他们想要的任何内容。您告诉处理器将字符串加载到寄存器中,然后调用库函数以使用该字符串。另一方面,C ++更复杂,更抽象。这在编写复杂的代码时具有巨大的优势,并允许编写起来更容易和更人性化的代码,但这显然是有代价的。如果在这种情况下与C相比,C ++的性能总是存在缺陷,因为C ++提供的功能远远超过完成这些基本任务所需的功能,因此会增加更多的开销

回答您的主要问题

我要为不吃的东西付费吗?

在这种情况下,。您并没有利用C ++所不能提供的任何优势,而仅仅是因为C ++可以帮助您解决这些简单的代码中的任何内容:它是如此的简单以至于您根本不需要C ++。


哦,还有一件事!

乍看之下,C ++的优点似乎并不明显,因为您编写了一个非常简单且很小的程序,但是看了一些更复杂的示例并发现了不同(两个程序做的完全相同):

C

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

希望您能清楚明白我的意思。还要注意你如何用C必须在使用一个较低的水平,以管理内存malloc以及free如何需要更谨慎的索引和大小,并采取输入和打印时,你如何需要非常具体。


27

首先有一些误解。首先,C ++程序不会生成 22条指令,它更像是22,000条指令(我从帽子上拉出了这个数字,但是大约在球场上)。同样,C代码也不会产生9条指令。这些只是您所看到的。

C代码要做的是,在做了很多您没有看到的事情之后,它从CRT调用了一个函数(通常但不一定以共享库的形式出现),然后检查返回值或句柄错误,并摆脱困境。根据编译器和优化设置,它甚至不是真正调用的,printf而是puts更原始的东西。
如果只用相同的方式调用相同的函数,那么您可能也已经或多或少用C ++编写了相同的程序(除了一些不可见的init函数)。或者,如果您想超级正确,则该函数以开头std::

实际上,相应的C ++代码根本不是一回事。尽管<iostream>它的整体以肥胖的丑陋猪而闻名,但它为小型程序增加了巨大的开销(在“真正的”程序中,您实际上并未注意到太多),但更为公平的解释是,它确实很糟糕很多你看不见的东西会起作用的。包括但不限于几乎所有杂乱无章的东西的神奇格式,包括不同的数字格式和区域设置以及诸如此类,缓冲以及适当的错误处理。错误处理?好吧,猜猜是什么,输出字符串实际上可能会失败,并且与C程序不同,C ++程序不会默默地忽略它。考虑什么std::ostream在引擎盖下进行,并且没有人知道,它实际上是轻量级的。不喜欢使用它,因为我非常讨厌流语法。但是,如果您考虑一下它的功能,那就太棒了。

但是可以肯定的是,C ++总体上还不如C高效。它不可能一样高效,因为它不是同一件事情,也不是在做同样的事情。如果没有别的,C ++会生成异常(以及在它们上生成,处理或失败的代码),并提供一些C不会提供的保证。因此,可以肯定的是,C ++程序一定需要更大一点。但是,从总体上看,这没有任何关系。相反,对于真实程序,我很少发现C ++的性能更好,因为出于某种原因,它似乎有助于进行更有利的优化。不要问我为什么特别不知道。

如果代替发射后不管,希望换的,最好的,你关心哪个是写C代码正确的(即你实际检查错误,程序的行为在错误的存在正确的),那么差别是微不足道的,如果存在。


16
很好的答案,除了以下断言:“但是,可以肯定,C ++的整体效率不如C高效”是完全错误的。C ++可以和C一样高效,并且足够高级的代码可以比等效的C代码高效。是的,由于必须处理异常,C ++会产生一些开销,但是在现代编译器上,与更好的无成本抽象所带来的性能提升相比,其开销可忽略不计。
康拉德·鲁道夫

如果我理解正确,是否std::cout还会引发异常?
Saher

6
@Saher:是的,也许。std::coutstd::basic_ostream和一个可以扔了,它如果重新抛出,否则,发生的例外配置这样做,或者它可以吞下例外。事实是,事情可能会失败,并且(大多数情况下)构建了C ++和C ++标准库,因此失败不容易被忽视。这是一个烦恼祝福(但是,比烦恼更多的祝福)。另一方面,C仅向您显示中指。您不检查返回码,也不知道发生了什么。
戴蒙

1
@KonradRudolph:的确如此,这就是我试图指出的问题:“我很少发现C ++的性能更好,因为出于某种原因,它似乎有助于进行更有利的优化。不要问我特别为什么”。目前尚不清楚原因为何,但并非只有很少的原因它会更好地进行优化。出于……什么原因。您会认为对于优化器来说都是一样的,但事实并非如此。
戴蒙

22

您正在为错误付出代价。在80年代,当编译器不足以检查格式字符串时,运算符重载被视为在io期间强制执行某种类型安全性的好方法。但是,它的每个横幅功能要么从一开始就实施得不好,要么从概念上讲破产了:

<iomanip>

C ++流io api中最令人反感的部分是此格式标头库的存在。除了有状态,丑陋和易于出错外,它还将格式化与流耦合。

假设您要打印出一行,该行的8位零填充十六进制unsigned int,后跟一个空格,后跟一个带小数点后3位的double。使用<cstdio>,您可以读取简洁的格式字符串。使用<ostream>,您必须保存旧状态,将对齐方式设置为右,设置填充字符,设置填充宽度,将基数设置为十六进制,输出整数,恢复保存的状态(否则您的整数格式将污染您的浮点格式),输出空格,将符号设置为固定,设置精度,输出双精度和换行符,然后恢复旧格式。

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

运算符重载

<iostream> 是如何不使用运算符重载的典型子代:

std::cout << 2 << 3 && 0 << 5;

性能

std::cout慢几倍printf()。猖fe的狂犬病和虚拟派遣确实造成了损失。

线程安全

双方<cstdio><iostream>在每一个函数调用是原子线程安全的。但是,printf()每次通话可以完成更多工作。如果使用该<cstdio>选项运行以下程序,则只会显示一行f。如果<iostream>在多核计算机上使用,则可能会看到其他内容。

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

对此示例的反驳是,大多数人都遵守纪律,无论如何永远不要从多个线程写入单个文件描述符。那么,在这种情况下,你必须观察到,<iostream>会帮忙,抓住每一个锁<<和每一个>>。而在中<cstdio>,您将不会经常锁定,甚至可以选择不锁定。

<iostream> 花费更多的锁以获得不一致的结果。


2
printf的大多数实现都有一个非常有用的本地化功能:带编号的参数。如果需要用两种不同的语言(例如英语和法语)生成某些输出,并且单词顺序不同,则可以将同一printf与不同的格式字符串一起使用,并且可以按不同的顺序打印参数。
gnasher729

2
流的有状态格式化一定给了很多我不知道该说些什么的错误。好答案。如果可以的话,将不止一次投票。
mathreadler

6
std::cout慢了好几倍printf()” —这个说法在整个网络上都是重复的,但是多年来一直不是真的。现代IOstream实现的性能与IO相当printf。后者还在内部执行虚拟调度,以处理缓冲的流和本地化的IO(由操作系统完成但仍然完成)。
Konrad Rudolph

3
@KevinZ很棒,但是它以一个特定的调用为基准,它展示了fmt的特定优势(单个字符串中有很多不同格式)。在更典型的用法中,printf和之间的差异会cout缩小。顺便说一句,这个站点上有大量这样的基准。
康拉德·鲁道夫

3
@KonradRudolph也不是。微基准测试通常会低估膨胀和间接调用的成本,因为它们不会耗尽真实程序所能使用的某些有限资源(例如寄存器,icache,内存,分支预测变量)。当您提到“更典型的用法”时,基本上是说您在其他地方有更多的膨胀,这很好,但是不在主题之列。我认为,如果您对性能没有要求,则无需使用C ++进行编程。
KevinZ

18

除了什么都其他的答案说,
这里还有一个事实std::endl一样的'\n'

不幸的是,这是一个普遍的误解。std::endl并不意味着“换行”,
而是“打印新行然后刷新流 ”。冲洗不便宜!

完全无视之间的差异printfstd::cout片刻,待功能eqvuialent到C例如,你的C ++例子应该是这样的:

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

这是一个示例,说明如果包含冲洗功能,示例应该是什么样的。

C

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

比较代码时,应始终注意按“喜欢”进行比较,并了解代码在做什么的含义。有时,即使是最简单的示例也比某些人意识到的要复杂。


实际上,using std::endl 等效于将换行符写入行缓冲stdio流的功能。stdout特别是,当连接到交互式设备时,必须是行缓冲或不缓冲的。我相信Linux坚持使用行缓冲选项。

实际上,iostream库没有行缓冲模式...实现行缓冲效果的方法正是std::endl用于输出换行符。

@Hurkyl坚持吗?那有什么用setvbuf(3)呢?还是说默认值是行缓冲的?仅供参考:通常,所有文件都是块缓冲的。如果流指向终端(如stdout通常所做的那样),则会对其进行行缓冲。默认情况下,标准错误流stderr始终未缓冲。
Pryftan

printf遇到换行符时不会自动刷新吗?
bool3max

1
@ bool3max那只会告诉我我的环境是做什么的,在其他环境中可能有所不同。即使它在所有最流行的实现中的行为相同,也并不意味着某处存在边缘情况。这就是为什么标准如此重要的原因-该标准规定所有实现是否必须相同,或者是否允许不同实现之间有所不同。
法拉普

16

虽然现有的技术答案是正确的,但我认为问题最终源于这种误解:

众所周知,在C ++中,您需要为所吃的东西付费。

这只是来自C ++社区的市场营销演讲。(为公平起见,每个语言社区都有营销演讲。)这并不意味着您可以严重依赖的任何具体内容。

“按使用量付费”应该意味着C ++功能仅在使用该功能时才有开销。但是“功能”的定义不是无限细化的。通常,您最终会激活具有多个方面的功能,即使您只需要这些方面的一部分,但实现将功能部分引入通常是不实际的或不可能的。

通常,许多(尽管可以说不是全部)语言都努力提高效率,并取得不同程度的成功。C ++规模庞大,但它的设计没有什么特别之处或不可思议之处,可以使其在此目标中取得圆满成功。


1
对于您不用的东西,我可以考虑两件事:例外和RTTI。而且我不认为这是营销话题。C ++本质上是一个功能更强大的C,它也“不用为使用的东西付费”。
Rakete1111 '18

2
@ Rakete1111长期以来,如果不抛出异常,则不会产生任何费用。如果您的程序连续抛出,则应重新设计。如果失败的条件超出了您的控制范围,则应在调用依赖条件为假的方法之前使用布尔返回的健全性检查来检查条件。
schulmaster

1
@schulmaster:当用C ++编写的代码需要与其他语言编写的代码交互时,异常会施加设计约束,因为如果模块知道如何相互协调,则非本地控制权传递只能在模块之间顺利进行。
超级猫

1
(尽管可以说不是全部)语言都在努力提高效率。绝对不是全部:神秘的编程语言努力做到新颖/有趣,但效率不高。 esolangs.org。其中一些功能(例如BrainFuck)效率低下。或例如,莎士比亚编程语言,最小大小为227个字节(codegolf)以打印所有整数。在用于生产用途的语言中,大多数确实旨在提高效率,但是有些语言(如bash)主要是为了方便起见,并且已知速度较慢。
彼得·科德斯

2
好吧,这是营销,但这几乎是完全正确的。您可以坚持但<cstdio>不包括<iostream>,就像您可以如何进行编译一样-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
KevinZ

11

C ++中的Input / Output函数编写精美,设计合理,易于使用。在许多方面,它们都是C ++中面向对象功能的展示。

但是您确实确实放弃了一些性能,但是与操作系统在较低级别上处理功能所花费的时间相比,这可以忽略不计。

您总是可以退一步使用C样式函数,因为它们是C ++标准的一部分,或者完全放弃了可移植性,并直接调用了操作系统。


23
“ C ++中的Input / Output函数是可笑的怪兽,他们努力将自己的Cthulian特性隐藏在有用的薄板后面。从许多方面来看,它们都是如何不设计现代C ++代码的展示”。可能会更准确。
user673679 '18

3
@ user673679:非常正确。C ++ I / O流的主要问题是底层:确实 存在很多复杂性,任何曾经处理过它们的人(我指的是std::basic_*stream向下)都知道传入的消息。它们被设计为广泛通用并通过继承进行扩展;但是由于它们的复杂性(实际上是在iostream上写的书),最终没有人这样做,以至于新的库正是为此而诞生的(例如boost,ICU等)。我怀疑我们将永远停止为这个错误付出代价。
edmz

1

正如您在其他答案中看到的那样,当您在通用库中进行链接并调用复杂的构造函数时,您需要付费。这里没有特别的问题,更多的是抱怨。我将指出一些现实世界的方面:

  1. Barne的核心设计原则是永远不要让效率成为使用C而不是C ++的原因。就是说,需要谨慎才能获得这些效率,并且偶尔有一些效率始终有效,但在C规范中并不是“技术上”的。例如,位字段的布局并未真正指定。

  2. 尝试浏览ostream。哦,天哪,它肿了!在那儿找到飞行模拟器,我不会感到惊讶。甚至stdlib的printf()通常也要运行约50K。这些并不是懒惰的程序员:printf大小的一半与大多数人从未使用过的间接精度参数有关。几乎每个真正受限制的处理器的库都会创建自己的输出代码,而不是printf。

  3. 大小的增加通常会提供更加封闭和灵活的体验。打个比方,一个自动售货机将以几枚硬币的价格出售一杯类似咖啡的物质,整个交易需要不到一分钟的时间。进入一家好的餐厅涉及到餐桌摆放,坐着,点菜,等待,拿起漂亮的杯子,拿到账单,以您选择的形式付款,添加小费以及希望旅途中有美好的一天。这是一种不同的体验,如果您和朋友一起享用一顿复杂的饭菜,会更加方便。

  4. 人们仍然写ANSI C,尽管很少使用K&RC。我的经验是,我们总是使用C ++编译器使用一些配置调整来编译它,以限制所拖入的内容。其他语言也有很好的论据:Go消除了多态性开销和疯狂的预处理器; 对于更智能的字段打包和内存布局,已经有一些很好的论据。恕我直言,我认为任何语言设计都应该从列出目标开始,就像PythonZen一样。

讨论很有趣。您问为什么不能拥有神奇的小型,简单,优雅,完整而灵活的库?

没有答案。不会有答案。那就是答案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.