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

281

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

最近我看到了一些通过指针传递函数参数而不是通过引用传递函数参数的例子。这样做有什么好处吗?

例如:

func(SPRITE *x);

使用一次调用

func(&mySprite);

对比。

func(SPRITE &x);
使用调用函数
func(mySprite);

别忘了使用 new 来创建指针以及由此产生的所有权问题。 - Martin York
7个回答

273

通过指针传递

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

通过引用传递

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

11
通过指针传递参数还会引起“是否转移所有权”的问题,但对于引用则不是这样。 - Frerich Raabe
56
我不同意“减少漏洞的机会”。当检查调用站点并且读者看到“foo(&s)”时,立即清楚s可能被修改。但是当你阅读“foo(s)”时,不清楚s是否可能被修改。这是漏洞的主要来源。也许某些类别的漏洞减少了,但总体来说,通过引用传递是漏洞的一个巨大来源。 - William Pursell
32
“Transparent” 的意思是“透明的”,指某物的表面或内部能够明显地看到或识别。在非物质事物中,指的是行为或组织具有公开、清晰和坦率的特点。 - Gbert90
5
如果你在调用 foo(&a) 的代码中看到这样的写法,那么你就知道 foo() 接受一个指针类型作为参数。如果你看到的是 foo(a),那么你就不知道它是接受引用还是其他类型的参数。 - Michael J. Davenport
4
在你的解释中,你建议“透明”意味着类似于“很明显调用程序正在传递指针,但不明显调用程序正在传递引用”。在Johannes的帖子中,他说“按指针传递--调用者必须取地址->不透明”,“按引用传递--调用者只需传递对象->透明”,这与你所说的几乎相反。我认为Gbert90的问题“你所说的“透明”是什么意思”仍然有效。 - Happy Green Kid Naps
显示剩余8条评论

253

指针可以接收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
不完整的答案。使用指针不能授权使用临时/晋升对象,也不能将指向对象用作堆栈式对象。它会暗示参数可以为NULL,但大多数情况下应禁止使用NULL值。请阅读litb的答案以获取完整的答案。 - paercebal
4
不,C语言根本没有按引用传递参数。在任何标准版本中都不支持func(int& a)这种写法。你可能是因为编译文件时出错,将其误认为是C++。 - Adam Rosenfield
1
你说得对。我的文件名是.cpp,改成.c后出现了错误。 - Jon Wheelock
1
一个引用参数可以接收NULL,@AdamRosenfield。将其作为func(*NULL)传递。然后在函数内部,使用if(&x == NULL)进行测试。我想这看起来很丑陋,但指针和引用参数之间的区别只是语法糖。 - Ken Jackson
1
每次阅读C++问题的评论区,我都感到震惊;我至少有90%的把握认为,C++问题的评论区实际上是最激烈的辩论场所:非常聪明/尖锐的评论直指问题核心,几乎没有1%的情商参与。 "不完整的答案"肯定是正确的,但我只是觉得有更好的表达方式。 - stucash
显示剩余7条评论

106
我喜欢cplusplus.com上一篇文章的推理:
  1. 当函数不想修改参数并且这个值容易复制(int、double、char、bool等简单类型以及std::string、std::vector和所有其他STL容器都不是简单类型)时,使用按值传递。

  2. 当值的复制代价高昂且函数不想修改指向的值且NULL是函数处理的有效预期值时,请使用const指针传递。

  3. 当值的复制代价高昂且函数想要修改指向的值且NULL是函数处理的有效预期值时,请使用非const指针传递。

  4. 当值的复制代价高昂且函数不想修改所引用的值且如果使用指针,则NULL不是一个有效的值时,请使用const引用传递。

  5. 当值的复制代价高昂且函数想要修改所引用的值且如果使用指针,则NULL不是一个有效的值时,请使用非const引用传递。

  6. 在编写模板函数时,没有明确的答案,因为有一些权衡需要考虑,这超出了本讨论的范围,但可以简单地说,大多数模板函数通过值或(const) 引用获取参数,但由于迭代器语法与指针语法类似(通过星号进行“解引用”),任何期望迭代器作为参数的模板函数也将默认接受指针(而不检查NULL,因为NULL迭代器概念具有不同的语法)。

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

我从中得出的结论是,在选择使用指针还是引用参数时,主要区别在于NULL是否是可接受的值。仅此而已。无论值是输入、输出、可修改等,都应该在函数的文档/注释中说明。

是的,对我来说,与NULL相关的术语是主要关注点。谢谢您的引用。 - binaryguy
还有一件事:多态性。 - Drizzle

72

Allen Holub的“Enough Rope to Shoot Yourself in the Foot”列出了以下两条规则:

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

他列举了为什么在C++中添加引用的几个原因:

  • 他们有必要定义复制构造函数
  • 它们对于运算符重载是必需的
  • const 引用允许您在避免复制的情况下具有按值传递语义

他的主要观点是,应该不将引用用作“输出”参数,因为在调用站点没有表明参数是引用还是值参数。因此,他的规则是仅将 const 引用用作参数。

就个人而言,我认为这是一个很好的经验法则,因为它使参数是否为输出参数更加清晰。然而,虽然我个人总体上同意这一点,但如果我的团队中的其他人争辩要将输出参数作为引用,则会被说服(一些开发人员非常喜欢它们)。


