4.1 通⽤接⼝
1. 通⽤接⼝
需求: 在王者荣耀游戏中设计⼀个选择英雄玩游戏的函数接⼝?
void playGameByAngela(Angela &angela);
void playGameByXiangYu(XiangYu &xiangyu);
void playGameByLuban(Luban &lanban);
void playGame(AngelaHero &angela);
void playGame(XiangYu &xiangyu);
void playGame(Luban &lanban);
void playGame(AngelaHero &angela);
void playGame(XiangYu &xiangyu);
void playGame(Luban &lanban);
void playGame(MonkSha &monkSha);

在 C++ 中可以使⽤⽗类的引⽤来引⽤⼦类的对象,也可以使⽤⽗类的指针来指向⼦类的对象,反过来则不⾏。“只能爹管⼉⼦,不能⼉⼦管爹”。
- void playGame(Hero &hero);
⽗类的函数接⼝是⽤ virtual 关键字修饰变成虚函数
class Hero{
public:
virtual void skill(void);
};
⼦类继承⽗类,对⽗类的函数接⼝进⾏重写
class Angela:public Hero
{
public:
virtual void skill(void);
};
class XiangYu:public Hero{
public:
virtual void skill(void);
};
class Luban:public Hero{
public:
virtual void skill(void);
};
⽗类的引⽤来引⽤⼦类的对象或⽗类的指针来指向⼦类的对象
void playGame(Hero &hero)
{
hero.skill();
}
4.2 多态 (polymorphic) 介绍
一、概念
不同的对象接收到同⼀消息的时候,产⽣不同的效果
二、理解
在⾯向对象中,多态是 **** 指通过基类 (⽗类) 的指针或者引⽤指向⼦类的对象,在运⾏时动态调⽤实际绑定对象函数的⾏为。即 “⼀个接⼝,多种实现”
三、分类

- 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调⽤合适的函数,如果有合适的函数就调⽤,没有的话就会发出警告或者报错;
- 动态多态是 ** 在程序运⾏时 **** 根据基类的引⽤(指针)** 指向的对象来确定⾃⼰具体该调⽤哪⼀个类的虚函数。
四、作用
把不同的⼦类对象都当作⽗类来看,可以屏蔽不同⼦类对象之间的差异,写出通⽤的代码,做出通⽤的编程,以适应需求的不断变化。多态是设计模式的基础,多态是框架的基础。
五、前提条件
- 要有继承
- 要有虚函数重写(被 virtual 声明的函数叫虚函数)
- 要有⽗类指针(⽗类引⽤)指向⼦类对象
六、实现步骤
- 基类 (⽗类) ⾥⾯的⽅法是⽤ virtual 关键字修饰,变成虚函数
- ⼦类继承⽗类重写⽗类⾥⾯的⽅法
- ⽤⽗类的指针或引⽤指向⼦类对象
- 通过⽗类指针或引⽤调⽤⼦类重写的⽗类⽅法
七、任务
在⼀个物联⽹设备中,有蓝⽛、4G、Wifi 三种⽆线传输数据形式,使⽤多态的思想实现数据传输的接⼝。
4.3 多态的实现原理
一、虚函数表与vptr指针
class Parent{
public:
virtual void vir_function1() {
cout << "vir_function1" << endl;
}
virtual void vir_function2() {
cout << "vir_function2" << endl;
}
private:
int a;
int b;
};
⽤ 64bit 编译器编译出 32bit 程序:
linux@ubuntu:~/cpp/lesson17$ g++ Test.cpp -g -m32
In file included from Test.cpp:1:0:
/usr/include/c++/7/iostream:38:10: fatal error: bits/c++config.h:
No such file or directory
#include <bits/c++config.h>
^~~~~~~~~~~~~~~~~~
解决⽅案:
sudo apt-get install g++-multilib
gdb a.out

