【问题标题】:Are there benefits of passing by pointer over passing by reference in C++?在 C++ 中通过指针传递比通过引用传递有什么好处吗?
【发布时间】:2010-09-24 23:49:33
【问题描述】:

在 C++ 中,指针传递比引用传递有什么好处?

最近,我看到了许多选择通过指针传递函数参数而不是通过引用传递的示例。这样做有什么好处吗?

例子:

func(SPRITE *x);

调用

func(&mySprite);

对比

func(SPRITE &x);

调用

func(mySprite);

【问题讨论】:

  • 不要忘记new 来创建指针以及由此产生的所有权问题。

标签: c++ pointers parameter-passing pass-by-reference


【解决方案1】:

指针传递

  • 调用者必须获取地址 -> 不透明
  • 可以提供 0 值来表示 nothing。这可用于提供可选参数。

通过引用传递

  • 调用者只是传递对象 -> 透明的。必须用于运算符重载,因为指针类型的重载是不可能的(指针是内置类型)。所以你不能用指针做string s = &str1 + &str2;
  • 不可能有 0 值 -> 调用函数不必检查它们
  • 对 const 的引用也接受临时对象:void f(const T& t); ... f(T(a, b, c));,不能像那样使用指针,因为您不能获取临时对象的地址。
  • 最后但同样重要的是,引用更易于使用 -> 出现错误的机会更少。

【讨论】:

  • 通过指针传递也会引发“所有权是否转移?”题。引用不是这种情况。
  • 我不同意“减少错误的机会”。当检查调用站点并且读者看到“foo(&s)”时,很明显 s 可以被修改。当您阅读“foo(s)”时,完全不清楚 s 是否可以修改。这是错误的主要来源。也许某类错误的可能性较小,但总的来说,通过引用传递是错误的巨大来源。
  • “透明”是什么意思?
  • @Gbert90,如果你在调用站点看到 foo(&a),你就知道 foo() 采用指针类型。如果你看到 foo(a),你不知道它是否需要引用。
  • @MichaelJ.Davenport - 在您的解释中,您建议“透明”表示类似于“显然调用者正在传递指针,但不明显调用者正在传递引用”。在 Johannes 的帖子中,他说“通过指针传递 - 调用者必须获取地址 -> 不透明”和“通过引用传递 - 调用者只是传递对象 -> 透明” - 这与您所说的几乎相反.我认为 Gbert90 的问题“你所说的“透明”是什么意思”仍然有效。
【解决方案2】:

指针可以接收NULL参数,引用参数不能。如果您有可能想要传递“无对象”,请使用指针而不是引用。

此外,通过指针传递可以让您在调用站点显式查看对象是通过值传递还是通过引用传递:

// Is mySprite passed by value or by reference?  You can't tell 
// without looking at the definition of func()
func(mySprite);

// func2 passes "by pointer" - no need to look up function definition
func2(&mySprite);

【讨论】:

  • 答案不完整。使用指针不会授权使用临时/提升对象,也不会授权使用指向对象作为类似堆栈的对象。并且它会建议参数可以为 NULL,而在大多数情况下,应该禁止 NULL 值。阅读 litb 的答案以获得完整的答案。
  • 第二个函数调用曾经被注释为func2 passes by reference。虽然我很欣赏您的意思是从高级角度“通过引用”传递,通过在代码级角度传递指针来实现,但这非常令人困惑(请参阅stackoverflow.com/questions/13382356/…)。
  • 我就是不买这个。是的,你传入了一个指针,因此它必须是一个输出参数,因为指向的不能是 const?
  • @JonWheelock:不,C 根本没有传递引用。 func(int& a) 在任何版本的标准中都不是有效的 C。您可能无意中将文件编译为 C++。
  • 引用参数可以接收 NULL,@AdamRosenfield。将其传递为func(*NULL)。然后在函数内部,使用if (&x == NULL) 进行测试。我想这看起来很难看,但是指针和引用参数之间的区别是语法糖。
【解决方案3】:

我喜欢来自“cplusplus.com”的一篇文章的推理:

  1. 当函数不想修改参数且值易于复制时传值(ints、doubles、char、bool等...简单类型。std::string、std:: vector 和所有其他 STL 容器都不是简单类型。)

  2. 当值复制成本高且函数不想修改指向的值且 NULL 是函数处理的有效预期值时,通过 const 指针传递。

  3. 当值复制成本高且函数想要修改指向的值且 NULL 是函数处理的有效预期值时,通过非常量指针传递。

  4. 当值复制成本高且函数不想修改引用的值时,通过 const 引用传递,如果使用指针,NULL 将不是有效值。

    李>
  5. 当值复制成本高且函数想要修改引用的值时,通过非连续引用传递,如果使用指针代替,NULL 将不是有效值。

    李>
  6. 在编写模板函数时,没有一个明确的答案,因为有一些权衡需要考虑,这超出了本讨论的范围,但只要说大多数模板函数通过以下方式获取参数就足够了值或 (const) 引用,但是因为迭代器语法类似于指针的语法(星号表示“取消引用”),任何期望迭代器作为参数的模板函数也将默认接受指针(并且不检查 NULL,因为 NULL迭代器概念有不同的语法)。

