笔记按照中国大学MOOC上北京大学郭炜老师主讲的程序设计与算法(三)C++面向对象程序设计所作,B站上也有资源。原课程链接如下:
其他各章节链接如下:
程序设计与算法(三)C++面向对象程序设计笔记 第一周 从C到C++
程序设计与算法(三)C++面向对象程序设计笔记 第二周 类和对象基础
程序设计与算法(三)C++面向对象程序设计笔记 第三周 类和对象提高
程序设计与算法(三)C++面向对象程序设计笔记 第四周 运算符重载
程序设计与算法(三)C++面向对象程序设计笔记 第五周 继承
程序设计与算法(三)C++面向对象程序设计笔记 第六周 多态
程序设计与算法(三)C++面向对象程序设计笔记 第七周 输入输出和模板
程序设计与算法(三)C++面向对象程序设计笔记 第八周 标准模板库STL(一)
程序设计与算法(三)C++面向对象程序设计笔记 第九周 标准模板库STL(二)
程序设计与算法(三)C++面向对象程序设计笔记 第十周 C++11新特性和C++高级主题
在类的定义中,前面有 virtual 关键字的成员函数就是虚函数,虚函数可以参与多态,普通的成员函数不能
virtual 关键字只用在类定义里的函数声明中,写函数体时不用
class base {
virtual int get() ;
};
int base::get()
{ }
构造函数和静态成员函数不能是虚函数
派生类的指针可以赋给基类指针
通过基类指针调用基类和派生类中的同名函数,被执行的就是基类的函数
通过基类指针调用基类和派生类中的同名虚函数时:
若该指针指向一个基类的对象,那么被调用是基类的虚函数
若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数
这种机制就叫做“多态”
class CBase {
public:
virtual void SomeVirtualFunction() { }
};
class CDerived:public CBase {
public:
virtual void SomeVirtualFunction() { }
};
int main() {
CDerived ODerived;
CBase * p = & ODerived;
p -> SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
return 0;
}
编译时没有办法确定这条语句调用哪一个类的 virtual function
派生类的对象可以赋给基类引用
通过基类引用调用基类和派生类中的同名虚函数时:
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(); //调用哪个虚函数取决于r引用哪种类型的对象
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::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;
}
在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少
游戏中有很多种怪物,每种怪物都有一个类与之对应(如 CSoldier , CDragon , CPhonex , CAngel ),每个怪物就是一个对象
怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的
游戏版本升级时,要增加新的怪物 —— 雷鸟,新增类 CThunderBird 。如何编程才能使升级时的代码改动和增加量较小?
为每个怪物类编写 Attack , FightBack 和 Hurted 成员函数
Attack 函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的 Hurted 函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack 成员函数,遭受被攻击怪物反击
Hurted 函数减少自身生命值,并表现受伤动作
FightBack 成员函数表现反击动作,并调用被反击对象的 Hurted 成员函数,使被反击对象受伤
设置基类 CCreature , 并且使 CDragon , CWolf 等其他类都从 CCreature 派生而来

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 ) ;
成员函数,已有的类可以原封不动
void CDragon::Attack(CCreature * p)
{
p->Hurted( m_nPower ); //多态
p->FightBack( this ); //多态
}
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
几何形体处理程序: 输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状
Input:
第一行是几何形体数目 n(不超过100)。下面有 n 行,每行以一个字母 c 开头
若 c 是 ‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高
若 c 是 ‘C’,则代表一个圆,本行后面跟着一个整数代表其半径
若 c 是 ‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度
Output:
按面积从小到大依次输出每个几何形体的种类及面积。每行一个几何形体,输出格式为:
形体名称: 面积
Sample Input:
3
R 3 5
C 9
T 3 4 5
Sample Output:
Triangle:6
Rectangle:15
Circle:254.34
#include
#include
#include
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 << "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 * pShapes[100];
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;
}
}
qsort(pShapes,n,sizeof( CShape* ),MyCompare );
for( i = 0;i <n;i ++ )
pShapes[i]->PrintInfo();
return 0;
}
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指向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;
else
return 0;
}
=0 意味着这是一个纯虚函数,连函数体都没有
s1、s2 指向 pShapes 数组里面待比较的 CShape* 类型指针元素。s1 是 void* 类型的指针,不知道 *s1 是多少个字节,不能拿到 s1 所指向的元素,编译器无法处理。要拿出 s1 指向的东西需要把 s1 强制转换成 CShape** 赋值给 p1,让 p1 作为指向指针的指针,指向 CShape* 类型指针。p1、p2 指向两个待比较的数组元素
(*p1)->Area() 这条语句通过基类指针去调用基类和派生类里面都有的同名虚函数,是多态
比较两个几何形体的面积,如果 *s1 指向的几何形体面积小,*s1 元素应该排在前面,返回-1。通过 MyCompare 比较函数就能够对 pShapes 数组里面的指针按照它们所指向的几何形体的面积从小到大进行排序
如果添加新的几何形体,比如五边形,则只需要从 CShape 派生出 CPentagon , 以及在 main 中
的 switch 语句中增加一个 case , 其余部分不变
用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法
class Base {
public:
void fun1() { 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->fun1();
return 0;
}
输出:
Derived:fun2()
fun1() 并不是虚函数。在 fun1() 里面调用 fun2() 等价于 this->fun2() 这种写法,而出现在 Base 类的成员函数里面的 this 指针类型是 Base*,是基类指针,fun2() 又是虚函数,通过基类指针调用虚函数,这条语句就是多态,程序执行到这条语句时执行的是哪个类的 fun2() 取决于此时 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(){ 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;
}
结果:
hello from son
constructing grandson
hello from grandson
destructing grandson
bye from myclass
派生类中和基类中虚函数同名同参数表的函数,不加 virtual 也自动成为虚函数
派生类对象生成,会从顶自下执行一个个基类的构造函数,首先执行 myclass 的构造函数,但是 myclass 的构造函数没有输出,接下来执行 son 的构造函数,调用 hello() 虚函数但不是多态,执行 son 这个类自己的 hello(),然后执行 grandson 的构造函数
main 结束 pson 对象消亡,先执行 grandson 的析构函数然后再执行 son 的析构函数,son 自己没有 bye(),从 myclass 继承
“多态”的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定 —— 这叫“动态联编”。“动态联编” 到底是怎么实现的呢?
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个字节就是用来放虚函数表的地址的
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qBeCYgp-1666237921685)(C++ 面向对象程序设计.assets/image-20221014170309328.png)]](https://1000bd.com/contentImg/2024/05/23/d4042bb57ec8ff96.png)
多态的函数调用语句被编译成一系列根据基类指针所指向的(或基类引用所引用的)对象中存放的虚函数表的地址,在虚函数表中查找虚函数地址,并调用虚函数的指令
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w38pPwKu-1666237921686)(C++ 面向对象程序设计.assets/image-20221014170354112.png)]](https://1000bd.com/contentImg/2024/05/23/c45d183d49017668.png)
#include
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 * p2 = (long long * ) pa;
* p2 = * p1;
pa->Func();
return 0;
}
输出:
B::Func
A::Func
指针提供了随意访问内存的能力,比如针对一个变量只访问其中的某一部分
由于 p1 是 long long *,long long 也占8B,所以 *p1 是8B的内容,8B正好是一个地址,这个地址放在 a 对象的开头,实际上是 class A 的虚函数表的地址
p2 指向 new 出来的 B 对象,B 对象的开头放着 class B 的虚函数表的地址
* p2 = * p1 把 p1 所指向地方的8B内容拷贝到 p2 所指向的地方,用 class A 的虚函数表地址替换 new 出来的 B 对象里面的 class B 的虚函数表地址,覆盖掉 B 对象的头8B
本来 pa 指向一个 class B 对象,class B 对象的头部就应该放着 class B 的虚函数表的地址,通过这个地址去查 class B 的虚函数表,能够查到的 Func 自然就是 class B 的 Func,所以就会调用 class B 的 Func
但是现在把 new 出来的对象前面8B替换成了 *p1,即 class A 的对象 a 的头8B,pa->Func() 此时就会找到 class A 的虚函数表
通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,比如通过一个基类指针指向一个 new 出来的派生类对象,delete 这个基类指针只调用基类的析构函数而没有调用派生类的虚构函数。但是删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数
解决办法:把基类的析构函数声明为 virtual 。派生类的析构函数可以 virtual 不进行声明。通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数
一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数
注意:不允许以虚函数作为构造函数
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();
delete pson;
return 0;
}
输出:
bye from grandson
bye from son
执行 grandson::~grandson() ,引起执行 son::~son()!
纯虚函数:没有函数体的虚函数
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( ) ; //ok
}
A( ){ //f( ); //错误
}
};
class B:public A {
public:
void f(){ cout<<"B:f()"<<endl; }
};
int main(){
B b;
b.g();
return 0;
}
输出:
B:f()
直接写 f() 和 this->f() 本质上是一样的,这条语句是多态,在基类的普通成员函数里调用纯虚函数没有问题,因为这样的调用语句是多态,真正被执行的一定是派生类的虚函数
在基类的构造函数或者析构函数里面调用基类的纯虚函数不是多态,要调用 f() 就一定是它自己的 f(),f() 连函数体都没有,编译时就会出错