gdb 调试:
- 增加调试信息
- -g
- 设置断点
- b function/line
- 按照层次打印
- set p pretty <on/off>
- 显示虚函数表
- set p obj <on/off>
- 数组内容层次显示
- set p array on
- 数据显示格式
- x 按⼗六进制格式显示变量
- d 按照⼗进制格式显示变量
- u 按⼗六进制格式显示⽆符号整型
- o 按⼋进制格式显示变量
- t 按⼆进制格式显示变量
- a 按⼗六进制格式显示变量
- c 按字符格式显示变量
- f 按浮点数格式显示变量
- 查看函数⼊⼝地址
- info line 10(查看第 10 ⾏函数的⼊⼝地址)
总结:
- 当类中声明虚函数时,编译器会在类中⽣成⼀个虚函数表
- 虚函数表存放的是虚函数的⼊⼝地址
- 虚函数表是由编译器⾃动⽣成与维护的
- virtual 成员函数会被编译器放⼊虚函数表中
- 存在虚函数时,每个对象中都有⼀个指向虚函数表的指针 (vptr 指针)
二、子类继承父类的虚函数表
#include <iostream>
using namespace std;
class Parent
{
public:
virtual void vir_function1()
{
cout << "virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "vir_function2" << endl;
}
private:
int a;
int b;
};
class Child:public Parent
{
public:
virtual void vir_function1()
{
cout << "virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "vir_function2" << endl;
}
private:
int c;
};
int main(void)
{
Child object;
return 0;
}

静态绑定 : 程序编译结束之后,就已经确定了需要调⽤的函数 (根据对象类型或指针的类型来调⽤函数)
动态绑定 : 程序编译的时候,没有确定需要调⽤的函数, 程序在运⾏的时候,才确定需要调⽤的函数
三、静态绑定与动态绑定
- 静态绑定 : 程序编译结束之后,就已经确定了需要调⽤的函数 (根据对象类型或指针的类型来调⽤函数)
- 动态绑定 : 程序编译的时候,没有确定需要调⽤的函数, 程序在运⾏的时候,才确定需要调⽤的函数
- 只有在多态的场合才会出现动态绑定,否则就是静态绑定


四、思考
问题 : 为什么⽤⽗类指针或引⽤指向⼦类对象的时候,可以根据⼦类对象的不同,调⽤不同⼦类对象的函数?
** 回答:** 因为在多态的场合是动态绑定的,** 程序运⾏的时候,会根据⼦类对象⾥⾯的 vptr 找到虚函数表,然后通过查虚函数表找到要调⽤的函数。** 由于是不同的⼦类对象,每个⼦类对象对应的虚函数表中函数实现不⼀样,最终调⽤的函数也就不⼀样了。
五、任务
请写出如下程序 (32bit 和 64bit) 运⾏的结果
#include <iostream>
using namespace std;
class A{
public:
virtual void function1(void)
{
cout << "A::function1" << endl;
}
private:
int a;
static int b;
};
int A::b = 0;
class B{
public:
void function2(void)
{
cout << "B::function2" << endl;
}
virtual void function(void)
{
cout << "B::function" << endl;
}
private:
int b;
};
class C:public A,public B
{
public:
void function1(void)
{
cout << "C:function1" << endl;
}
void function2(void)
{
cout << "C:function2" << endl;
}
private:
int c;
};
void function1(A &obj)
{
obj.function1();
}
void function2(B &obj)
{
obj.function2();
}
int main(void)
{
C obj;
cout << "sizeof(obj) : " << sizeof(obj) << endl;
function1(obj);
function2(obj);
return 0;
}
4.4 重载、覆盖、隐藏
一、重载
成员函数被重载的特征:
- 1. 相同的范围(在同⼀个类中)
- 2. 函数名字相同
- 3. 参数不同
- 4. virtual 关键字可有可⽆。
二、覆盖
覆盖是指派⽣类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派⽣类与基类)
- 函数名字相同;
- 参数、返回值相同
- 基类函数必须有 virtual 关键字
#include <iostream>
using namespace std;
class Parent
{
public:
virtual void vir_function1()
{
cout << "virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "vir_function2" << endl;
}
private:
int a;
int b;
};
class Child:public Parent
{
public:
virtual void vir_function1()
{
cout << "virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "vir_function2" << endl;
}
private:
int c;
};
int main(void)
{
Child object;
return 0;
}

