[31] 引用与值的语义
(Part of C++ FAQ Lite, Copyright © 1991-2009, Marshall Cline, cline@parashift.com)


简体中文版翻译:Zhiguo Zhang

FAQs in section [31]:


[31.1] 什么是值和/或引用传递,在C++用哪个最好?

对于引用,被赋值的是一个指针拷贝。而值传递,被赋值的是值的拷贝而不是指针。C++中你可以选择使用赋值操作符或者拷贝值(值传递),或者使用指针拷贝来复制一个指针(引用传递)。C++也允许你重写复制操作符来实现你想要的操作,但是默认选择是拷贝值。

引用传递的好处:灵活和动态绑定(只有使用指针或者引用的时候,才能获得动态绑定)。

值传递的好处:速度。因为值传递需要拷贝一个对象(而不是一个指针),你可能很奇怪为什么会这样。事实是大家通常使用一个对象,而不是拷贝多个对象,因此偶尔的拷贝开销比间接指针访问对象带来的开销要小。

三种情况你会获得一个对象而不是对象指针:本地对象,全局或者静态对象,以及类的非指针成员对象。最重要的是后者(对象组合)。

下个FAQ会给出更多的值/引用传递的信息。请阅读所有内容以有个全面认识。前几个倾向于使用值传递,如果你只阅读前几个,可能你会得到一个片面的认识。

赋值还包括其他问题(比如浅拷贝和深拷贝),这里不讨论这些。

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.2] 什么是虚成员,如何/为什么在C++中使用?

"虚成员(Virtual Data)"允许子类改变父类的成员对象。C++并不严格支持虚成员,但是可以模拟实现。虽然实现的不是很漂亮。

模拟实现要求基类要有一个成员对象指针,子类必须提供一个新对象,基类的成员对象指针指向这个新对象。基类可以有一个或者多个正常的构造函数提供成员指针对象的对象(通过new),基类的析构函数将会“delete”这个对象。

例如, Stack类可能有个Array成员对象(使用指针)而子类StrechableStack可以重写基类的Array成员为StrechableArray。要使这个实现,StretchableArray必须从Array继承,这样Stack类可以使用Array*Stack类的正常构造函数可以初始化Array*new Array, 但是Stack类也要有一个构造函数(很可能protected属性的构造函数)可以接受一个来自子类的Array* StretchableStack类的构造函数为基类的这个特殊构造函数提供new StretchableArray对象。

好处:

缺点:

换句话讲,我们简化了StrechableStack的实现代码,但是所有的用户都要付出代价。不幸的是,不仅StrechableStack用户而且Stack用户都要付出这个代价。

请阅读本节其他内容。(这样你会有一个全面认识)

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.3] 怎么区别虚拟数据和动态数据?

最简单的办法是虚函数分析法。虚函: 虚函数意味着“声明(签名)”在子类中必须一样,但是“定义(实现)”可以被重写。继承的成员函数的重写是子类的静态属性,不会随着任何特定对象的改变而动态改变,也不可能应为子类的不同实例而有不同的实现。

现在重新阅读上面段落,但是要做下面替换:

这样你就可以定义虚数据

另外一种方法是辨别"per-object"成员函数和"dynamic" 成员函数。 "per-object" 成员函数是指在不同的实例中实现有可能不同的成员函数,可以使用函数指针实现,这个指针可以是const,因为该指针在对象的生命周期中不会被改变。而"dynamic" 成员函数是指将会随时间而动态改变的成员函数,也可以由函数指针实现,但是函数指针不能为const

概括一下上面的分析,数据成员有三种概念:

他们相似的原因是他们都不被C++支持,只有很少情况下可以这样使用。在这种情况下,模拟机制都是相同的:通过指向基类(很可能是抽象类)的指针。在支持“first class abstraction mechanisms的语言中,可能这种区别很明显一些,因为他们将会有各自不同的语法表示。

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.4] 应该通常使用数据成员对象指针或者使用组合

组合。

一般来说,你的成员对象应该被包含在组合对象中(并不总是这样,包装器(Wrapper)对象是一个你可以使用指针或者引用的好例子; N-to-1-uses-a关系也需要指针或者引用)。

