PPC的C/C++和人工智能学习笔记
每一篇学习笔记,都只是为了更好地掌握和理解

C++语言基础(10)_类的继承(续2)多态

C++基础,今天继续学习类的继承。主要内容是基类和派生类中同名函数以及由虚函数引起的多态行为。多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码(或者不变的接口)来实现可变的算法。

 

基类和派生类中的重名函数:

先看下面的代码:

class A {

public:

void bb() { cout << “A-bb” << endl; }

};

class B:public A {

public:

void bb() { cout << “B-bb” << endl; }

};

int main() {

A a, *pa=&a;

B b, *pb=&b;

pa->bb(); //输出 A-bb

pb->bb(); //输出 B-bb 同名函数

pb->A::bb();//输出 A-bb 加了作用域限制

pa = &b; //把 子类对象赋值给父类类型

pa->bb(); //输出 A-bb

pb = (B*)&a; //把父类对象强转赋值给子类类型

pb->bb(); //输出 B-bb

return 0;

}

我们在上一节课里面提到:对象的成员数据和成员函数(非虚成员函数)是分开存放的,成员函数(非虚)其实在类定义的时候就已经在内存中存在了,与对象是否创建无关。意思是说,访问哪个成员函数,其实是由对象的类的类型决定的,定义为什么类型的对象,访问的就是什么类型的类的成员函数,所以,上例中:pa=&b; 把B类型的对象的数据强转为A类型,pa->bb()调用的是A类的bb(),是因为pa是A类型的对象指针。而pb=&a; pb是B类型的对象指针,所以pb->bb()调用的是B类的函数。

 

虚函数的引入:

上面一再提到:成员函数是独立于类对象数据的,而且也写明了必须是非虚函数。其实:虚函数是存在于类对象的数据中的,它存放在对象的首地址开始的4直接中,是一个指向虚函数表的指针。虚函数的引入,让成员函数(虚函数)可以体现在类对象的数据中。这个就是最大的区别,我们利用此特性,可以实现“多态”(后面再描述)。

在基类的成员函数前面加上virtual关键字,该成员函数就是虚函数。

 

虚函数的一些特点:

1)父类中有同名函数且为虚函数,子类的同名函数不用加关键字也是虚函数;

2)父类有虚函数,子类不管重不重写基类的同名函数,子类都有虚函数列表;

3)子类中有自己的虚函数列表,不继承父类的虚函数列表,但是会继承父类虚函数列表中的表项;

4)子类中重写了父类的虚函数,子类的同名虚函数会在虚函数列表中覆盖父类的同名虚函数。

5)类的静态成员函数不能是虚函数,类的构造函数不能是虚函数。虚函数的访问权限一般是public,假如是protected和private没多大意义。

6)虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数。

7)虚关键字virtual只要在声明中添加,假如函数声明和定义分开的话,定义中不需要加虚关键字(也不能加)。

问题:为什么构造函数不能为虚函数

因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保vtbl的构建成功呢?

注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。

 

问题:相同的类构建的实例对象,其虚表的地址一样吗?

答案是一样的。

B x,y; 我们可以看到这x和y的内存的前4个字节中的数据是一样的。而这4个字节就是指向虚表首地址的一个指针。

 

问题:派生类同时继承多个有虚函数的基类的情况:

问题:有虚继承、有虚函数的情况

问题:为什么要用虚析构函数:

首先看一个情况:

class B:public A{}

B b;

b.~B(); //我们在这里显式调用b的析构,会发现执行完B的析构以后,还会继续执行A的析构函数!

那么假如 A *a = new B; delete a; 此时,不会调用B的析构,所以我们把A的析构写成虚析构,就可以先执行B的析构,再执行A的析构。

 

分析:若A类的析构函数不声明为virtual,则A *a = new B; delete a;在析构的时候调用的将是A类的析构函数,而没有调用B类的析构函数,此时造成了内存泄露。所以析构函数必须声明为虚函数,调用的将是子类B的析构函数,我们还需要知道的一点是,子类析构函数,一定会调用父类析构函数,释放父类对象,则内存安全释放。

多态类的析构函数定义成虚函数,为的是在delete基类指针时能实际调用到最终继承类的析构函数,如果不这样,就只有基类子对象会析构,就会发生你所说的有的资源得不

到释放的问题。

