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

📄 chapter15.html

📁 《C++编程思想》中文版。。。。。。。。。。。。。
💻 HTML
📖 第 1 页 / 共 5 页
字号:
part of a system tend not to propagate to other parts of the system as they do
in
C.</FONT><A NAME="_Toc305593266"></A><A NAME="_Toc305628738"></A><A NAME="_Toc312374045"></A><A NAME="_Toc472655022"></A><BR></P></DIV>
<A NAME="Heading440"></A><FONT FACE = "Verdana"><H2 ALIGN="LEFT">
How C++ implements late binding</H2></FONT>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">How can late
binding<A NAME="Index2435"></A> happen? All the work goes on behind the scenes
by the compiler, which installs the necessary late-binding mechanism when you
ask it to (you ask by creating virtual functions). Because programmers often
benefit from understanding the mechanism of virtual functions in C++, this
section will elaborate on the way the compiler implements this
mechanism.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">The keyword
<A NAME="Index2436"></A><B>virtual</B> <A NAME="Index2437"></A>tells the
compiler it should not perform early binding. Instead, it should automatically
install all the mechanisms necessary to perform late binding. This means that if
you call <B>play(&#160;)</B> for a <B>Brass</B> object <I>through an address for
the base-class </I><B>Instrument</B>, you&#8217;ll get the proper
function.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">To accomplish this, the typical
compiler</FONT><A NAME="fnB54" HREF="#fn54">[54]</A><FONT FACE="Georgia">
creates a single table (called the VTABLE<A NAME="Index2438"></A>) for each
class that contains <B>virtual</B> functions. The compiler places the addresses
of the virtual functions for that particular class in the VTABLE. In each class
with virtual functions, it secretly places a pointer, called the <I>vpointer</I>
<A NAME="Index2439"></A>(abbreviated as VPTR<A NAME="Index2440"></A>), which
points to the VTABLE for that object. When you make a virtual function call
through a base-class pointer (that is, when you make a polymorphic
call<A NAME="Index2441"></A><A NAME="Index2442"></A>), the compiler quietly
inserts code to fetch the VPTR and look up the function address in the VTABLE,
thus calling the correct function and causing late binding to take
place.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">All of this &#8211; setting up the VTABLE
for each class, initializing the VPTR, inserting the code for the virtual
function call &#8211; happens automatically, so you don&#8217;t have to worry
about it. With virtual functions, the proper function gets called for an object,
even if the compiler cannot know the specific type of the
object.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">The following sections go into this
process in more
detail.</FONT><A NAME="_Toc312374046"></A><A NAME="_Toc472655023"></A><BR></P></DIV>
<A NAME="Heading441"></A><FONT FACE = "Verdana"><H3 ALIGN="LEFT">
Storing type
information<BR><A NAME="Index2443"></A><A NAME="Index2444"></A><A NAME="Index2445"></A><A NAME="Index2446"></A></H3></FONT>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">You can see that there is no explicit
type information stored in any of the classes. But the previous examples, and
simple logic, tell you that there must be some sort of type information stored
in the objects; otherwise the type could not be established at runtime. This is
true, but the type information is hidden. To see it, here&#8217;s an example to
examine the sizes of classes that use virtual functions compared with those that
don&#8217;t:</FONT><BR></P></DIV>

<BLOCKQUOTE><FONT SIZE = "+1"><PRE><font color=#009900>//: C15:Sizes.cpp</font>
<font color=#009900>// Object sizes with/without virtual functions</font>
#include &lt;iostream&gt;
<font color=#0000ff>using</font> <font color=#0000ff>namespace</font> std;

<font color=#0000ff>class</font> NoVirtual {
  <font color=#0000ff>int</font> a;
<font color=#0000ff>public</font>:
  <font color=#0000ff>void</font> x() <font color=#0000ff>const</font> {}
  <font color=#0000ff>int</font> i() <font color=#0000ff>const</font> { <font color=#0000ff>return</font> 1; }
};

<font color=#0000ff>class</font> OneVirtual {
  <font color=#0000ff>int</font> a;
<font color=#0000ff>public</font>:
  <font color=#0000ff>virtual</font> <font color=#0000ff>void</font> x() <font color=#0000ff>const</font> {}
  <font color=#0000ff>int</font> i() <font color=#0000ff>const</font> { <font color=#0000ff>return</font> 1; }
};

<font color=#0000ff>class</font> TwoVirtuals {
  <font color=#0000ff>int</font> a;
<font color=#0000ff>public</font>:
  <font color=#0000ff>virtual</font> <font color=#0000ff>void</font> x() <font color=#0000ff>const</font> {}
  <font color=#0000ff>virtual</font> <font color=#0000ff>int</font> i() <font color=#0000ff>const</font> { <font color=#0000ff>return</font> 1; }
};

