多态非常非常重要,面试的时候经常出,这块一定要认真学。
我们知道继承就是复用,那什么是多态呢?
多态就是多种形态。
具体点就是我们要做某个动作,当不同的对象去完成时会产生出不同的结果。
只要是做同样的一件事,不同的人去做,结果不一样,那就可以称为多态。
在程序当中就相当于调用一个函数,不同对象去调,它就会产生不同的结果。
下面我们看一下,多态是怎么玩的。
首先学习多态我们第一步要学习虚函数。
关键字virtual, 把virtual加在一个函数的前面,我们就叫做虚函数。
这两个函数什么关系呢?
按照我们以前的理解,成员函数函数名相同,子类隐藏父类,我们叫做隐藏关系。
现在有一个新的关系,它们函数名相同,参数相同,返回值相同,
并且是virtual,它们不叫隐藏,它们叫做重写或者覆盖。
这里没有重载的关系,重载必须在同一个作用域。
我们接下来看一下多态是怎样的。
既可以传父类也可以传子类,因为可以赋值兼容。
你传父类它会调用父类的虚函数,你传子类它会调用子类的虚函数。
也就意味着不同的对象去买票,达到的结果是不一样的。
单看下面这个代码是不知道调用什么的。
多态的条件:
如果不满足条件就实现不了多态。
为什么需要多态?
有些地方没有多态解决不了问题。
先来看一个应用场景。
后定义的先析构没有什么问题。
换一个场景。
有一个父类的指针,父类的指针能指向父类的对象,
父类的指针也能指向子类对象。
这个析构函数调出事了。
如果这个子类里有一些资源要释放,没调用析构函数就内存泄漏了。
为什么?
我们先看一下delete由什么东西构成。由两部分构成。
之前讲到过,析构函数不会用上面的名称,它会改成统一的名称,
所以父类和子类的析构函数构成隐藏关系。
为什么要这样改?
因为多态原因。
如果多态的概念,按照我们以前的理解,类型是什么就调用什么的析构函数。
但是这不是我们期望的结果。
我们这里不期望按类型去调,这里可能会造成内存泄漏。
我们这里期望按照指向的对象去调用析构函数
在这里要想正确的调用,必须是多态。
多态的条件上面也提到了,上面的条件缺一不可。
如果不满足会怎么样呢?我们现在来具体看一下。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
多态的条件:
1.虚函数的重写 - - 三同(函数名、参数、返回值)
2.父类指针或者引用去调用
不满足多态会怎么样?
虽然这两个函数构成隐藏,但是这里并不会体现出隐藏。
子类对象去调用才会有隐藏的关系体现。
子类去调用父类的函数调用不到需要指定,这就是隐藏。
大家看一下上面这个结果是多少?
大家可能认为上面都是全价,因为不满足多态,但是看结果。
这里就要提到虚函数重写的两个例外。
上面可以认为子类可以不写virtual, 也可以满足多态。
为什么呢?
它是这样认为的,如果父类不写virtual,那肯定就不是虚函数了。
但是现在这种情况,它认为子类重写了虚函数。
重写体现的叫接口继承,也就是说子类把父类函数的声明给继承下来了,
然后重写继承父类这个函数的实现。
所以就算子类不加virtual也是虚函数,因为父类是虚函数,子类继承下来了。
实际当中我们也不会像上面一样写代码。
它这个语法的设计,唯一有一个优势在这里。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
~Student()
{
cout << "~Student()" << endl;
}
};
void Func(Person* p)
{
p->BuyTicket();
delete p;
}
int main()
{
Func(new Person);
Func(new Student);
return 0;
}
我期望上面能够正确的调用析构函数。
但是这里的析构函数调用的不对。
现在我希望delete p调用析构函数也构成多态。
父类的析构函数加个virtual就可以了。
父类只需要加virtual就构成多态,子类加不加都无所谓。
这里还有一个例外,返回值可以不同。
返回值不同可以是什么样子的呢?
它不是随便写。
这样会报错。
返回值可以不同,必须是父子关系的指针或者引用
这个例外叫做协变。
最常见的场景就是这样。
协变在实际的应用场景用的很少。
operator的赋值要不要构成虚函数的重写?
不需要。因为子类的成员函数必须调用父类来完成,是复用而不是重写。
是一个其他的父子类也是可以的
这样也可以。都叫做协变。
条件二,必须是父类的指针或者引用去调用
如果是对象去调用会怎样?
判断多态需要把多态的条件严格卡清楚,至于为什么后面会讲原理
下面先来看一道题。
首先子类的指针能不能调用test();?
因为B这个对象虽然没有test();但是它继承了A的test();
条件一:
然后分析一下构不构成多态
缺省参数不同也不影响,它要求的是参数的类型。
条件二:
继承不会把参数类型改了,所以还是A*
所以满足多态
这道题好变态。但是还没有结束,这道题还可以变形。
现在结果是多少?
这里它不满足多态,不是父类的指针或者引用去调用,不满足多态就是正常情况,
哪个对象去调用就调用哪个对象的函数。
如果不构成多态跟重写没有什么关系,就是正常调用。
普通函数的继承是一种实现继承,
虚函数的继承是一种接口继承,目的是为了重写实现,达成多态。
所以如果不实现多态,不要把函数定义成虚函数。
final可以修饰一个类,让这个类不能被继承。
这个叫做最终类。
final还可以用来修饰虚函数,这个虚函数就不能被重写。
如果不想虚函数被继承,就加final,但是实际当中这样的场景极少。
虚函数的意义就是被重写,重写的意义就是满足多态的条件,
所以这里很矛盾,搞了一个虚函数又不想被重写,那搞虚函数有什么意义。
override可以检查是否重写,不是报错
虚函数就是为重写而生的,重写是为多态而生的
不重写的虚函数是没什么意义的,override可以强制我们重写。
我们可以在虚函数的后面加上赋值0;那这个函数就叫做纯虚函数。
包含纯虚函数的类就叫做抽象类。
抽象类的特点是:不能实例化出对象。
只有重写纯虚函数,子类才能实例化出对象。
纯虚函数不用写实现,直接写声明就可以了。
这个东西的意义是什么?
这个类在现实世界当中没有对应的实体。我不想让这个类实例化出对象。
这个类只是为了让别人能够复用。
比如:Person, Fruit;
抽象类有一个特点,比如一个类去继承了它,这个不能实例化出对象。
为什么?包含纯虚函数的类就叫做抽象类。它继承了也就包含了。
怎么让上面这个子类不是抽象类?
我可以完成重写。
纯虚函数的意义?强制了子类去进行重写。
子类就能够实例化出对象,如果这个类不能实例化出对象,也就没什么意义。
纯虚函数跟override的区别是什么?
一个在父类,一个在子类。一个是检查重写一个是间接的强制重写。
class Car
{
public:
// 纯虚函数 -- 抽象类 -- 不能实例化出对象
virtual void Drive() = 0;
};
// 一个类型在现实中没有对应的实体,我们就可以一个类定义为抽象类
class BMW :public Car
{
public:
virtual void Drive()
{
cout << "BMW-操控" << endl;
}
};
int main()
{
// Car car;
BMW bmwCar;
return 0;
}
语法已经讲的差不多了,我们下一篇文章会讲多态的原理。