【问题标题】:C++ function parameters: use a reference or a pointer (and then dereference)?C++ 函数参数:使用引用还是指针(然后取消引用)?
【发布时间】:2010-12-11 16:04:07
【问题描述】:

我得到了一些代码,其中一些参数是指针,然后指针被取消引用 提供价值。我担心指针取消引用会花费周期,但是在查看之后 之前的一篇 StackOverflow 文章:How expensive is it to dereference a pointer?,也许没关系。

这里有一些例子:


bool MyFunc1(int * val1, int * val2)
{
    *val1 = 5;
    *val2 = 10;
    return true;
}

bool MyFunc2(int &val1, int &val2)
{
    val1 = 5;
    val2 = 10;
    return true;
}

就风格而言,我个人更喜欢通过引用传递,但有一个版本更好 (在流程周期方面)比另一个?

【问题讨论】:

  • 这是您应该担心的那种宏优化。生成最好的代码是编译器的工作(它考虑的因素比普通人要多得多,只有绝对最好的汇编编写者才会挑战编译器)。您(作为开发人员)的工作是提出执行任务的最佳算法(因为编译器无法做到这一点)。至于风格,人们会双向争论(我更喜欢参考,因为我不需要检查 NULL)。
  • ++ 没有人应该只有 1 个代表 :-)
  • n 这种情况(短函数)可能值得使用内联。由于您关心性能,我假设您在短时间内多次调用这个小函数(或类似的东西)。
  • @Martin:你的意思是“微观优化”——“宏观”是人们应该担心的大问题……
  • 请注意,如果需要与 C 向后兼容,则应使用指针。否则我同意 Loki。

标签: c++ pointers performance


【解决方案1】:

我的经验法则是,如果参数可以为 NULL,则通过指针传递,即在上述情况下是可选的,如果参数不应该为 NULL,则使用引用。

【讨论】:

  • 对。除非分析表明有必要,否则不应尝试这种微优化。在那之前,更喜欢一个好的工程。我碰巧同意这一点。不幸的是,我不能给它 3 票。
  • 我个人的偏好是始终使用引用,当您遇到“无引用”的情况时,请使用boost::optional<T&>(是的,它是专门为与引用一起工作而编写的!),给你“可选参考”。优点是您不会得到任何可能令人讨厌的指针转换,也不会得到指针算术(因此您不能将 *a+1 错误输入为 a+1 并让它编译并执行您没想到的事情做)。
【解决方案2】:

从性能的角度来看,这可能无关紧要。其他人已经回答了。

话虽如此,我还没有发现在这种情况下添加指令会产生明显差异的情况。 我确实意识到,对于一个被调用数十亿次的函数,它可能会有所作为。 通常,您不应该针对此类“优化”调整您的编程风格。

【讨论】:

  • +1 表示“在所有错误的地方寻找优化”的概念。
【解决方案3】:

您可以从编译器中获取汇编代码并进行比较。至少在 GCC 中,它们产生相同的代码。