http://www.cplusplus.com/articles/z6vU7k9E/

我从中得到的是,选择使用指针或引用参数的主要区别在于 NULL 是否是可接受的值。就是这样。

值是输入、输出、可修改等,毕竟应该在关于函数的文档/cmets中。

【讨论】:

  • 是的,对我来说,与 NULL 相关的术语是这里的主要关注点。谢谢引用..
【解决方案4】:

Allen Holub 的“足够的绳子射中自己的脚”列出了以下 2 条规则:

120. Reference arguments should always be `const`
121. Never use references as outputs, use pointers

他列出了向 C++ 添加引用的几个原因:

  • 它们是定义复制构造函数所必需的
  • 它们是运算符重载所必需的
  • const 引用允许您在避免复制的同时拥有按值传递的语义

他的主要观点是不应将引用用作“输出”参数,因为在调用站点没有指示参数是引用参数还是值参数。所以他的规则是只使用const 引用作为参数。

就个人而言,我认为这是一个很好的经验法则,因为它可以更清楚地说明参数何时是输出参数。然而,虽然我个人总体上同意这一点,但我确实允许自己被团队中其他人的意见所左右,如果他们主张将输出参数作为参考(一些开发人员非常喜欢它们)。

【讨论】:

  • 我在那个论点中的立场是,如果函数名称很明显,无需检查文档,参数将被修改,那么非常量引用是可以的。所以我个人允许“getDetails(DetailStruct &result)”。那里的指针引发了 NULL 输入的丑陋可能性。
  • 这是误导。即使有些人不喜欢引用,它们也是语言的重要组成部分,应该照此使用。这种推理就像说不要使用模板,你总是可以使用 void* 的容器来存储任何类型。阅读 litb 的答案。
  • 我不明白这有什么误导性——有时需要参考,有时即使可以,最佳实践也可能建议不要使用它们。对于语言的任何特性都可以这样说——继承、非成员朋友、运算符重载、MI 等等......
  • 顺便说一句,我同意 litb 的回答非常好,而且肯定比这个更全面 - 我只是选择专注于讨论避免使用引用作为输出参数的理由。
  • 这个规则用在google c++ style guide:google-styleguide.googlecode.com/svn/trunk/…
【解决方案5】:

对前面帖子的澄清:


引用是获得非空指针的保证。 (虽然我们经常这样对待它们。)

虽然代码非常糟糕,就像带你离开木棚的糟糕代码一样,但以下代码将编译并运行:(至少在我的编译器下。)

bool test( int & a)
{
  return (&a) == (int *) NULL;
}

int
main()
{
  int * i = (int *)NULL;
  cout << ( test(*i) ) << endl;
};

我对引用的真正问题在于其他程序员,以下称为 IDIOTS,他们在构造函数中分配,在析构函数中释放,并且无法提供复制构造函数或 operator=()。

foo(BAR bar)foo(BAR & bar) 之间突然有了天壤之别。 (自动按位复制操作被调用。析构函数中的释放被调用两次。)

谢天谢地,现代编译器会采用这种对同一指针的双重释放。 15年前,他们没有。 (在 gcc/g++ 下,使用 setenv MALLOC_CHECK_ 0 重新访问旧方法。)在 DEC UNIX 下,相同的内存被分配给两个不同的对象。那里有很多调试乐趣...


更实际:

  • 引用隐藏了您正在更改存储在其他地方的数据。
  • 很容易将引用与复制对象混淆。
  • 指针让它一目了然!

【讨论】:

  • 这不是函数或引用的问题。你违反了语言规则。自己取消引用空指针已经是未定义的行为。 “引用并不能保证获得非空指针。”:标准本身就是这样说的。其他方式构成未定义的行为。
  • 我同意 litb。虽然是真的,但您向我们展示的代码比其他任何东西都更具破坏性。有很多方法可以破坏任何东西,包括“引用”和“指针”符号。
  • 我确实说过这是“带你走出木棚坏代码”!同样,您也可以拥有 i=new FOO;删除我;测试(*i);另一个(不幸的是,很常见)悬空指针/引用事件。
  • 问题实际上不是 dereferencing NULL ,而是 USING 那个取消引用(null)的对象。因此,从语言实现的角度来看,指针和引用之间确实没有区别(除了语法之外)。是有不同期望的用户。
  • 不管你对返回的引用做什么,当你说*i时,你的程序就会有未定义的行为。例如,编译器可以看到这段代码并假设“好的,这段代码在所有代码路径中都有未定义的行为,所以这整个函数一定是不可访问的。”然后它将假设没有采用导致此功能的所有分支。这是定期执行的优化。
