我应该使用指针还是引用来远程分配变量?

277
给一个函数传入原始变量进行操作,哪种做法更好呢?
unsigned long x = 4;

void func1(unsigned long& val) {
     val = 5;            
}
func1(x);

或者:

void func2(unsigned long* val) {
     *val = 5;
}
func2(&x);

IOW:有没有任何理由选择一个而不是另一个?

1
参考资料当然很有价值,但我来自C语言,指针无处不在。要先精通指针才能理解引用的价值。 - Jay D
这如何与函数式编程的引用透明目标相匹配?如果您始终希望函数返回新对象并且从不内部改变状态,特别是不改变传递给函数的变量的状态,那么该概念是否仍然适用于像C ++这样的语言中的指针和引用?(请注意,我假设某人已经拥有引用透明的目标。我不想讨论它是否是一个好目标。) - ely
尽可能使用引用。当没有选择时,使用用户指针。 - Ferruccio
12个回答

306

我的经验法则是:

如果您想要进行指针算术运算(例如,递增指针地址以浏览数组)或者必须传递一个NULL指针,请使用指针。

否则,请使用引用。


11
有关指针为NULL的观点很好。如果您有一个指针参数,那么您必须明确检查它是否为NULL,或者搜索该函数的所有用法,以确保它永远不会是NULL。对于引用,不需要进行这种努力。 - Richard Corden
28
请问需要翻译的内容是:“Explain what you mean by arithmetic. A new user may not understand that you want to adjust what the pointer is pointing at.” 对吗?如果是,我的翻译如下:请解释一下你所说的算术运算。新用户可能不理解你想要调整指针指向的含义。 - Martin York
9
马丁,通过算术运算,我指的是你传递一个指向结构体的指针,但你知道它不是一个简单的结构体而是一个结构体数组。在这种情况下,你可以使用[]来对其进行索引,也可以通过在指针上使用++/--进行算术运算。以上就是简要的区别。 - Nils Pipenbrinck
2
Martin,你只能直接使用指针来完成这个任务。不能使用引用。当然,你可以取一个引用的指针并在实践中做同样的事情,但如果这样做,你最终会得到非常混乱的代码。 - Nils Pipenbrinck
1
@RichardCorden 引用在运行时并不能完全保证其有效性。你的代码仍然可能产生未定义行为,只是问题出现的语义位置与程序在运行时表现不同而已。你无法在运行时检查你正在使用的引用是否有效。 - martinkunev
显示剩余5条评论

75

我真的认为建立以下函数调用编码准则会对你有益:

  1. 像其他地方一样,始终正确使用 const

    • 注意:这意味着,除了输出值(参见第3项)和按值传递的值(参见第4项)可以不带有 const 说明符之外,其他所有内容都必须带有它。
  2. 只有在当前上下文中值为0/NULL时才通过指针传递值。

    • 理由1:作为 调用者,您知道无论传入什么,它都必须处于可用状态。

    • 理由2:作为 被调用者,您知道传入的任何值都是可用的。因此,不需要对该值进行 NULL 检查或错误处理。

    • 理由3:如果可能,请始终在编译时捕获错误。理由1和2将受到 编译器强制执行

  3. 如果函数参数是输出值,则通过引用传递它。

    • 理由:我们不想破坏第2点...
  4. 仅在值是 POD (Plain old Datastructure)、足够小(存储器方面)或以其他方式足够便宜(时间方面)复制时,才选择 "按值传递" 而不是 "按 const 引用传递"。

    • 理由:避免不必要的复制。
    • 注意:足够小足够便宜 不是绝对可衡量的。

@Kleist:代表@MSalters说,可能有很多原因。其中一个可能是您已经分配了要填充的内存,例如预定大小的std::vector<> - Johann Gerell
指出可能非常明显的是,有时您也想通过引用传递指针,就像普通引用一样,它消除了在设置它所指向的指针之前检查外部指针是否为NULL的需要(因为没有外部指针)。当然,首先应该使用智能指针来处理所有这些内容(如果您计划更改所指向的值,则通过引用传递它)。 - WhozCraig
@WhozCraig:我尽量遵循“最小惊奇原则”,除非在人类历史上这是最后的选择,否则不会添加“通过引用传递指针”。;-) - Johann Gerell
@JohannGerell 无论如何都不能责怪你。 - WhozCraig
除了你写的所有内容都非常有道理之外,我发现很难在其中看到一个真正(响应式)的答案。 - Wolf
显示剩余7条评论

24

这最终还是主观的。到目前为止的讨论很有用,但我认为这没有一个正确或决定性的答案。很多情况下都要取决于样式指南和您当时的需求。

