何时使用引用与指针


381

我了解指针与引用的语法和一般语义,但是我应该如何确定何时在API中使用引用或指针多少合适?

自然,有些情况需要一个或另一个(operator++需要一个引用参数),但是总的来说,我发现我更喜欢使用指针(和const指针),因为语法很清楚地表明,变量是以破坏性方式传递的。

例如下面的代码:

void add_one(int& n) { n += 1; }
void add_one(int* const n) { *n += 1; }
int main() {
  int a = 0;
  add_one(a); // Not clear that a may be modified
  add_one(&a); // 'a' is clearly being passed destructively
}

使用指针,总是(更)明显的是怎么回事,所以对于API和诸如此类的情况,其中最关注清晰度的指针不是比引用更合适吗?这是否意味着仅在必要时才使用引用(例如operator++)?两者之间是否存在性能问题?

编辑(已过时):

除了允许使用NULL值和处理原始数组外,选择还取决于个人喜好。我接受了下面引用Google的C ++样式指南的答案,因为他们提出的观点是“引用可能会造成混淆,因为引用具有值语法但具有指针语义。”

由于清理不应该为NULL的指针参数需要进行额外的工作(例如,add_one(0)将在运行时调用指针版本并在运行时中断),因此从可维护性的角度来看,使用必须在存在对象的引用是有意义的,尽管这很遗憾。失去语法清晰度。


4
似乎您已经决定何时使用哪个。就我个人而言,无论是否修改它,我都希望传递要作用的对象。如果一个函数使用了一个指针,那就告诉我它正在作用于指针,即在数组中将它们用作迭代器。
本杰明·林德利

1
@Schnommus:公平地说,我主要使用TextMate。不过,我认为从一眼就可以看出含义是可取​​的。
CONNEC

4
关于什么add_one(a);是不清楚这a回事进行修改?它在代码中说的很对:添加一个
GManNickG 2011年

31
@connec:Google C ++样式指南不被认为是好的C ++样式指南。这是与Google的旧C ++代码库一起使用的样式指南(即适合他们的东西)。接受以此为基础的答案无济于事。只需阅读您的评论和解释,您就已经对这个问题有了明确的看法,并且正在寻找其他人来确认您的观点。这样一来,您就可以根据自己想要/期望听到的内容进行问答。
马丁·约克

1
只需通过命名方法即可解决addOneTo(...)。如果这不是您要执行的操作,请查看声明。
stefan

Answers:


296

尽可能使用引用,必须使用指针。

避免指针,直到不能。

原因是与其他任何构造相比,指针使事情变得更难遵循/阅读,更不安全,更危险。

因此,经验法则是仅在没有其他选择时才使用指针。

例如,当函数在某些情况下可以返回nullptr且假定会返回时,返回指向对象的指针是有效选项。也就是说,更好的选择是使用类似的东西boost::optional

另一个示例是使用指向原始内存的指针进行特定的内存操作。应该将其隐藏并定位在非常狭窄的代码部分中,以帮助限制整个代码库中的危险部分。

在您的示例中,使用指针作为参数毫无意义,因为:

  1. 如果您提供nullptr作为参数,那么您将进入未定义的行为领域;
  2. 引用属性版本不允许(没有易于发现的技巧)1问题。
  3. 对于用户而言,引用属性版本更易于理解:您必须提供有效的对象,而不是可以为null的对象。

如果函数的行为必须在有或没有给定对象的情况下都可以使用,则使用指针作为属性建议您可以将其nullptr作为参数传递,这对函数很好。那是用户与实施之间的一种契约。


49
我不确定指针会使什么更难阅读吗?这是一个非常简单的概念,可以很清楚地说明何时可能要修改某些内容。如果我要说的是什么,在没有迹象表明发生什么情况时更难阅读,为什么不应该add_one(a)返回结果而不是通过引用来设置呢?
CONNEC

46
@connec:如果add_one(a)令人困惑,那是因为它的名称不正确。add_one(&a)会有同样的困惑,只是现在您可能要增加指针而不是对象。add_one_inplace(a)将避免所有混乱。
Nicol Bolas

