⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 8832.htm

📁 C++细节解释
💻 HTM
字号:
<HTML>
<HEAD>
<meta http-equiv='Content-Type' content='text/html; charset=gb2312'>


<style >
.fst{padding:0px 15px;width:770px;border-left:0px solid #000000;border-right:0px solid #000000}
.fstdiv3 img{border:0px;border-right:8px solid #eeeecc;border-top:6px solid #eeeecc}
</style>
<title>
Effective C++ 2e Item22
</title>
</HEAD>
<BODY >
<center>

<div align=center><div class=fst align=left><div class=fstdiv3 id=print2>
<b>
Effective C++ 2e Item22&nbsp;</b><p>条款22: 尽量用“传引用”而不用“传值”</p> 
<p>c语言中,什么都是通过传值来实现的,c++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参总是通过“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。</p> 
<p>正如我在本书的导言中所指出的,“通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。例如,看下面这个(只是假想的)类的结构:</p> 
<p>class person {<br>public:<br>&nbsp; person();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 为简化,省略参数<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <br>&nbsp; ~person();</p> 
<p>&nbsp; ...</p> 
<p>private:<br>&nbsp; string name, address;<br>};</p> 
<p>class student: public person {<br>public:<br>&nbsp; student();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 为简化,省略参数<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // <br>&nbsp; ~student();</p> 
<p>&nbsp; ...</p> 
<p>private:<br>&nbsp; string schoolname, schooladdress;<br>};</p> 
<p>现在定义一个简单的函数returnstudent,它取一个student参数(通过值)然后立即返回它(也通过值)。定义完后,调用这个函数:</p> 
<p>student returnstudent(student s) { return s; }</p> 
<p>student plato;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // plato(柏拉图)在<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // socrates(苏格拉底)门下学习</p> 
<p>returnstudent(plato);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 调用returnstudent</p> 
<p>这个看起来无关痛痒的函数调用过程,其内部究竟发生了些什么呢?</p> 
<p>简单地说就是:首先,调用了student的拷贝构造函数用以将s初始化为plato;然后再次调用student的拷贝构造函数用以将函数返回值对象初始化为s;接着,s的析构函数被调用;最后,returnstudent返回值对象的析构函数被调用。所以,这个什么也没做的函数的成本是两个student的拷贝构造函数加上两个student析构函数。</p> 
<p>但没完,还有!student对象中有两个string对象,所以每次构造一个student对象时必须也要构造两个string对象。student对象还是从person对象继承而来的,所以每次构造一个student对象时也必须构造一个person对象。一个person对象内部有另外两个string对象,所以每个person的构造也必然伴随另两个string的构造。所以,通过值来传递一个student对象最终导致调用了一个student拷贝构造函数,一个person拷贝构造函数,四个string拷贝构造函数。当student对象被摧毁时,每个构造函数对应一个析构函数的调用。所以,通过值来传递一个student对象的最终开销是六个构造函数和六个析构函数。因为returnstudent函数使用了两次传值(一次对参数,一次对返回值),这个函数总共调用了十二个构造函数和十二个析构函数!</p> 
<p>在c++编译器的设计者眼里,这是最糟糕的情况。编译器可以用来消除一些对拷贝构造函数的调用(c++标准——见条款50——描述了具体在哪些条件下编译器可以执行这类的优化工作,条款m20给出了例子)。一些编译器也这样做了。但在不是所有编译器都普遍这么做的情况下,一定要对通过值来传递对象所造成的开销有所警惕。</p> 
<p>为避免这种潜在的昂贵的开销,就不要通过值来传递对象,而要通过引用:</p> 
<p>const student&amp; returnstudent(const student&amp; s)<br>{ return s; }</p> 
<p>这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建。</p> 
<p>通过引用来传递参数还有另外一个优点:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的。例如,假设设计这么一套实现图形窗口系统的类:</p> 
<p>class window {<br>public:<br>&nbsp; string name() const;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 返回窗口名<br>&nbsp; virtual void display() const;&nbsp;&nbsp;&nbsp; // 绘制窗口内容<br>};</p> 
<p>class windowwithscrollbars: public window {<br>public:<br>&nbsp; virtual void display() const;<br>};</p> 
<p>每个window对象都有一个名字,可以通过name函数得到;每个窗口都可以被显示,着可以通过调用display函数实现。display声明为virtual意味着一个简单的window基类对象被显示的方式往往和价格昂贵的windowwithscrollbars对象被显示的方式不同(见条款36,37,m33)。</p> 
<p>现在假设写一个函数来打印窗口的名字然后显示这个窗口。下面是一个用错误的方法写出来的函数:</p> 
<p>// 一个受“切割问题”困扰的函数<br>void printnameanddisplay(window w)<br>{<br>&nbsp; cout &lt;&lt; w.name();<br>&nbsp; w.display();<br>}</p> 
<p>想象当用一个windowwithscrollbars对象来调用这个函数时将发生什么:</p> 
<p>windowwithscrollbars wwsb;</p> 
<p>printnameanddisplay(wwsb);</p> 
<p>参数w将会作为一个windows对象而被创建(它是通过值来传递的,记得吗?),所有wwsb所具有的作为windowwithscrollbars对象的行为特性都被“切割”掉了。printnameanddisplay内部,w的行为就象是一个类window的对象(因为它本身就是一个window的对象),而不管当初传到函数的对象类型是什么。尤其是,printnameanddisplay内部对display的调用总是window::display,而不是windowwithscrollbars::display。</p> 
<p>解决切割问题的方法是通过引用来传递w:</p> 
<p>// 一个不受“切割问题”困扰的函数<br>void printnameanddisplay(const window&amp; w)<br>{<br>&nbsp; cout &lt;&lt; w.name();<br>&nbsp; w.display();<br>}</p> 
<p>现在w的行为就和传到函数的真实类型一致了。为了强调w虽然通过引用传递但在函数内部不能修改,就要采纳条款21的建议将它声明为const。</p> 
<p>传递引用是个很好的做法,但它会导致自身的复杂性,最大的一个问题就是别名问题,这在条款17进行了讨论。另外,更重要的是,有时不能用引用来传递对象,参见条款23。最后要说的是,引用几乎都是通过指针来实现的,所以通过引用传递对象实际上是传递指针。因此,如果是一个很小的对象——例如int——传值实际上会比传引用更高效。<br>
</p>
</DIV></div></div>

</center></BODY></HTML>

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -