在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);
func(int& a)
这种写法。你可能是因为编译文件时出错,将其误认为是C++。 - Adam Rosenfieldfunc(*NULL)
传递。然后在函数内部,使用if(&x == NULL)
进行测试。我想这看起来很丑陋,但指针和引用参数之间的区别只是语法糖。 - Ken Jackson我从中得出的结论是,在选择使用指针还是引用参数时,主要区别在于NULL是否是可接受的值。仅此而已。无论值是输入、输出、可修改等,都应该在函数的文档/注释中说明。
当函数不想修改参数并且这个值容易复制(int、double、char、bool等简单类型以及std::string、std::vector和所有其他STL容器都不是简单类型)时,使用按值传递。
当值的复制代价高昂且函数不想修改指向的值且NULL是函数处理的有效预期值时,请使用const指针传递。
当值的复制代价高昂且函数想要修改指向的值且NULL是函数处理的有效预期值时,请使用非const指针传递。
当值的复制代价高昂且函数不想修改所引用的值且如果使用指针,则NULL不是一个有效的值时,请使用const引用传递。
当值的复制代价高昂且函数想要修改所引用的值且如果使用指针,则NULL不是一个有效的值时,请使用非const引用传递。
在编写模板函数时,没有明确的答案,因为有一些权衡需要考虑,这超出了本讨论的范围,但可以简单地说,大多数模板函数通过值或(const) 引用获取参数,但由于迭代器语法与指针语法类似(通过星号进行“解引用”),任何期望迭代器作为参数的模板函数也将默认接受指针(而不检查NULL,因为NULL迭代器概念具有不同的语法)。
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
引用用作参数。
就个人而言,我认为这是一个很好的经验法则,因为它使参数是否为输出参数更加清晰。然而,虽然我个人总体上同意这一点,但如果我的团队中的其他人争辩要将输出参数作为引用,则会被说服(一些开发人员非常喜欢它们)。
swap
函数在复制-交换惯用法中扮演着非常重要的角色,但是如果不将其参数声明为非const引用,则无法完成其预期功能。在此处使用指针会很麻烦,并且容易打破其无异常承诺。 - Earth Engine对前面的帖子进行澄清:
引用并不能保证获得非空指针。(尽管我们通常将它们视为这样。)
虽然下面的代码非常糟糕,简直是能把你带到木棚后面去的坏代码,但以下代码将编译并运行:(至少在我的编译器下如此。)
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下,同一内存被分配给两个不同的对象。这里有很多调试乐趣...
更加实际:
*i
,你的程序就具有未定义的行为。例如,编译器可以看到这段代码并假设“好的,在所有代码路径中这个代码都有未定义的行为,所以整个函数必须是不可达的。”然后它会假设所有通往该函数的分支都没有被执行。这是一个经常执行的优化。 - David Stone这里的大多数答案无法解决在函数签名中使用原始指针所固有的歧义性问题,表达意图方面存在问题,它们具体如下:
调用者不知道指针是指向单个对象还是指向“对象数组”的开头。
调用者不知道指针是否“拥有”其指向的内存,即函数是否应该释放内存(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_or
和map
接口、获取值或抛出异常的方法以及constexpr
支持。这可以由您完成。
总之,不要使用原始指针,考虑一下这些指针在您的代码中实际意味着什么,并利用标准库抽象或编写自己的抽象来改进代码。
其实不是这样的。在内部,通过引用传递本质上是传递所引用对象的地址。因此,通过传递指针来获得效率上的优势并不存在。
然而,通过引用传递确实有一个好处。您可以确保传递的任何对象/类型都有一个实例。如果传递指针,则有可能收到空指针。通过使用按引用传递,您将隐式地将 NULL 检查推迟至调用者函数的更高层次。
new
来创建指针以及由此产生的所有权问题。 - Martin York