<font color=#0000ff>int</font> main() {
  cout &lt;&lt; <font color=#004488>"int: "</font> &lt;&lt; <font color=#0000ff>sizeof</font>(<font color=#0000ff>int</font>) &lt;&lt; endl;
  cout &lt;&lt; <font color=#004488>"NoVirtual: "</font>
       &lt;&lt; <font color=#0000ff>sizeof</font>(NoVirtual) &lt;&lt; endl;
  cout &lt;&lt; <font color=#004488>"void* : "</font> &lt;&lt; <font color=#0000ff>sizeof</font>(<font color=#0000ff>void</font>*) &lt;&lt; endl;
  cout &lt;&lt; <font color=#004488>"OneVirtual: "</font>
       &lt;&lt; <font color=#0000ff>sizeof</font>(OneVirtual) &lt;&lt; endl;
  cout &lt;&lt; <font color=#004488>"TwoVirtuals: "</font>
       &lt;&lt; <font color=#0000ff>sizeof</font>(TwoVirtuals) &lt;&lt; endl;
} <font color=#009900>///:~</font></PRE></FONT></BLOCKQUOTE>

<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">With no virtual functions, the size of
the object is exactly what you&#8217;d expect: the size of a
single</FONT><A NAME="fnB55" HREF="#fn55">[55]</A><FONT FACE="Georgia">
<B>int</B>. With a single virtual function in <B>OneVirtual</B>, the size of the
object is the size of <B>NoVirtual</B> plus the size of a <B>void</B> pointer.
It turns out that the compiler inserts a single pointer (the VPTR) into the
structure if you have one <I>or more</I> virtual functions. There is no size
difference between <B>OneVirtual</B> and <B>TwoVirtuals</B>. That&#8217;s
because the VPTR points to a table of function addresses. You need only one
table because all the virtual function addresses are contained in that single
table.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">This example required at least one data
member. If there had been no data members, the C++ compiler would have forced
the objects to be a nonzero size
<A NAME="Index2447"></A><A NAME="Index2448"></A>because each object must have a
distinct address. If you imagine indexing into an array of zero-sized objects,
you&#8217;ll understand. A &#8220;dummy&#8221; member is inserted into objects
that would otherwise be zero-sized. When the type information is inserted
because of the <B>virtual</B> keyword, this takes the place of the
&#8220;dummy&#8221; member. Try commenting out the <B>int a</B> in all the
classes in the example above to see
this.</FONT><A NAME="_Toc312374047"></A><A NAME="_Toc472655024"></A><BR></P></DIV>
<A NAME="Heading442"></A><FONT FACE = "Verdana"><H3 ALIGN="LEFT">
Picturing virtual
functions<BR><A NAME="Index2449"></A><A NAME="Index2450"></A></H3></FONT>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">To understand exactly what&#8217;s going
on when you use a virtual function, it&#8217;s helpful to visualize the
activities going on behind the curtain. Here&#8217;s a drawing of the array of
pointers <B>A[ ] </B>in <B>Instrument4.cpp</B>:</FONT><BR></P></DIV>
<DIV ALIGN="CENTER"><FONT FACE="Georgia"><IMG SRC="TIC2Vo16.gif"></FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">The array of <B>Instrument</B> pointers
has no specific type information; they each point to an object of type
<B>Instrument</B>. <B>Wind</B>, <B>Percussion</B>, <B>Stringed</B>, and
<B>Brass</B> all fit into this category because they are derived from
<B>Instrument</B> (and thus have the same interface as <B>Instrument</B>, and
can respond to the same messages), so their addresses can also be placed into
the array. However, the compiler doesn&#8217;t know that they are anything more
than <B>Instrument</B> objects, so left to its own devices it would normally
call the base-class versions of all the functions. But in this case, all those
functions have been declared with the <B>virtual</B> keyword, so something
different happens.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">Each time you create a class that
contains virtual functions, or you derive from a class that contains virtual
functions, the compiler creates a unique VTABLE <A NAME="Index2451"></A>for that
class, seen on the right of the diagram. In that table it places the addresses
of all the functions that are declared virtual in this class or in the base
class. If you don&#8217;t override a function that was declared virtual in the
base class, the compiler uses the address of the base-class version in the
derived class. (You can see this in the <B>adjust</B> entry in the <B>Brass</B>
VTABLE.) Then it places the VPTR <A NAME="Index2452"></A>(discovered in
<B>Sizes.cpp</B>) into the class. There is only one VPTR for each object when
using simple inheritance like this. The VPTR must be initialized to point to the
starting address of the appropriate VTABLE. (This happens in the constructor,
which you&#8217;ll see later in more detail.)</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">Once the VPTR is initialized to the
proper VTABLE, the object in effect &#8220;knows&#8221; what type it is. But
this self-knowledge is worthless unless it is used at the point a virtual
function is called.</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">When you call a virtual function through
a base class address (the situation when the compiler doesn&#8217;t have all the
information necessary to perform early binding), something special happens.
Instead of performing a typical function call, which is simply an
assembly-language <B>CALL</B> to a particular address, the compiler generates
different code to perform the function call. Here&#8217;s what a call to
<B>adjust(&#160;)</B> for a <B>Brass</B> object looks like, if made through an
<B>Instrument</B> pointer (An <B>Instrument</B> reference produces the same
result):</FONT><BR></P></DIV>
<DIV ALIGN="CENTER"><FONT FACE="Georgia"><IMG SRC="TIC2Vo17.gif"></FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">The compiler begins with the
<B>Instrument</B> pointer, which points to the starting address of the object.
All <B>Instrument</B> objects or objects derived from <B>Instrument</B> have
their VPTR in the same place (often at the beginning of the object), so the
compiler can pick the VPTR out of the object. The VPTR points to the starting
address of the VTABLE. All the VTABLE function addresses are laid out in the
same order, regardless of the specific type of the object. <B>play(&#160;)</B>
is first, <B>what(&#160;)</B> is second, and <B>adjust(&#160;)</B> is third. The
compiler knows that regardless of the specific object type, the
<B>adjust(&#160;)</B> function is at the location VPTR+2. Thus, instead of
saying, &#8220;Call the function at the absolute location
<B>Instrument::adjust</B>&#8221; (early
binding<A NAME="Index2453"></A><A NAME="Index2454"></A><A NAME="Index2455"></A>;
the wrong action), it generates code that says, in effect, &#8220;Call the
function at VPTR+2.&#8221; Because the fetching of the VPTR and the
determination of the actual function address occur at runtime, you get the
desired late binding. You send a message to the object, and the object figures
out what to do with
it.</FONT><A NAME="_Toc312374048"></A><A NAME="_Toc472655025"></A><BR></P></DIV>
<A NAME="Heading443"></A><FONT FACE = "Verdana"><H3 ALIGN="LEFT">
Under the hood</H3></FONT>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">It can be helpful to see the
assembly-language code generated by a virtual function
<A NAME="Index2456"></A><A NAME="Index2457"></A><A NAME="Index2458"></A><A NAME="Index2459"></A>call,
so you can see that late-binding is indeed taking place. Here&#8217;s the output
from one compiler for the call </FONT><BR></P></DIV>
<BLOCKQUOTE><FONT SIZE = "+1"><PRE>i.adjust(1);</PRE></FONT></BLOCKQUOTE>

<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">inside the function <B>f(Instrument&amp;
i)</B>:</FONT><BR></P></DIV>

<BLOCKQUOTE><FONT SIZE = "+1"><PRE>push  1
push  si
mov   bx, word ptr [si]
call  word ptr [bx+4]
add   sp, 4</PRE></FONT></BLOCKQUOTE>

<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">The arguments of a C++ function call,
like a C function call, are pushed on the stack from right to left (this order
is required to support C&#8217;s variable argument lists), so the argument
<B>1</B> is pushed on the stack first. At this point in the function, the
register <B>si</B> (part of the Intel X86 processor architecture) contains the
address of <B>i</B>. This is also pushed on the stack because it is the starting
address of the object of interest. Remember that the starting address
corresponds to the value of <B>this<A NAME="Index2460"></A></B>, and <B>this</B>
is quietly pushed on the stack as an argument before every member function call,
so the member function knows which particular object it is working on. So
you&#8217;ll always see one more than the number of arguments pushed on the
stack before a member function call (except for <B>static</B> member functions,
which have no <B>this</B>).</FONT><BR></P></DIV>
<DIV ALIGN="LEFT"><P><FONT FACE="Georgia">Now the actual virtual function call must
be performed. First, the VPTR <A NAME="Index2461"></A>must be produced, so the
VTABLE <A NAME="Index2462"></A>can be found. For this compiler the VPTR is
inserted at the beginning of the object, so the contents of <B>this</B>
correspond to the VPTR. The line</FONT><BR></P></DIV>
<BLOCKQUOTE><FONT SIZE = "+1"><PRE>mov bx, word ptr [si]</PRE></FONT></BLOCKQUOTE>

⌨️ 快捷键说明

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