目录
一. 多态的概念
多态,就是多种状态。体现在C++中,就是不同的对象来执行同样的行为会产生不同的结果。
在生活中,多态也有相关的例子,如:在购买门票时,一般成年人购买全票、学生购买半价票、老年人及儿童免票,在使用扫码领红包时,新用户红包额度10~99元,老用户红包额度0~10元等,都是多态的常见应用。

二. 多态的定义
2.1 虚函数的定义
要实现多态,首先要实现虚函数,虚函数就是在类中定义成员函数时,使用virtual进行声明。对于存在虚函数的类,在类实例化出来的对象中会存储一个虚函数表指针,虚表函数指针指向一张虚表,虚表中存储有虚函数的地址。
这里要注意区分虚拟继承中的虚基指针和虚基表,虚继承中的虚基表里存储的是当前类和公共父类在内存中存储位置的偏移量。
演示代码2.1:(定义虚函数)
class Person
{
public:
//定义BuyTicket买票虚函数
virtual void BuyTicket()
{
std::cout << "full fare ticket" << std::endl;
}
int _age = 20;
};

2.2 虚函数的重写
虚函数的重写,就是子类中的虚函数,覆盖原来父类中的虚函数,虚函数重写需要满足以下条件:
- 执行重写和被重写的函数都为virtual函数。
- 两函数分别位于继承体系的父类和子类中。
- 函数名称相同、参数相同、返回值相同(协变和析构函数例外)
注:子类中的虚函数,即使不用virtual声明,也可以覆盖父类的虚函数,但是父类虚函数则不可以省略virtual,一般建议virtual不要省略。
演示代码2.2:(虚函数重写的实现)
class Student: public Person
{
public:
//重写父类Person中的BuyTicket函数
virtual void BuyTicket()
{
std::cout << "half fare ticket" << std::endl;
}
};
对于虚函数重写,还有两个例外情况:
- 函数协变:如果父类和子类虚函数的函数名和参数相同,但返回值类型不同,也可能构成虚函数重写,但要求基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用。
- 析构函数:如果使用virtual声明父类和子类的析构函数,即使它们的函数名不同,也会构成重写,因为C++编译器会将所有析构函数的函数名同一处理为destructor。
演示代码2.3:(协变)
class A {};
class B: public A {};
class Person
{
public:
virtual A* BuyTicket()
{
std::cout << "full fare ticket" << std::endl;
return nullptr;
}
int _age = 20;
};
class Student: public Person
{
public:
//协变 -- 构成函数重写
virtual B* BuyTicket()
{
std::cout << "half fare ticket" << std::endl;
return nullptr;
}
};
2.3 构成多态的条件及多态的定义和调用方法
构成多态的条件:
- 虚函数的重写。
- 通过父类的指针或者引用来调用虚函数。(直接通过父类对象调用无法实现多态)
演示代码2.3:(多态的定义和调用)
class Person
{
public:
virtual void BuyTicket()
{
std::cout << "full fare ticket" << std::endl;
}
int _age = 20;
};
class Student : public Person
{
public:
//重写虚函数
virtual void BuyTicket()
{
std::cout << "half fare ticket" << std::endl;
}
};
void ByReference(Person& rps) //通过父类引用调用虚函数 -- 构成多态
{
rps.BuyTicket();
}
void ByPtr(Person* pps) //通过父类指针调用虚函数 -- 构成多态
{
pps->BuyTicket();
}
void ByObject(Person ps) //直接通过父类对象调用虚函数 -- 无法构成多态
{
ps.BuyTicket();
}
int main()
{
Person ps;
Student sd;
//1.通过父类引用调用虚函数可以实现多态
ByReference(ps); //full fare ticket
ByReference(sd); //half fare ticket
std::cout << std::endl;
//2.通过父类指针调用虚函数可以实现多态
ByPtr(&ps); //full fare ticket
ByPtr(&sd); //half fare ticket
std::cout << std::endl;
//3.直接通过父类对象调用虚函数无法实现多态
ByObject(ps); //full fare ticket
ByObject(sd); //full fare ticket
std::cout << std::endl;
return 0;
}

