我应该在成员数据中使用指针还是引用?


138

这是一个简化的示例来说明问题:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

所以B负责更新C.我的一部分跑通过棉绒的代码并将其whinged关于参考构件:棉绒#1725。这是关于照顾默认拷贝和分配的,这很公平,但是默认拷贝和分配对于指针也很不利,因此那里没有什么好处。

我总是尝试使用引用,因为裸露的指针不确定地介绍了谁负责删除该指针。我更喜欢按值嵌入对象,但是如果需要指针,可以在拥有指针的类的成员数据中使用auto_ptr,并将对象作为引用传递给周围。

当指针可以为null或可以更改时,我通常只在成员数据中使用指针。还有其他理由更喜欢指针而不是数据成员的引用吗?

难道说包含引用的对象是不可分配的,因为引用一旦初始化就不应更改?


“但是使用指针时默认的复制和分配也很糟糕”:这是不相同的:如果不是const,可以随时更改指针。引用通常总是const!(你会看到这一点,如果你试图您的会员变更为“A&const的一个,”编译器(至少GCC)会提醒你的是,引用是常量反正即使没有“常量”的关键字。
MMMMMMMM

这样做的主要问题是,如果有人这样做B b(A()),您就会被搞砸了,因为您最终将得到一个悬空的引用。
log0

Answers:


68

避免使用引用成员,因为它们限制了类的实现可以做的事情(如您提到的那样,阻止了赋值运算符的实现),并且对类可以提供的内容没有任何好处。

问题示例:

  • 您必须在每个构造函数的初始化程序列表中初始化引用:无法将这个初始化程序分解为另一个函数(直到C ++ 0x,反正 编辑: C ++现在已经委派了构造函数
  • 参考不能反弹或为空。这可能是一个优点,但是如果代码需要更改以允许重新绑定或成员为null,则需要更改成员的所有用法
  • 与指针成员不同,引用不能轻易地被智能指针或迭代器替换,因为重构可能需要
  • 每当使用引用时,它看起来就像值类型(.运算符等),但表现得像指针(可能悬空)-因此,例如Google样式指南不鼓励使用它

65
您提到的所有事物都是要避免的好事物,因此,如果引用可以帮助您,那么它们就是好事,也不错。初始化列表是初始化数据的最佳位置。很多时候,您必须隐藏赋值运算符,而不必使用引用。“不能反弹”-好的,变量的重用是一个坏主意。
Mykola Golubyev

4
@Mykola:我同意你的看法。我希望成员在初始化程序列表中进行初始化,我避免使用空指针,并且变量更改其含义当然不好。我们的意见不同之处在于,我不需要或不希望编译器为我强制执行此操作。它并没有使类更容易编写,在该领域我也没有遇到过会遇到的错误,并且偶尔我欣赏从始终使用指针成员(适当时使用智能指针)的代码中获得的灵活性。
詹姆斯·霍普金

我认为不必隐藏赋值是一个虚假的参数,因为如果类包含一个指针,无论如何它都不负责删除,则不必隐藏赋值-默认值可以。我认为这是最好的答案,因为它提供了充分的理由说明为什么在成员数据中应该优先使用指针而不是引用。
markh44,2009年

4
James,不是因为它使代码易于编写,而是因为使代码易于阅读。将引用作为数据成员,您不必怀疑它在使用时是否可以为null。这意味着您可以查看所需上下文较少的代码。
Len Holgate 2010年

4
它使单元测试和模拟变得更加困难,几乎不可能使用,取决于所使用的数量,再加上“影响图爆炸”,恕我直言,有充分的理由说明它们永远不会使用它们。
克里斯·黄·利弗

155

我自己的经验法则:

  • 当您希望对象的寿命取决于其他对象的寿命时,请使用引用成员:这是一种明确的说法,如果没有另一个类的有效实例,则不允许该对象存活-因为没有分配和通过构造函数获取引用初始化的义务。这是设计类的一种好方法,而无需假设其实例是否属于另一个类。您仅假设他们的生活与其他实例直接相关。它允许您以后更改使用类实例的方式(使用新实例,作为本地实例,作为类成员,由管理器中的内存池生成等)。
  • 在其他情况下,请使用指针:如果希望以后更改成员,请使用指针或const指针以确保仅读取指向的实例。如果该类型是可复制的,则无论如何都不能使用引用。有时,您还需要在特殊的函数调用(例如init())之后初始化成员,然后您别无选择,只能使用指针。但是:在所有成员函数中使用断言可快速检测错误的指针状态!
  • 如果您希望对象的生存期取决于外部对象的生存期,并且还需要该类型是可复制的,则在构造函数中使用指针成员但引用参数,这样可以在构造上指示此对象的生存期取决于在参数的生存期中,实现仍使用指针进行复制。只要这些成员仅通过复制进行更改,并且您的类型没有默认构造函数,则该类型应满足两个目标。

1
我认为您和Mykola的答案是正确的,并促进了无指针(少读指针)编程。
艾米特

37

对象很少允许赋值和其他类似比较的东西。如果您考虑使用诸如“部门”,“员工”,“董事”之类的对象的业务模型,那么很难想象将一个员工分配给另一个员工的情况。

因此,对于业务对象,最好将一对一和一对多关系描述为引用而不是指针。

可以将一或零的关系描述为一个指针。

因此,没有“我们不能分配”的因素。
很多程序员只是习惯使用指针,这就是为什么他们会找到任何参数以避免使用引用的原因。

将指针作为成员将迫使您或您的团队成员在使用前以“以防万一”的注释一次又一次地检查指针。如果指针可以为零,则该指针可能被用作标志,这很不好,因为每个对象都必须扮演自己的角色。


2
+1:我经历过。只有某些通用数据存储类需要assignemtn和copy-c'tor。对于更高级的业务对象框架,还应该使用其他复制技术,这些技术还可以处理“不复制唯一字段”和“添加到另一个父对象”等内容。因此,您可以使用高级框架来复制业务对象并禁止低级作业。
mmmmmmmm

2
+1:一些共识:阻止定义赋值运算符的参考成员不是重要的论点。当您正确地陈述另一种方式时,并非所有对象都具有值语义。我也同意我们不希望出于逻辑原因不希望'if(p)'散布在整个代码中。但是正确的方法是通过类不变式:一个定义明确的类不会对成员是否可以为null存有疑问。如果代码中的任何指针可以为null,那么我希望它能被很好地注释。
詹姆斯·霍普金

@JamesHopkin我同意精心设计的类可以有效地使用指针。除了防止NULL外,引用还保证您的引用已初始化(指针不需要初始化,因此可以指向其他任何对象)。引用还会告诉您有关所有权的信息,即该对象不拥有该成员。如果您始终对聚合成员使用引用,那么您将非常有信心地说指针成员是复合对象(尽管在很多情况下复合成员都由指针表示)。
weberc2 2015年


6

在某些重要情况下,根本不需要分配。这些通常是轻量级算法包装器,可在不超出范围的情况下促进计算。此类对象是引用成员的主要候选对象,因为您可以确保它们始终持有有效的引用,而无需复制。

在这种情况下,请确保使赋值运算符(通常还包括复制构造函数)不可用(通过继承boost::noncopyable或声明为私有)。

但是,正如用户pts所评论的那样,大多数其他对象并非如此。在这里,使用参考成员可能是一个巨大的问题,通常应避免使用。


6

正如每个人似乎都在发布一般规则一样,我将提供两个:

  • 永远不要将use引用用作类成员。我从未在自己的代码中这样做(除非向自己证明我在这条规则中是对的),并且无法想象我会这样做的情况。语义太混乱了,实际上并不是引用设计的目的。

  • 在将参数传递给函数(基本类型除外)或算法需要复制时,始终,总是使用引用。

这些规则很简单,使我受益匪浅。我留下了使用智能指针(但请不要使用auto_ptr)作为其他类的成员的规则。


2
在第一个方面不太同意,但是对于价值理由的重视不是分歧。+1
大卫·罗德里格斯(DavidRodríguez)-dribeas,2009年

(不过,我们似乎失去了SO的好选民的支持)
James Hopkin

2
我之所以投反对票,是因为“永远不要将引用用作类成员”,以下理由对我而言是不合适的。正如我的回答(并在最佳答案中评论)以及我自己的经验所解释的,在某些情况下,这只是一件好事。在我被一家很好地使用它的公司聘用之前,我一直以为像你一样,这使我明白,在某些情况下它会有所帮助。实际上,我认为真正的问题更多是语法和隐含含义,使引用成为成员需要程序员理解他在做什么。
克莱姆

1
Neil>我同意,但是不是“意见”使我投了反对票,而是“从不”的主张。由于在这里争论似乎无济于事,我将取消我的投反对票。
克莱姆

2
可以通过解释有关引用成员语义的混淆来改进此答案。这个理由很重要,因为从表面上看,指针在语义上似乎更加模棱两可(它们可能没有初始化,并且它们对底层对象的所有权一无所知)。
weberc2 2015年

4

是的:说包含引用的对象不应该是可分配的,因为一旦初始化就不应更改引用吗?

我对数据成员的经验法则:

  • 永远不要使用引用,因为它会阻止分配
  • 如果您的班级负责删除,请使用boost的scoped_ptr(比auto_ptr更安全)
  • 否则,使用指针或const指针

2
当我需要分配业务对象时,我什至无法命名。
Mykola Golubyev

1
+1:我要特别注意auto_ptr成员-任何具有它们的类都必须明确定义赋值和复制构造(生成的极不可能是必需的)
James Hopkin

auto_ptr也防止(明智的)分配。

2
@Mykola:我还体验到98%的业务对象不需要分配或复制。我通过在这些类的标题中添加一个小宏来防止这种情况,该宏使副本c'tors和operator =()私有,没有实现。因此,在98%的对象中,我也使用引用,因此很安全。
mmmmmmmm

1
作为成员的引用可用于明确声明该类实例的寿命直接取决于其他类(或相同类)实例的寿命。“切勿使用引用,因为它会阻止分配”,它假定所有类都必须允许分配。就像假设所有类都必须允许复制操作一样……
Klaim

2

当指针可以为null或可以更改时,我通常只在成员数据中使用指针。还有其他理由更喜欢指针而不是数据成员的引用吗?

是。您代码的可读性。指针使该成员成为引用(具有讽刺意味的是:)),而不是包含的对象,这一点显而易见,因为使用该成员时必须取消引用。我知道有人认为这是过时的做法,但我仍然认为这只是防止混乱和错误。


1
Lakos在“大型C ++软件设计”中也对此进行了论证。
2009年

我相信Code Complete也支持这一点,但我并不反对。它并不过分引人注目……
markh44,2009年

2

我建议不要使用参考数据成员,因为您永远不知道谁将从您的班级中派生出来,以及他们可能想做什么。他们可能不想使用引用的对象,但是作为引用,您已迫使他们提供有效的对象。我已经对自己做了足够的工作,以停止使用参考数据成员。


0

如果我正确理解了这个问题...

作为函数参数而不是指针的引用: 如您所指出的,指针并不能明确指出谁拥有指针的清理/初始化。当希望使用指针时,最好使用一个共享点,它是C ++ 11的一部分,而在通过接受指向数据的类的生命周期中不能保证数据的有效性时,则建议使用weak_ptr。也是C ++ 11的一部分。将引用用作函数参数可确保引用不为null。您必须破坏语言功能才能解决此问题,而且我们也不在乎松散的加农炮编码器。

引用成员变量: 关于数据有效性,如上。这样可以保证所指向的数据有效,然后进行引用。

将变量有效性的责任移到代码中的较早位置,不仅可以清理较后的代码(示例中的类A),还可以使使用它的人清楚知道。

在您的示例中,这有点令人困惑(我确实会尝试找到一个更清晰的实现),由于B是A的成员,因此B的A可以保证B的生命周期,因此引用可以加强这一点并且(可能)更加清晰。

并且以防万一(这不太可能发生,因为在您的代码上下文中它没有任何意义),另一种选择是,使用未引用的,非指针的A参数,将复制A,并使该范例无用-我真的不认为这是您的意思。

同样,这也保证了您无法更改引用的数据,因为在这里可以修改指针。仅当对数据的引用/指针不可更改时,const指针才有效。

如果不能保证B的A参数被设置或可以重新分配,则指针是有用的。有时,弱指针对于实现而言太冗长了,很多人要么不知道weak_ptr是什么,要么就不喜欢它们。

撕开这个答案,请:)

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.