C++面向对象程序设计-北京大学-郭炜【课程笔记(八)】
- 1、虚函数和多态的基本概念
- `1.1、虚函数`
- 1.2、多态
- `多态`的表现形式一
- `多态`的表现形式二
- 2、多态实例:魔法门之英雄无敌
- 2.1、**非多态的实现方法:**
- 2.2、**多态的实现方法**
- 3、多态实例:几何形体程序
- 3.1、qsort函数的介绍
- 3.2、几何形体程序
- 3.2、铭记口诀
- 3.3、多态例题2:
- 3.3、多态实例3:与构造函数
- 4、多态的实现原理
- 5、虚析构函数、纯虚函数和抽象类
- 5.1、虚析构函数
- 5.2、纯虚函数和抽象类
- 5.3、虚函数和纯虚函数的区别
开始课程:P28 1_1. 虚函数和多态的基本概念
课程链接:程序设计与算法(三)C++面向对象程序设计 北京大学 郭炜
课程PPT:github提供的对应课程PPT
1、虚函数和多态的基本概念
1.1、虚函数
- 在类的定义中,前面有virtual关键字的成员函数就是虚函数。
class base
{
virtual int get();
};
int base::get() {}
virtual
关键字只用在类定义里的函数声明中,写函数体时不用。-
- 什么是函数体:函数体指的是函数定义中包含的代码块,用于实现函数的功能。 在函数定义中,通常会指定函数的名称、参数列表和返回值类型,而函数体则是具体实现函数功能的地方。
- 构造函数和静态成员函数不能是虚函数
1.2、多态
多态
的表现形式一
派生类的指针可以赋给基类指针。
- 通过基类指针调用基类和派生类中的同名虚函数时:
-
- (1)、若该指针指向一个基类的对象,那么被调用是基类的虚函数;
-
- (2)、若该指针指向一个派生类的对象,那么被调用的是派生类的许函数。
以上这种机制就叫做“多态”。
- (2)、若该指针指向一个派生类的对象,那么被调用的是派生类的许函数。
#include <iostream>
using namespace std;
class CBase
{
public:
virtual void SomeVirtualFunction()
{
cout << "基类" << endl;
}
};
class CDerived:public CBase
{
public:
virtual void SomeVirtualFunction()
{
cout << "派生类" << endl;
}
};
int main()
{
CDerived ODerived;
CBase * p = & ODerived;
// 调用哪个虚函数取决于p指向哪种类型的对象
// 即派生类CDerived的对象
p -> SomeVirtualFunction();
return 0;
}
OUT:
beida_lesson % g++ 25.cpp -o 25
beida_lesson % ./25
派生类
多态
的表现形式二
派生类的对象可以赋值给基类引用
- 通过基类引用调用基类和派生类中的同名虚函数时:
-
- (1)、若该引用引用的是一个基类的对象,那么被调用时基类的虚函数
-
- (2)、若该引用引用的是体格派生类的对象,那么被调用的是派生类的许函数。
以上这种机制就叫做“多态”。
- (2)、若该引用引用的是体格派生类的对象,那么被调用的是派生类的许函数。
#include <iostream>
using namespace std;
class CBase
{
public:
virtual void SomeVirtualFunction()
{
cout << "基类" << endl;
}
};
class CDerived:public CBase
{
public:
virtual void SomeVirtualFunction()
{
cout << "派生类" << endl;
}
};
int main()
{
CDerived ODerived;
// 派生类的指针可以赋给基类指针
// CBase * p = & ODerived;
// p -> SomeVirtualFunction();
//2、派生类的对象可以赋值给基类引用
CBase & r = ODerived;
r.SomeVirtualFunction();
// 调用哪个虚函数取决于p\r指向哪种类型的对象
// 即派生类CDerived的对象
return 0;
}
OUT:
beida_lesson % g++ 25.cpp -o 25
beida_lesson % ./25
派生类
例:
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::Print
pa = pb;
pa -> Print(); // b.Print()被调用,输出:B::Print
pa = pd;
pa -> Print(); // d.Print()被调用,输出:D::Print
pa = pe;
pa -> Print(); // e.Print()被调用,输出:E::Print
return 0;
}
2、多态实例:魔法门之英雄无敌
2.1、非多态的实现方法:
#include <iostream>
using namespace std;
class CCreature
{
protected:
int nPower; // 代表攻击力
int nLifeValue; // 代表生命值2
};
class CDragon:public CCreature
{
public:
void Attack(CWolf * pWolf)
{
// 表现攻击动作的代码
pWolf -> Hurted(nPower);
pWolf -> FightBack(this); // 表示Attack的对象:攻击的发起者
}
void Attack(CGhost * pGhost)
{
// 表现攻击动作的代码
pGhost -> Hurted(nPower);
pGhost -> 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
成员函数。对于其他类特使如此。
因为每个动物的攻击和反击方式不一样,所以不能直接在基类CCreature中定义Attack和FightBack成员函数。每一个动物要想攻击另一个动物必须含有攻击这个动物的成员函数,所以一旦添加新的怪物那么就需要在原有每个怪物类中添加Attack和FightBack两个成员函数。
2.2、多态的实现方法
3、多态实例:几何形体程序
3.1、qsort函数的介绍
qsort函数的声明
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
- base – 指向要排序的数组的第一个元素的指针。
- nitems – 由 base 指向的数组中元素的个数。
- size – 数组中每个元素的大小,以字节为单位。
- *compar:回调函数的函数指针,需要用户自己实现回调函数
3.2、几何形体程序
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class CShape
{
public:
virtual double Area() = 0; // 纯虚函数
virtual void PrintInfo() = 0;
};
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 << "Rectangle" << 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 << "Traingle" << Area() << endl;
}
CShape * pShape[100]; // 基类的指针数组
int MyCompare(const void * s1, const void * s2)
{
double a1, a2;
CShape * * p1; // s1,s2是void *,不可写“* s1”来取得s1指向的内容
CShape * * p2;
p1 = (CShape * *)s1; // s1,s2指向pShape数组中的元素,数组元素的类型是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;
else
return 0;
}
int main()
{
int i; int n;
CRectangle * pr; CCircle * pc; CTriangle * pt;
cout << "请输入几何形体的数量:" << endl;
cin >> n; // 输入几何形体的数量
for(i=0; i<n; i++)
{
char c;
cout << "请输入几何形体的种类R/C/T:" << endl;
cin >> c; // 输入几何形体的种类
switch(c)
{
case 'R':
pr = new CRectangle(); // new一个对象
cout << "请输入矩形的长和宽w/h:" << endl;
cin >> pr->w >> pr->h;
pShape[i] = pr;
break;
case 'C':
pc = new CCircle();
cout << "请输入圆形的半径r::" << endl;
cin >> pc->r;
pShape[i] = pc;
break;
case 'T':
pt = new CTriangle();
cout << "请输入三角形的三个边长a/b/c:" << endl;
cin >> pt->a >> pt->b >> pt->c;
pShape[i] = pt;
break;
}
}
qsort(pShape, n, sizeof(CShape*), MyCompare);
for(i=0; i<n; i++)
{
pShape[i] -> PrintInfo();
}
return 0;
}
OUT
beida_lesson % ./27
请输入几何形体的数量:
3
请输入几何形体的种类R/C/T:
R
请输入矩形的长和宽w/h:
4
5
请输入几何形体的种类R/C/T:
C
请输入圆形的半径r::
3
请输入几何形体的种类R/C/T:
T
请输入三角形的三个边长a/b/c:
3
4
5
Traingle6
Rectangle20
Rectangle28.26
3.2、铭记口诀
优点:
1、
如果添加心得几何形体,比如五边形,则只需要从CShape派生出CPentagon,以及在main中的switch语句中增加一个case,其余部分不变有木有!
2、用基类指针数组存放指向各种派生类对象的指标,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法 。
铭记口诀:
1、派生类的指针可以赋值给基类指针
2、派生类的对象可以赋值给基类引用
3、用基类指针数组存放指向各种派生类对象的指标,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法 。
3.3、多态例题2:
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class Base
{
public:
void fun1() {fun2();}
// 等于void fun1() {this->fun2();}
// this是基类指针,fun2是虚函数,所以是多态
virtual void fun2() {cout << "Base::fun2()" << endl;}
};
class Derived:public Base
{
public:
virtual void fun2()
{
cout << "Derived:fun2()" << endl;
}
};
int main()
{
Derived d;
Base * pBase = & d;
// 这里pBase指针指向的是派生类的对象d,所以this指向的是派生类的fun2
pBase->fun1();
return 0;
}
OUT:
Derived:fun2()
在非构造函数,非析构函数的成员函数中调用虚函数,是多态。编译时即可确定,调用的函数是
自己的类或基类中定义的函数
,不会等到运行时才决定调用自己的还是派生类的函数。
- 注意事项:派生类中和基类中虚函数同名同参数表的函数,不加
virtual
也自动成为虚函数。
3.3、多态实例3:与构造函数
本节课程链接:看不懂对着课程就容易理解了,简单的。
派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数
构造函数和析构函数函数中调用虚函数不是多态
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
class myclass
{
public:
virtual void hello() {cout << "hello from myclass" << endl;};
virtual void bye() {cout << "bye from myclass" << endl;}
};
class son:public myclass
{
public:
// 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数
void hello() {cout << "hello from son" << endl;}; // 也是虚函数
son() {hello();}; // 构造函数和析构函数函数中调用虚函数不是多态
~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()
{
grandson gson; // 派生类对象,先从基类先后的构造函数开始运行。
son *pson;
pson = &gson;
pson->hello(); // 多态
return 0;
}
//OUT
hello from son
constructing grandson
hello from grandson
destructing grandson
bye from myclass
4、多态的实现原理
#include<iostream>
using namespace std;
class A
{
public:
virtual void Func()
{
cout << "A::Func" << endl;
}
};
class B:public A
{
public:
virtual void Func()
{
cout << "B::Func" << endl;
}
};
int main()
{
A a;
A * pa = new B();
pa -> Func();
//64位程序指针位8字节
long long * p1 = (long long *) & a; // 强制类型转化为long long形
long long * p2 = (long long *) pa;
// 用class A虚函数表的地址替换掉派生类class B虚函数表的地址:因为虚函数表地址在前函数变量之前![请添加图片描述](https://img-blog.csdnimg.cn/direct/303255d7e6814f33b23c0d628d4990f0.png)
* p2 = * p1;
pa -> Func();
return 0;
}
// OUT
beida_lesson % ./30
B::Func
A::Func
5、虚析构函数、纯虚函数和抽象类
5.1、虚析构函数
对比案例如下所示:
5.2、纯虚函数和抽象类
- 纯虚函数:没有函数体的虚函数
class A
{
private:
int a;
public:
virtual void Print() = 0; // 纯虚函数
void fun() {cout << "fun" ;}
}
- 包含纯虚函数的类叫抽象类
#include<iostream>
using namespace std;
class A
{
public:
virtual void f() = 0; // 纯虚函数
void g() {this->f()}; // OK,多态
A() {
// f(); //错误
}
};
class B:public A
{
public:
void f() {cout << "B:f()"<<endl;}
};
int main()
{
B b;
b.g();
return 0;
}
// OUT
B:f()
5.3、虚函数和纯虚函数的区别
-
定义一个函数为虚函数,不代表函数为不被实现的函数。
-
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
-
定义一个函数为纯虚函数,才代表函数没有被实现
。 -
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。