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

Are there benefits of passing by pointer over passing by reference in C++?

提问人:Matt Pascoe 提问时间:12/3/2008 最后编辑:SobiMatt Pascoe 更新时间:5/14/2020 访问量:156344

问:

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

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

例:

func(SPRITE *x);

用一个电话

func(&mySprite);

HSP的

func(SPRITE &x);

用一个电话

func(mySprite);
C++ 指针参数 传递 传递引用

评论

0赞 Martin York 12/7/2018
不要忘记创建指针以及由此产生的所有权问题。new

答:

273赞 Johannes Schaub - litb 12/3/2008 #1

通过指针传递

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

通过引用传递

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

评论

11赞 Frerich Raabe 10/31/2009
通过指针也会引发“所有权是否转移”的问题。参考文献并非如此。
56赞 William Pursell 11/21/2011
我不同意“减少错误的机会”。当检查调用站点时,读者看到“foo( &s )”,很明显 s 可能被修改了。当你读到“foo( s )”时,根本不清楚 s 是否可以被修改。这是错误的主要来源。也许出现某一类错误的可能性较小,但总的来说,通过引用传递是一个巨大的错误来源。
32赞 Gbert90 5/14/2012
你说的“透明”是什么意思?
5赞 Michael J. Davenport 9/15/2012
@Gbert90,如果你在调用站点上看到 foo(&a),你就知道 foo() 采用指针类型。如果你看到 foo(a),你不知道它是否需要引用。
4赞 Happy Green Kid Naps 11/17/2012
@MichaelJ.Davenport -- 在你的解释中,你建议“透明”的意思是“很明显,调用者正在传递指针,但调用者正在传递引用并不明显”。在 Johannes 的帖子中,他说“通过指针传递 -- 调用者必须获取地址 - >不透明”和“通过引用传递 -- 调用者只是传递对象 - >透明的” -- 这几乎与你所说的相反。我认为 Gbert90 的问题“你所说的”透明“是什么意思”仍然有效。
253赞 Adam Rosenfield 12/3/2008 #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);

评论

26赞 paercebal 12/3/2008
不完整的答案。使用指针不会授权使用临时/提升对象,也不会授权将指向对象用作类似堆栈的对象。它将建议在大多数情况下应禁止 NULL 值时,参数可以为 NULL。阅读 litb 的答案以获得完整的答案。
4赞 Adam Rosenfield 10/12/2015
@JonWheelock:不,C 根本没有通过引用。 在任何版本的标准中都不是有效的 C。您可能不小心将文件编译为 C++。func(int& a)
1赞 Jon Wheelock 10/12/2015
你是对的。我的文件名是 .cpp,更改为 .c 后出现错误。
1赞 Ken Jackson 2/7/2019
引用参数可以接收 NULL,@AdamRosenfield。将其传递为 .然后在函数内部,用 .我想这看起来很丑,但指针和引用参数之间的区别是句法糖。func(*NULL)if (&x == NULL)
1赞 stucash 3/3/2023
每次阅读 C++ 问题的评论部分时,我都会惊呆了;我至少有 90% 的把握,C++ 问题的评论部分实际上是辩论者最激烈的地方:非常聪明/尖锐的评论,最多涉及 1% 的 EQ。“不完整的答案”肯定是对的,但我只是觉得有更好的说法。
3赞 Brian 12/3/2008 #3

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

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

评论

1赞 Greg Rogers 12/3/2008
这既是优点也是缺点。许多 API 使用 NULL 指针来表示有用的东西(即 NULL timespec wait forever,而 value 表示等待那么久)。
0赞 Johannes Schaub - litb 12/3/2008
有时,您甚至可以通过使用引用来获得性能,因为它们不需要占用任何存储空间,也不需要为自己分配任何地址。无需间接。
1赞 foraidt 12/3/2008
@Brian:我不想吹毛求疵,但是:我不会说在获得参考时一定会得到一个实例。如果函数的调用方取消引用被调用方无法知道的悬空指针,则仍然存在悬空引用。
0赞 Konrad Rudolph 12/3/2008
包含无关联引用的程序不是有效的 C++。因此,是的,代码可以假定所有引用都有效。
2赞 rmeador 12/3/2008
我绝对可以取消引用空指针,编译器将无法分辨......如果编译器无法判断它是“无效的C++”,它真的无效吗?
72赞 Michael Burr 12/3/2008 #4

艾伦·霍鲁布(Allen Holub)的《足以射自己的脚的绳索》(Enough Rope to Shoot Yourself in the Foot)列出了以下2条规则:

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

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

  • 它们是定义复制构造函数所必需的
  • 它们对于操作员过载是必需的
  • const引用允许您具有按值传递语义,同时避免复制

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

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