【讨论】:

  • 当然可以。但是你应该吗?工程应该放在首位,只要它没有被证明会严重阻碍性能。因此,IMO 这个问题应该从工程 POV 中回答。 (我喜欢stackoverflow.com/questions/1650792/1650849#1650849.
  • 据我了解,问题是关于“流程周期”,而不是两个选项之间的功能差异。
  • 这个问题很清楚地表明詹纳更喜欢它,但担心速度。为什么不应该告诉他担心是错误的?
  • 我不是告诉他了吗?它们之间可能存在巨大差异,也可能没有。如果通过引用传递背后有一些魔法怎么办?如果它慢 1000 倍怎么办?除非您尝试/阅读/询问,否则您不会知道。我相信了解您所使用语言的低级语义很重要,这样您才能做出正确的决定。这就像争论在自然语言中使用哪个词,却不知道这些词的实际含义。
  • “我不是告诉他了吗?”无论如何,不​​在你的答案中。 “如果通过引用传递的背后有一些魔法怎么办?”然后我们将讨论语义,而不是性能。 “如果它慢 1000 倍怎么办?”如果是这样,就没有必要查看汇编代码了。
【解决方案4】:

这将被否决,因为它是 Old Skool,但我通常更喜欢指针,因为它更容易浏览代码并查看我传递给函数的对象是否可以被修改,特别是如果它们是简单的数据类型,如 int和浮动。

【讨论】:

  • 虽然我碰巧不在这个阵营中(我同意这个论点:stackoverflow.com/questions/1650792/1650849#1650849),但我认为这是一个有效的论点。 +1
  • 糟糕,我只是在添加我自己的回复后才注意到您的回复(这基本上重复了您的论点)。为你 +1。
  • 我会修改为:更容易看出,在使用函数调用读取代码时,该函数调用可能会修改它的一些参数。 IE。 swap(a, b)swap(&a, &b).
  • Nah:int* test = new int(5); // 10 行代码:swap(test, &b);
【解决方案5】:

对于使用引用参数和指针参数有不同的指导方针,根据不同的要求量身定制。在我看来,在泛型 C++ 开发中应该应用的最有意义的规则如下:

  1. 重载运算符时使用引用参数。 (在这种情况下,您实际上别无选择。这就是首先引入引用的目的。)

  2. 对复合(即逻辑上“大”)输入参数使用 const-reference。即 input 参数应该通过值(“原子”值)或常量引用(“聚合”值)传递。 output 参数和 input-output 参数使用指针。 不要对输出参数使用引用

考虑到上述情况,您程序中绝大多数的引用参数应该是 const-references。如果您有一个非常量引用参数并且它不是运算符,请考虑改用指针。

按照上述约定,您将能够在调用时看到函数是否可能修改其参数之一:可能修改的参数将使用显式 & 或作为现有指针传递。

还有另一个流行的规则,即可以为 null 的东西应该作为指针传递,而不能为 null 的东西应该作为引用传递。我可以想象这在一些非常狭隘和非常具体的情况下可能是有意义的,但总的来说这是一个主要的反规则。只是不要这样做。如果您想表达某个指针不能为空的事实,请将相应的断言作为函数的第一行。

至于性能考虑,指针传递和引用传递绝对没有性能差异。这两种参数在物理层面上是完全一样的。即使函数被内联,现代编译器也应该足够聪明以保持等价性。

【讨论】:

    【解决方案6】:

    这是使用 g++ 生成的程序集的不同之处。 a.cpp 是指针,b.cpp 是引用。

    $ g++ -S a.cpp
    
    $ g++ -S b.cpp
    
    $ diff a.S b.S
    1c1
    <       .file   "a.cpp"
    ---
    >       .file   "b.cpp"
    4,6c4,6
    < .globl __Z7MyFunc1PiS_
    <       .def    __Z7MyFunc1PiS_;        .scl    2;      .type   32;     .endef
    < __Z7MyFunc1PiS_:
    ---
    > .globl __Z7MyFunc1RiS_
    >       .def    __Z7MyFunc1RiS_;        .scl    2;      .type   32;     .endef
    > __Z7MyFunc1RiS_:
    

    只是函数名略有不同;内容相同。当我使用g++ -O3 时,我得到了相同的结果。

    【讨论】:

      【解决方案7】:

      引用与指针非常相似,但有一个很大的区别:引用不能为 NULL。所以你不需要检查它们是否是真正可用的对象(比如指针)。

      因此我假设编译器会产生相同的代码。

      【讨论】:

      • 请注意这一点,因为引用实际上可以为 NULL。当然,您不应该检查它,但请记住,您的代码仍然可能在 NULL 引用上崩溃(以防它对您的应用程序很重要)。
      • TTBOMK,如果有人恶意调用未定义的行为以设置对NULL的引用,则引用只能是NULL。如果您因为 的可能性而想要小心,那么您也不能信任代码中的任何其他内容。
      • @Jim:在有效程序中引用不能为 NULL。当然,您可以执行令人讨厌的 hack 来实现它,但此时允许程序将您的屏幕变为蓝色并召唤 Zeus 与您的表亲一起睡觉,因此引用为 NULL 是最少的问题。
      • @Kaz:这可能取决于你的表兄弟。 :)
      • References 通常不能为 null,但要注意的情况是您存储了对超出范围的变量的引用。例如,在函数中返回对局部变量的引用。
      【解决方案8】:

      所有其他答案都已经指出,就运行时性能而言,这两个函数都不优于另一个函数。

      但是,我认为前一个函数在可读性方面优于另一个函数,因为像这样的调用

      f( &a, &b );
      

      清楚地表示传递了对某个变量的引用(对我来说,这敲响了“这个对象可能被函数修改”的铃声)。采用引用而不是指针的版本看起来像

      f( a, b );
      

      看到a 在调用后发生了变化,我感到相当惊讶,因为我无法从调用中看出该变量是通过引用传递的。

      【讨论】:

        【解决方案9】:

        从性能的角度来看,任何有能力的编译器都应该解决这个问题,这在任何情况下都不太可能成为瓶颈。如果您真的在这么低的水平上工作,那么汇编代码分析和对真实数据的性能分析无论如何都将成为您工具包的重要组成部分。

        从维护的角度来看,你真的不应该允许某人在不检查它们是否为空的情况下将参数作为指针传递。

        所以你最终无缘无故地编写了大量的空检查代码。

        基本上来自:

        1. 创建对象
        2. 作为非常量引用传入

        收件人:

        1. 创建对象
        2. 获取对象的地址
        3. 传入地址
        4. 检查地址是否指向空
        5. 取消对非常量引用的引用,以便在整个函数中使用

        虽然编译器会解析出所有这些垃圾,但代码的阅读者不会。在扩展功能或计算代码的作用时,需要考虑额外的代码。还编写了更多代码,这增加了可能包含错误的行数。由于它是指针,因此这些错误更有可能是古怪的未定义行为错误。

        它还鼓励较弱的程序员编写这样的代码:

        int* a = new int(4); // Don't understand why this has to be a pointer
        int* b = new int(5); // Don't understand why this has to be a pointer
        MyFunc2(a, b);
        int& a_r = *a;
        int& b_r = *b;
        

        是的,这很糟糕,但我从不太了解指针模型的新程序员那里看到了这一点。

        同时,考虑到潜在的损失,我认为整个“我可以在不查看实际标题的情况下看到它是否会被修改”有点虚假优势。如果您不能立即从代码的上下文中分辨出哪些是输出参数,那么这就是您的问题,再多的指针也救不了您。如果你必须有一个 & 来标识你的输出参数,我可以介绍一下:

        MyFunc2(/*&*/a, /*&*/b)
        

        & 符号的所有“可读性”,没有相关的指针风险。

        但是,在维护方面,一致性为王。如果有你正在集成的现有代码,它作为指针传递(例如类或库的其他函数),那么没有充分的理由成为一个疯狂的反叛者并走自己的路。这真的会引起混乱。

        【讨论】:

          【解决方案10】:

          您应该看到为目标机器生成的汇编代码...考虑到函数调用总是在恒定时间内完成,而在实际机器上该时间真的可以忽略不计...

          【讨论】:

            【解决方案11】:

            如果您需要执行以下操作来记住函数上的“a”将被更改。什么会阻止您反转具有相同类型的参数或犯一些令人讨厌的错误,当您必须调用某个函数时,您必须将原型保存在您的内存中,或者使用一些在工具提示上显示它的 IDE。没有任何借口,我不喜欢引用,因为我不知道函数是否会改变它,这不是一个有效的参数。

            MyFunc2(/*&*/a, /*&*/b)
            

            【讨论】:

              猜你喜欢
              • 2011-11-23
              • 2019-05-20
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-06-17
              • 2021-07-25
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多