虽然指针具有一些不同的功能(无论是否可以为空),但输出参数最大的实际区别纯粹在于语法上。例如,Google的C ++样式指南(https://google.github.io/styleguide/cppguide.html#Reference_Arguments)规定只能使用指针作为输出参数,并且只允许使用const引用。原因在于可读性:具有值语法的东西不应具有指针语义。我并不是说这一定是对或错,但我认为这里的重点在于这是一个样式问题,而不是正确性问题。


引用具有值语法但指针语义含义是什么意思? - Eric Andrew Lewis
看起来你正在传递一个副本,因为“按引用传递”的部分只在函数定义(值语法)中显而易见,但你并没有复制你传递的值,实际上你在底层传递了一个指针,这使得函数能够修改你的值。 - phant0m
人们不应忘记,Google C++风格指南备受诟病。 - Deduplicator

15

指针

  • 指针是一个保存内存地址的变量。
  • 指针声明包括一个基本类型,一个*和一个变量名。
  • 指针可以在其生命周期内指向任意数量的变量。
  • 一个当前不指向有效内存位置的指针被赋予空值(即零)。

BaseType* ptrBaseType;
BaseType objBaseType;
ptrBaseType = &objBaseType;
  • &是一个一元运算符,它返回其操作数的内存地址。

  • 解引用运算符(*)用于访问指针所指向变量中存储的值。

  •    int nVar = 7;
       int* ptrVar = &nVar;
       int nVar2 = *ptrVar;
    

    参考资料

    • 引用 (&) 就像是一个现有变量的别名。

    • 引用 (&) 就像是一个常量指针,会自动解引用。

    • 通常在函数参数列表和函数返回值中使用。

    • 引用在创建时必须被初始化。

    • 一旦引用被初始化为一个对象,就不能更改为另一个对象的引用。

    • 你不能拥有 NULL 引用。

    • const 引用可以引用一个 const int。它使用一个临时变量,其值等于 const 常量的值。

    int i = 3;    //integer declaration
    int * pi = &i;    //pi points to the integer i
    int& ri = i;    //ri is refers to integer i – creation of reference and initialization
    

    这里输入图片描述

    这里输入图片描述


    1
    我喜欢你的比较表。我注意到第二行有一个小错别字:“_Pointer_可以在任何时候初始化”。 - Ikem Krueger

    7

    如果您要修改变量的值,应该传递指针。尽管从技术上讲,传递引用或指针是相同的,但在您的用例中传递指针更易读,因为它“广告”了函数将更改该值的事实。


    2
    如果您遵循约翰·格雷尔的指导方针,非const引用也可以表示可更改的变量,因此指针在这里并没有优势。 - Alexander Kondratskiy
    5
    @AlexanderKondratskiy: 你没有抓住重点...你无法立即在调用点看到调用的函数是否将参数作为const或非const引用接受,但是您可以看到参数是通过&x还是x传递的,并使用该约定来编码参数是否有可能被修改。(也就是说,在某些情况下,您需要传递一个const指针,因此约定只是一个提示。可以说,怀疑某些东西可能被修改而实际上不会被修改比认为不会被修改而实际上会被修改更加安全。) - Tony Delroy

    6

    5
    如果您有一个参数需要指示缺少值,通常的做法是将参数设置为指针值并传入NULL。
    从安全角度来看,在大多数情况下,更好的解决方案是使用boost::optional。这使您能够通过引用和作为返回值传递可选值。
    // Sample method using optional as input parameter
    void PrintOptional(const boost::optional<std::string>& optional_str)
    {
        if (optional_str)
        {
           cout << *optional_str << std::endl;
        }
        else
        {
           cout << "(no string)" << std::endl;
        }
    }
    
    // Sample method using optional as return value
    boost::optional<int> ReturnOptional(bool return_nothing)
    {
        if (return_nothing)
        {
           return boost::optional<int>();
        }
    
        return boost::optional<int>(42);
    }
    

    3

    引用是一个隐式指针。基本上,你可以改变引用所指向的值,但你不能改变引用指向其他东西。因此,我的建议是:如果你只想改变参数的值,请将其作为引用传递;但如果你需要改变参数指向不同对象,则使用指针传递。


    3
    考虑一下C#的out关键字。编译器要求调用方法的调用者对任何输出参数应用out关键字,即使它已经知道它们是什么。这旨在提高可读性。尽管现代IDE使我倾向于认为这是语法(或语义)高亮的工作。

    笔误:应为语义(semantic),而非“symantic”;我同意可以使用高亮显示而不是写出来(在C#中),或者使用&符号(对于C语言,因为没有引用)。 - peenut

    2

    指针:

    • 可以被赋值为 nullptr(或 NULL)。
    • 在调用点,如果您的类型本身不是指针,则必须使用 &, 明确表明您正在修改对象。
    • 指针可以重新绑定。

    引用:

    • 不能为 null。
    • 一旦绑定,就不能更改。
    • 调用者不需要显式使用 &。这有时被认为是不好的, 因为您必须去函数的实现中查看参数是否被修改。

    对于那些不知道的人来说,一个小提示:nullptr或NULL只是0。https://dev59.com/sHRB5IYBdhLWcg3w77kn - Serguei Fedorov
    4
    nullptr与0不同。尝试int a=nullptr;。参考链接:https://dev59.com/b3M_5IYBdhLWcg3wq1CF - Johan Lundberg

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接