一.相关知识点
函数调用捆绑
把函数体与函数调用相联系称为捆绑(binding)。当捆绑在程序运行之前(由编译器和连接器)完成时,称为早捆绑。我们可能没有听到过这个术语,因为在过程语言中是不会有的:C编译只有一种函数调用,就是早捆绑。上面程序中的问题是早捆绑引起的,因为编译器在只有 instrument地址时它不知道正确的调用函数。解决方法被称为晚捆绑,这意味着捆绑在运行时发生,基于对象的类型。晚捆绑又称为动态捆绑或运行时捆绑。当一个语言实现晚捆绑时,必须有一种机制在运行时确定对象的类型和合适的调用函数。这就是,编译器还不知道实际的对象类型,但它插入能找到和调用正确函数体的代码。晚捆绑机制因语言而异,但可以想象,一些种类的类型信息必须装在对象自身中。
虚函数
对于特定的函数,为了引起晚捆绑, C++要求在基类中声明这个函数时使用 virtual关键字。晚捆绑只对virtual起作用,而且只发生在我们使用一个基类的地址时,并且这个基类中有virtual函数,尽管它们也可以在更早的基类中定义。为了创建一个virtual成员函数,可以简单地在这个函数声明的前面加上关键字 virtual。对于这个函数的定义不要重复,在任何派生类函数重定义中都不要重复它(虽然这样做无害)。
如果一个函数在基类中被声明为 virtual,那么在所有的派生类中它都是virtual的。在派生类中virtual函数的重定义通常称为越位。
撩开面纱
如果能看到由虚函数调用而产生的汇编语言代码,这将是很有帮助的,这样可以看到后捆绑实际上是如何发生的。下面是在函数f(instrument&
i)中调用
i.adjust(1) ;
某个编译器所产生的输出:
C++函数调用的参数与C函数调用一样,是从右向左进栈的(这个顺序是为了支持 C的变量参数表),所以参数1首先压栈。在这个函数的这个地方,寄存器 si( intel x86处理器的一部分)存放i的首地址。因为它是被选中的对象的首地址,它也被压进栈。记住,这个首地址对应于this的值,正因为调用每个成员函数时 this都必须作为参数压进栈,所以成员函数知道它工作在哪个特殊对象上。这样,我们总能看到,在成员函数调用之前压栈的次数等于参数个数加一(除了static成员函数,它没有this)。
现在,必须实现实际的虚函数调用。首先,必须产生 VPTR,使得能找到VTABLE。对于这个编译器, VPTR在对象的开头,所以this的内容对应于VPTR。下面这一行
mov bx , word ptr[si]
取出si(即this)所指的字,它就是VPTR。将这个VPTR放入寄存器bx中,放在bx中的这个VPTR指向这个VTABLE的首地址,但被调用的函数在 VTABLE中不是第0个位置,而是第二个位置(因为它是这个表中的第三个函数)。对于这种内存模式,每个函数指针是两个字节长,所以编译器对 VPTR加四,计算相应的函数地址所在的地方,注意,这是编译时建立的常值。所以我们只要保证在第二个位置上的指针恰好指向adjust()。幸好编译器仔细处理,并保证在VTABLE中的所有函数指针都以相同的次序出现。
一旦在VTABLE中相应函数指针的地址被计算出来,就调用这个函数。所以取出这个地址并马上在这个句子中调用:
call word ptr [bx+4]
最后,栈指针移回去,以清除在调用之前压入栈的参数。在 C和C++汇编代码中,我们将常常看到调用者清除这些参数,但这依处理器和编译器的实现而有所变化。
C++对此提供了一种机制,称为纯虚函数。下面是它的声明语法:
virtual void x() = 0;
这样做,等于告诉编译器在 VTABLE中为函数保留一个间隔,但在这个特定间隔中不放地址。只要有一个函数在类中被声明为纯虚函数,则 VTABLE就是不完全的。包含有纯虚函数的类称为纯抽象基类。如果一个类的VTABLE是不完全的,当某人试图创建这个类的对象时,编译器做什么呢?由于它不能安全地创建一个纯抽象类的对象,所以如果我们试图制造一个纯抽象类的对象,编译器就发出一个出错信息。这样,编译器就保证了抽象类的纯洁性,我们就不用担心误用它了。
对象切片
当多态地处理对象时,传地址与传值有明显的不同。所有在这里已经看到的例子和将会看到的例子都是传地址的,而不是传值的。这是因为地址都有相同的长度,传派生类型(它通常稍大一些)对象的地址和传基类(它通常小一点)对象的地址是相同的。如前面解释的,使用多态的目的是让对基类对象操作的代码也能操作派生类对象。
如果使用对象而不是使用地址或引用进行向上映射,发生的事情会使我们吃惊:这个对象被“切片”,直到所剩下来的是适合于目的的子对象。
对象切片实际上是去掉了对象的一部分,而不是象使用指针或引用那样简单地改变地址的内容。因此,对象向上映射不常做,事实上,通常要提防或防止这种操作。我们可以通过在基类中放置纯虚函数来防止对象切片。这时如果进行对象切片就将引起编译时的出错信息。
析构函数和虚拟析构函数
构造函数不能是虚的(在附录B中的技术只类似于虚构造函数)。但析构函数能够且常常必须是虚的。构造函数有其特殊的工作。它首先调用基本构造函数,然后调用在继承顺序中的更晚派生的构造函数,如此一块一块地把对象拼起来。类似的,析构函数也有一个特殊的工作—它必须拆卸可能属于某类层次的对象。为了做这些工作,它必须按照构造函数调用相反的顺序,调用所有的析构函数。这就是,析构函数自最晚派生的类开始,并向上到基类。这是安全且合理的:当前的析构函数能知道基类成员仍是有效的,因为它知道它是从哪一个派生而来的,但不知道从它派生出哪些。应当记住,构造函数和析构函数是必须遵守调用层次唯一的地方。在所有其他函数中,只是某个函数被调用,而无论它是虚的还是非虚的。同一个函数的基类版本在通常的函数中被调用(无论虚否)的唯一的方法是直接地调用这个函数。通常,析构函数的活动是很正常的。但是,如果我们想通过指向某个对象的类的指针操纵这个对象(这就是,通过它的一般接口操纵这个对象),会发生什么现象呢?这在面向对象的程序设计中确实很重要。当我们想
delete在栈中已经用new创建的类的对象的指针时,就会出现这个问题。如果这个指针是指向基类的,编译器只能知道在 delete期间调用这个析构函数的基类版本。我们已经知道,虚函数被创建恰恰是为了解决同样的问题。幸好,析构函数可以是虚函数,于是一切问题就迎刃而解了。虽然析构函数象构造函数一样,是“例外”函数,但析构函数可以是虚的,这是因为这个对象已经知道它是什么类型(而在构造期间则不然)。一旦对象已被构造,它的VPTR就已被初始化了,所以虚函数调用能发生。如果我们创建一个纯虚析构函数,我们就必须提供函数体,因为(不像普通函数)在类层次中所有析构函数都总是被调用。这样,这个纯虚析构函数的函数体以调用结束。
二.相关代码
1.
<span style="font-size:18px;"><strong>/*WIN2.cpp*/ #include <iostream.h> enum note { middleC, Csharp, Cflat }; class instrument { public: void play(note) const { cout << "instrument::play" << endl; } }; class wind : public instrument { public: void play(note) const { cout << "wind::play" << endl; } }; void tune(instrument& i) { i.play(middleC); } int main() { wind flute; tune(flute); return 0; }</strong></span>
2.
<span style="font-size:18px;"><strong>/*WIN3.cpp*/ #include <iostream.h> enum note { middleC, Csharp, Cflat }; class instrument { public: virtual void play(note) const { cout << "instrument::play" << endl; } }; class wind : public instrument { public: void play(note) const { cout << "wind::play" << endl; } }; void tune(instrument& i) { i.play(middleC); } int main() { wind flute; tune(flute); return 0; }</strong></span>
3.
<span style="font-size:18px;"><strong>/*WIN4.cpp*/ #include <iostream.h> enum note { middleC, Csharp, Cflat }; class instrument { public: virtual void play(note) const { cout << "instrument::play" << endl; } virtual char* what() const { return "instrument"; } virtual void adjust(int) {} }; class wind : public instrument { public: void play(note) const { cout << "wind::play" << endl; } char* what() const { return "wind"; } void adjust(int) {} }; class percussion : public instrument { public: void play(note) const { cout << "percussion::play" << endl; } char* what() const { return "percussion"; } void adjust(int) {} }; class string : public instrument { public: void play(note) const { cout << "string::play" << endl; } char* what() const { return "string"; } void adjust(int) {} }; class brass : public wind { public: void play(note) const { cout << "brass::play" << endl; } char* what() const { return "brass"; } }; class woodwind : public wind { public: void play(note) const { cout << "woodwind::play" << endl; } char* what() const { return "woodwind"; } }; void tune(instrument& i) { i.play(middleC); } void f(instrument& i) { i.adjust(1); } //instrument* A[] = { // new wind, // new percussion, // new string, // new brass //数组A[]存放指向基类instrument 的指针,所以在数组初始化过程中发生向上映射 //}; int main() { wind flute; percussion drum; string violin; brass flugelhorn; woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); return 0; }</strong></span>
4.
<span style="font-size:18px;"><strong>/*SIZES.cpp*/ #include <iostream.h> class no_virtual //不带虚函数,对象的长度恰好就是所期望的:单个 i n t的长度 { int a; public: void x() const {} int i() const { return 1; } }; class one_virtual //而带有单个虚函数的one_virtual,对象的长度是no_virtual的长度加上一个void指针的长度 { int a; public: virtual void x() const {} int i() const { return 1; } }; class two_virtual //在one_virtual 和 two_virtuals之间没有区别。这是因为 VPTR指向一个存放地址的表,只需要一个指针 //,因为所有虚函数地址都包含在这个表中。 { int a; public: virtual void x() const {} virtual int i() const { return 1; } }; int main() { cout << "int: " << sizeof(int) << endl; cout << "no_virtual: " << sizeof(no_virtual) << endl; cout << "void*: " << sizeof(void*) << endl; cout << "one_virtual: " << sizeof(one_virtual) << endl; cout << "two_virtual: " << sizeof(two_virtual) << endl; return 0; }</strong></span>
5.
<span style="font-size:18px;"><strong>/*EARLY.cpp*/ #include <iostream.h> class base { public: virtual int f() const { return 1; } }; class derived : public base { public: int f() const { return 2; } }; int main() { derived d; base* b1 = &d; base& b2 = d; base b3; cout << "b1->f() = " << b1->f() << endl; cout << "b2.f() = " << b2.f() << endl; //在b1->f()和b2.f()中,使用地址,就意味着信息不完全: b1和b2可能表示base的地址也可能 //表示其派生对象的地址,所以必须用虚函数。 cout << "b3.f() = " << b3.f() << endl; //而当调用 b3.f()时不存在含糊,编译器知道确切的类型和知道它是一个对象,所以它不可能 //是由base派生的对象,而确切的只是一个 base return 0; }</strong></span>
6.
<span style="font-size:18px;"><strong>/*WINDS.cpp*/ /*纯虚函数是非常有用的,因为它们使得类有明显的抽象性,并告诉用户和编译器希望如何 使用。 注意,纯虚函数防止对纯抽象类的函数以传值方式调用。这样,它也是防止对象意外使用 值向上映射的一种方法。这样就能保证在向上映射期间总是使用指针或引用。 纯虚函数防止产生V TA B L E,但这并不意味着我们不希望对其他函数产生函数体。我们常 常希望调用一个函数的基类版本,即便它是虚拟的。把公共代码放在尽可能靠近我们的类层次 根的地方,这是很好的想法。这不仅节省了代码空间,而且能允许使改变的传播变得容易。*/ #include <iostream.h> enum note { middleC, Csharp, Cflat }; class instrument { public: virtual void play(note) const = 0; virtual char* what() const = 0; virtual void adjust(int) = 0; }; class wind : public instrument { public: void play(note) const { cout << "wind::play" << endl; } char* what() const { return "wind"; } void adjust(int) {} }; class percussion : public instrument { public: void play(note) const { cout << "percussion::play" << endl; } char* what() const { return "percussion"; } void adjust(int) {} }; class string : public instrument { public: void play(note) const { cout << "string::play" << endl; } char* what() const { return "string"; } void adjust(int) {} }; class brass : public wind { public: void play(note) const { cout << "brass::play" << endl; } char* what() const { return "brass"; } }; class woodwind : public wind { public: void play(note) const { cout << "woodwind::play" << endl; } char* what() const { return "woodwind"; } }; void tune(instrument& i) { i.play(middleC); } void f(instrument& i) { i.adjust(1); } int main() { wind flute; percussion drum; string violin; brass flugelhorn; woodwind recorder; tune(flute); tune(drum); tune(violin); tune(flugelhorn); tune(recorder); f(flugelhorn); return 0; }</strong></span>
7.
<span style="font-size:18px;"><strong>/*PVDEF.cpp*/ #include <iostream.h> class base { public: virtual void v() const = 0; virtual void f() const = 0 { cout << "base::f()\n"; } }; void base::v() const { cout << "base::v()\n"; } class d : public base { public: void v() const { base::v(); } void f() const { base::f(); } }; int main() { d D; D.v(); D.f(); return 0; }</strong></span>
8.
<span style="font-size:18px;"><strong>/*当在派生类中增加新的虚函数*/ /*ADDV.cpp*/ #include <iostream.h> class base { int i; public: base(int I) : i(I) {} virtual int value() const { return i; } }; class derived : public base { public: derived(int I) : base(I) {} int value() const { return base::value() * 2; } virtual int shift(int x) const { return base::value() << x; } }; int main() { base* B[] = { new base(7), new derived(7) }; cout << "B[0]->value() = " << B[0]->value() << endl; cout << "B[1]->value() = " << B[1]->value() << endl; //!cout << "B[1]->shift(3) = " //! << B[1]->shift(3) << endl; return 0; }</strong></span>
9.
<span style="font-size:18px;"><strong>/*SLICE.cpp*/ /*函数call()通过传值传递一个类型为base的对象。然后对于这个 base对象调用虚函数sum()。 我们可能希望第一次调用产生 10,第二次调用产生57。实际上,两次都产生10。 在这个程序中,有两件事情发生了。第一,call()接受的只是一个 base对象,所以所有在这 个函数体内的代码都将只操作与 base相关的数。对call()的任何调用都将引起一个与 base大小相 同的对象压栈并在调用后清除。这意味着,如果一个由base派生来类对象被传给 call,编译器 接受它,但只拷贝这个对象对应于base的部分,切除这个对象的派生部分*/ #include <iostream.h> class base { int i; public: base(int I = 0) : i(I) {} virtual int sum() const { return i; } }; class derived : public base { int j; public: derived(int I = 0, int J = 0) : base(I), j(J) {} int sum() const { return base::sum() + j; } }; void call(base b) { cout << "sum = " << b.sum() << endl; } int main() { base b(10); derived d(10, 47); call(b); call(d); return 0; }</strong></span>
10.
<span style="font-size:18px;"><strong>/*PVDEST.cpp*/ #include <iostream.h> class base { public: virtual ~base() = 0 { cout << "~base()" << endl; } }; class derived : public base { public: ~derived() { cout << "~derived()" << endl; } }; int main() { base* bp = new derived; delete bp; return 0; }</strong></span>
三.习题+解答
1. 创建一个非常简单的“shape”层次:基类称为 shape,派生类称为 circle、 square和triangle。在基类中定义一个虚函数 draw(),再在这些派生类中重定义它。创建指向我们在堆中创建的shape对象的指针数组(这样就形成了指针向上映射)。并且通过基类指针调用 draw(),检验这个虚函数的行为。如果我们的调试器支持,就用单步执行这个例子。
#include <iostream.h> enum note { middleC, Csharp, Cflat }; class shape { public: virtual void draw(note) const { cout << "shape::draw()" << endl; } }; class circle : public shape { public: void draw(note) const { cout << "circle::draw()" << endl; } }; class square : public shape { public: void draw(note) const { cout << "square::draw()" << endl; } }; class triangle : public shape { public: void draw(note) const { cout << "triangle::draw()" << endl; } }; void show(shape& i) { i.draw(middleC); } int main() { shape Shape; circle Circle; square Square; triangle Triangle; show(Shape); show(Circle); show(Square); show(Triangle); return 0; }
2. 修改练习1,使得draw()是纯虚函数。尝试创建一个类型为 shape的对象。尝试在构造函数内调用这个纯虚函数,结果如何。给出 draw()的一个定义。
#include <iostream.h> class shape { int i; public: shape(int I = 0) : i(I) {} //virtual void draw() const = 0; }; class circle : public shape { int j; public: circle(int I = 0, int J = 0) : shape(I), j(J) { //shape::draw(); } //void draw() const ; }; int main() { shape S(10); circle C(10); return 0; }
3. 写出一个小程序以显示在普通成员函数中调用虚函数和在构造函数中调用虚函数的不同。这个程序应当证明两种调用产生不同的结果。
a.
#include <iostream.h> enum note { middleC, Csharp, Cflat }; class shape { int i; public: shape(int I = 0) : i(I) {} virtual void draw(note) const { cout << "shape::draw()" << endl; }; }; class circle : public shape { int j; public: circle(int I = 0, int J = 0) : shape(I), j(J) { shape::draw(middleC); cout << "circle::circle()" << endl; } void draw(note) const { /*shape::draw(middleC); cout << "circle::draw()" << endl;*/ } }; void show(shape& i) { i.draw(middleC); } int main() { shape S(10); circle C(10); show(S); show(C); return 0; }
运行结果如下:
b.
#include <iostream.h> enum note { middleC, Csharp, Cflat }; class shape { int i; public: shape(int I = 0) : i(I) {} virtual void draw(note) const { cout << "shape::draw()" << endl; }; }; class circle : public shape { int j; public: circle(int I = 0, int J = 0) : shape(I), j(J) { /*shape::draw(middleC); cout << "circle::circle()" << endl;*/ } void draw(note) const { shape::draw(middleC); cout << "circle::draw()" << endl; } }; void show(shape& i) { i.draw(middleC); } int main() { shape S(10); circle C(10); show(S); show(C); return 0; }
运行结果:
4. 在EARLY.CPP中,我们如何能知道编译器是用早捆绑还是晚捆绑进行调用?根据我们自己的编译器来确定。
#include <iostream.h> class base { public: virtual int f() const { cout << "base::f()" << endl; return 1; } }; class derived : public base { public: int f() const { cout << "derived::f()" << endl; return 2; } }; int main() { derived d; base* b1 = &d; base& b2 = d; base b3; cout << "b1->f() = " << b1->f() << endl;//晚 cout << "b2.f() = " << b2.f() << endl;//晚 //在b1->f()和b2.f()中,使用地址,就意味着信息不完全: b1和b2可能表示base的地址也可能 //表示其派生对象的地址,所以必须用虚函数。 cout << "b3.f() = " << b3.f() << endl;//早 //而当调用 b3.f()时不存在含糊,编译器知道确切的类型和知道它是一个对象,所以它不可能 //是由base派生的对象,而确切的只是一个 base return 0; }
5. (中级)创建一个不带成员和构造函数而只有一个虚函数的基类 class X,创建一个从X继承的类class Y,它没有显式的构造函数。产生汇编代码并检验它,以确定 X的构造函数是否被创建和调用,如果是的,这些代码做什么?解释我们的发现。 X没有缺省构造函数,但是为什么编译器不报告出错?
#include <iostream.h> class X { public: virtual void f() const {}; }; class Y : public X {}; int main() { X x; Y y; return 0; }
汇编代码:
6. (中级)修改练习 5,让每个构造函数调用一个虚函数。产生汇编代码。确定在每个构造函数内VPTR在何处被赋值。在构造函数内编译器使用虚函数机制吗?确定为什么这些函数的本地版本仍被调用。
#include <iostream.h> class X { public: virtual void f() const { cout << "X::f()" << endl; } X() { X::f(); } }; class Y : public X { public: Y() { X::f(); } void f() const { cout << "Y::f()" << endl; } }; int main() { X x;//X::f() Y y;//X::f() X::f() return 0; }
汇编代码:
7. (高级)参数为传值方式传递的对象的函数调用如果不用早捆绑,则虚调用可能会侵入不存在的部分。这可能吗?写一些代码强制虚调用,看是否会引起冲突。解释这个现象,检验当对象以传值方式传递时会发生什么现象。
不懂后期补充
#include <iostream.h> class A { int i; public: virtual void f(int I) const { cout << "A::f()" << endl; } }; class B { public: void f(int I) const { cout << "B::f()" << endl; } }; int main() { A a; a.f(1); B b; b.f(1); return 0; }
8. (高级)通过我们的处理器的汇编语言信息或者其他技术,找出简单调用所需的时间数及虚函数调用的时间数,从而得出虚函数调用需要多用多少时间。
#include <iostream> #include "E:\VC++\7_31_1\cpptime.h" using namespace std; class X { public: virtual void f() const { cout << "X::f()" << " "; } void g() const { cout << "X::g()" << " "; } }; int main() { X x;//X::f() Time start1; for(int i = 0; i < 500; ++i) { x.f(); } Time end1; cout << endl; cout << "start1 = " << start1.ascii(); cout << "end1 = " << end1.ascii(); cout << "delta1 = " << end1.delta(&start1) << endl; Time start2; for(i = 0; i < 500; ++i) { x.g(); } Time end2; cout << endl; cout << "start2 = " << start2.ascii(); cout << "end2 = " << end2.ascii(); cout << "delta2 = " << end2.delta(&start2) << endl; return 0; }
运行结果如下:
经验证,明显看出虚函数调用和函数调用在相同条件下所用时间长。
以上代码仅供参考,如有错误请大家指出,谢谢大家~
版权声明:本文为博主原创文章,未经博主允许不得转载。