评论

10赞 Steve Jessop 12/3/2008
我在这个论点中的立场是,如果函数名称在不检查文档的情况下完全明显地表明参数将被修改,那么非常量引用是可以的。所以就我个人而言,我会允许“getDetails(DetailStruct &result)”。那里的指针提出了 NULL 输入的丑陋可能性。
3赞 David Rodríguez - dribeas 12/3/2008
这是误导性的。即使有些人不喜欢引用,它们也是语言的重要组成部分,应该这样使用。这种推理就像在说,不要使用模板,你总是可以使用 void* 的容器来存储任何类型。阅读 litb 的答案。
5赞 Michael Burr 12/3/2008
我看不出这有什么误导性 - 有时需要参考,有时最佳实践可能会建议即使您可以使用它们也不要使用它们。对于语言的任何特性都可以这样说——继承、非成员好友、运算符重载、MI 等......
0赞 Michael Burr 12/3/2008
顺便说一句,我同意 litb 的答案非常好,而且肯定比这个更全面——我只是选择专注于讨论避免使用引用作为输出参数的基本原理。
1赞 Anton Daneyko 12/1/2010
此规则用于 google c++ 风格指南:google-styleguide.googlecode.com/svn/trunk/...
10赞 Mr.Ree 12/3/2008 #5

对上述帖子的澄清:


引用不能保证获得非 null 指针。(尽管我们经常这样对待它们。

虽然代码非常糟糕,就像把你带到木棚代码后面一样,但以下内容将编译并运行:(至少在我的编译器下。

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 下,同一内存被分配给两个不同的对象。那里有很多调试的乐趣......


更实际的是:

  • 引用隐藏了您正在更改存储在其他地方的数据。
  • 很容易将 Reference 与 Copied 对象混淆。
  • 指针使它变得显而易见!

评论

18赞 Johannes Schaub - litb 12/3/2008
这不是函数或引用的问题。你违反了语言规则。取消引用 null 指针本身已经是未定义的行为。“引用不能保证获得非空指针”:标准本身说它们是。其他方式构成未定义的行为。
1赞 paercebal 12/3/2008
我同意litb。虽然是真的,但您向我们展示的代码比其他任何事情都更具破坏性。有一些方法可以破坏任何东西,包括“引用”和“指针”符号。
1赞 Mr.Ree 12/4/2008
我确实说过这是“把你带到木棚坏代码后面”!同样,您也可以有 i=new FOO;删去i;测试(*i);另一个(不幸的是常见的)悬空指针/引用事件。
1赞 Mr.Ree 12/4/2008
实际上,问题不在于取消引用 NULL,而在于使用取消引用的 (null) 对象。因此,从语言实现的角度来看,指针和引用之间实际上没有区别(语法除外)。是用户有不同的期望。
2赞 David Stone 6/28/2015
不管你对返回的引用做了什么,在你说的那一刻,你的程序都有未定义的行为。例如,编译器可以看到此代码并假设“好吧,此代码在所有代码路径中都有未定义的行为,因此整个函数必须无法访问。然后,它将假设所有导致此函数的分支都没有被采用。这是定期执行的优化。*i
105赞 R. Navega 10/6/2015 #6

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

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

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

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

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

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

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

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

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

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

评论

0赞 binaryguy 1/22/2016
是的,对我来说,与 NULL 相关的术语是这里的主要关注点。谢谢报价..
0赞 Chukwujiobi Canon 7/31/2023
还有一件事:多态性。
9赞 themeeman 5/14/2020 #7

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

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

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

  • 调用方不知道是否可以安全地传递到函数中。nullptr

所有这些问题都可以通过参考来解决:

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

  • 引用从不拥有它们所指的记忆,它们只是对记忆的一种看法。

  • 引用不能为 null。

这使得参考文献成为一般用途的更好候选者。然而,参考文献并不完美 - 有几个主要问题需要考虑。

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

因此,当我们想要一个具有明确间接的可为空的引用时,我们似乎应该寻求权利?错!T*

抽象

在我们对可空性的绝望中,我们可能会伸手去拿,并简单地忽略前面列出的所有缺点和语义歧义。相反,我们应该追求C++最擅长的东西:抽象。如果我们简单地编写一个环绕指针的类,我们就会获得表现力,以及可空性和显式间接性。T*

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

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

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

我们可以从这里使接口更复杂,例如添加相等运算符、monadic 和 interface、获取值或抛出异常的方法 support。这可以由你来完成。get_ormapconstexpr

总之,与其使用原始指针,不如推理这些指针在代码中的实际含义,然后利用标准库抽象或编写自己的抽象。这将显著改进您的代码。