在 C++ 中通过指针传递比通过引用传递有什么好处?
最近,我看到了许多选择通过指针传递函数参数而不是通过引用传递的示例。这样做有好处吗?
例子:
func(SPRITE *x);
打电话给
func(&mySprite);
对比
func(SPRITE &x);
打电话给
func(mySprite);
在 C++ 中通过指针传递比通过引用传递有什么好处?
最近,我看到了许多选择通过指针传递函数参数而不是通过引用传递的示例。这样做有好处吗?
例子:
func(SPRITE *x);
打电话给
func(&mySprite);
对比
func(SPRITE &x);
打电话给
func(mySprite);
nothing
. 这可用于提供可选参数。string s = &str1 + &str2;
使用指针。void f(const T& t); ... f(T(a, b, c));
,不能像那样使用指针,因为您不能获取临时地址。指针可以接收 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);
我喜欢来自“cplusplus.com”的一篇文章的推理:
当函数不想修改参数且值易于复制时按值传递(ints、doubles、char、bool 等...简单类型。std::string、std::vector 和所有其他 STL容器不是简单的类型。)
当值复制成本高且函数不想修改指向的值且 NULL 是函数处理的有效预期值时,通过 const 指针传递。
当值复制成本很高并且函数想要修改指向的值并且 NULL 是函数处理的有效预期值时,通过非常量指针传递。
当值复制成本高且函数不想修改引用的值时,通过 const 引用传递,如果使用指针代替,NULL 将不是有效值。
当值复制成本高且函数想要修改引用的值时,通过非连续引用传递,如果使用指针代替,NULL 将不是有效值。
在编写模板函数时,没有一个明确的答案,因为有一些权衡需要考虑,这超出了本讨论的范围,但可以说大多数模板函数通过值或 (const) 引用来获取参数, 但是因为迭代器语法类似于指针的语法(星号表示“取消引用”),任何期望迭代器作为参数的模板函数也将默认接受指针(并且不检查 NULL,因为 NULL 迭代器概念具有不同的语法)。
我从中得到的是,选择使用指针或引用参数之间的主要区别在于 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;
};
我对引用的真正问题在于其他程序员,以下称为IDIOTS,他们在构造函数中分配,在析构函数中释放,并且无法提供复制构造函数或 operator=()。
突然之间foo(BAR bar)和foo(BAR & bar)之间有了天壤之别。(自动按位复制操作被调用。析构函数中的释放被调用两次。)
值得庆幸的是,现代编译器将采用相同指针的这种双重释放。15年前,他们没有。(在 gcc/g++ 下,使用setenv MALLOC_CHECK_ 0重新访问旧方法。)结果,在 DEC UNIX 下,相同的内存被分配给两个不同的对象。那里有很多调试乐趣......
更实际:
就表达意图而言,这里的大多数答案都未能解决在函数签名中具有原始指针的固有歧义。问题如下:
调用者不知道指针是指向单个对象,还是指向对象“数组”的开头。
调用者不知道指针是否“拥有”它指向的内存。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
我们可以从这里使接口变得更复杂,例如添加相等运算符、单子get_or
和map
接口、获取值或抛出异常的方法、constexpr
支持。这可以由你来完成。
总之,不要使用原始指针,而是推理这些指针在您的代码中的实际含义,并利用标准库抽象或编写您自己的抽象。这将显着改进您的代码。
并不真地。在内部,通过引用传递实际上是通过传递被引用对象的地址来执行的。因此,通过指针传递确实没有任何效率提升。
但是,通过引用传递确实有一个好处。保证您有一个正在传入的任何对象/类型的实例。如果您传入一个指针,那么您将面临收到 NULL 指针的风险。通过使用传递引用,您将隐式 NULL 检查向上推到函数的调用者。