【解决方案6】:

就表达意图而言,这里的大多数答案都未能解决在函数签名中具有原始指针的固有歧义。问题如下:

  • 调用者不知道指针是指向单个对象,还是指向对象“数组”的开头。

  • 调用者不知道指针是否“拥有”它所指向的内存。 IE,该功能是否应该释放内存。 (foo(new int) - 这是内存泄漏吗?)。

  • 调用者不知道nullptr是否可以安全地传递给函数。

所有这些问题都通过引用解决:

  • 引用始终指向单个对象。

  • 引用从不拥有它们所引用的内存,它们只是内存的视图。

  • 引用不能为空。

这使得引用更适合一般用途。然而,参考并不完美 - 有几个主要问题需要考虑。

  • 没有显式间接。这不是原始指针的问题,因为我们必须使用&amp; 运算符来表明我们确实在传递一个指针。例如,int a = 5; foo(a); 这里根本不清楚 a 是通过引用传递的,并且可以修改。
  • 可空性。当我们实际上希望我们的引用可以为空时,指针的这种弱点也可以成为一种优势。看到 std::optional&lt;T&amp;&gt; 无效(有充分的理由),指针为我们提供了您想要的可空性。

所以看来,当我们想要一个显式间接的可空引用时,我们应该使用T* 对吗?错了!

抽象

在我们对可空性绝望的情况下,我们可能会找到T*,而忽略前面列出的所有缺点和语义模糊性。相反,我们应该追求 C++ 最擅长的:抽象。如果我们简单地编写一个包装指针的类,我们将获得表现力,以及可空性和显式间接性。

template <typename T>
struct optional_ref {
  optional_ref() : ptr(nullptr) {}
  optional_ref(T* t) : ptr(t) {}
  optional_ref(std::nullptr_t) : ptr(nullptr) {}

  T& get() const {
    return *ptr;
  }

  explicit operator bool() const {
    return bool(ptr);
  }

private:
  T* ptr;
};

这是我能想到的最简单的界面,但它可以有效地完成工作。 它允许初始化引用、检查值是否存在以及访问该值。我们可以这样使用它:

void foo(optional_ref<int> x) {
  if (x) {
    auto y = x.get();
    // use y here
  }
}

int x = 5;
foo(&x); // explicit indirection here
foo(nullptr); // nullability

我们已经实现了我们的目标!现在让我们看看与原始指针相比的好处。

  • 界面清楚地表明该引用应该只引用一个对象。
  • 显然它不拥有它所指的内存,因为它没有用户定义的析构函数,也没有删除内存的方法。
  • 调用者知道nullptr 可以传入,因为函数作者明确要求optional_ref

我们可以从这里使接口更复杂,例如添加相等运算符,一个单子get_ormap 接口,一个获取值或抛出异常的方法,constexpr 支持。这可以由您完成。

总之,不要使用原始指针,而是推理这些指针在您的代码中的实际含义,并利用标准库抽象或编写您自己的抽象。这将显着改进您的代码。

【讨论】:

    【解决方案7】:

    不是真的。在内部,通过引用传递实际上是通过传递被引用对象的地址来执行的。因此,通过指针传递确实没有任何效率提升。

    但是,通过引用传递确实有一个好处。保证您有一个正在传入的任何对象/类型的实例。如果您传入一个指针,那么您将面临收到 NULL 指针的风险。通过使用按引用传递,您将隐式 NULL 检查向上推到函数的调用者。

    【讨论】:

    • 这既是优点也是缺点。许多 API 使用 NULL 指针来表示有用的东西(即 NULL timespec 永远等待,而 value 意味着等待那么久)。
    • @Brian:我不想吹毛求疵,但是:我会说一个是保证在获取时获取实例一个参考。如果函数的调用者取消引用一个被调用者不知道的悬空指针,悬空引用仍然是可能的。
    • 有时您甚至可以通过使用引用来获得性能,因为它们不需要占用任何存储空间并且没有为自己分配任何地址。不需要间接。
    • 包含悬空引用的程序不是有效的 C++。因此,是的,代码可以假设所有引用都是有效的。
    • 我绝对可以取消引用一个空指针,编译器无法判断...如果编译器无法判断它是“无效的 C++”,它真的无效吗?
    猜你喜欢
    • 2020-05-14
    • 2016-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-14
    • 2012-03-03
    • 2010-10-10
    相关资源
    最近更新 更多