完全包含成员对象性能优于指针的原因有三点:

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.5] 什么是使用成员对象指针的3个相对性能开销?

前一节FAQ列举了3个相对性能开销:

因此完全包含成员对象允许重要的优化,而这在使用对象指针的情况下是不可能的。这是具有引用语义的编程语言为什么面临继承性能挑战的主要原因。

请阅读下面3FAQ一遍获得全面理解!

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.6] “内联虚函数的会被内联吗?

有时...

当对象是个指针或者引用的时候, 虚函 调用不能被内联,因为函数必须被动态调用。原因:编译器无法知道实际的代码来调用直到运行时(即动态),因为该代码可能是来自一个派生类,调用函数编译以后才创建的。

因此,只有当编译器知道虚函数调用的目标的确切类的时候,内联虚函数才有可能被内联。发生这种情况只有在编译器知道一个实际的对象,也就是说,本地对象,全局/静态对象,或在组合的完全包含对象,而不是一个指针或引用的时候。

注意,内联和非内联之间的差别远远超过普通函数调用和虚函数调用的差别。例如,普通函数调用和虚函数调用的差别常常只有两个额外的内存引用,但内联函数和非内联函数的差别可以多达一个数量级(数以亿计的调用无关紧要的成员函数, 内联虚函数的损失可能会导致25倍的差距!Doug Lea, "Customization in C++," proc Usenix C++ 1990])

这种顿悟的实际后果:不要陷在无休止的辩论中(或销售策略!),来比较编译器/语言的虚函数调用的成本。和具有扩展内联成员函数调用的语言/编译器做比较是没有任何意义的。也就是说,许多语言实现厂商带鼓吹他们的调度策略是如何好,但如果没有内联成员函数的话,系统的整体性能会很差,正是因为靠内联调度,他们才具有最好的性能。

注意:请阅读下面的2FAQs一遍了解另一方面!

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.7] 听起来像我不应该使用引用?

不对。

引用是个好东西。我们不能生活在没有引用。我们只是不希望我们的软件使用太多的指针。在C + +中,你可以挑选你想要引用语义(指针/引用)以及值语义(如对象包含其他对象等)。在一个大的系统中,应该有一个平衡。然而,如果你无论什么都使用指针的话,你将得到许多速度方面的问题。

求解问题的对象往往要比更高层次的对象占用更多的存储空间。这些问题空间抽象类的ID通常比他们的更重要,因此以用语义应该被用于求解问题的对象。

请注意,这些求解问题的对象通常在较高的抽象层次,相比那些处于解决方案空间的对象来说。因此求解问题的对象通常有一个相对较低的使用频率。因此,C ++中为我们提供了一个理想的情况:对于那些需要独特的身份的对象,或过大而不能复制我们选择使用引用语义,对于其他对象我们可以选择值语义。因此,最高使用频率的对象将最终使用值语义,因为在灵活性方面我们没有损失,但是在性能方面,实现了我们最需要的!

这些只是真正的面向对象设计的诸多问题中的一部分。精通面向对象设计/C++需要需要时间和高质量的训练。如果你想有一个强有力的工具,你要投入时间和精力。

不要停下来! 无比阅读下一个问题!!

TopBottomPrevious sectionNext sectionSearch the FAQ ]


[31.8] 引用的性能问题是否意味着我要使用值传递?

不是。

前面FAQ谈论的是成员对象,而不是参数。一般而言,对象是继承层次结构的一部分,应该通过引用或指针来传递,而不是值传递,因为只有这样你才能得到(期望的)动态绑定(按值传递和继承不相符,因为派生类对象将会被 ,当按值传递到一个基类对象的时候)。

除非另有其他的理由,成员对象应当按值传递,参数应按引用传递。以前的FAQ里面讨论了应该按引用传递的成员对象的其他的理由

TopBottomPrevious sectionNext sectionSearch the FAQ ]


E-Mail E-mail the author
C++ FAQ LiteTable of contentsSubject indexAbout the author©Download your own copy ]
Revised Jan 2, 2009