一、多态是什么
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
在继承中构成多态的两个条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
也就是说,父类的指针或者引用可以指向父类,也可以指向子类,指向父类时调用父类的虚函数,指向子类时调用子类的虚函数,如果调用的不是虚函数,则会调用父类的成员函数
这里是使用虚函数的简单例子、
#include<iostream>
using namespace std;
class person
{
public:
virtual void buyticket()
{
cout << "买票,全价" << endl;
}
};
class student :public person
{
public:
virtual void buyticket()
{
cout << "买票,半价" << endl;
}
};
int main()
{
person p;
p.buyticket();
student s;
s.buyticket();
person& p1 = s;
p1.buyticket();
return 0;
}
运行结果
p1是s中的person切片的引用,但是它的输出结果仍然是半价,这就是多态的结果
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
但是有两个特殊情况
1.派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}//基类返回基类
};
class Student : public Person {
public:
virtual B* f() {return new B;}//派生类返回派生类
};
2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
为什么需要重写析构函数,即为什么需要将析构函数处理成虚函数?
这里是一个例子
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
int* a;
};
class B :public A
{
public:
B()
:b(nullptr)
{
cout << "B()" << endl;
b = new int[10];
}
~B()
{
cout << "~B()" << endl;
delete[]b;
}
public:
int* b;
};
int main()
{
A* a = new B;
delete a;
return 0;
}
运行结果
很明显只是将基类析构了,派生类中new出来的数组没有处理。因为a是A类的指针,调用delete时会调用A类切片的析构,B的部分没有处理,那么只要我们将析构函数处理成虚函数就可以
class A
{
public:
A()
{
cout << "A()" << endl;
}
virtual ~A()
{
cout << "~A()" << endl;
}
int* a;
};
class B :public A
{
public:
B()
:b(nullptr)
{
cout << "B()" << endl;
b = new int[10];
}
virtual ~B()
{
cout << "~B()" << endl;
delete[]b;
}
public:
int* b;
};
int main()
{
A* a = new B;
delete a;
return 0;
}
这里加上了virtual,再次运行
因为a指向的是派生类,且析构函数是虚函数,所以调用派生类的析构函数
二、重载、重定义(隐藏)、重写(覆盖)的对比
其中重载是最容易区分的,其次,只要意识到不构成重写就构成重定义,就很容易区分这三者
class person
{
public:
virtual void buyticket()
{
cout << "买票,全价" << endl;
}
};
class student :public person
{
public:
virtual void buyticket(int)
{
cout << "买票,半价" << endl;
}
};
这里的buyticket函数,参数不同,所以不构成重写,所以构成重定义,父类指针调父类函数,子类指针调子类函数
三、抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
四、多态的原理
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
这里的运行结果
明明只有一个int类型成员变量,为什么占用8个字节呢?
通过监视窗口,我们发现这里实例化出来的对象多了一个_vfptr指针
我们对上面的代码进行改进
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive:public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
让Derive继承Base,并重写Func1函数
打开监视窗口
很明显,d中Base的虚表和b中虚表的Fun1不一样了,而Fun2还是一样的,原因是Derive对Func1进行了重写(覆盖),而没有重写Func2,而Func3不是虚函数,所以没有存放在虚表
那么现在多态的原理呼之欲出了,当基类指针或引用指向基类时,在虚表中找到的是基类的虚函数,即没有被重写的虚函数;当基类指针或引用指向派生类时,在虚表中找到的是派生类的虚函数,即重写后的虚函数