#include <iostream>
using namespace std;
class Parent
{
public:
virtual void vir_function1()
{
cout << "Parent virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "Parent vir_function2" << endl;
}
private:
int a;
int b;
};
class Child:public Parent
{
public:
virtual void vir_function1()
{
cout << "Child virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "Child vir_function2" << endl;
}
private:
int c;
};
int main(void)
{
Child object;
Parent *p = &object;
p->vir_function1();//Child virtual_function1
p->vir_function2();//Child virtual_function2
return 0;
}
三、隐藏
这⾥ “隐藏” 是指派⽣类的函数屏蔽了与其同名的基类函数,规则如下:
- 1. 如果派⽣类的函数与基类的函数同名,但是参数不同。此时,不论有⽆ virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
- 2. 如果派⽣类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时基类的函数被隐藏(注意别与覆盖混淆)。
#include <iostream>
using namespace std;
class Parent
{
public:
void vir_function1()
{
cout << "Parent virtual_function1" << endl;
}
virtual void vir_function2()
{
cout << "Parent virtual_function2" << endl;
}
private:
int a;
int b;
};
class Child:public Parent
{
public:
virtual void vir_function1()
{
cout << "Child virtual_function1" << endl;
}
virtual void vir_function2(int a)
{
cout << "Child virtual_function2" << endl;
}
private:
int c;
};
int main(void)
{
Parent object;
object.vir_function1();//Parent virtual_function1
object.vir_function2();//Parent virtual_function2
cout << "-----------------------" << endl;
Child child;
child.vir_function1();//Child virtual_function1(隐藏了⽗类函数)
//error:no matching function for call to ‘Child::vir_function2()’
child.vir_function2();//(隐藏了⽗类函数)
//maybe you must child.Parent::vir_function2();
cout << "-----------------------" << endl;
Parent *p = &child; //behavior depends on type of the pointer
p->vir_function1();//Parent virtual_function1
//
p->vir_function2();//Parent virtual_function2
return 0;
}
四、任务
写出可以体现出重载、覆盖、隐藏的代码
4.5 虚析构函数
一、分析代码
#include <iostream>
using namespace std;
class A{
public:
A(){
cout << "A()" << endl;
aptr = new char[100];
}
~A(){
cout << "~A()" << endl;
delete[] aptr;
}
private:
char *aptr;
};
class B:public A{
public:
B(){
cout << "B()" << endl;
bptr = new char[100];
}
~B(){
cout << "~B()" << endl;
delete[] bptr;
}
private:
char *bptr;
};
void free_function(A *p)
{
delete p;
}
int main(void)
{
A *aobject = new A;
free_function(aobject);
cout << "--------------------------" << endl;
B *bobject = new B;
free_function(bobject);
return 0;
}
程序运⾏结果如下:
A()
~A()
--------------------------
A()
B()
~A()
二、将基类的析构函数变成虚函数
#include <iostream>
using namespace std;
class A{
public:
A(){
cout << "A()" << endl;
aptr = new char[100];
}
~A(){
cout << "~A()" << endl;
delete[] aptr;
}
private:
char *aptr;
};
class B:public A{
public:
B(){
cout << "B()" << endl;
bptr = new char[100];
}
~B(){
cout << "~B()" << endl;
delete[] bptr;
}
private:
char *bptr;
};
void free_function(A *p)
{
delete p;
}
int main(void)
{
A *aobject = new A;
free_function(aobject);
cout << "--------------------------" << endl;
B *bobject = new B;
free_function(bobject);
return 0;
}
程序运⾏结果如下:
A()
~A()
--------------------------
A()
B()
~B()
~A()
三、分析
1. 基类析构函数不是虚函数时 free_function 实现

2. 基类析构函数是虚函数时 free_function 实现

3.gdb 调试打印 B 类对象

四、总结
- 将基类的析构函数声明为虚函数,则派⽣类的析构函数⾃动为虚函数
- 基类的指针或引⽤指向派⽣类对象的时候,可以通过删除基类指针调⽤派⽣类析构函数和基类 析构函数,防⽌资源的未释放
- 建议将基类的析构函数声明为虚函数,防⽌在多态场合造成资源未释放问题
996

被折叠的 条评论
为什么被折叠?



