提问人:Matt Pascoe 提问时间:12/3/2008 最后编辑:SobiMatt Pascoe 更新时间:5/14/2020 访问量:156344
在 C++ 中,按指针传递比按引用传递有什么好处吗?
Are there benefits of passing by pointer over passing by reference in C++?
问:
在 C++ 中,通过指针传递比通过引用传递有什么好处?
最近,我看到许多示例选择通过指针传递函数参数而不是通过引用传递。这样做有什么好处吗?
例:
func(SPRITE *x);
用一个电话
func(&mySprite);
HSP的
func(SPRITE &x);
用一个电话
func(mySprite);
答:
通过指针传递
- 呼叫者必须获取地址 - >不透明
- 可以提供 0 值来表示 。这可用于提供可选参数。
nothing
通过引用传递
- 调用方只是将对象传递>透明的。必须用于运算符重载,因为指针类型无法重载(指针是内置类型)。所以你不能使用指针。
string s = &str1 + &str2;
- 没有 0 可能的值 -> 被调用的函数不必检查它们
- 对 const 的引用也接受临时值:,指针不能这样使用,因为您不能获取临时值的地址。
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);
评论
func(int& a)
func(*NULL)
if (&x == NULL)
没有。在内部,按引用传递实质上是通过传递被引用对象的地址来执行的。因此,通过传递指针确实没有任何效率提升。
但是,通过引用确实有一个好处。保证您具有传入的任何对象/类型的实例。如果传入指针,则有收到 NULL 指针的风险。通过使用按引用传递,您将隐式 NULL 检查上推到函数的调用方。
评论
艾伦·霍鲁布(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
就个人而言,我认为这是一个很好的经验法则,因为它可以更清楚地说明参数何时是输出参数。然而,虽然我个人总体上同意这一点,但我确实允许自己被团队中其他人的意见所左右,如果他们主张将输出参数作为参考(一些开发人员非常喜欢它们)。
评论
对上述帖子的澄清:
引用不能保证获得非 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 对象混淆。
- 指针使它变得显而易见!
评论
*i
我喜欢“cplusplus.com”的一篇文章的推理:
当函数不想修改参数并且值易于复制时,按值传递(ints、doubles、char、bool 等简单类型。 std::string、std::vector 和所有其他 STL 容器都不是简单类型。
当值的复制成本很高且函数不想修改指向的值时,通过 const 指针传递 AND NULL 是函数处理的有效预期值。
当值的复制成本很高,并且函数想要修改指向的值时,通过非常量指针传递 AND NULL 是函数处理的有效预期值。
当值的复制成本很高且函数不想修改引用的值时,通过常量引用传递 AND 如果改用指针,则 NULL 将不是有效值。
当复制值的成本很高并且函数想要修改引用的值时,通过非连续引用传递 AND 如果改用指针,NULL 将不是有效值。
在编写模板函数时,没有一个明确的答案,因为需要考虑一些超出本讨论范围的权衡,但足以说明大多数模板函数通过值或(常量)引用来获取其参数,但是由于迭代器语法类似于指针的语法(星号表示“取消引用”),任何期望迭代器作为参数的模板函数在默认情况下也将接受指针(并且不检查NULL,因为 NULL 迭代器概念具有不同的语法)。
我从中得出的结论是,选择使用指针或引用参数之间的主要区别在于 NULL 是否为可接受的值。就是这样。
毕竟,该值是输入、输出、可修改等都应该在有关函数的文档/注释中。
评论
这里的大多数答案都未能解决在函数签名中使用原始指针在表达意图方面的固有歧义。问题如下:
调用方不知道指针是指向单个对象,还是指向对象“数组”的开头。
调用方不知道指针是否“拥有”它指向的内存。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
我们已经实现了我们的目标!现在让我们看看与原始指针相比的好处。
- 该接口清楚地表明,引用应仅引用一个对象。
- 显然,它不拥有它所引用的内存,因为它没有用户定义的析构函数,也没有删除内存的方法。
- 调用方知道可以传入,因为函数作者明确要求
nullptr
optional_ref
我们可以从这里使接口更复杂,例如添加相等运算符、monadic 和 interface、获取值或抛出异常的方法 support。这可以由你来完成。get_or
map
constexpr
总之,与其使用原始指针,不如推理这些指针在代码中的实际含义,然后利用标准库抽象或编写自己的抽象。这将显著改进您的代码。
评论
new