文章目录
1 、概念
多态:同一指令,针对不同对象,产生不同行为。
多态的类型
-
静态多态(静态联编):编译时多态。形式:函数重载、运算符重载、模板。
-
动态多态(动态联编):运行时多态。形式:虚函数。
多态与虚函数不是等价的,动态多态的体现必须要有虚函数,调用虚函数并不一定体现多态。
2、虚函数
2.1、虚函数的定义
虚函数:在成员函数前加 virtual 关键字,在末尾可添加 override 关键字。
派生类重写基类的虚函数
- 函数同名
- 返回值类型相同
- 参数列表相同:参数类型、参数个数、参数顺序
不能设置为虚函数的函数
- 普通函数:非成员函数
- 静态成员函数:编译时绑定。虚函数的调用需要对象,需要 this 指针,而静态函数没有 this 指针,可以不使用对象调用,使用类名+作用域限定符调用
- 内联成员函数:没有必要,内联函数本身是为了减少函数调用的代价,而虚函数需要创建虚函数表,失去内联的意义。
- 非成员函数的友元函数
- 构造函数:
- 从继承观点来看,构造函数不能被继承,而虚函数可以被派生类重写。
- 从存储角度,如果构造函数是虚函数,则需用通过虚表来调用,但是对象还没有实例化,没有内存空间,无法通过虚函数指针找到虚表。
- 从语义角度,构造函数就是为了初始化数据成员,然而虚函数是为了在完全不了解细节情况下也能正确处理对象,虚函数要对不同类型的对象产生不同的动作。如果构造函数是虚函数,那么对象都没有产生,无法完成想要的操作。
2.2、虚函数的实现机制
2.2.1、实现原理
- 虚函数指针 vfptr:指向虚表
- 虚函数表(虚表):虚函数的入口地址。注意:多基继承时,只有第一个虚表存放虚函数入口地址,其他虚表存放跳转指令,指向第一个虚表。
当基类定义虚函数的时候,就会在基类对象的存储布局的前面多一个虚函数指针,指向自己的虚函数表,存放虚函数的入口地址。当派生类继承该基类的时候,把基类的虚函数吸收过来,派生类虚函数指针指向自己的虚函数表,若派生类重写该虚函数,则派生类虚函数表中对应的虚函数的入口地址被覆盖 override。
2.2.2、多态被激活的条件
- 基类定义虚函数
- 派生类重定义虚函数
- 创建派生类对象
- 基类的指针(引用)指向(绑定)到派生类对象
- 使用基类指针(引用)调用虚函数
2.2.3、测试虚表
测试虚表的存在,一级指针指向派生类对象(虚表的首地址),二级指针指向虚表中的虚函数,打印虚表。
#include <iostream>
using std::cout;
using std::endl;
class Base {
public:
Base(long base)
: _base(base)
{ cout << "Base(long)" << endl; }
virtual void func1() { cout << "Base::func1()" << endl; }
virtual void func2() { cout << "Base::func2()" << endl; }
virtual void func3() { cout << "Base::func3()" << endl; }
private:
long _base;
};
class Derived
: public Base
{
public:
Derived(long base, long derived)
: Base(base)
, _derived(derived)
{ cout << "Derived(long,long)" << endl; }
virtual void func1() {
cout << "Derived::func1() _derived:" << _derived << endl;
}
virtual void func2() {
cout << "Derived::func2()" << endl;
}
private:
long _derived;
};
// 通过二级指针验证虚函数表的存在,
void test() {
Derived d(10, 100);
cout << "----- 打印虚函数表中的虚函数地址 -----" << endl;
// 一级指针,指向派生类对象的首地址,即虚函数表的地址
long *pvtable = (long*)&d;
for(int idx = 0; idx < 3; ++idx) {
// 打印虚函数的地址
cout << pvtable[idx] << endl;
}
cout << "----- 调用虚函数表中的虚函数,不传 this 指针 -----" << endl;
// 二级指针,指向派生类对象地址的地址,即虚函数的地址
long **pVtable = (long **)&d;
typedef void(* Function)();
for(int idx = 0; idx < 3; ++idx) {
// 回调虚函数,没有传this指针
Function f = (Function)pVtable[0][idx];
f();
}
cout << "----- 调用虚函数表中的虚函数,传入 this 指针-----" << endl;
typedef void (*Function2)(Derived*);
for(int idx = 0; idx < 3; ++idx) {
// 回调虚函数,传入this指针
Function2 f2 = (Function2)pVtable[0][idx];
f2(&d);
}
}
int main(void) {
test();
return 0;
}
2.3、虚函数的访问
- 指针访问:基类的指针指向派生类对象,动态联编,体现多态
- 引用访问:基类的引用绑定派生类对象,动态联编,体现多态
- 成员函数访问: this 指针调用虚函数,表现动态多态
- 对象访问:对象调用虚函数,静态联编,不体现多态
- 构造函数或析构函数:静态联编,只会调用自己的虚函数
测试在构造函数或析构函数中,调用虚函数
#include <iostream>
using std::cout;
using std::endl;
class Grandpa {
public:
Grandpa() {
cout << "Grandpa()" << endl;
}
virtual void func1() {
cout << "Grandpa::func1()" << endl;
}
virtual void func2() {
cout << "Grandpa::func2()" << endl;
}
~Grandpa() {
cout << "~Grandpa()" << endl;
}
};
class Father: public Grandpa {
public:
Father() {
cout << "Father()" << endl;
func1(); // 构造函数调用虚函数
}
virtual void func1() {
cout << "Father::func1()" << endl;
}
virtual void func2() {
cout << "Father::func2()" << endl;
}
~Father() {
cout << "~Father()" << endl;
func2(); // 析构函数调用虚函数
}
};
class Son: public Father {
public:
Son() {
cout << "Son()" << endl;
}
virtual void func1() {
cout << "Son::func1()" << endl;
}
virtual void func2() {
cout << "Son::func2()" << endl;
}
~Son() {
cout << "~Son()" << endl;
}
};
int main() {
Son son; // 栈对象,自动销毁
return 0;
}
测试结果:构造函数或析构函数中调用虚函数,表现的是静态联编,只会调用自己的虚函数
/*
构造过程:grandfather -> father -> son
析构过程:~son-> ~father -> ~grandfather
*/
Grandpa()
Father()
Father::func1() // son还未创建,此时只能调用father的func1
Son()
~Son()
~Father()
Father::func2() // son已经销毁,此时只能调用father的func2
~Grandpa()
2.4 、纯虚函数
纯虚函数:只有声明,没有实现,作为函数接口存在。
virtual 返回类型 函数名(参数列表) = 0;
2.4.1、抽象类
抽象类作为函数接口存在,不能创建对象,但可以创建抽象类的指针和引用
抽象类的形式
- 纯虚函数:声明了纯虚函数的类,就是抽象类。若派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。
- protected 修饰构造函数的类。可以派生新类,但不能创建对象。
// 基类定义为抽象类,不能创建基类对象
class Base {
protected:
Base(long base): _base(base) {}
protected:
long _base;
};
class Derived : public Base {
public:
Derived(long base, long derived)
: Base(base) // 可以调用基类的构造函数,创建派生类
, _derived(derived) {}
private:
long _derived;
}
2.4.2、开闭原则
面向对象的设计原则:开闭原则
特点:对扩展开放,对修改关闭
测试
#include <math.h>
#include <iostream>
using std::cout;
using std::endl;
// 抽象类:纯虚函数作为函数接口
class Figure {
public:
virtual void display() const = 0;
virtual double area() const = 0;
};
// 通过引用访问虚函数,表现动态多态
void func(const Figure &fig) {
fig.display();
cout << "'s area is : " << fig.area() << endl;
}
class Rectangle: public Figure {
public:
Rectangle(double length = 0, double width = 0)
: _length(length)
, _width(width)
{ cout << "Rectangle(double = 0, double = 0)" << endl;}
void display() const override {
cout << "Rectangle ";
}
double area() const override {
return _length * _width;
}
~Rectangle() {
cout << "~Rectangle()" << endl;
}
private:
double _length;
double _width;
};
class Circle: public Figure {
public:
Circle(double radius = 0)
: _radius(radius)
{ cout << "Circle(double = 0)" << endl; }
void display() const override {
cout << "Circle ";
}
double area() const override {
return _radius * _radius * 3.14159;
}
~Circle() {
cout << "~Circle()" << endl;
}
private:
double _radius;
};
class Triangle
: public Figure
{
public:
Triangle(double a = 0, double b = 0, double c = 0)
: _a(a)
, _b(b)
, _c(c)
{ cout << "Triangle(double = 0, double = 0, double = 0)" << endl; }
void display() const override {
cout << "Triangle " ;
}
double area() const override {
double tmp = (_a + _b + _c)/2;
return sqrt(tmp * (tmp - _a) * (tmp - _b) * (tmp - _c));
}
~Triangle() {
cout << "~Triangle()" << endl;
}
private:
double _a;
double _b;
double _c;
};
int main(int argc, char **argv) {
Rectangle rectangle(10, 12);
Circle circle(10);
Triangle triangle(3, 4, 5);
cout << endl;
func(rectangle);
func(circle);
func(triangle);
return 0;
}
2.5、虚析构函数
多态的问题:如果一个基类的指针指向派生类的对象,当 delete 该指针释放派生类对象,系统只会执行基类的析构函数,不会执行派生类的析构函数,发生内存泄漏。
为了防止内存泄漏,只要基类中定义了虚函数,必须将基类的析构函数设置为虚函数,派生类的析构函数自动成为为虚函数。
基类和派生类的函数名虽然看起来不同,不符合重写规则,但实际上每个类只有一个析构函数,编译器将析构函数名统一解释为 destructor,实现重写。
例:父类是虚析构函数,子类重写了父类的析构函数,当 delete base 指针时 pbase->~destructor
即重写后的子类析构函数,子类析构后,调用父类的析构函数。
#include <string.h>
#include <iostream>
using std::cout;
using std::endl;
class Base {
public:
Base(const char *pbase)
: _pbase(new char[strlen(pbase) + 1]())
{
cout << "Base(const char *)" << endl;
strcpy(_pbase, pbase);
}
// 将基类的析构函数声明为虚函数:~destructor
virtual ~Base() {
cout << "~Base()" << endl;
if(_pbase) {
delete [] _pbase;
_pbase = nullptr;
}
}
private:
char *_pbase;
};
class Derived: public Base {
public:
Derived(const char *pbase, const char *pderived)
: Base(pbase)
, _pderived(new char[strlen(pderived) + 1]())
{
cout << "Derived(const char *, const char *)" << endl;
strcpy(_pderived, pderived);
}
// 重写发生在派生类的析构函数: ~destructor
// 基类的析构函数虚化 -> 派生类的析构函数虚化 -> 名字相同发生重写
~Derived() {
cout << "~Derived()" << endl;
if(_pderived) {
delete [] _pderived;
_pderived = nullptr;
}
}
private:
char *_pderived;
};
int main() {
Base *pbase = new Derived("hello", "world");
// 执行析构函数 pbase->~destructor()
// 先执行派生类对象的析构函数,再执行基类(Base*)的析构函数
delete pbase;
return 0;
}
2.6、重载 覆盖 隐藏
-
重载:同一个作用域,函数名相同,参数列表不同。
-
覆盖 | 重定义 | 重写:基类与派生类中的虚函数,函数名相同,参数列表相同。
-
隐藏:基类与派生类,函数名相同,派生类屏蔽了基类的同名数据成员。使用基类的作用域才能访问到其同名函数。