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


225

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

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

例:

func(SPRITE *x);

打电话给

func(&mySprite);

func(SPRITE &x);

打电话给

func(mySprite);

不要忘记new创建一个指针以及随之而来的所有权问题。
马丁·约克

Answers:


216

指针可以接收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);

18
答案不完整。使用指针不会授权使用临时/提升对象,也不会授权将指针对象用作类似堆栈的对象。并建议在大多数情况下应禁止使用NULL值时,该参数可以为NULL。阅读litb的答案以获得完整答案。
paercebal,

第二个函数调用曾经被注释过func2 passes by reference。我理解您的意思是从高层角度“通过引用”传递(通过在代码层角度传递指针来实现),这非常令人困惑(请参阅stackoverflow.com/questions/13382356/…)。
Lightness Races in Orbit

我只是不买这个。是的,您传入了一个指针,因此它必须是一个输出参数,因为指向的对象不能是const?
deworde

我们没有在C中传递引用的方法吗?我正在使用最新版本的代码块(mingw),然后选择一个C项目。仍按引用传递(func(int&a))起作用。还是从C99或C11起可用?
乔恩·惠勒克

1
@JonWheelock:不,C根本没有传递引用。 func(int& a)在任何标准版本中都不是有效的C。您可能是无意间将文件编译为C ++。
亚当·罗森菲尔德

245

通过指针

  • 来电者必须输入地址->不透明
  • 可以提供一个0值来表示nothing。这可以用来提供可选参数。

通过参考

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

7
通过指针传递还会引发“所有权是否转移?” 题。引用不是这种情况。
弗里希·拉贝

43
我不同意“错误的机会更少”。当检查呼叫站点并且读者看到“ foo(&s)”时,很明显可以修改s。当您阅读“ foo(s)”时,是否可以修改s尚不清楚。这是错误的主要来源。也许出现某些类别的错误的机会较小,但是总的来说,通过引用传递是大量错误的来源。
威廉·珀塞尔

25
“透明”是什么意思?
Gbert90

2
@ Gbert90,如果在呼叫站点上看到foo(&a),则知道foo()采用指针类型。如果看到foo(a),则不知道它是否需要引用。
Michael J. Davenport 2012年

3
@ MichaelJ.Davenport -在您的解释中,建议“透明”表示“显然调用者正在传递指针,但不明显调用者正在传递引用”。在Johannes的帖子中,他说“通过指针传递-呼叫者必须采用地址->不透明”和“通过引用传递-呼叫者只是传递对象->透明”-几乎与您说的相反。我认为Gbert90的问题“透明”是什么意思仍然有效。
快乐的绿色孩子午睡

66

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

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

  2. 当复制该值非常昂贵并且该函数不想修改指向的值时,通过const指针传递,并且NULL是该函数处理的有效预期值。

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

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

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

  6. 在编写模板函数时,没有明确的答案,因为有一些折衷考虑的问题超出了本次讨论的范围,但是可以说大多数模板函数都按值或(const)引用获取其参数。 ,但是,因为迭代器语法与指针的语法相似(星号为“取消引用”),所以任何以迭代器作为参数的模板函数在默认情况下也将接受指针(并且不检查NULL,因为NULL迭代器的概念具有不同的语法) )。

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

我从中得到的是选择使用指针或引用参数之间的主要区别是NULL是否是可接受的值。而已。

毕竟,该值的输入,输出,可修改等应在有关功能的文档/注释中。


是的,对我来说,与NULL相关的术语是这里的主要问题。Thx的报价..
binaryguy

62

艾伦·霍鲁布(Allen Holub)的《用绳子足以使自己脚下开枪》列出了以下两个规则:

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

他列出了为什么将引用添加到C ++的几个原因:

  • 它们是定义副本构造函数所必需的
  • 它们是操作员重载所必需的
  • const 引用使您在避免复制的同时具有按值传递的语义

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

就我个人而言,我认为这是一个很好的经验法则,因为它可以使参数在何时成为输出参数变得更加清晰。但是,尽管我个人总体上同意这一点,但是如果他们争用输出参数作为参考,我确实会让自己受到团队中其他人的意见的影响(一些开发人员非常喜欢他们)。