20
有一点,引用可以指向可以像指针一样轻松消失的内存。因此,它们不一定比指针更安全。持久和传递引用与指针一样危险。
Doug T.

6
@Klaim我的意思是原始指针。我的意思是C ++具有指针NULLnullptr,并且有一个原因。给出“从不使用指针”和/或“从不使用NULL,始终使用boost::optional” 并不是一个很好的考虑甚至是不切实际的建议。那太疯狂了。别误会,在C ++中使用原始指针的频率要比在C语言中少,但它们仍然有用,它们不像某些C ++人们喜欢的那样“危险”(这也是一种夸张),然后再次:什么时候更容易使用指针并return nullptr;指示缺失值呢?为什么要导入整个Boost?

5
@Klaim“使用NULL是不正确的做法”-现在太荒谬了。而且if已经过时了,应该改用while() { break; }吧?另外,不用担心,我已经看过并使用了大型代码库,是的,如果您粗心大意,那么所有权就是一个问题。但是,如果您遵守约定,则不要一贯使用它们并注释和记录您的代码。但是毕竟,我应该只使用C,因为我对C ++太傻了,对吧?

62

性能完全相同,因为引用在内部作为指针实现。因此,您不必为此担心。

关于何时使用引用和指针,没有公认的约定。在某些情况下,您必须返回或接受引用(例如,复制构造函数),但除此之外,您还可以根据需要自由进行操作。我遇到的一个相当普通的约定是,当参数必须引用现有对象时使用引用,而在NULL值正常时使用指针。

一些编码约定(例如Google的约定)规定,应始终使用指针或const引用,因为引用的语法有些不清楚:它们具有引用行为,但具有值语法。


10
对此,Google的风格指南指出,为此添加一点,即函数的输入参数应为const引用,输出应为指针。我之所以喜欢它,是因为当您阅读函数签名时,它很清楚什么是输入,什么是输出。

44
@Dan:Google样式指南适用于Google的旧代码,不应用于现代编码。实际上,对于新项目而言,这是一种非常糟糕的编码风格。
GManNickG 2011年

13
@connec:让我这样说:null是一个完全有效的指针值。任何有指针的地方,我都可以将其值设置为null。因此,您的第二个版本add_one损坏add_one(0); // passing a perfectly valid pointer value,kaboom。您需要检查它是否为空。有人会反驳:“好吧,我只是记录一下我的函数不适用于null”。很好,但是您就不能回答这个问题了:如果您要查看文档以查看是否可以使用null,那么您还将看到函数声明
GManNickG 2011年

8
如果是参考,您会发现情况确实如此。但是,这样的反驳忽略了这一点:引用在语言级别上强制执行即它引用现有对象,并且可能不为null,而指针没有这种限制。我认为很明显,语言级别的强制执行比文档级别的执行更强大,并且更不容易出错。某些人会尝试通过以下方式来对此进行反驳:“看,空引用:int& i = *((int*)0);。这不是有效的反驳。上一代码中的问题在于指针的使用,而不是引用。引用永远不会为空,句点
GManNickG 2011年

12
您好,我看到评论中没有语言律师,因此请允许我补救:引用通常由指针实现,但标准没有规定。使用其他某种机制的实施将是100%投诉。
Thomas Bonini 2011年

34

C ++ FAQ精简版 -

尽可能使用引用,而必须使用指针。

每当不需要“重新设置”时,通常比引用更喜欢引用。这通常意味着引用在类的公共接口中最有用。引用通常显示在对象的外观上,而指针则显示在对象的外观上。

上面的例外是函数的参数或返回值需要“前哨”引用,即不引用对象的引用。通常最好通过返回/获取一个指针,并赋予NULL指针特殊的意义来完成此操作(引用必须始终是别名对象,而不是取消引用的NULL指针)。

注意:旧的C语言程序员有时不喜欢引用,因为它们提供的引用语义在调用者的代码中并未明确。然而,经过一些C ++经验,人们很快意识到这是一种信息隐藏的形式,它是一种资产而不是一种负债。例如,程序员应该用问题的语言而不是机器的语言来编写代码。


