C++多态
问题引入
如果子类定义了与父类中原型相同的函数会发生什么?
- 子类会覆盖父类的同名成员函数(子类重定义父类同名成员)
class Animal
{
public:
void cry() //叫声
{
cout<<"Animal how to cay"<<endl;
}
};
class Dog:public Animal
{
public:
void cry()
{
cout<<"Dog:汪汪"<<endl;
}
};
void animalCry(Animal* base)
{
base->cry();
}
/*******main*******/
Animal *base = new Animal;
animalCry(base); //调用父类的cry
base = new Dog;
animalCry(base); //调用父类的cry
从上面可以看出,父类指针无论是指向自己,还是指向子类,调用的cry都是父类的,这不是我们所期望的,我们所期望的是:
- 根据父类指针指向的对象的实际类型,调用不同类的cry函数
- 父类指针指向父类,调用父类cry函数
- 父类指针指向子类,调用子类cry函数
实现这种同一事物在不同场景下的不同表现形式,叫做多态
多态实现
在父类重写的函数的前面加上virtual关键字(子类可写可不写)
virtual void cry() //叫声
{
cout << "Animal how to cay" << endl;
}
多态成立的三要素:
- 1,要有继承
- 2,要有虚函数重写
- 3,要有父类指针(引用)指向子类对象
多态是设计模式的基础,多态是框架的基石
多态的意义
为什么要使用多态呢?有什么好处?
案例:定义形状(Shape)类,从中派生矩形和圆形,利用多态求不同形状的面积
class Shape
{
public:
Shape(int x = 0, int y = 0)
{
this->m_x = x;
this->m_y = y;
}
void setPosition(int x, int y)
{
m_x = x;
m_y = y;
}
virtual void getArea()
{
cout << "我是形状类,没有面积" << endl;
}
~Shape()
{
cout << "~Shape()" << endl;
}
protected:
int m_x;
int m_y;
};
//矩形类,宽度,高度
class Rect :public Shape
{
public:
Rect(int x = 0, int y = 0, int length = 0, int width = 0) :m_len(length), m_width(width), Shape(x, y)
{
}
void getArea()
{
cout << "我是矩形,我的面积是:" << m_width * m_len << endl;
}
~Rect()
{
cout << "~Rect()" << endl;
}
protected:
private:
int m_width; //宽度
int m_len; //长度
};
//矩形类,宽度,高度
class Circle :public Shape
{
public:
Circle(int x = 0, int y = 0, int r = 0) :m_r(r), Shape(x, y)
{
}
void getArea()
{
cout << "我是圆形,我的面积是:" << 3.14 * m_r * m_r << endl;
}
~Circle()
{
cout << "~Circle()" << endl;
}
protected:
private:
int m_r;//半径
};
//等边三角形
class Trilateral : public Shape
{
public:
void getArea()
{
cout << "我是等边三角形" << endl;
}
~Trilateral()
{
cout << "~Trilateral()" << endl;
}
private:
};
void show(Shape* shape)
{
shape->getArea();
}
int main()
{
Shape* base = new Rect(20, 50, 60, 30);
base->getArea();
delete base;
base = new Circle(20, 20, 50);
base->getArea();
delete base;
return 0;
}
虚析构函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
析构函数必须是虚函数,在有继承关系和父类指针指向子类对象时。如果析构函数不是虚函数,则对象析构时可能存在内在泄漏的问题。
本类指针指向本类对象
不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放;
Rect* prect = new Rect(20, 50, 60, 30);
delete prect;
父类指针指向子类对象
- 若析构函数是虚函数(即加上virtual关键词),delete时基类和子类都会被释放;
- 若析构函数不是虚函数(即不加virtual关键词),delete时只释放基类,不释放子类;
Shape* base = new Rect(20, 50, 60, 30);
delete base;
函数重载、重写、重定义
class Parent
{
public:
virtual void func()
{
cout << "Parent::func()" << endl;
}
virtual void func(int a,int b)
{
cout << "Parent::func(int a,int b) " <<a<<" "<<b<< endl;
}
};
class Child:public Parent
{
public:
virtual void func(int a, int b)
{
cout << "Child::func(int a, int b) " << a << " " << b << endl;
}
virtual void func(int a, int b, int c)
{
cout << "Child::func(int a, int b, int c) " << a << " " << b <<" "<<c<< endl;
}
};
- 函数重载:必须在同一个类(作用域)中进行;子类无法重载父类中的函数,只能进行重定义。
- 函数重定义:一旦子类写了和父类同名的成员函数,父类的函数将被覆盖,无法直接访问
Child c;
c.func(); //无法访问
c.Parent::func(); //显式访问父类成员
- 函数重写:重写发生在父类与子类之间,并且父类和子类中的函数具有完全相同的函数原型;使用virtual声明的函数重写,会在父类指针调用时,根据指针指向的实际对象的类型,形成多态特性;如果不加virtual,叫作重定义。
final和override
final可以用来修饰父类的虚函数,表示在子类中禁止对改虚函数进行重写
override可以用来修饰子类的虚函数,表示是虚函数重写
动态联边和静态联编
联编是指一个程序模块、代码之间互相关联的过程。
静态联编:是程序的匹配、连接在编译阶段实现,也称为早期匹配。
- 重载函数的使用就是静态联编
动态联编:是程序的匹配、连接在运行阶段实现,也称为迟绑定。(将函数体和函数调用关联起来,就叫绑定)
- 分支语句就是动态联编,多态也是动态联编
多态中的动态联编如何实现的?
如果我们声明了类中的成员函数为虚函数,那么C++编译器会为类生成一个虚函数表,通过这个表即可实现动态联编。
虚函数表是什么?如何找到它?
-
虚函数表是顺序存放虚函数地址的,虚表是顺序表(数组),依次存放着类里面的虚函数。
-
虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d3qPJ5uX-1615733384101)(assets/image-20210311180154347.png)]
- 既然虚函数表是一个顺序表,那么它的首地址存放在哪里呢?其实当我们在类中定义了virtual函数时,C++编译器会偷偷的给对象添加一个vptr指针,vptr指针就是存的虚函数表的首地址。
如何证明vptr指针的存在
- 用sizeof计算Parent的大小,发现是4个字节,说明类里面有个隐藏vptr指针
sizeof(Parent);
示例:
class Parent
{
public:
virtual void f() { cout << "Parent::f()" << endl; }
virtual void g() { cout << "Parent::g()" << endl; }
virtual void h() { cout << "Parent::h()" << endl; }
};
通过调试发现vptr指针是确实存在的,而且指向的数组存着所有的虚函数的地址。
如何找到vptr指针,去调用虚函数
操作比较繁琐,咱们逐步分析
- vptr指针是类的第一个成员,所以只需要对对象取地址&p,即可得到vptr指针的首地址
- 指针是四个字节,所以把&p强转为int*,即可把对象分为每个元素都是四个字节的数组,然后获得第一个元素,即vptr指针(*(int*)&p) 或者()(int*)&p)[0]。
- 因为vptr指针存储的是虚函数表的首地址,所以先把vptr指针转成int*指针,然后就可以访问每个成员了(这里的每个成员都是函数指针了)*((int*)(*(int*)&p)+i)
- 最后,把得到的结果转为函数指针即可调用了
typedef void(*FunType)();
Parent p;
FunType func = (FunType)*((int*)(*(int*)&p)+0);
func();
//还可以循环调个遍
for (int i = 0; i < 3; i++)
{
FunType func = (FunType) * ((int*)(*(int*)&p) + i);
func();
}
纯虚函数和抽象类
纯虚函数:纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本(如果不定义,那么派生类也是抽象类)
纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)
virtual 类型 函数名(参数列表) = 0; //纯虚函数
抽象类:一个具有纯虚函数的基类称为抽象类。
-
特点
- 不能被实例化(定义对象)
- 可以定义指针或引用
-
案例:把求各种形状的面积,改为抽象类的方式
面向对象三大概念:
- 封装:突破了C语言函数的概念
- 继承:代码复用——可以用以前人写的代码
- 多态:可以使用未来人写的代码
纯虚函数*:纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本(如果不定义,那么派生类也是抽象类)
纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)
virtual 类型 函数名(参数列表) = 0; //纯虚函数
抽象类:一个具有纯虚函数的基类称为抽象类。
-
特点
- 不能被实例化(定义对象)
- 可以定义指针或引用
-
案例:把求各种形状的面积,改为抽象类的方式
面向对象三大概念:
- 封装:突破了C语言函数的概念
- 继承:代码复用——可以用以前人写的代码
- 多态:可以使用未来人写的代码