第六章 多态与虚函数

6.1 多态和虚函数的基本概念

引言

多态是面向对象程序设计里面非常重要的这个机制。它能很有效的提高程序的可扩充性。

有些程序设计语言有被对象继承的概念,但是没有多态的概念,那这样的程序设计语言只能被称作基于对象的程序设计语言,而不能称为面向对象的语言, 比方说visual basic。

虚函数

在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。

class base {<span style="color:#ff0000;">virtual</span> int get() ;
};
int base::get()
{ }

virtual 关键字只用在类定义里的函数声明中,写函数体时不用。

构造函数和静态成员函数不能是虚函数。

虚函数跟普通函数的本质差别实际上就在于虚函数可以参与多态, 而普通的成员函数不能。

多态有两种表现形式

多态的表现形式一,基类指针

派生类的指针可以赋给基类指针。(赋值兼容规则)

通过基类指针调用基类和派生类中的同名虚函数时:
(1)若该指针指向一个基类的对象,那么被调用是基类的虚函数;
(2)若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制就叫做“多态”。

示例

class CBase {public:virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {public :virtual void SomeVirtualFunction() { }
};
int main() {CDerived ODerived;CBase * p = & ODerived;p -> SomeVirtualFunction(); //调用哪个虚函数取决于<span style="color:#ff0000;">p指向哪种类型的对象,而不是p的指针类型</span>return 0;
}

在编译的时候是没有办法确定这条语句调用哪一个类的SomeVirtualFunction的。

多态的表现形式二,基类引用

派生类的对象可以赋给基类引用

通过基类引用调用基类和派生类中的同名虚函数时:
(1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
(2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制也叫做“多态”。

示例

class CBase {public:virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {public :virtual void SomeVirtualFunction() { }
};
int main() {CDerived ODerived;CBase & r = ODerived;r.SomeVirtualFunction(); //调用哪个虚函数取决于<span style="color:#ff0000;">r引用哪种类型的对象,而不是r的引用类型</span>return 0;
} 

多态的简单示例

class A {public :virtual void Print( ){ cout << "A::Print"<<endl ; }
};
class B: public A {public :virtual void Print( ) { cout << "B::Print" <<endl; }
};
class D: public A {public:virtual void Print( ) { cout << "D::Print" << endl ; }
};
class E: public B {virtual void Print( ) { cout << "E::Print" << endl ; }
};
int main() {A a; B b; E e; D d;A * pa = &a; B * pb = &b;D * pd = &d ; E * pe = &e;pa->Print(); // a.Print()被调用,输出:A::Printpa = pb;pa -> Print(); //b.Print()被调用,输出:B::Printpa = pd;pa -> Print(); //d. Print ()被调用,输出:D::Printpa = pe;pa -> Print(); //e.Print () 被调用,输出:E::Printreturn 0;
}

多态的作用

在面向对象的程序设计中使用多态,能够增强程序的 可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

6.2 使用多态的游戏程序实例

游戏《魔法门之英雄无敌》

游戏中有很多种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象。类:CDragon,类:CSoldier,类CPhonex ,类:CAngel
怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的。
游戏版本升级时,要增加新的怪物--雷鸟。如何编程才能使升级时的代码改动和增加量较小?新增类:CThunderBird

基本思路:

为每个怪物类编写 Attack、FightBack和 Hurted成员函数。

  • Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。
  • Hurted函数减少自身生命值,并表现受伤动作。
  • FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。
设置基类 CCreature,并且使CDragon, CWolf等其他类都从CCreature派生而来。

非多态的实现方法

class class CCreature {protected: int nPower ; //代表攻击力int nLifeValue ; //代表生命值
};
class CDragon:public CCreature {public:void Attack(CWolf * pWolf) {...表现攻击动作的代码pWolf->Hurted( nPower);pWolf->FightBack( this);}void Attack( CGhost * pGhost) {...表现攻击动作的代码pGhost->Hurted( nPower);pGohst->FightBack( this);}void Hurted ( int nPower) {....表现受伤动作的代码nLifeValue -= nPower;}void FightBack( CWolf * pWolf) {....表现反击动作的代码pWolf ->Hurted( nPower / 2);}void FightBack( CGhost * pGhost) {....表现反击动作的代码pGhost->Hurted( nPower / 2 );}
}

有n种怪物,CDragon 类中就会有n个 Attack 成员函数,以及 n个FightBack 成员函数。对于其他类也如此。

非多态的实现方法的缺点

如果游戏版本升级,增加了新的怪物雷鸟 CThunderBird,则程序改动较大。
所有的类都需要增加两个成员函数:void Attack( CThunderBird * pThunderBird) ; void FightBack( CThunderBird * pThunderBird) ;
在怪物种类多的时候,工作量较大有木有!!!

多态的实现方法

//基类 CCreature:
class CCreature {protected :int m_nLifeValue, m_nPower;public:virtual void Attack( CCreature * pCreature) {}virtual void Hurted( int nPower) { }virtual void FightBack( CCreature * pCreature) { }
};

基类只有一个 Attack 成员函数;也只有一个 FightBack成员函数;所有CCreature 的派生类也是这样。

//派生类 CDragon:
class CDragon : public CCreature {public:virtual void Attack( CCreature * pCreature);virtual void Hurted( int nPower);virtual void FightBack( CCreature * pCreature);
};
void CDragon::Attack(CCreature * p)
{…表现攻击动作的代码 p->Hurted(m_nPower); //多态p->FightBack(this); //多态
}
void CDragon::Hurted( int nPower)
{…表现受伤动作的代码m_nLifeValue-= nPower;
}
void CDragon::FightBack(CCreature * p)
{…表现反击动作的代码 p->Hurted(m_nPower/2); //多态
} 

多态实现方法的优势

如果游戏版本升级,增加了新的怪物雷鸟 CThunderBird……

只需要编写新类CThunderBird, 不需要在已有的类里专门为新怪物增加:
void Attack( CThunderBird * pThunderBird) ;
void FightBack( CThunderBird * pThunderBird) ;
成员函数,已有的类可以原封不动,没压力啊!!!

原理

CDragon Dragon; CWolf Wolf; CGhost Ghost;
CThunderBird Bird;
Dragon.Attack( & Wolf); //(1)
Dragon.Attack( & Ghost); //(2)
Dragon.Attack( & Bird); //(3) 

根据多态的规则,上面的(1),(2),(3)进入到CDragon::Attack函数后,能分别调用:
CWolf::Hurted
CGhost::Hurted
CBird::Hurted

void CDragon::Attack(<span style="color:#3333ff;">CCreature * p</span>)
{p->Hurted(m_nPower); //多态p->FightBack(this); //多态
} 

6.3 更多多态程序实例

几何形体处理程序

几何形体处理程序: 输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状。
Input:
第一行是几何形体数目n(不超过100).下面有n行,每行以一个字母c开头.
若 c 是 ‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高;
若 c 是 ‘C’,则代表一个圆,本行后面跟着一个整数代表其半径;
若 c 是 ‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度。
Output:
按面积从小到大依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:
形体名称:面积

#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{public:<span style="color:#ff0000;">virtual double Area() = 0; //纯虚函数virtual void PrintInfo() = 0;</span>
};
class CRectangle:public CShape
{public:int w,h;virtual double Area();virtual void PrintInfo();
};
class CCircle:public CShape {public:int r;virtual double Area();virtual void PrintInfo();
};
class CTriangle:public CShape {public:int a,b,c;virtual double Area();virtual void PrintInfo();
};
double CRectangle::Area() {return w * h;
}
void CRectangle::PrintInfo() {cout << "Rectangle:" << Area() << endl;
}
double CCircle::Area() {return 3.14 * r * r ;
}
void CCircle::PrintInfo() {cout << "Circle:" << Area() << endl;
}
double CTriangle::Area() {double p = ( a + b + c) / 2.0;return sqrt(p * ( p - a)*(p- b)*(p - c));
}
void CTriangle::PrintInfo() {cout << "Triangle:" << Area() << endl;
}

那我们怎么实现它呢?显然我们要概括所有几何形体的共同特点,形成一个基类叫做CShape。这里不同的几何形体它的属性不一样,因为有的是半径有的边长什么的,所以没有共同点。但是它们的方法,他们的操作是有共同特点的。比方说,它们都能够求面积,都能够输出自己信息。所以,我们就在这个基类里面编写了两个成员函数。它们反映了不同几何形体对象的共同特点。

这两个成员函数我们都把它变成纯虚函数(=0)。纯虚函数没有函数体。

为什么我们不需要对CShape去编写求面积和打印信息的这些函数呢?因为在这个程序里面任何一个几何形体,它要么是矩形,要么是圆,要么是三角形。它不会是一个光秃秃的、抽象的CShape。不存在一个叫做CShape的这种类型的具体的几何形体。所以我们就不需要为CShape和这样的类去编写具体的如何求面积的、如何打印信息的这样的函数了,因此我们就干脆让它=0,变成纯虚函数。

<span style="color:#ff0000;">CShape * pShapes[100];</span>
int MyCompare(const void * s1, const void * s2);
int main()
{int i; int n;CRectangle * pr; CCircle * pc; CTriangle * pt;cin >> n;for( i = 0;i < n;i ++ ) {char c;cin >> c;switch(c) {case 'R':pr = new CRectangle();cin >> pr->w >> pr->h;pShapes[i] = pr;break;case 'C':pc = new CCircle();cin >> pc->r;pShapes[i] = pc;break;case 'T':pt = new CTriangle();cin >> pt->a >> pt->b >> pt->c;pShapes[i] = pt;break;}}<span style="color:#ff0000;"> qsort(pShapes,n,sizeof( CShape*),MyCompare);</span>for( i = 0;i <n;i ++)<span style="color:#ff0000;">pShapes[i]->PrintInfo();//pShapes[i]是基类指针,PrintInfo是基类和派生类里面都有的同名虚函数。因此整条语句,就是一个多态语句。</span>return 0;
}int MyCompare(const void * s1, const void * s2)
{double a1,a2;<span style="color:#ff0000;">CShape * * p1;</span> // s1,s2 是 void * ,不可写 “* s1”来取得s1指向的内容<span style="color:#ff0000;">CShape * * p2; //你写*s1的话编译的时候会出错。因为这个s1它是void的类型的指针。那么*s1到底是多少个字节呢?不知道,所以编译器没有办法处理*s1。</span>p1 = ( CShape * * ) s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape *p2 = ( CShape * * ) s2; // 故 p1,p2都是指向指针的指针,类型为 CShape **a1 = (*p1)->Area(); // * p1 的类型是 Cshape * ,是基类指针,故此句为多态a2 = (*p2)->Area();if( a1 < a2 )return -1;else if ( a2 < a1 )return 1;elsereturn 0;
}

程序注解:那这个程序我们用什么东西来存放各种不同类型的几何形体呢?
一共有三种几何形体,如果你就开三个数组去存放它就是比较啰嗦。加上有四个几何形体,你就还得再加一个数组。显然是不和算的。包括你用三个数组去存的话,它整个的所有的几何形体都要按面积排序,这时候你怎么处理呢?你把三个数组分别排序也不行,对不对?因为不同数组,不同类型的几何形体之间的面积还要进行比较,嗯,您可能还要另外开一个索引数组。总而言之就很麻烦。
有了多态这种概念,我们就可以采取简单的做法。这里我们可以用一个 基类的指针数组,PShapes用来存放各个几何形体。纯粹的说就是,这个数组里的每一个元素都是一个基类指针。但是由于是基类指针,它可以指向派生类的对象,所以我们可以让这个数组里面,每一个元素都去指向各自不同的这个几何形体。所有几何形体都是new出来的对象。然后它们的地址都被放到这个基类数组里面去。然後我们排序的时候,就是对这个基类指针数组进行排序。

怎么体现可扩充性了?如果添加新的几何形体,比如五边形,则只需要 从CShape派生出CPentagon,以及 在main中的switch语句中增加一个case,其余部分不变有木
有!这个循环和这个排序的语句都不用动。其他已有的类就更加不用动了。所以我们所做的修改就是特别少的。

总结:用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法

多态的又一例子

class Base {public:void fun1() { fun2(); }<span style="color:#ff0000;">virtual</span> void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {public:<span style="color:#ff0000;">virtual</span> void fun2() { cout << "Derived:fun2()" << endl; }
};
int main() {Derived d;Base * pBase = & d;pBase->fun1();//输出什么?return 0;
}

输出: Derived:fun2()

Base::fun1() 相当于 void fun1() { this->fun2(); } //this是基类指针,fun2是虚函数,所以是多态

  • base 的这个成员函数fun1里边的this指针是基类指针。在这里fun2又是虚函数,那么通过基类指针调用 虚函数,这条语句就是多态。也就是说,当程序执行到这条语句的时候,执行的是哪一个类的fun2取决于指至This指针到底指向的是哪一种类型的对象。
  • 那我们分析,在main里面pBase->fun1,指至pbase是指向一个派生类的对象d的。那进到fun1里面,this指针指向的东西自然也就是这个d。所以,此时this指针指向的是一个派生类的对象。

在非构造函数,非析构函数的成员函数中调用虚函数,是多态!!!

构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生类的函数。(在这种情况下,编译的时候就能确定到底调用的函数是什么?那当然应该是这个类自己的那个虚函数。那么自己没有的话,就是它的直接基类中定义的那个虚函数。它不会等到运行的时候才决定调用的是自己的还是派生类的函数。)

class myclass {public:virtual void hello(){cout<<"hello from myclass"<<endl; };virtual void bye(){cout<<"bye from myclass"<<endl;}
};
class son:public myclass{ public:void hello(){ cout<<"hello from son"<<endl;};son(){ <span style="color:#ff0000;">hello();</span> };//<span style="color: rgb(255, 0, 0); font-family: Arial, Helvetica, sans-serif;">派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数</span>~son(){ bye(); };
};
class grandson:public son{ public:void hello(){cout<<"hello from grandson"<<endl;};void bye() { cout << "bye from grandson"<<endl;}grandson(){cout<<"constructing grandson"<<endl;};~grandson(){cout<<"destructing grandson"<<endl;};
};
int main(){<span style="color:#ff0000;">grandson gson;</span><span style="color:#3333ff;">//这里有一个派生类的对象生成,所以会从顶至下执行一个个基类的构造函数。首先执行是myclass的构造函数</span>son *pson;pson=&gson;pson->hello(); //多态return 0;
}
结果:
hello from son
constructing grandson
hello from grandson
destructing grandson
bye from myclass

派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数。

为什么在构造函数和析构函数中调用虚函数不应该是多态?

当然编译器就是这么设计的。那至于为什么要这种设计,想想看,派生类对象在初始化的时候会先执行里面的基类对象的构造函数。
那也就是说在基类对象的构造函数执行期间派生类对象它自己内部分成员对象实际上是还没有被初始化的。
那如果在基类的构造函数执行期间调用的虚函数、你就允许这虚函数是多态的话,那么在基类构造函数执行期间就会调用了派生类的虚函数。而这个派生类对象它自己的成员变量还没有初始化好你在这个派生类对象上面就执行了成员函数, 那这个成员函数的执行的结果就是有可能不正确的。
因此我们不能够在,基类的构造函数里面就去执行派生类的这个虚函数。
所以在基类的构造函数里面调用虚函数,就不是多态。
那在析构函数里面调用虚函数也不是多态,它的道理跟构造函数的情况是类似的。

6.4 多态实现原理

思考

“多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 ---- 这叫“动态联编”。“动态联编” 是怎么实现的呢?

提示:请看下面例子程序:

class Base {public:int i;virtual void Print() { cout << "Base:Print" ; }
};
class Derived : public Base{public:int n;virtual void Print() { cout <<"Drived:Print" << endl; }
};
int main() {Derived d;cout << sizeof( Base) << ","<< sizeof( Derived ) ;return 0;
}
程序运行输出结果: 8, 12
为什么都多了4个字节?

多态实现的关键 --- 虚函数表

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。虚函数表中列出了该类的虚函数地址。多出来的4个字节就是用来放虚函数表的地址的。

总结:类(有虚函数的类)有虚函数表,对象有虚函数表的指针。对象的前4个字节就是存虚函数表的地址。

虚函数表,是编译器自动生成的,加到可执行文件里面去的。当可执行程序被装入内存时,和一个类所对应的虚函数表也就被装到内存里面去了。

一个base的对象b,它的前四个字节存放的就是base类所对应的虚函数表的指针。
接下来才开始存放自己的成员变量i。
通过虚函数表指针就能够找到base类所对应的虚函数表,这个虚函数表是是在内存里面的。这base类的虚函数表里面放的些什么东西呢?
放的是base类的所有虚函数在内存里面的地址。通过虚函数表就能够查到虚函数在内存里的地址。
有了虚函数在内存里面的地址,就能够调用这个虚函数了。调用一个函数本来就是跳到那个函数的地址上去执行嘛!当然你还是得再跳回来。

多态是怎么实现的呢?假设我们用一个pBase的指针指向某个对象。这个对象它有可能是基类的对象,也有可能是这个派生类的对象。
那么基类指针调用虚函数,这条多态语句被编译器处理后会生成一大堆的指令。这些指令并不是简简单单的就跳转到某个函数的地址去执行那个函数。
这个一系列指令首先根据基类指针所指向的,或者说基类引用所引用的那个对象中存放的虚函数表的地址(这对象里面的全四个字节就是虚函数表的地址)找到这个对象所属的类对应的那张虚函数表。然後,再在这个虚函数表中去查找虚函数的地址。找到虚函数的地址以后,就去调用哪个虚函数。
就是对于一条多态语句,编译器编译出来的指令执行过程会比较复杂的这么一大套。那当然了, 在这一切指令执行的过程中,这些对象里面放着的是哪一个类的虚函数表的指针,最终就会跑到哪一个类的虚函数表里面去查找虚函数的地址,那最终被调用的就是那一个类的虚函数。嗯,这也是多态实现的这个原理。
知道这个原理,你对这个面向对象对多态什么的理解就会更加深入了一层。

虚函数的时间和空间的代价

多态能够有效的提高程序的可扩充性。但做到这点,代价就是多态的程序在运行期间会有额外的时间和空间上的开销。

  • 空间上的开销就是每一个有虚函数的类的对象里面都会多出来四个字节,用来存放虚函数表的地址。每个对象都会多四个字节!那当然就是空间上的额外开销。
  • 时间上的额外开销表现在对多态的函数调用语句。把它编译出来的一系列指令,需要执行查询函数表这样一个过程, 那查询函数表就是时间上的开销。

其他补充:

在C++的面对对象思想中,虚函数起到了很关键的作用,当一个类中拥有至少一个虚函数,那么编译器就会构建出一个虚函数表(virtual method table)来指示这些函数的地址,假如继承该类的子类定义并实现了一个同名并具有同样函数签名(function siguature)的方法重写了基类中的方法,那么虚函数表会将该函数指向新的地址。此时多态性就体现出来了:当我们将基类的指针或引用指向子类的对象的时候,调用方法时,就会顺着虚函数表找到对应子类的方法而非基类的方法。

当然虚函数表的存在对于效率上会有一定的影响,首先构建虚函数表需要时间,根据虚函数表寻到到函数也需要时间。
因为这个原因如果没有继承的需要,一般不必在类中定义虚函数。但是对于继承来说,虚函数就变得很重要了,这不仅仅是实现多态性的一个重要标志,同时也是dynamic_cast转换能够进行的前提条件。
(参考: 此文最后部分。)

6.5 虚析构函数

问题:

class CSon{public: ~CSon() { };
};
class CGrandson : CSon{public: ~CGrandson() { }; }
int main()
{<span style="color:#ff0000;">CSon *p = new CGrandson;delete p;</span>return 0;
}

基类的指针,所以理所当然会去调用基类的机构函数,但是逻辑上讲呢,这个指针本身又指向的是一个派生类的对象,那么分配的也是一个派生类对应的这样的一个内存空间,
那么这个时候,它应该调用的还有派生类的析构函数。但是目前的程序设计角度上来看呢,编译器是不会知道它需要调用派生类的析构函数的。

问题即:通过 基类的指针 删除 派生类对象 时—>只调用基类的析构函数
Vs. 删除一个 派生类的对象 时:先调用 派生类的析构函数,再调用 基类的析构函数

解决办法:

把基类的析构函数声明为virtual
• 派生类的析构函数 virtual可以不进行声明
• 通过 基类的指针 删除 派生类对象 时
  首先调用 派生类的析构函数
  然后调用 基类的析构函数类

如果定义了虚函数, 则最好将析构函数也定义成虚函数

Note: 不允许以虚函数作为构造函数

class son{public:~son() { cout<<"bye from son"<<endl; };
};
class grandson : public son{public:~grandson(){ cout<<"bye from grandson"<<endl; };
};
int main(){son *pson;pson=new grandson;delete pson;return 0;
}
输出结果: bye from son
没有执行grandson::~grandson()!!!
class son{public:virtual ~son() { cout<<"bye from son"<<endl; };
};
class grandson : public son{public:~grandson(){ cout<<"bye from grandson"<<endl; };
};
int main() {son *pson;pson= new grandson;   //我的理解,这里new一个派生类对象,所以会先自动调用基类的构造函数,再派生类的构造函数。delete pson;  //我的理解,这里delete一个基类指针,所以去调用基类的析构函数,由于基类的析构函数是虚函数,多态,所以调用指针实际指向对象的析构函数,即派生类的析构函数。再执行派生类的析构函数时,我们知道执行完派生类的析构函数,会自动调用基类的析构函数。return 0;
}
输出结果: bye from grandsonbye from son
<span style="color:#3333ff;">执行grandson::~grandson(),
引起执行son::~son()!!!</span>

如 5.4 派生类和组合的构造函数 小节中所述,在执行一个派生类的构造函数之前,总是先执行基类的构造函数;派生类的析构函数被执行时,,执行完派生类的析构函数后, 自动调用基类的析构函数。

6.6 纯虚函数和抽象类

纯虚函数

纯虚函数: 没有函数体的虚函数

class A {private:int a;public:virtual void Print( ) = 0 ; //纯虚函数void fun() { cout << “fun”; }
};

抽象类

抽象类: 包含纯虚函数的类

  • 只能作为 基类 来派生新类使用
  • 不能创建抽象类的对象
  • 抽象类的指针和引用 —> 由抽象类派生出来的类的对象
    A a; // 错, A 是抽象类, 不能创建对象
    A * pa; // ok, 可以定义抽象类的指针和引用
    pa = new A; //错误, A 是抽象类, 不能创建对象

纯虚函数和抽象类

抽象类中,

  • 在 成员函数 内可以调用纯虚函数
  • 在 构造函数/析构函数 内部不能调用纯虚函数
如果一个类从抽象类派生而来—>它实现了基类中的所有纯虚函数, 才能成为非抽象类

class A {public:virtual void f() = 0; //纯虚函数void g( ) { this->f( ); } //okA( ){ } //f( ); // 错误
};
class B : public A{public:void f(){ cout<<"B: f()"<<endl; }
};
int main(){B b;b.g();return 0;
}
输出结果:B:f()

小结:

  • 构造函数和静态成员函数不能是虚函数。  //是否能为虚函数
  • 在非构造函数,非析构函数的成员函数中调用虚函数,是多态。
  • 在构造函数和析构函数中调用虚函数,不是多态。  //成员函数调用虚函数是否为多态
  • 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数。  //
  • 类(有虚函数的类)有虚函数表,对象有虚函数表的指针。对象的前4个字节就是存虚函数表的地址。
  • 通过虚函数表指针能够找到对象所属类所对应的虚函数表。通过虚函数表能够查到虚函数在内存里的地址。  //多态实现原理
  • 如果定义了虚函数, 则最好将析构函数也定义成虚函数。  //

PKU C++程序设计实习 学习笔记3 多态与虚函数相关推荐

  1. Windows程序设计_Chap02_Unicode_学习笔记

    Windows程序设计_Chap02_Unicode_学习笔记 ――By: Neicole(2013.05.24) 01. 开篇 <Windows程序设计>的第2章,主要内容为Unicod ...

  2. 复习笔记(五)——C++多态和虚函数

    静态绑定与动态绑定 静态绑定: -编译时就能确定一条函数调用语句要调用的函数 -在程序编译时多态性体现在函数和运算符的重载上 动态绑定: -运行时才能确定函数调用语句调用的函数 -程序运行时的多态性通 ...

  3. Windows事件等待学习笔记(三)—— WaitForSingleObject函数分析

    Windows事件等待学习笔记(三)-- WaitForSingleObject函数分析 要点回顾 WaitForSingleObject NtWaitForSingleObject KeWaitFo ...

  4. Python学习笔记——for循环和range函数

    Python学习笔记--for循环和range函数 Python的for循环 for 目标 in 表达式 :循环体 案例一 >>> example = 'abcdef' >&g ...

  5. pandas学习笔记:pandas.Dataframe.rename()函数用法

    pandas学习笔记:pandas.Dataframe.rename()函数用法 pandas.Dataframe.rename()函数主要是用来修改Dataframe数据的行名和列名. 主要用到的参 ...

  6. OpenCV学习笔记(5)_ ellipse绘制函数浅析

    OpenCV学习笔记(5)_ ellipse绘制函数浅析 文章目录 OpenCV学习笔记(5)_ ellipse绘制函数浅析 1. ellipse第一种重载--绘制椭圆弧 1.1 函数原型 1.2 参 ...

  7. python学习笔记(五)---替换函数

    python学习笔记(五)-替换函数 replace()函数替换内容 用法: replace('需要替换的内容','替换后的内容',替换次数) 备:如果需要替换单引号需要加'\' 例子: str = ...

  8. ① ESP8266 开发学习笔记_By_GYC 【更新 ets_printf 函数 使ESP_IDF 能够支持浮点数打印】

    ① ESP8266 开发学习笔记_By_GYC [更新 ets_printf 函数 使ESP_IDF 能够支持浮点数打印] 在我们日常的开发过程中,经常使用到的一个功能就是串口打印功能.在ESP826 ...

  9. CodeMonkey过关学习笔记系列:71-85关 函数

    CodeMonkey过关学习笔记系列:71-75关 •"函数"农场 (FUNCTION FARM) 71 ~ 85 第 71 关挑战 "函数"农场step di ...

最新文章

  1. 基于jquery仿天猫分类导航banner切换
  2. python基础语法-三大内建数据结构之字典(dict)
  3. 15行代码AC_ 【蓝桥杯】兴趣小组(解题报告+思考)
  4. 手型向下 点击一下 福昕_PPT多张缩略图点击放大展示
  5. c语言由声明部分,C语言期末复习.doc
  6. 横跨7个版本的OpenStack无感知热升级在360的落地与实践
  7. [转]apache MPM介绍
  8. 数据结构思维 第九章 `Map`接口
  9. 力扣 验证二叉搜索树
  10. 使用RateLimiter完成简单的大流量限流
  11. 从ISSCC2021论文看未来技术发展趋势
  12. php视频怎么转mp4,PHP实现将视频转成MP4并获取视频预览图的方法_php技巧
  13. 【JavaWeb】实现网页验证码
  14. 选择适合你的虚拟现实体验
  15. 概率论及概率图模型基础
  16. 解决blur与click冲突
  17. 慕容垂:百万战骨风云里——激荡的鲜卑史略之三(转载)
  18. 腾讯与NBA锁定五年独家合作
  19. R1 协议:基于以太坊的去中心化交易协议
  20. 使用font-awesome小图标

热门文章

  1. 【2018春运-AI大战黄牛党】智能抢票选座,机器学习阻击黄牛党
  2. 【效率】GitHub 标星 119K+!这些神器仅需一行代码即可下载全网视频!
  3. 第七章 模块(module)
  4. python3字符编码与文件处理终极版
  5. 渗透测试-文件包含漏洞
  6. 关于VSCode用SSH连接OpenEuler
  7. Typora 编辑器 怎么 制作大纲 以及 大纲级别
  8. Linux下安装mysql以及配置用户与数据导入
  9. Python之pytest单元测试方法
  10. php 取url 文件名,php 获取当前访问的url文件名的方法小结