任何一个对象的析构都包含了其子对象的析构,特别要强调的是,对象的析构过程不是简单的析构函数体的执行,由编译器产生的析构过程代码是递归地对子对象按照构造顺序反序析构再执行当前析构函数体中的代码。

 

子类对象赋值给父类对象时的问题:

A是父类,B是子类。

A a; B b; a=b; //直接赋值将发生截断

A *pa; B b; pa=&b; //通过指针不会发生截断

A& ya = b; //通过引用不会发生截断

多重继承的时候,居然也可以!!!

B *pb;
C c; //C多重继承于 A 和 B
pb = &c;
printf(“%p,%p\n”, pb, &c); //发现:这2个地址居然是不一样的

通过虚函数实现多态:

多态,需要用基类类型指向派生类对象(一般用指针),并且行为用虚函数表示。

A是基类,B、C是子类。

A *pa=NULL;

B b;

C c;

pa = &b; //pa->f() 将执行 B类中重写的虚函数f

pa = &c; //pa->f() 将执行 C类中重写的虚函数f

 

纯虚函数:

有纯虚函数的类做为抽象基类存在,不能实例化对象的类;

所以抽象基类的构造一般是保护;

抽象基类只是做为基类存在,如果派生类继承抽象基类,但没有重写纯虚函数,该派生类还是做为抽象基类存在;

写法:virtual void aa() = 0;

 

虚函数有默认参数的情况:

class A

{

public:

virtual void out(int i = 10) { cout << “A  ” << i << endl; }

};

class B : public A

{

public:

void out(int i = 20) { cout << “B ” << i << endl; }

};

int main()

{

A * pa = new A();

pa->out(); //输出 A 10 ,(默认值) 没有问题

pa->out(30); //输出 A 30 ,没有问题

B * pb = new B();

pb->out(); //输出 B 20,(默认值) 没有问题

pb->out(40); //输出 B 40, 没有问题

pa = pb;

pa->out();  //输出 B 10 !这里出问题了,调用的是B的函数,但是 默认值居然是A的

pa->out(40); //输出B 40,没问题

delete(pa);

delete(pb);

return 0;

}

永远记住:“绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)

 

练习:重写之前的三个NPC类(不需要函数实现)

//练习:用类的继承实现三个类,商店NPC,任务NPC,怪物NPC。
enum typeNPC { shopNPC, taskNPC, monsterNPC };

class CBaseNpc { //npc基类
protected:
 const int m_id; //npc编号
 const string m_name; //npc姓名
public:
 CBaseNpc(int id,string name):m_id(id),m_name(name) {}
 virtual ~CBaseNpc() {}
public:
 int GetID() const { return m_id; }
 const string & GetName() const { return m_name; }
 virtual void NpcAction(){} //虚函数,npc的Action
};

class CShopNpc:public CBaseNpc { //商店类npc
private:
 typeNPC m_type; //npc类型
 int ** m_shoplist; //商品列表指针
public:
 CShopNpc(int id, string name) :CBaseNpc(id, name) { /*初始化商品列表啥的*/ }
 ~CShopNpc() {}
public:
 typeNPC GetType() const { return m_type; }
 void SetType(typeNPC type) { m_type = type; }
 void NpcAction() { cout << "商品列表:" << endl; }
};

class CTaskNpc :public CBaseNpc { //任务类npc
private:
 typeNPC m_type; //npc类型
 int **m_tasklist; //任务列表
public:
 CTaskNpc(int id, string name):CBaseNpc(id,name){}
 ~CTaskNpc() {}
public:
 typeNPC GetType() const { return m_type; }
 void SetType(typeNPC type) { m_type = type; }
 void NpcAction() { cout << "任务列表:" << endl; }
};

class CMonsterNpc :public CBaseNpc { //任务类npc
private:
 typeNPC m_type; //npc类型
 int m_exp; //npc经验
 int ** m_skills; //npc技能
public:
 CMonsterNpc(int id, string name) :CBaseNpc(id, name) {}
 ~CMonsterNpc() {}
public:
 typeNPC GetType() const { return m_type; }
 void SetType(typeNPC type) { m_type = type; }
 void NpcAction() { cout << "开始战斗吧:" << endl; }
};

(2017-03-29 www.vsppc.com)

学习笔记未经允许不得转载:PPC的C/C++和人工智能学习笔记 » C++语言基础(10)_类的继承(续2)多态

分享到:更多 ()

评论 抢沙发

评论前必须登录!