一、多态的概念
多态的概念:望文生义,就是多种状态,具体点就是对于一件事情,不同的对象去完成时会产生出不同的状态 (结果)。
举个例子:比如买票这个行为,当普通人买票时,是全价买票;儿童买票时,是免费买票;而军人买票时是优先买票。
二、多态的定义及实现
C++的多态必须满足两个条件:
1 必须通过基类的指针或者引用调用虚函数
2 被调用的函数是虚函数,且必须完成对基类虚函数的重写
子类虚函数可以不写 virtual (实际中最好加上),但是父类虚函数必须写 virtual,才能满足虚函数重写;
返回值可以不同,但必须是父子关系的指针或者引用 (在这里的父子没有限制,可以是其他的父子关系),这也称之为协变;
具体实现:
class Person //成人
{
public:
virtual void fun()
{
cout << "全价票" << endl; //成人票全价
}
};
class Student : public Person //学生
{
public:
virtual void fun() //子类完成对父类虚函数的重写
{
cout << "半价票" << endl;//学生票半价
}
};
void BuyTicket(Person* p)
{
p->fun();
}
int main()
{
Student st;
Person p;
BuyTicket(&st);//子类对象切片过去
BuyTicket(&p);//父类对象传地址
}
以上例子为什么要传地址,直接传对象不行吗?
答案当然是不行的,因为多态构成的第一个条件不满足。
如果满足多态,编译器会调用指针指向对象的虚函数,而与指针的类型无关。如果不满足多态,编译器会直接根据指针的类型去调用虚函数。
三、虚函数
用virtual修饰的关键字就是虚函数。
虚函数只能是类中非静态的成员函数。
在类外面的函数不能是虚函数。
虚函数的重写(覆盖)
虚函数的重写:虚函数 + 三同 (基类和派生类的虚函数的返回值、参数、函数名都相同)。
class A
{
public:
virtual void Print() { std::cout << "haha" << std::endl; }
};
class B : public A
{
public:
virtual void Print() { std::cout << "hehe" << std::endl; }
};
虚函数重写的两个例外:
1.协变
子类的虚函数和父类的虚函数的返回值可以不同,也能构成重载。但需要子类的返回值是一个子类的指针或者引用,父类的返回值是一个父类的指针或者引用,且返回值代表的两个类也成继承关系。这个叫做协变。
class A
{};
class B : public A
{}; //B继承A
class Person
{
public:
virtual A* fun()//返回A类指针
{
return nullptr;
}
};
class Student
{
public:
//返回B类指针,虽然返回值不同,也构成重写
virtual B* fun()//子类重写父类虚函数
{
return nullptr;
}
};
2.析构函数的重写
子类的虚函数可以不写 virtual,但是父类虚函数必须写 virtual,才能符合虚函数重写的条件。
只要父类的析构函数用virtual修饰,无论子类是否有virtual,都构成析构。
class A
{
public:
// 基类的虚函数必须加上 virtual
virtual void Print() { std::cout << "haha" << std::endl; }
};
class B : public A
{
public:
// 派生类的虚函数可以不写 virtual, 但实际中最好加上
void Print() { std::cout << "hehe" << std::endl; }
};
四、多态调用和普通调用
普通调用, 即调用函数的对象的类型是什么,就调用什么类型的函数。
上面之所以是普通调用,因为它不满足构成多态的条件:
- 虚函数的重写;
- 基类的指针或者引用调用虚函数。
只要符合上面的两个条件就构成多态调用。
多态调用和普通调用是相对立的:
- 如果是普通调用,就看对象的类型决定调用什么接口;
- 如果是多态调用,就看指向的是什么对象,如果指向派生类,就调派生类的接口;如果指向基类,就调基类的接口。
五、C++11 中 override 和 final
1. final关键字
在学习继承的时候,我们知道, final 可以修饰一个类,表明这个类不可被继承;
实际上,final 也可以修饰一个虚函数,表示这个虚函数不可以被重写。
因此,final 的作用有两个:
- 修饰一个类,表明该类不可被继承;
- 修饰一个虚函数,表明该虚函数不可被重写
class A
{
public:
virtual void print() final
{
std::cout << "haha" << std::endl;
}
};
class B : public A
{
public:
virtual void print()
{
std::cout << "hehe" << std::endl;
}
};
2. override关键字
用 override 修饰的虚函数,如果没有完成重写,则会编译报错。
class A
{
public:
virtual void print()
{
std::cout << "haha" << std::endl;
}
};
class B : public A
{
public:
// 此时就会编译报错, 因为派生类没有完成对 print 这个虚函数的重写
virtual void print(int val) override
{
std::cout << "hehe" << std::endl;
}
};
六、重载,覆盖(重写),重定义(隐藏)
七、接口继承和实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口 (即函数的声明),目的是为了虚函数的重写 (重写函数的实现),达成多态,继承的是虚函数的接口;
- 强调一下虚函数的继承是一种接口继承,更准确地说,应该是,虚函数的继承, 在构成多态的条件下是接口继承;而如果派生类没有重写虚函数,但是继承了该虚函数,那么这仍然是继承了函数的实现,不会产生多态效果,称为实现继承;
- 补充一点,派生类继承基类的函数,并不是说基类的函数在派生类中也有一份,而是说派生类可以使用基类的函数 (当然受访问权限和继承方式的约束),继承后的基类函数依旧属于基类;
- 最后,一般情况下,如果目的不是实现多态,不要把函数定义成虚函数,可以避免不必要的开销和复杂性。