2.4 final和override关键字(C++11)
- final:可以用来修饰某个类或者虚函数,表示某个类不能被继承,或者某个虚函数不能重写。
- override:用来修饰派生类中的虚函数,如果经override修饰的虚函数没有实现对某一虚函数的重写,那么编译报错。
演示代码2.4:(final和override的使用)
class A final
{ /*类的成员*/ };
class B: public A { }; // 报错,A无法被继承
class AA
{
public:
//final修饰了虚函数,无法被重写
virtual void func1() final { std::cout << "class AA -> func()" << std::endl; }
};
class BB : public AA
{
public:
//override检查func2是否重写了某个虚函数(没重写就报错)
virtual void func2() override { std::cout << "class AA -> func()" << std::endl; }
};
2.5 函数的重载、重写和重定义的概念对比
2.6 析构函数的重写
由于C++编译器会将析构函数的函数名统一处理为destructor,因此,如果析构函数使用virtual进行声明,那么父类和子类的析构函数就会构成重写。
一般要求父类和子类中的析构函数都使用virtual声明为虚函数。如果不声明为virtual,在一般常见下,不会存在问题,但在演示代码2.5的情况下,就会出现问题。假设使用父类指针A* ptra指向一块new开辟的子类对象空间(A* = new B),那么在使用delete释放A指向的空间时,就会调用父类的析构函数而不是子类的析构函数,因为delete会根据指针变量的类型来决定调用哪个析构函数。而如果将父类和子类的析构函数均声明为virtual,就不会出现上面的问题。
演示代码2.5:(析构函数重写)
//继承体系1:不使用virtual声明析构函数
class A
{
public:
~A() { std::cout << "class A -> ~A()" << std::endl; }
};
class B : public A
{
public:
~B() { std::cout << "class B -> ~B()" << std::endl; }
};
//继承体系2:使用virtual声明析构函数
class AA
{
public:
virtual ~AA() { std::cout << "class AA -> ~AA()" << std::endl; }
};
class BB : public AA
{
public:
virtual ~BB() { std::cout << "class BB -> ~BB()" << std::endl; }
};
int main()
{
A* pa1 = new A;
A* pb1 = new B;
//析构函数没有声明为虚函数,都是调用父类的析构函数
delete pa1;
delete pb1; //都调用父类析构函数
std::cout << std::endl;
AA* paa1 = new AA;
AA* pbb1 = new BB;
//析构函数声明了虚函数,根据实际指向内容的类型,来确定调用父类析构函数还是子类析构函数
delete paa1; //调用父类析构函数
delete pbb1; //先调子类析构再调父类析构
return 0;
}

三. 抽象类
3.1 抽象类的概念
- 抽象类:就是包含纯虚函数的类,抽象类不能实例化出对象。
- 纯虚函数:就是在一般的虚函数后面加上=0的虚函数。
如果不希望某个类被实例化,那么就应当将这个类定义为抽象类。
演示代码3.1:(定义抽象类)
class A
{
public:
//纯虚函数,被=0修饰
virtual void func1() = 0 {} ;
int _a1 = 1;
};
3.2 抽象类的继承问题
抽象类不能被实例化,但可以被继承。在以抽象类为父类的继承体系中,必须在子类中对父类中的纯虚函数进行重写,如果不重写父类纯虚函数,子类也就无法实例化出对象。
演示代码3.2:(定义以抽象类为父类的继承体系)
class A
{
public:
//纯虚函数,被=0修饰
virtual void func1() = 0 {} ;
int _a1 = 1;
};
class B : public A
{
public:
//重写父类纯虚函数
virtual void func1() { std::cout << "class B -> func1()" << std::endl; }
};