10
在那个争论中,我的立场是如果函数名让人完全明白参数会被修改(不需要查看文档),那么非 const 引用就可以使用。因此,我个人认为允许使用 "getDetails(DetailStruct &result)"。在这种情况下,使用指针会带来 NULL 输入的丑陋可能性。 - Steve Jessop
3
这是具有误导性的。即使有些人不喜欢引用,但它们是语言的重要组成部分,应该按照其作用来使用。这种推理方式就像说不要使用模板,你总可以使用void*容器来存储任何类型。请阅读litb的回答。 - David Rodríguez - dribeas
5
我不认为这会引导误解——有时需要引用,而有时候即使可以使用最佳实践也可能建议不使用。对于语言的任何特性都是如此——继承、非成员友元、运算符重载、MI等等…… - Michael Burr
1
这个规则适用于Google C++风格指南:http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Reference_Arguments - Anton Daneyko
我认为这些规则有一个非常重要的例外:swap函数在复制-交换惯用法中扮演着非常重要的角色,但是如果不将其参数声明为非const引用,则无法完成其预期功能。在此处使用指针会很麻烦,并且容易打破其无异常承诺。 - Earth Engine
显示剩余3条评论

10

对前面的帖子进行澄清:


引用并不能保证获得非空指针。(尽管我们通常将它们视为这样。)

虽然下面的代码非常糟糕,简直是能把你带到木棚后面去的坏代码,但以下代码将编译并运行:(至少在我的编译器下如此。)

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

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

我的真正问题在于引用,而是与其他程序员(以下简称“白痴”)有关,在构造函数中分配,在析构函数中释放,并且未提供复制构造函数或operator=()。

突然之间,foo(BAR bar)foo(BAR&bar)之间存在很大的差异。 (会自动调用位拷贝操作。析构函数中的释放会被调用两次。)

幸运的是,现代编译器会捕获到同一指针的双重释放。15年前他们不会。(在gcc/g++下,使用setenv MALLOC_CHECK_ 0可以重新访问旧方法。)结果,在DEC UNIX下,同一内存被分配给两个不同的对象。这里有很多调试乐趣...


更加实际:

  • 引用隐藏了您正在更改的存储在其他地方的数据。
  • 很容易混淆引用和复制的对象。
  • 指针使其变得明显!

18
这并不是函数或引用的问题,而是违反了语言规则。解除一个空指针本身就是未定义行为。“引用并不能保证得到一个非空指针。”:标准本身就这么说。其他方式都构成未定义行为。 - Johannes Schaub - litb
1
我同意litb的观点。尽管如此,您展示给我们的代码更像是破坏行为,而不是其他什么。有很多方法可以破坏任何东西,包括“引用”和“指针”符号。 - paercebal
1
我确实说过这是“把你带到木棚后面的糟糕代码”!同样,你也可以有i=new FOO; delete i; test(*i); 这是另一种(不幸常见的)悬空指针/引用出现。 - Mr.Ree
1
实际上,问题并不在于解除引用NULL,而是在于使用那个被解除引用的(null)对象。因此,从语言实现的角度来看,指针和引用之间实际上没有区别(除了语法)。这是用户有不同的期望。 - Mr.Ree
2
无论你如何使用返回的引用,一旦你输入 *i,你的程序就具有未定义的行为。例如,编译器可以看到这段代码并假设“好的,在所有代码路径中这个代码都有未定义的行为,所以整个函数必须是不可达的。”然后它会假设所有通往该函数的分支都没有被执行。这是一个经常执行的优化。 - David Stone
显示剩余4条评论

9

这里的大多数答案无法解决在函数签名中使用原始指针所固有的歧义性问题,表达意图方面存在问题,它们具体如下:

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

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

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

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

  • 引用总是引用单个对象。

  • 引用从不拥有它们引用的内存,它们仅仅是对内存的一种视图。

  • 引用不能为null。

这使得引用成为更好的通用选择。然而,引用并非完美——需要考虑几个主要问题。

  • 没有显式的间接引用。这不是原始指针的问题,因为我们必须使用&运算符来表明我们确实传递了一个指针。例如,int a = 5; foo(a);在这里根本无法清楚地表明a是按引用传递的,可能会被修改。
  • 可空性。指针的这种弱点也可以是一种优势,当我们实际上希望我们的引用可为空时。鉴于std::optional<T&>无效(因为有很好的理由),指针给了我们所需的可空性。

因此,当我们需要具有显式间接引用的可空引用时,我们应该使用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支持。这可以由您完成。

总之,不要使用原始指针,考虑一下这些指针在您的代码中实际意味着什么,并利用标准库抽象或编写自己的抽象来改进代码。


3

其实不是这样的。在内部,通过引用传递本质上是传递所引用对象的地址。因此,通过传递指针来获得效率上的优势并不存在。

然而,通过引用传递确实有一个好处。您可以确保传递的任何对象/类型都有一个实例。如果传递指针,则有可能收到空指针。通过使用按引用传递,您将隐式地将 NULL 检查推迟至调用者函数的更高层次。


1
这既是优点也是缺点。许多API使用NULL指针表示某些有用的含义(例如,NULL timespec表示永久等待,而值则表示等待那么长时间)。 - Greg Rogers
1
@Brian:我不想挑剔,但是:我不会说在获取引用时一定会得到一个实例。如果函数的调用者取消引用悬空指针,那么仍然可能出现悬空引用,而被调用方无法知道。 - foraidt
包含悬空引用的程序不是有效的C++。因此,是的,代码可以假定所有引用都是有效的。 - Konrad Rudolph
2
我绝对可以取消引用空指针,编译器却无法察觉......如果编译器察觉不到这是“无效的C ++”,那它真的算无效吗? - rmeador
是的,即使编译器无法检测到,它仍然是无效的。仅仅创建一个悬空引用就会导致未定义的行为。您可以(好吧,您确实没有选择)在假定程序处于已定义状态的情况下编写代码。 - Steve Jessop
显示剩余2条评论

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