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

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

例子:

func(SPRITE *x);

伴随着一声呼唤

func(&mySprite);

vs.

func(SPRITE &x);

伴随着一声呼唤

func(mySprite);

传递指针

调用者必须取地址->不透明 0值可以提供为没有任何意义。这可用于提供可选参数。

通过引用传递

Caller just passes the object -> transparent. Has to be used for operator overloading, since overloading for pointer types is not possible (pointers are builtin types). So you can't do string s = &str1 + &str2; using pointers. No 0 values possible -> Called function doesn't have to check for them Reference to const also accepts temporaries: void f(const T& t); ... f(T(a, b, c));, pointers cannot be used like that since you cannot take the address of a temporary. Last but not least, references are easier to use -> less chance for bugs.


指针可以接收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检查推到函数的调用者的上一层。


Allen Holub在《足够的绳子砸自己的脚》一书中列出了以下2条规则:

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

他列出了在c++中添加引用的几个原因:

它们是定义复制构造函数所必需的 它们是操作符重载所必需的 Const引用允许使用值传递语义,同时避免复制

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

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


以上职位说明:


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

而可怕的坏代码,就像把你带出木棚后面的坏代码一样,下面的代码将编译和运行:(至少在我的编译器下)。

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


更实际:

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


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

Pass by value when the function does not want to modify the parameter and the value is easy to copy (ints, doubles, char, bool, etc... simple types. std::string, std::vector, and all other STL containers are NOT simple types.) Pass by const pointer when the value is expensive to copy AND the function does not want to modify the value pointed to AND NULL is a valid, expected value that the function handles. Pass by non-const pointer when the value is expensive to copy AND the function wants to modify the value pointed to AND NULL is a valid, expected value that the function handles. Pass by const reference when the value is expensive to copy AND the function does not want to modify the value referred to AND NULL would not be a valid value if a pointer was used instead. Pass by non-cont reference when the value is expensive to copy AND the function wants to modify the value referred to AND NULL would not be a valid value if a pointer was used instead. When writing template functions, there isn't a clear-cut answer because there are a few tradeoffs to consider that are beyond the scope of this discussion, but suffice it to say that most template functions take their parameters by value or (const) reference, however because iterator syntax is similar to that of pointers (asterisk to "dereference"), any template function that expects iterators as arguments will also by default accept pointers as well (and not check for NULL since the NULL iterator concept has a different syntax). http://www.cplusplus.com/articles/z6vU7k9E/

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

不管这个值是输入的、输出的、可修改的等等,都应该在关于函数的文档/注释中。


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

调用者不知道指针是指向单个对象,还是指向对象“数组”的开始。 调用者不知道指针是否“拥有”它所指向的内存。IE的函数是否应该释放内存。(foo(new int) -这是内存泄漏吗?) 调用者不知道nullptr是否可以安全地传递给函数。

这些问题都是通过参考文献来解决的:

引用总是引用一个对象。 引用从来不拥有它们所引用的内存,它们仅仅是对内存的一个视图。 引用不能为空。

这使得参考资料更适合用于一般用途。然而,推荐信并不完美——有几个主要问题需要考虑。

没有明确的间接性。对于原始指针,这不是一个问题,因为我们必须使用&操作符来显示我们确实传递了一个指针。例如,int a = 5;foo ();这里完全不清楚a是通过引用传递的,是否可以修改。 Nullability。当我们希望引用为空时,指针的这个缺点也可以成为优点。鉴于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_or和map接口、获取值或抛出异常的方法、constexpr支持。那可以由你来做。

总之,与其使用原始指针,不如考虑这些指针在代码中的实际含义,并利用标准库抽象或编写自己的抽象。这将极大地改进您的代码。