8
我在该参数中的立场是,如果在不检查文档的情况下使函数名称完全显而易见,则将修改参数,那么可以使用非常量引用。因此,我个人允许使用“ getDetails(DetailStruct&result)”。那里的指针增加了输入NULL的可能性。
史蒂夫·杰索普

3
这是误导。即使有些人不喜欢参考文献,它们也是该语言的重要组成部分,应作为参考。这道理就像是说不要使用模板,您可以始终使用void *容器来存储任何类型。阅读litb的答案。
大卫·罗德里格斯(DavidRodríguez)-德里贝斯

4
我看不出这是怎么引起误解的-有时需要引用,并且有时最佳实践可能建议即使您可以也不使用它们。对于该语言的任何功能-继承,非成员朋友,运算符重载,MI等
Michael Burr

顺便说一句,我同意litb的答案非常好,而且肯定比这个答案更全面-我刚刚选择集中精力讨论避免使用引用作为输出参数的基本原理。
Michael Burr

1
这条规则在谷歌用于c ++风格指南:google-styleguide.googlecode.com/svn/trunk/...
安东Daneyko

9

对以上职位的说明:


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

在糟糕透顶的代码中(例如,将您带到错综复杂的代码后面),将编译并运行以下代码:(至少在我的编译器下)。

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


实际上:

  • 参考隐藏您正在更改其他地方存储的数据。
  • 将引用与复制的对象混淆很容易。
  • 指针很明显!

16
那不是函数或引用的问题。您正在违反语言规则。本身取消引用空指针已经是未定义的行为。“引用不能保证获得非空指针。”:标准本身说是这样。其他方式构成未定义的行为。
Johannes Schaub-litb

1
我同意litb。的确如此,但您向我们展示的代码比其他任何东西都更具破坏性。存在破坏任何事物的方法,包括“引用”和“指针”表示法。
paercebal

1
我确实说过,这是“把您带到经过精心设计的错误代码后面”!同样,您也可以拥有i = new FOO; 删除我 测试(* i); 另一个(不幸的是常见的)悬垂的指针/引用出现。
Ree先生

1
实际上,这并不是在问题上取消引用 NULL,而是使用已取消引用(空)的对象。这样,从语言实现的角度来看,指针和引用之间确实没有区别(语法除外)。拥有不同期望的是用户。
Ree先生

2
无论您对返回的引用进行什么操作,无论您说什么*i,程序都具有未定义的行为。例如,编译器可以看到此代码并假定“好,此代码在所有代码路径中均具有未定义的行为,因此,整个函数必须不可访问。” 然后,将假定不采用导致该功能的所有分支。这是定期执行的优化。
David Stone

5

就表达意图而言,此处的大多数答案都无法解决在函数签名中包含原始指针所固有的歧义。问题如下:

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

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

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

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

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

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

  • 引用不能为空。

这使参考成为更好的通用候选。但是,参考文献并不完美-需要考虑几个主要问题。

  • 没有显式间接。对于原始指针,这不是问题,因为我们必须使用&运算符来表明我们确实在传递指针。例如,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

从这里开始,我们可以使接口更加复杂,例如添加相等运算符,monadic get_ormap接口,获取值或引发异常的方法,constexpr支持。那可以由你完成。

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


3

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

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


1
那既是优点也是缺点。许多API使用NULL指针表示有用的东西(即NULL timespec永远等待,而值表示等待那么长时间)。
格雷格·罗杰斯

1
@Brian:我不想挑剔,但是:我不会说一定会在获得引用时保证获得实例。如果函数的调用者取消引用被调用者不知道的悬空指针,则仍然可以使用悬空引用。
foraidt

有时您甚至可以通过使用引用来获得性能,因为它们不需要占用任何存储空间,也不需要为其分配任何地址。不需要间接。
Johannes Schaub-litb

包含悬挂引用的程序不是有效的C ++。因此,是的,代码可以假定所有引用均有效。
Konrad Rudolph

2
我绝对可以取消引用空指针,编译器将无法判断...如果编译器无法判断其为“无效的C ++”,那么它真的无效吗?
rmeador
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.