1
我猜您可能会争辩说,如果您使用的是API,则应该熟悉它的功能,并且知道所传递的参数是否已被修改……要考虑的事情,但是我发现自己与C程序员(尽管我很少有C经验)。我要补充一点,尽管语法更清晰对程序员和机器都有利。
CONNEC

1
@connec:确保C程序员对其语言正确无误。但是不要犯将C ++视为C的错误。这是一种完全不同的语言。如果您将C ++视为C,则最终会写成相等的引用C with class(不是C ++)。
马丁·约克

15

我的经验法则是:

  • 将指针用于传出或输入/输出参数。因此可以看出该值将被更改。(您必须使用&
  • 如果NULL参数是可接受的值,请使用指针。(请确保const是传入参数)
  • 如果传入参数不能为NULL并且不是基本类型,请使用引用作为参数(const T&),。
  • 返回新创建的对象时,请使用指针或智能指针。
  • 使用指针或智能指针作为结构或类成员,而不是引用。
  • 使用引用进行别名(例如int &current = someArray[i]

不管您使用哪一种,如果它们不明显,请不要忘记记录它们的功能及其参数的含义。


14

免责声明:除了引用不能为NULL或“反弹”(这意味着thay不能更改其别名的对象)的事实外,它还取决于口味,所以我不再赘述“这个更好”。

也就是说,我不同意您在帖子中的最后声明,因为我认为代码不会因引用而变得不清楚。在您的示例中

add_one(&a);

可能比

add_one(a);

因为您知道a的值很可能会改变。另一方面,功能的签名

void add_one(int* const n);

还是不清楚:n是单个整数还是数组?有时,您只能访问(记录不充分的)标题和签名,例如

foo(int* const a, int b);

一见钟情并不容易理解。

恕我直言,当不需要(重新)分配或重新绑定(如前所述)时,引用与指针一样好。此外,如果开发人员仅将指针用于数组,则函数签名的含糊性会降低一些。更不用说运算符语法更易于使用引用阅读的事实。


感谢您清楚地说明两种解决方案在何处会变得越来越清晰。我最初是在指针训练营中,但这很有道理。
Zach Beavon-Collin's

12

与其他人一样已经回答了:始终使用引用,除非该变量的存在NULL/ nullptr真正有效的状态。

约翰·卡马克(John Carmack)在这个问题上的观点是相似的:

NULL指针是C / C ++中最大的问题,至少在我们的代码中是如此。将单个值同时用作标志和地址会导致大量致命问题。尽可能使用C ++引用而不是指针。虽然引用“确实”只是一个指针,但它具有非空的隐式约定。当指针变成引用时执行NULL检查,然后您可以忽略此问题。

http://www.altdevblogaday.com/2011/12/24/static-code-analysis/

编辑2012-03-13

用户Bret Kuhns正确地指出:

C ++ 11标准已经完成。我认为应该在该线程中提到大多数代码应结合引用,shared_ptr和unique_ptr来做得很好。

的确如此,但是即使使用智能指针替换原始指针,问题仍然存在。

例如,std::unique_ptrstd::shared_ptr都可以通过其默认构造函数构造为“空”指针:

……意味着在不验证它们为空的情况下使用它们可能会导致崩溃,这正是J. Carmack讨论的全部内容。

然后,我们遇到了一个有趣的问题:“如何传递智能指针作为函数参数?”

乔恩(Jon)对C ++问题的回答-传递对boost :: shared_ptr的引用,以及以下注释表明,即使那样,通过复制或引用传递智能指针也不如人们希望的那样清晰(我喜欢我自己的“默认情况下是按引用”,但我可能错了。


1
C ++ 11标准已经完成。我认为是该线程该提起大多数代码结合使用shared_ptr,和的完美表现的时候了unique_ptr。所有权语义和输入/输出参数约定通过这三部分和常量的组合来处理。除了在处理遗留代码和非常优化的算法时,C ++中几乎不需要原始指针。使用它们的那些区域应尽可能地封装,并将任何原始指针转换为语义上适当的“现代”等价物。
Bret Kuhns 2012年

1
很多时候,不应该传递智能指针,而应该对它们的空性进行测试,然后将其包含的对象通过引用传递。真正应该传递智能指针的唯一时间是与其他对象进行转移(unique_ptr)或共享(shared_ptr)所有权时。
卢克·沃思

@povman:我完全同意:如果所有权不是接口的一部分(并且除非要对其进行修改,否则就不应该),那么我们不应将智能指针作为参数(或返回值)传递。当所有权是接口的一部分时,事情变得更加复杂。例如,Sutter / Meyers争论如何将unique_ptr作为参数传递:通过复制(Sutter)还是通过r值引用(Meyers)?一个反模式依赖于传递一个指向全局shared_ptr的指针,该指针有失效的风险(解决方案是将智能指针复制到堆栈上)
paercebal 2015年

7

这不是品味的问题。以下是一些确定的规则。

如果要在声明范围内引用静态声明的变量,请使用C ++引用,这样绝对安全。静态声明的智能指针也是如此。通过引用传递参数是此用法的一个示例。

如果要引用范围大于声明范围的任何内容,则应使用引用计数的智能指针,以使其完全安全。

为了语法上的方便,您可以使用引用来引用集合的元素,但这并不安全。该元素可以随时删除。

为了安全地保存对集合元素的引用,必须使用引用计数的智能指针。


5

任何性能差异都非常小,以至于无法使用不太清楚的方法来证明其合理性。

首先,没有提到参考通常是优越的情况是const参考。对于非简单类型,传递a const reference可以避免创建临时类型,并且不会引起您担心的混乱(因为未修改值)。在这里,强迫一个人传递一个指针会引起您所担心的混乱,因为看到采用并传递给函数的地址可能会使您认为值已更改。

无论如何,我基本上都同意你的看法。当不太清楚这是函数的作用时,我不喜欢使用引用来修改其值的函数。我也更喜欢在这种情况下使用指针。

当您需要返回复杂类型的值时,我倾向于使用引用。例如:

bool GetFooArray(array &foo); // my preference
bool GetFooArray(array *foo); // alternative

在这里,函数名称使您清楚地知道要以数组形式返回信息。因此,没有混乱。

引用的主要优点是它们始终包含有效值,比指针干净,并且不需要任何额外语法即可支持多态。如果没有这些优点,则没有理由更喜欢引用而不是指针。


4

Wiki复制-

这样做的结果是,在许多实现中,通过引用对具有自动或静态生存期的变量进行操作,尽管其语法与直接访问它的语法相似,但可能涉及昂贵的隐藏取消引用操作。引用是C ++语法上有争议的功能,因为引用使标识符的间接级别变得模糊。也就是说,与通常指针在句法上脱颖而出的C代码不同,在一大段C ++代码中,如果被访问的对象被定义为局部变量或全局变量,或者它是否是对对象的引用(隐式指针),则可能不是立即显而易见的。其他位置,尤其是在代码混合了引用和指针的情况下。这方面会使编写较差的C ++代码更难于阅读和调试(请参阅别名)。

我100%同意这一点,这就是为什么我认为只有在有充分理由的情况下才应使用参考。


我在很大程度上也同意,但是我转而认为,对于纯粹的语法问题,失去针对NULL指针的内置保护的代价太高了,尤其是-尽管更为明确-指针语法非常丑陋无论如何。
CONNEC

我想这种情况也是一个重要因素。我认为在当前代码库主要使用指针时尝试使用引用将是一个坏主意。如果你希望自己是参考文献,然后的事实,他们这样隐含不那么重要,也许..
user606723

3

注意事项:

  1. 指针可以NULL,引用不能NULL

  2. 引用更易于使用,const当我们不想更改值而只需要函数中的引用时,可以将其用作引用。

  3. *while一起使用的指针与一起使用&

  4. 需要进行指针算术运算时,请使用指针。

  5. 您可以具有指向空类型的指针,int a=5; void *p = &a;但不能具有对空类型的引用。

指针与参考

void fun(int *a)
{
    cout<<a<<'\n'; // address of a = 0x7fff79f83eac
    cout<<*a<<'\n'; // value at a = 5
    cout<<a+1<<'\n'; // address of a increment by 4 bytes(int) = 0x7fff79f83eb0
    cout<<*(a+1)<<'\n'; // value here is by default = 0
}
void fun(int &a)
{
    cout<<a<<'\n'; // reference of original a passed a = 5
}
int a=5;
fun(&a);
fun(a);

决定何时使用什么

指针:用于数组,链接列表,树实现和指针算术。

参考:函数参数和返回类型。


2

尽可能使用引用 ”规则存在问题,如果您想保留引用以供进一步使用,则会出现此问题。为了举例说明,假设您有以下课程。

class SimCard
{
    public:
        explicit SimCard(int id):
            m_id(id)
        {
        }

        int getId() const
        {
            return m_id;
        }

    private:
        int m_id;
};

class RefPhone
{
    public:
        explicit RefPhone(const SimCard & card):
            m_card(card)
        {
        }

        int getSimId()
        {
            return m_card.getId();
        }

    private:
        const SimCard & m_card;
};

首先,RefPhone(const SimCard & card)通过引用传递构造函数中的参数似乎是一个好主意,因为它可以防止将错误/空指针传递给构造函数。它以某种方式鼓励在堆栈上分配变量并从RAII中受益。

PtrPhone nullPhone(0);  //this will not happen that easily
SimCard * cardPtr = new SimCard(666);  //evil pointer
delete cardPtr;  //muahaha
PtrPhone uninitPhone(cardPtr);  //this will not happen that easily

但是随后,临时者开始摧毁你的幸福世界。

RefPhone tempPhone(SimCard(666));   //evil temporary
//function referring to destroyed object
tempPhone.getSimId();    //this can happen

因此,如果您盲目地坚持引用,则在传递无效指针的可能性与存储对销毁对象的引用的可能性之间进行权衡,其效果基本相同。

编辑:请注意,我坚持以下规则:“在任何可能的地方使用引用,在任何必须的地方使用指针。直到不能使用指针为止。” 从最受好评和接受的答案(其他答案也表明如此)。尽管应该很明显,但是示例并不能说明这样的引用是不好的。但是,它们可能会像指针一样被滥用,并且可能给代码带来自己的威胁。


指针和引用之间存在以下差异。

  1. 当涉及传递变量时,按引用传递看起来像按值传递,但是具有指针语义(行为类似于指针)。
  2. 引用不能直接初始化为0(空)。
  3. 引用(引用,未引用的对象)不能被修改(相当于“ * const”指针)。
  4. const引用可以接受临时参数。
  5. 本地const引用延长了临时对象的寿命

考虑到这些因素,我目前的规则如下。

  • 将引用用于将在功能范围内本地使用的参数。
  • 当可接受的参数值0(空)或需要存储参数以备将来使用时,请使用指针。如果0(空)是可接受的,我在参数中添加“ _n”后缀,请使用受保护的指针(如Qt中的QPointer)或仅对其进行记录。您也可以使用智能指针。使用共享指针必须比使用普通指针更加小心(否则,最终可能会因设计内存泄漏和责任混乱而告终)。

3
您的示例的问题不在于引用是不安全的,而是您依赖对象实例范围之外的某些内容来保持私有成员的生命。const SimCard & m_card;只是写得不好的代码。
plamenko '16

@plamenko我很害怕您不了解该示例的目的。是否const SimCard & m_card正确取决于上下文。这篇文章中的信息并不是说引用是不安全的(如果人们努力尝试,引用可能是不安全的)。信息是,您不应盲目地坚持“尽可能使用引用”的口头禅。示例是积极使用“尽可能使用引用”原则的结果。这应该很清楚。
doc

有两件事困扰着您,因为我认为这可能会误导试图了解此事的人。1.帖子是单向的,很容易让人觉得引用不好。您仅提供了一个如何不使用引用的示例。2.您在示例中不清楚问题出在哪里。是的,临时会得到破坏,但并不是那一行错了,这是该类的实现。
plamenko '16

实际上,您不应该拥有像这样的成员const SimCard & m_card。如果您希望临时使用高效,请添加explicit RefPhone(const SimCard&& card)构造函数。
plamenko '16

@plamenko如果您不具备基本的理解能力,那么除了被我的帖子误导之外,您还有更大的问题。我不知道该如何清楚。看第一句话。“尽可能使用引用”的口头禅存在一个问题!您在我的帖子中哪里找到引用不好的陈述?在我的文章结尾,您已经写了引用的使用位置,那么您如何得出这样的结论?这不是问题的直接答案吗?
doc

1

以下是一些准则。

一个函数使用传递的数据而不修改它:

  1. 如果数据对象较小,例如内置数据类型或较小的结构,则按值传递它。

  2. 如果数据对象是数组,请使用指针,因为这是您唯一的选择。使指针成为const指针。

  3. 如果数据对象是大小合适的结构,请使用const指针或const引用来提高程序效率。您可以节省复制结构或类设计所需的时间和空间。使指针或引用为const。

  4. 如果数据对象是类对象,请使用const引用。类设计的语义通常需要使用引用,这是C ++添加此功能的主要原因,因此,传递类对象参数的标准方法是通过引用。

一个函数修改调用函数中的数据:

1.如果数据对象是内置数据类型,请使用指针。如果发现诸如fixit(&x)之类的代码,其中x是一个整数,则很明显,此函数打算修改x。

2.如果数据对象是数组,请使用唯一的选择:指针。

3.如果数据对象是结构,请使用引用或指针。

4.如果数据对象是类对象,请使用引用。

当然,这些只是指导原则,可能有做出不同选择的原因。例如,cin使用基本类型的引用,因此您可以使用cin >> n代替cin >>&n。


0

引用更干净,更易于使用,并且在隐藏信息方面做得更好。但是,引用不能重新分配。如果需要先指向一个对象,然后再指向另一个对象,则必须使用指针。引用不能为null,因此,如果有问题的对象可能为null的可能性,则不能使用引用。您必须使用一个指针。如果要自己处理对象,即要在堆而不是堆栈上为对象分配内存空间,则必须使用指针

int *pInt = new int; // allocates *pInt on the Heap

0

您正确编写的示例应如下所示

void add_one(int& n) { n += 1; }
void add_one(int* const n)
{
  if (n)
    *n += 1;
}

这就是为什么在可能的情况下最好使用引用...


-1

只是放入一角钱。我刚刚进行了测试。那个很时髦。我只是让g ++使用指针而不是使用引用来创建同一小型程序的汇编文件。查看输出时,它们完全相同。除了符号命名。因此,查看性能(在一个简单示例中)没有问题。

现在是关于指针与引用的话题。恕我直言,我认为透明是最重要的。一读到隐性行为,我的脚趾就会开始卷曲。我同意引用不能为NULL是一种很好的隐式行为。

取消引用NULL指针不是问题。它会使您的应用程序崩溃并且易于调试。更大的问题是包含无效值的未初始​​化指针。这很可能导致内存损坏,从而导致没有明确来源的不确定行为。

我认为这是引用比指针安全得多的地方。我同意先前的说法,即接口(应该清楚地记录在案,请参见按合同设计,Bertrand Meyer)将参数的结果定义为函数。现在考虑到所有这些,我的喜好是尽可能/尽可能使用引用。


-2

对于指针,您需要它们指向某物,因此指针会占用内存空间。

例如,采用整数指针的函数将不采用整数变量。因此,您需要首先创建一个指针,然后再传递给函数。

作为参考,它不会消耗内存。您有一个整数变量,可以将其作为参考变量传递。而已。您不需要专门为其创建参考变量。


4
不。带有指针的函数不需要分配指针变量:您可以传递临时变量&address。如果引用是对象的成员,则引用肯定会消耗内存,此外,所有现存的编译器实际上都将引用实现为地址,因此无论在参数传递还是取消引用方面,您都不会节省任何精力。
underscore_d
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.